blob: 015c7532c49b683285181872e51bd0bb9fcf3c1a [file] [log] [blame]
package org.robolectric.shadows;
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.robolectric.RoboSettings;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Scheduler;
@RunWith(RobolectricTestRunner.class)
public class ShadowLooperTest {
// testName is used when creating background threads. Makes it
// easier to debug exceptions on background threads when you
// know what test they are associated with.
@Rule
public TestName testName = new TestName();
// Helper method that starts the thread with the same name as the
// current test, so that you will know which test invoked it if
// it has an exception.
private HandlerThread getHandlerThread() {
HandlerThread ht = new HandlerThread(testName.getMethodName());
ht.start();
return ht;
}
// Useful class for checking that a thread's loop() has exited.
private class QuitThread extends Thread {
private boolean hasContinued = false;
private Looper looper;
private CountDownLatch started = new CountDownLatch(1);
public QuitThread() {
super(testName.getMethodName());
}
@Override
public void run() {
Looper.prepare();
looper = Looper.myLooper();
started.countDown();
Looper.loop();
hasContinued = true;
}
}
private QuitThread getQuitThread() throws InterruptedException {
QuitThread qt = new QuitThread();
qt.start();
qt.started.await();
return qt;
}
@Test
public void mainLooper_andMyLooper_shouldBeSame_onMainThread() {
assertThat(Looper.myLooper()).isSameAs(Looper.getMainLooper());
}
@Test
public void differentThreads_getDifferentLoopers() {
HandlerThread ht = getHandlerThread();
assertThat(ht.getLooper()).isNotSameAs(Looper.getMainLooper());
}
@Test
public void mainLooperThread_shouldBeTestThread() {
assertThat(Looper.getMainLooper().getThread()).isSameAs(Thread.currentThread());
}
@Test
public void shadowMainLooper_shouldBeShadowOfMainLooper() {
assertThat(ShadowLooper.getShadowMainLooper()).isSameAs(shadowOf(Looper.getMainLooper()));
}
@Test
public void getLooperForThread_returnsLooperForAThreadThatHasOne() throws InterruptedException {
QuitThread qt = getQuitThread();
assertThat(ShadowLooper.getLooperForThread(qt)).isSameAs(qt.looper);
}
@Test
public void getLooperForThread_returnsLooperForMainThread() {
assertThat(ShadowLooper.getLooperForThread(Thread.currentThread())).isSameAs(Looper.getMainLooper());
}
@Test
public void idleMainLooper_executesScheduledTasks() {
final boolean[] wasRun = new boolean[]{false};
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
wasRun[0] = true;
}
}, 2000);
assertThat(wasRun[0]).named("first").isFalse();
ShadowLooper.idleMainLooper(1999);
assertThat(wasRun[0]).named("second").isFalse();
ShadowLooper.idleMainLooper(1);
assertThat(wasRun[0]).named("last").isTrue();
}
@Test
public void idleConstantly_runsPostDelayedTasksImmediately() {
ShadowLooper.idleMainLooperConstantly(true);
final boolean[] wasRun = new boolean[]{false};
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
wasRun[0] = true;
}
}, 2000);
assertThat(wasRun[0]).isTrue();
}
@Test(expected = RuntimeException.class)
public void shouldThrowRuntimeExceptionIfTryingToQuitMainLooper() throws Exception {
Looper.getMainLooper().quit();
}
@Test
public void shouldNotQueueMessagesIfLooperIsQuit() throws Exception {
HandlerThread ht = getHandlerThread();
Looper looper = ht.getLooper();
looper.quit();
assertThat(shadowOf(looper).hasQuit()).named("hasQuit").isTrue();
assertThat(shadowOf(looper).post(new Runnable() {
@Override
public void run() {
}
}, 0)).named("post").isFalse();
assertThat(shadowOf(looper).postAtFrontOfQueue(new Runnable() {
@Override
public void run() {
}
})).named("postAtFrontOfQueue").isFalse();
assertThat(shadowOf(looper).getScheduler().areAnyRunnable()).named("areAnyRunnable").isFalse();
}
@Test
public void shouldThrowawayRunnableQueueIfLooperQuits() throws Exception {
HandlerThread ht = getHandlerThread();
Looper looper = ht.getLooper();
shadowOf(looper).pause();
shadowOf(looper).post(new Runnable() {
@Override
public void run() {
}
}, 0);
looper.quit();
assertThat(shadowOf(looper).hasQuit()).named("hasQuit").isTrue();
assertThat(shadowOf(looper).getScheduler().areAnyRunnable()).named("areAnyRunnable").isFalse();
assertThat(shadowOf(looper.getQueue()).getHead()).named("queue").isNull();
}
@Test
public void threadShouldContinue_whenLooperQuits() throws InterruptedException {
QuitThread test = getQuitThread();
assertThat(test.hasContinued).named("beforeJoin").isFalse();
test.looper.quit();
test.join(5000);
assertThat(test.hasContinued).named("afterJoin").isTrue();
}
@Test
public void shouldResetQueue_whenLooperIsReset() {
HandlerThread ht = getHandlerThread();
Looper looper = ht.getLooper();
Handler h = new Handler(looper);
ShadowLooper sLooper = shadowOf(looper);
sLooper.pause();
h.post(new Runnable() {
@Override
public void run() {
}
});
assertThat(shadowOf(looper.getQueue()).getHead()).named("queue").isNotNull();
sLooper.reset();
assertThat(sLooper.getScheduler().areAnyRunnable()).named("areAnyRunnable").isFalse();
assertThat(shadowOf(looper.getQueue()).getHead()).named("queue").isNull();
}
@Test
public void shouldSetNewScheduler_whenLooperIsReset() {
HandlerThread ht = getHandlerThread();
Looper looper = ht.getLooper();
ShadowLooper sLooper = shadowOf(looper);
Scheduler old = sLooper.getScheduler();
sLooper.reset();
assertThat(old).isNotSameAs(sLooper.getScheduler());
}
@Test
public void resetThreadLoopers_shouldQuitAllNonMainLoopers() throws InterruptedException {
QuitThread test = getQuitThread();
assertThat(test.hasContinued).named("hasContinued:before").isFalse();
ShadowLooper.resetThreadLoopers();
test.join(5000);
assertThat(test.hasContinued).named("hasContinued:after").isTrue();
}
@Test(timeout = 1000)
public void whenTestHarnessUsesDifferentThread_shouldStillHaveMainLooper() {
assertThat(Looper.myLooper()).isSameAs(Looper.getMainLooper());
}
@Test
public void resetThreadLoopers_fromNonMainThread_shouldThrowISE() throws InterruptedException {
final AtomicReference<Throwable> ex = new AtomicReference<>();
Thread t = new Thread() {
@Override
public void run() {
try {
ShadowLooper.resetThreadLoopers();
} catch (Throwable t) {
ex.set(t);
}
}
};
t.start();
t.join();
assertThat(ex.get()).isInstanceOf(IllegalStateException.class);
}
@Test
public void soStaticRefsToLoopersInAppWorksAcrossTests_shouldRetainSameLooperForMainThreadBetweenResetsButGiveItAFreshScheduler() throws Exception {
Looper mainLooper = Looper.getMainLooper();
Scheduler scheduler = shadowOf(mainLooper).getScheduler();
shadowOf(mainLooper).quit = true;
assertThat(RuntimeEnvironment.application.getMainLooper()).isSameAs(mainLooper);
Scheduler s = new Scheduler();
RuntimeEnvironment.setMasterScheduler(s);
ShadowLooper.resetThreadLoopers();
Application application = new Application();
ReflectionHelpers.callInstanceMethod(application, "attach", ReflectionHelpers.ClassParameter.from(Context.class, RuntimeEnvironment.application.getBaseContext()));
assertThat(Looper.getMainLooper()).named("Looper.getMainLooper()").isSameAs(mainLooper);
assertThat(application.getMainLooper()).named("app.getMainLooper()").isSameAs(mainLooper);
assertThat(shadowOf(mainLooper).getScheduler()).named("scheduler").isNotSameAs(scheduler);
assertThat(shadowOf(mainLooper).getScheduler()).named("scheduler").isSameAs(s);
assertThat(shadowOf(mainLooper).hasQuit()).named("quit").isFalse();
}
@Test
public void getMainLooperReturnsNonNullOnMainThreadWhenRobolectricApplicationIsNull() {
RuntimeEnvironment.application = null;
assertThat(Looper.getMainLooper()).isNotNull();
}
private void setAdvancedScheduling() {
RoboSettings.setUseGlobalScheduler(true);
}
@Test
public void reset_setsGlobalScheduler_forMainLooper_byDefault() {
ShadowLooper sMainLooper = ShadowLooper.getShadowMainLooper();
Scheduler s = new Scheduler();
RuntimeEnvironment.setMasterScheduler(s);
sMainLooper.reset();
assertThat(sMainLooper.getScheduler()).isSameAs(s);
}
@Test
public void reset_setsGlobalScheduler_forMainLooper_withAdvancedScheduling() {
setAdvancedScheduling();
ShadowLooper sMainLooper = ShadowLooper.getShadowMainLooper();
Scheduler s = new Scheduler();
RuntimeEnvironment.setMasterScheduler(s);
sMainLooper.reset();
assertThat(sMainLooper.getScheduler()).isSameAs(s);
}
@Test
public void reset_setsNewScheduler_forNonMainLooper_byDefault() {
HandlerThread ht = getHandlerThread();
ShadowLooper sLooper = shadowOf(ht.getLooper());
Scheduler old = sLooper.getScheduler();
sLooper.reset();
assertThat(sLooper.getScheduler()).isNotSameAs(old);
assertThat(sLooper.getScheduler()).isNotSameAs(RuntimeEnvironment.getMasterScheduler());
}
@Test
public void reset_setsSchedulerToMaster_forNonMainLooper_withAdvancedScheduling() {
HandlerThread ht = getHandlerThread();
ShadowLooper sLooper = shadowOf(ht.getLooper());
Scheduler s = new Scheduler();
RuntimeEnvironment.setMasterScheduler(s);
setAdvancedScheduling();
sLooper.reset();
assertThat(sLooper.getScheduler()).isSameAs(s);
}
@Test
public void resetThreadLoopers_resets_background_thread_schedulers() {
HandlerThread backgroundThread = new HandlerThread("resetTest");
backgroundThread.start();
Looper backgroundLooper = backgroundThread.getLooper();
Handler handler = new Handler(backgroundLooper);
Runnable empty = new Runnable() {
@Override
public void run() {}
};
// There should be at least two iterations of this loop because resetThreadLoopers calls
// 'quit' on background loopers once, which also resets the scheduler.
for (int i = 0; i < 5; i++) {
assertThat(shadowOf(backgroundLooper).getScheduler().size()).isEqualTo(0);
assertThat(shadowOf(backgroundLooper).getScheduler().getCurrentTime()).isEqualTo(100L);
handler.post(empty);
handler.postDelayed(empty, 5000);
// increment scheduler's time by 5000
shadowOf(backgroundLooper).runToEndOfTasks();
assertThat(shadowOf(backgroundLooper).getScheduler().getCurrentTime()).isEqualTo(5100L);
ShadowLooper.resetThreadLoopers();
}
}
@Test
public void myLooper_returnsMainLooper_ifMainThreadIsSwitched() throws InterruptedException {
final AtomicReference<Looper> myLooper = new AtomicReference<>();
Thread t = new Thread(testName.getMethodName()) {
@Override
public void run() {
myLooper.set(Looper.myLooper());
}
};
RuntimeEnvironment.setMainThread(t);
t.start();
try {
t.join(1000);
assertThat(myLooper.get()).isSameAs(Looper.getMainLooper());
} finally {
RuntimeEnvironment.setMainThread(Thread.currentThread());
}
}
@Test
public void getMainLooper_shouldBeInitialized_onBackgroundThread_evenWhenRobolectricApplicationIsNull() throws Exception {
RuntimeEnvironment.application = null;
final AtomicReference<Looper> mainLooperAtomicReference = new AtomicReference<>();
Thread backgroundThread = new Thread(new Runnable() {
@Override
public void run() {
Looper mainLooper = Looper.getMainLooper();
mainLooperAtomicReference.set(mainLooper);
}
}, testName.getMethodName());
backgroundThread.start();
backgroundThread.join();
assertThat(mainLooperAtomicReference.get()).named("mainLooper").isSameAs(Looper.getMainLooper());
}
@Test
public void schedulerOnAnotherLooper_shouldNotBeMaster_byDefault() {
HandlerThread ht = getHandlerThread();
assertThat(shadowOf(ht.getLooper()).getScheduler()).isNotSameAs(RuntimeEnvironment.getMasterScheduler());
}
@Test
public void schedulerOnAnotherLooper_shouldBeMaster_ifAdvancedSchedulingEnabled() {
setAdvancedScheduling();
HandlerThread ht = getHandlerThread();
assertThat(shadowOf(ht.getLooper()).getScheduler()).isSameAs(RuntimeEnvironment.getMasterScheduler());
}
@Test
public void withAdvancedScheduling_shouldDispatchMessagesOnBothLoopers_whenAdvancingForegroundThread() {
setAdvancedScheduling();
ShadowLooper.pauseMainLooper();
HandlerThread ht = getHandlerThread();
Handler handler1 = new Handler(ht.getLooper());
Handler handler2 = new Handler();
final ArrayList<String> events = new ArrayList<>();
handler1.postDelayed(new Runnable() {
@Override
public void run() {
events.add("handler1");
}
}, 100);
handler2.postDelayed(new Runnable() {
@Override
public void run() {
events.add("handler2");
}
}, 200);
assertThat(events).named("start").isEmpty();
Scheduler s = ShadowLooper.getShadowMainLooper().getScheduler();
assertThat(s).isSameAs(RuntimeEnvironment.getMasterScheduler());
assertThat(s).isSameAs(shadowOf(ht.getLooper()).getScheduler());
final long startTime = s.getCurrentTime();
s.runOneTask();
assertThat(events).named("firstEvent").containsExactly("handler1");
assertThat(s.getCurrentTime()).named("firstEvent:time").isEqualTo(100 + startTime);
s.runOneTask();
assertThat(events).named("secondEvent").containsExactly("handler1", "handler2");
assertThat(s.getCurrentTime()).named("secondEvent:time").isEqualTo(200 + startTime);
}
@Test
public void resetThreadLoopers_clears_messages() {
HandlerThread backgroundThread = new HandlerThread("resetTest");
backgroundThread.start();
Looper backgroundLooper = backgroundThread.getLooper();
Handler handler = new Handler(backgroundLooper);
for (int i = 0; i < 5; i++) {
handler.sendEmptyMessageDelayed(1, 100);
ShadowLooper.resetThreadLoopers();
assertThat(handler.hasMessages(1)).isFalse();
}
}
@After
public void tearDown() {
RoboSettings.setUseGlobalScheduler(false);
}
}