package com.google.android.apps.common.testing.ui.espresso.base; import com.google.android.apps.common.testing.ui.espresso.IdlingResourceTimeoutException; import com.google.common.base.Optional; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.test.suitebuilder.annotation.LargeTest; import android.util.Log; import junit.framework.TestCase; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * Unit test for {@link UiControllerImpl}. */ public class UiControllerImplTest extends TestCase { private static final String TAG = UiControllerImplTest.class.getSimpleName(); private LooperThread testThread; private AtomicReference<UiControllerImpl> uiController = new AtomicReference<UiControllerImpl>(); private ThreadPoolExecutor asyncPool; private IdlingResourceRegistry idlingResourceRegistry; private static class LooperThread extends Thread { private final CountDownLatch init = new CountDownLatch(1); private Handler handler; private Looper looper; @Override public void run() { Looper.prepare(); handler = new Handler(); looper = Looper.myLooper(); init.countDown(); Looper.loop(); } public void quitLooper() { looper.quit(); } public Looper getLooper() { try { init.await(); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } return looper; } public Handler getHandler() { try { init.await(); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } return handler; } } @Override public void setUp() throws Exception { super.setUp(); testThread = new LooperThread(); testThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { Log.e(TAG, "Looper died: ", ex); } }); testThread.start(); idlingResourceRegistry = new IdlingResourceRegistry(testThread.getLooper()); asyncPool = new ThreadPoolExecutor(3, 3, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); EventInjector injector = null; if (Build.VERSION.SDK_INT > 15) { InputManagerEventInjectionStrategy strat = new InputManagerEventInjectionStrategy(); strat.initialize(); injector = new EventInjector(strat); } else { WindowManagerEventInjectionStrategy strat = new WindowManagerEventInjectionStrategy(); strat.initialize(); injector = new EventInjector(strat); } uiController.set(new UiControllerImpl( injector, new AsyncTaskPoolMonitor(asyncPool), Optional.<AsyncTaskPoolMonitor>absent(), idlingResourceRegistry, testThread.getLooper() )); } @Override public void tearDown() throws Exception { testThread.quitLooper(); asyncPool.shutdown(); super.tearDown(); } public void testLoopMainThreadTillIdle_sendsMessageToRightHandler() { final CountDownLatch latch = new CountDownLatch(3); testThread.getHandler(); // blocks till initialized; final Handler firstHandler = new Handler( testThread.looper, new Handler.Callback() { private boolean counted = false; @Override public boolean handleMessage(Message me) { if (counted) { fail("Called 2x!!!!"); } counted = true; latch.countDown(); return true; } }); final Handler secondHandler = new Handler( testThread.looper, new Handler.Callback() { private boolean counted = false; @Override public boolean handleMessage(Message me) { if (counted) { fail("Called 2x!!!!"); } counted = true; latch.countDown(); return true; } }); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { firstHandler.sendEmptyMessage(1); secondHandler.sendEmptyMessage(1); uiController.get().loopMainThreadUntilIdle(); latch.countDown(); } })); try { assertTrue( "Timed out waiting for looper to process all events", latch.await(10, TimeUnit.SECONDS)); } catch (InterruptedException e) { fail("Failed with exception " + e); } } public void testLoopForAtLeast() throws Exception { final CountDownLatch latch = new CountDownLatch(2); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { testThread.getHandler().post(new Runnable() { @Override public void run() { latch.countDown(); } }); uiController.get().loopMainThreadForAtLeast(1000); latch.countDown(); } })); assertTrue("Never returned from UiControllerImpl.loopMainThreadForAtLeast();", latch.await(10, TimeUnit.SECONDS)); } public void testLoopMainThreadUntilIdle_fullQueue() { final CountDownLatch latch = new CountDownLatch(3); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { Log.i(TAG, "On main thread"); Handler handler = new Handler(); Log.i(TAG, "Equeueing test runnable 1"); handler.post(new Runnable() { @Override public void run() { Log.i(TAG, "Running test runnable 1"); latch.countDown(); } }); Log.i(TAG, "Equeueing test runnable 2"); handler.post(new Runnable() { @Override public void run() { Log.i(TAG, "Running test runnable 2"); latch.countDown(); } }); Log.i(TAG, "Hijacking thread and looping it."); uiController.get().loopMainThreadUntilIdle(); latch.countDown(); } })); try { assertTrue( "Timed out waiting for looper to process all events", latch.await(10, TimeUnit.SECONDS)); } catch (InterruptedException e) { fail("Failed with exception " + e); } } public void testLoopMainThreadUntilIdle_fullQueueAndAsyncTasks() throws Exception { final CountDownLatch latch = new CountDownLatch(3); final CountDownLatch asyncTaskStarted = new CountDownLatch(1); final CountDownLatch asyncTaskShouldComplete = new CountDownLatch(1); asyncPool.execute(new Runnable() { @Override public void run() { asyncTaskStarted.countDown(); while (true) { try { asyncTaskShouldComplete.await(); return; } catch (InterruptedException ie) { // cant interrupt me. ignore. } } } }); assertTrue("async task is not starting!", asyncTaskStarted.await(2, TimeUnit.SECONDS)); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { Log.i(TAG, "On main thread"); Handler handler = new Handler(); Log.i(TAG, "Equeueing test runnable 1"); handler.post(new Runnable() { @Override public void run() { Log.i(TAG, "Running test runnable 1"); latch.countDown(); } }); Log.i(TAG, "Equeueing test runnable 2"); handler.post(new Runnable() { @Override public void run() { Log.i(TAG, "Running test runnable 2"); latch.countDown(); } }); Log.i(TAG, "Hijacking thread and looping it."); uiController.get().loopMainThreadUntilIdle(); latch.countDown(); } })); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(2, TimeUnit.SECONDS)); assertEquals("Not all main thread tasks have checked in", 1L, latch.getCount()); asyncTaskShouldComplete.countDown(); assertTrue("App should be idle.", latch.await(5, TimeUnit.SECONDS)); } public void testLoopMainThreadUntilIdle_emptyQueue() { final CountDownLatch latch = new CountDownLatch(1); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { uiController.get().loopMainThreadUntilIdle(); latch.countDown(); } })); try { assertTrue("Never returned from UiControllerImpl.loopMainThreadUntilIdle();", latch.await(10, TimeUnit.SECONDS)); } catch (InterruptedException e) { fail("Failed with exception " + e); } } public void testLoopMainThreadUntilIdle_oneIdlingResource() throws InterruptedException { OnDemandIdlingResource fakeResource = new OnDemandIdlingResource("FakeResource"); idlingResourceRegistry.register(fakeResource); final CountDownLatch latch = new CountDownLatch(1); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { Log.i(TAG, "Hijacking thread and looping it."); uiController.get().loopMainThreadUntilIdle(); latch.countDown(); } })); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(2, TimeUnit.SECONDS)); fakeResource.forceIdleNow(); assertTrue("App should be idle.", latch.await(5, TimeUnit.SECONDS)); } public void testLoopMainThreadUntilIdle_multipleIdlingResources() throws InterruptedException { OnDemandIdlingResource fakeResource1 = new OnDemandIdlingResource("FakeResource1"); OnDemandIdlingResource fakeResource2 = new OnDemandIdlingResource("FakeResource2"); OnDemandIdlingResource fakeResource3 = new OnDemandIdlingResource("FakeResource3"); // Register the first two right away and one later (once the wait for the first two begins). idlingResourceRegistry.register(fakeResource1); idlingResourceRegistry.register(fakeResource2); final CountDownLatch latch = new CountDownLatch(1); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { Log.i(TAG, "Hijacking thread and looping it."); uiController.get().loopMainThreadUntilIdle(); latch.countDown(); } })); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS)); fakeResource1.forceIdleNow(); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS)); idlingResourceRegistry.register(fakeResource3); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS)); fakeResource2.forceIdleNow(); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS)); fakeResource3.forceIdleNow(); assertTrue("App should be idle.", latch.await(5, TimeUnit.SECONDS)); } @LargeTest public void testLoopMainThreadUntilIdle_timeout() throws InterruptedException { OnDemandIdlingResource goodResource = new OnDemandIdlingResource("GoodResource"); OnDemandIdlingResource kindaCrappyResource = new OnDemandIdlingResource("KindaCrappyResource"); OnDemandIdlingResource badResource = new OnDemandIdlingResource("VeryBadResource"); idlingResourceRegistry.register(goodResource); idlingResourceRegistry.register(kindaCrappyResource); idlingResourceRegistry.register(badResource); final CountDownLatch latch = new CountDownLatch(1); assertTrue(testThread.getHandler().post(new Runnable() { @Override public void run() { Log.i(TAG, "Hijacking thread and looping it."); try { uiController.get().loopMainThreadUntilIdle(); } catch (IdlingResourceTimeoutException e) { latch.countDown(); } } })); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(4, TimeUnit.SECONDS)); goodResource.forceIdleNow(); assertFalse( "Should not have stopped looping the main thread yet!", latch.await(12, TimeUnit.SECONDS)); kindaCrappyResource.forceIdleNow(); assertTrue( "Should have caught IdlingResourceTimeoutException", latch.await(11, TimeUnit.SECONDS)); } }