1 package org.robolectric.shadows; 2 3 import static com.google.common.truth.Truth.assertThat; 4 import static org.robolectric.Shadows.shadowOf; 5 6 import android.app.Application; 7 import android.content.Context; 8 import android.os.Handler; 9 import android.os.HandlerThread; 10 import android.os.Looper; 11 import androidx.test.core.app.ApplicationProvider; 12 import androidx.test.ext.junit.runners.AndroidJUnit4; 13 import java.util.ArrayList; 14 import java.util.concurrent.CountDownLatch; 15 import java.util.concurrent.atomic.AtomicReference; 16 import org.junit.After; 17 import org.junit.Rule; 18 import org.junit.Test; 19 import org.junit.rules.TestName; 20 import org.junit.runner.RunWith; 21 import org.robolectric.RoboSettings; 22 import org.robolectric.RuntimeEnvironment; 23 import org.robolectric.util.ReflectionHelpers; 24 import org.robolectric.util.Scheduler; 25 26 @RunWith(AndroidJUnit4.class) 27 public class ShadowLooperTest { 28 29 // testName is used when creating background threads. Makes it 30 // easier to debug exceptions on background threads when you 31 // know what test they are associated with. 32 @Rule 33 public TestName testName = new TestName(); 34 35 // Helper method that starts the thread with the same name as the 36 // current test, so that you will know which test invoked it if 37 // it has an exception. 38 private HandlerThread getHandlerThread() { 39 HandlerThread ht = new HandlerThread(testName.getMethodName()); 40 ht.start(); 41 return ht; 42 } 43 44 // Useful class for checking that a thread's loop() has exited. 45 private class QuitThread extends Thread { 46 private boolean hasContinued = false; 47 private Looper looper; 48 private CountDownLatch started = new CountDownLatch(1); 49 50 public QuitThread() { 51 super(testName.getMethodName()); 52 } 53 54 @Override 55 public void run() { 56 Looper.prepare(); 57 looper = Looper.myLooper(); 58 started.countDown(); 59 Looper.loop(); 60 hasContinued = true; 61 } 62 } 63 64 private QuitThread getQuitThread() throws InterruptedException { 65 QuitThread qt = new QuitThread(); 66 qt.start(); 67 qt.started.await(); 68 return qt; 69 } 70 71 @Test 72 public void mainLooper_andMyLooper_shouldBeSame_onMainThread() { 73 assertThat(Looper.myLooper()).isSameAs(Looper.getMainLooper()); 74 } 75 76 @Test 77 public void differentThreads_getDifferentLoopers() { 78 HandlerThread ht = getHandlerThread(); 79 assertThat(ht.getLooper()).isNotSameAs(Looper.getMainLooper()); 80 } 81 82 @Test 83 public void mainLooperThread_shouldBeTestThread() { 84 assertThat(Looper.getMainLooper().getThread()).isSameAs(Thread.currentThread()); 85 } 86 87 @Test 88 public void shadowMainLooper_shouldBeShadowOfMainLooper() { 89 assertThat(ShadowLooper.getShadowMainLooper()).isSameAs(shadowOf(Looper.getMainLooper())); 90 } 91 92 @Test 93 public void getLooperForThread_returnsLooperForAThreadThatHasOne() throws InterruptedException { 94 QuitThread qt = getQuitThread(); 95 assertThat(ShadowLooper.getLooperForThread(qt)).isSameAs(qt.looper); 96 } 97 98 @Test 99 public void getLooperForThread_returnsLooperForMainThread() { 100 assertThat(ShadowLooper.getLooperForThread(Thread.currentThread())).isSameAs(Looper.getMainLooper()); 101 } 102 103 @Test 104 public void idleMainLooper_executesScheduledTasks() { 105 final boolean[] wasRun = new boolean[]{false}; 106 new Handler().postDelayed(new Runnable() { 107 @Override 108 public void run() { 109 wasRun[0] = true; 110 } 111 }, 2000); 112 113 assertThat(wasRun[0]).named("first").isFalse(); 114 ShadowLooper.idleMainLooper(1999); 115 assertThat(wasRun[0]).named("second").isFalse(); 116 ShadowLooper.idleMainLooper(1); 117 assertThat(wasRun[0]).named("last").isTrue(); 118 } 119 120 @Test 121 public void idleConstantly_runsPostDelayedTasksImmediately() { 122 ShadowLooper.idleMainLooperConstantly(true); 123 final boolean[] wasRun = new boolean[]{false}; 124 new Handler().postDelayed(new Runnable() { 125 @Override 126 public void run() { 127 wasRun[0] = true; 128 } 129 }, 2000); 130 131 assertThat(wasRun[0]).isTrue(); 132 } 133 134 @Test(expected = RuntimeException.class) 135 public void shouldThrowRuntimeExceptionIfTryingToQuitMainLooper() throws Exception { 136 Looper.getMainLooper().quit(); 137 } 138 139 @Test 140 public void shouldNotQueueMessagesIfLooperIsQuit() throws Exception { 141 HandlerThread ht = getHandlerThread(); 142 Looper looper = ht.getLooper(); 143 looper.quit(); 144 assertThat(shadowOf(looper).hasQuit()).named("hasQuit").isTrue(); 145 assertThat(shadowOf(looper).post(new Runnable() { 146 @Override 147 public void run() { 148 } 149 }, 0)).named("post").isFalse(); 150 151 assertThat(shadowOf(looper).postAtFrontOfQueue(new Runnable() { 152 @Override 153 public void run() { 154 } 155 })).named("postAtFrontOfQueue").isFalse(); 156 assertThat(shadowOf(looper).getScheduler().areAnyRunnable()).named("areAnyRunnable").isFalse(); 157 } 158 159 @Test 160 public void shouldThrowawayRunnableQueueIfLooperQuits() throws Exception { 161 HandlerThread ht = getHandlerThread(); 162 Looper looper = ht.getLooper(); 163 shadowOf(looper).pause(); 164 shadowOf(looper).post(new Runnable() { 165 @Override 166 public void run() { 167 } 168 }, 0); 169 looper.quit(); 170 assertThat(shadowOf(looper).hasQuit()).named("hasQuit").isTrue(); 171 assertThat(shadowOf(looper).getScheduler().areAnyRunnable()).named("areAnyRunnable").isFalse(); 172 assertThat(shadowOf(looper.getQueue()).getHead()).named("queue").isNull(); 173 } 174 175 @Test 176 public void threadShouldContinue_whenLooperQuits() throws InterruptedException { 177 QuitThread test = getQuitThread(); 178 assertThat(test.hasContinued).named("beforeJoin").isFalse(); 179 test.looper.quit(); 180 test.join(5000); 181 assertThat(test.hasContinued).named("afterJoin").isTrue(); 182 } 183 184 @Test 185 public void shouldResetQueue_whenLooperIsReset() { 186 HandlerThread ht = getHandlerThread(); 187 Looper looper = ht.getLooper(); 188 Handler h = new Handler(looper); 189 ShadowLooper sLooper = shadowOf(looper); 190 sLooper.pause(); 191 h.post(new Runnable() { 192 @Override 193 public void run() { 194 } 195 }); 196 assertThat(shadowOf(looper.getQueue()).getHead()).named("queue").isNotNull(); 197 sLooper.reset(); 198 assertThat(sLooper.getScheduler().areAnyRunnable()).named("areAnyRunnable").isFalse(); 199 assertThat(shadowOf(looper.getQueue()).getHead()).named("queue").isNull(); 200 } 201 202 @Test 203 public void shouldSetNewScheduler_whenLooperIsReset() { 204 HandlerThread ht = getHandlerThread(); 205 Looper looper = ht.getLooper(); 206 ShadowLooper sLooper = shadowOf(looper); 207 Scheduler old = sLooper.getScheduler(); 208 sLooper.reset(); 209 assertThat(old).isNotSameAs(sLooper.getScheduler()); 210 } 211 212 @Test 213 public void resetThreadLoopers_shouldQuitAllNonMainLoopers() throws InterruptedException { 214 QuitThread test = getQuitThread(); 215 assertThat(test.hasContinued).named("hasContinued:before").isFalse(); 216 ShadowLooper.resetThreadLoopers(); 217 test.join(5000); 218 assertThat(test.hasContinued).named("hasContinued:after").isTrue(); 219 } 220 221 @Test(timeout = 1000) 222 public void whenTestHarnessUsesDifferentThread_shouldStillHaveMainLooper() { 223 assertThat(Looper.myLooper()).isSameAs(Looper.getMainLooper()); 224 } 225 226 @Test 227 public void resetThreadLoopers_fromNonMainThread_shouldThrowISE() throws InterruptedException { 228 final AtomicReference<Throwable> ex = new AtomicReference<>(); 229 Thread t = new Thread() { 230 @Override 231 public void run() { 232 try { 233 ShadowLooper.resetThreadLoopers(); 234 } catch (Throwable t) { 235 ex.set(t); 236 } 237 } 238 }; 239 t.start(); 240 t.join(); 241 assertThat(ex.get()).isInstanceOf(IllegalStateException.class); 242 } 243 244 @Test 245 public void soStaticRefsToLoopersInAppWorksAcrossTests_shouldRetainSameLooperForMainThreadBetweenResetsButGiveItAFreshScheduler() throws Exception { 246 Looper mainLooper = Looper.getMainLooper(); 247 Scheduler scheduler = shadowOf(mainLooper).getScheduler(); 248 shadowOf(mainLooper).quit = true; 249 assertThat(ApplicationProvider.getApplicationContext().getMainLooper()).isSameAs(mainLooper); 250 Scheduler s = new Scheduler(); 251 RuntimeEnvironment.setMasterScheduler(s); 252 ShadowLooper.resetThreadLoopers(); 253 Application application = new Application(); 254 ReflectionHelpers.callInstanceMethod( 255 application, 256 "attach", 257 ReflectionHelpers.ClassParameter.from( 258 Context.class, 259 ((Application) ApplicationProvider.getApplicationContext()).getBaseContext())); 260 261 assertThat(Looper.getMainLooper()).named("Looper.getMainLooper()").isSameAs(mainLooper); 262 assertThat(application.getMainLooper()).named("app.getMainLooper()").isSameAs(mainLooper); 263 assertThat(shadowOf(mainLooper).getScheduler()).named("scheduler").isNotSameAs(scheduler); 264 assertThat(shadowOf(mainLooper).getScheduler()).named("scheduler").isSameAs(s); 265 assertThat(shadowOf(mainLooper).hasQuit()).named("quit").isFalse(); 266 } 267 268 @Test 269 public void getMainLooperReturnsNonNullOnMainThreadWhenRobolectricApplicationIsNull() { 270 RuntimeEnvironment.application = null; 271 assertThat(Looper.getMainLooper()).isNotNull(); 272 } 273 274 private void setAdvancedScheduling() { 275 RoboSettings.setUseGlobalScheduler(true); 276 } 277 278 @Test 279 public void reset_setsGlobalScheduler_forMainLooper_byDefault() { 280 ShadowLooper sMainLooper = ShadowLooper.getShadowMainLooper(); 281 Scheduler s = new Scheduler(); 282 RuntimeEnvironment.setMasterScheduler(s); 283 sMainLooper.reset(); 284 assertThat(sMainLooper.getScheduler()).isSameAs(s); 285 } 286 287 @Test 288 public void reset_setsGlobalScheduler_forMainLooper_withAdvancedScheduling() { 289 setAdvancedScheduling(); 290 ShadowLooper sMainLooper = ShadowLooper.getShadowMainLooper(); 291 Scheduler s = new Scheduler(); 292 RuntimeEnvironment.setMasterScheduler(s); 293 sMainLooper.reset(); 294 assertThat(sMainLooper.getScheduler()).isSameAs(s); 295 } 296 297 @Test 298 public void reset_setsNewScheduler_forNonMainLooper_byDefault() { 299 HandlerThread ht = getHandlerThread(); 300 ShadowLooper sLooper = shadowOf(ht.getLooper()); 301 Scheduler old = sLooper.getScheduler(); 302 sLooper.reset(); 303 assertThat(sLooper.getScheduler()).isNotSameAs(old); 304 assertThat(sLooper.getScheduler()).isNotSameAs(RuntimeEnvironment.getMasterScheduler()); 305 } 306 307 @Test 308 public void reset_setsSchedulerToMaster_forNonMainLooper_withAdvancedScheduling() { 309 HandlerThread ht = getHandlerThread(); 310 ShadowLooper sLooper = shadowOf(ht.getLooper()); 311 Scheduler s = new Scheduler(); 312 RuntimeEnvironment.setMasterScheduler(s); 313 setAdvancedScheduling(); 314 sLooper.reset(); 315 assertThat(sLooper.getScheduler()).isSameAs(s); 316 } 317 318 @Test 319 public void resetThreadLoopers_resets_background_thread_schedulers() { 320 HandlerThread backgroundThread = new HandlerThread("resetTest"); 321 backgroundThread.start(); 322 Looper backgroundLooper = backgroundThread.getLooper(); 323 Handler handler = new Handler(backgroundLooper); 324 Runnable empty = new Runnable() { 325 @Override 326 public void run() {} 327 }; 328 // There should be at least two iterations of this loop because resetThreadLoopers calls 329 // 'quit' on background loopers once, which also resets the scheduler. 330 for (int i = 0; i < 5; i++) { 331 assertThat(shadowOf(backgroundLooper).getScheduler().size()).isEqualTo(0); 332 assertThat(shadowOf(backgroundLooper).getScheduler().getCurrentTime()).isEqualTo(100L); 333 handler.post(empty); 334 handler.postDelayed(empty, 5000); 335 // increment scheduler's time by 5000 336 shadowOf(backgroundLooper).runToEndOfTasks(); 337 assertThat(shadowOf(backgroundLooper).getScheduler().getCurrentTime()).isEqualTo(5100L); 338 ShadowLooper.resetThreadLoopers(); 339 } 340 } 341 342 @Test 343 public void myLooper_returnsMainLooper_ifMainThreadIsSwitched() throws InterruptedException { 344 final AtomicReference<Looper> myLooper = new AtomicReference<>(); 345 Thread t = new Thread(testName.getMethodName()) { 346 @Override 347 public void run() { 348 myLooper.set(Looper.myLooper()); 349 } 350 }; 351 RuntimeEnvironment.setMainThread(t); 352 t.start(); 353 try { 354 t.join(1000); 355 assertThat(myLooper.get()).isSameAs(Looper.getMainLooper()); 356 } finally { 357 RuntimeEnvironment.setMainThread(Thread.currentThread()); 358 } 359 } 360 361 @Test 362 public void getMainLooper_shouldBeInitialized_onBackgroundThread_evenWhenRobolectricApplicationIsNull() throws Exception { 363 RuntimeEnvironment.application = null; 364 final AtomicReference<Looper> mainLooperAtomicReference = new AtomicReference<>(); 365 366 Thread backgroundThread = new Thread(new Runnable() { 367 @Override 368 public void run() { 369 Looper mainLooper = Looper.getMainLooper(); 370 mainLooperAtomicReference.set(mainLooper); 371 } 372 }, testName.getMethodName()); 373 backgroundThread.start(); 374 backgroundThread.join(); 375 376 assertThat(mainLooperAtomicReference.get()).named("mainLooper").isSameAs(Looper.getMainLooper()); 377 } 378 379 @Test 380 public void schedulerOnAnotherLooper_shouldNotBeMaster_byDefault() { 381 HandlerThread ht = getHandlerThread(); 382 assertThat(shadowOf(ht.getLooper()).getScheduler()).isNotSameAs(RuntimeEnvironment.getMasterScheduler()); 383 } 384 385 @Test 386 public void schedulerOnAnotherLooper_shouldBeMaster_ifAdvancedSchedulingEnabled() { 387 setAdvancedScheduling(); 388 HandlerThread ht = getHandlerThread(); 389 assertThat(shadowOf(ht.getLooper()).getScheduler()).isSameAs(RuntimeEnvironment.getMasterScheduler()); 390 } 391 392 @Test 393 public void withAdvancedScheduling_shouldDispatchMessagesOnBothLoopers_whenAdvancingForegroundThread() { 394 setAdvancedScheduling(); 395 ShadowLooper.pauseMainLooper(); 396 HandlerThread ht = getHandlerThread(); 397 Handler handler1 = new Handler(ht.getLooper()); 398 Handler handler2 = new Handler(); 399 final ArrayList<String> events = new ArrayList<>(); 400 handler1.postDelayed(new Runnable() { 401 @Override 402 public void run() { 403 events.add("handler1"); 404 } 405 }, 100); 406 handler2.postDelayed(new Runnable() { 407 @Override 408 public void run() { 409 events.add("handler2"); 410 } 411 }, 200); 412 assertThat(events).named("start").isEmpty(); 413 Scheduler s = ShadowLooper.getShadowMainLooper().getScheduler(); 414 assertThat(s).isSameAs(RuntimeEnvironment.getMasterScheduler()); 415 assertThat(s).isSameAs(shadowOf(ht.getLooper()).getScheduler()); 416 final long startTime = s.getCurrentTime(); 417 s.runOneTask(); 418 assertThat(events).named("firstEvent").containsExactly("handler1"); 419 assertThat(s.getCurrentTime()).named("firstEvent:time").isEqualTo(100 + startTime); 420 s.runOneTask(); 421 assertThat(events).named("secondEvent").containsExactly("handler1", "handler2"); 422 assertThat(s.getCurrentTime()).named("secondEvent:time").isEqualTo(200 + startTime); 423 } 424 425 @Test 426 public void resetThreadLoopers_clears_messages() { 427 HandlerThread backgroundThread = new HandlerThread("resetTest"); 428 backgroundThread.start(); 429 Looper backgroundLooper = backgroundThread.getLooper(); 430 Handler handler = new Handler(backgroundLooper); 431 for (int i = 0; i < 5; i++) { 432 handler.sendEmptyMessageDelayed(1, 100); 433 ShadowLooper.resetThreadLoopers(); 434 assertThat(handler.hasMessages(1)).isFalse(); 435 } 436 } 437 438 @After 439 public void tearDown() { 440 RoboSettings.setUseGlobalScheduler(false); 441 } 442 } 443