Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +1 −6 Original line number Diff line number Diff line Loading @@ -20,9 +20,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothProfile; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; Loading @@ -33,13 +31,10 @@ import com.android.systemui.SysuiTestCase; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; @RunWith(AndroidTestingRunner.class) @RunWithLooper public class BluetoothControllerImplTest extends SysuiTestCase { private LocalBluetoothManager mMockBluetoothManager; Loading @@ -52,7 +47,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { @Before public void setup() throws Exception { mTestableLooper = TestableLooper.get(this); mTestableLooper = new TestableLooper(); mMockBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager.class); mDevices = new ArrayList<>(); mMockDeviceManager = mock(CachedBluetoothDeviceManager.class); Loading tests/testables/src/android/testing/AndroidTestingRunner.java +15 −33 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ import android.support.test.internal.runner.junit4.statement.RunAfters; import android.support.test.internal.runner.junit4.statement.RunBefores; import android.support.test.internal.runner.junit4.statement.UiThreadStatement; import android.testing.TestableLooper.LooperFrameworkMethod; import android.testing.TestableLooper.LooperStatement; import android.testing.TestableLooper.RunWithLooper; import org.junit.After; Loading @@ -30,7 +30,6 @@ import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import java.util.ArrayList; import java.util.List; /** Loading @@ -50,21 +49,28 @@ public class AndroidTestingRunner extends BlockJUnit4ClassRunner { @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { method = looperWrap(method, test, method); final Statement statement = super.methodInvoker(method, test); return shouldRunOnUiThread(method) ? new UiThreadStatement(statement, true) : statement; return shouldRunOnUiThread(method) ? new UiThreadStatement( methodInvokerInt(method, test), true) : methodInvokerInt(method, test); } protected Statement methodInvokerInt(FrameworkMethod method, Object test) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = mKlass.getAnnotation(RunWithLooper.class); if (annotation != null) { return new LooperStatement(super.methodInvoker(method, test), annotation.setAsMainLooper(), test); } return super.methodInvoker(method, test); } protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { List befores = looperWrap(method, target, this.getTestClass().getAnnotatedMethods(Before.class)); List befores = this.getTestClass().getAnnotatedMethods(Before.class); return befores.isEmpty() ? statement : new RunBefores(method, statement, befores, target); } protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) { List afters = looperWrap(method, target, this.getTestClass().getAnnotatedMethods(After.class)); List afters = this.getTestClass().getAnnotatedMethods(After.class); return afters.isEmpty() ? statement : new RunAfters(method, statement, afters, target); } Loading @@ -82,30 +88,6 @@ public class AndroidTestingRunner extends BlockJUnit4ClassRunner { return annotation == null ? 0L : annotation.timeout(); } protected List<FrameworkMethod> looperWrap(FrameworkMethod method, Object test, List<FrameworkMethod> methods) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = mKlass.getAnnotation(RunWithLooper.class); if (annotation != null) { methods = new ArrayList<>(methods); for (int i = 0; i < methods.size(); i++) { methods.set(i, LooperFrameworkMethod.get(methods.get(i), annotation.setAsMainLooper(), test)); } } return methods; } protected FrameworkMethod looperWrap(FrameworkMethod method, Object test, FrameworkMethod base) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = mKlass.getAnnotation(RunWithLooper.class); if (annotation != null) { return LooperFrameworkMethod.get(base, annotation.setAsMainLooper(), test); } return base; } public boolean shouldRunOnUiThread(FrameworkMethod method) { if (mKlass.getAnnotation(UiThreadTest.class) != null) { return true; Loading tests/testables/src/android/testing/TestableLooper.java +72 −143 Original line number Diff line number Diff line Loading @@ -15,21 +15,20 @@ package android.testing; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.TestLooperManager; import android.support.test.InstrumentationRegistry; import android.util.ArrayMap; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; /** Loading @@ -39,35 +38,65 @@ import java.util.Map; */ public class TestableLooper { private final Method mNext; private final Method mRecycleUnchecked; private Looper mLooper; private MessageQueue mQueue; private boolean mMain; private Object mOriginalMain; private MessageHandler mMessageHandler; private int mParsedCount; private Handler mHandler; private Message mEmptyMessage; private TestLooperManager mQueueWrapper; public TestableLooper(Looper l) throws Exception { this(InstrumentationRegistry.getInstrumentation().acquireLooperManager(l), l); } private TestableLooper(TestLooperManager wrapper, Looper l) throws Exception { mQueueWrapper = wrapper; setupQueue(l); public TestableLooper() throws Exception { this(true); } private TestableLooper(Looper looper, boolean b) throws Exception { setupQueue(looper); public TestableLooper(boolean setMyLooper) throws Exception { setupQueue(setMyLooper); mNext = mQueue.getClass().getDeclaredMethod("next"); mNext.setAccessible(true); mRecycleUnchecked = Message.class.getDeclaredMethod("recycleUnchecked"); mRecycleUnchecked.setAccessible(true); } public Looper getLooper() { return mLooper; } private void setupQueue(Looper l) throws Exception { mLooper = l; private void clearLooper() throws NoSuchFieldException, IllegalAccessException { Field field = Looper.class.getDeclaredField("sThreadLocal"); field.setAccessible(true); ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null); sThreadLocal.set(null); } private boolean setForCurrentThread() throws NoSuchFieldException, IllegalAccessException { if (Looper.myLooper() != mLooper) { Field field = Looper.class.getDeclaredField("sThreadLocal"); field.setAccessible(true); ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null); sThreadLocal.set(mLooper); return true; } return false; } private void setupQueue(boolean setMyLooper) throws Exception { if (setMyLooper) { clearLooper(); Looper.prepare(); mLooper = Looper.myLooper(); } else { Constructor<Looper> constructor = Looper.class.getDeclaredConstructor( boolean.class); constructor.setAccessible(true); mLooper = constructor.newInstance(true); } mQueue = mLooper.getQueue(); mHandler = new Handler(mLooper); } Loading @@ -92,7 +121,9 @@ public class TestableLooper { * tests. */ public void destroy() throws NoSuchFieldException, IllegalAccessException { mQueueWrapper.release(); if (Looper.myLooper() == mLooper) { clearLooper(); } if (mMain && mOriginalMain != null) { Field field = mLooper.getClass().getDeclaredField("sMainLooper"); field.setAccessible(true); Loading Loading @@ -133,26 +164,26 @@ public class TestableLooper { private boolean parseMessageInt() { try { Message result = mQueueWrapper.next(); Message result = (Message) mNext.invoke(mQueue); if (result != null) { // This is a break message. if (result == mEmptyMessage) { mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); return false; } if (mMessageHandler != null) { if (mMessageHandler.onMessageHandled(result)) { result.getTarget().dispatchMessage(result); mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); } else { mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); // Message handler indicated it doesn't want us to continue. return false; } } else { result.getTarget().dispatchMessage(result); mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); } } else { // No messages, don't continue parsing Loading @@ -168,14 +199,10 @@ public class TestableLooper { * Runs an executable with myLooper set and processes all messages added. */ public void runWithLooper(RunnableWithException runnable) throws Exception { new Handler(getLooper()).post(() -> { try { boolean set = setForCurrentThread(); runnable.run(); } catch (Exception e) { throw new RuntimeException(e); } }); processAllMessages(); if (set) clearLooper(); } public interface RunnableWithException { Loading @@ -194,131 +221,33 @@ public class TestableLooper { return sLoopers.get(test); } public static class LooperFrameworkMethod extends FrameworkMethod { private HandlerThread mHandlerThread; public static class LooperStatement extends Statement { private final boolean mSetAsMain; private final Statement mBase; private final TestableLooper mLooper; private final TestableLooper mTestableLooper; private final Looper mLooper; private final Handler mHandler; public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) { super(base.getMethod()); public LooperStatement(Statement base, boolean setAsMain, Object test) { mBase = base; try { mLooper = setAsMain ? Looper.getMainLooper() : createLooper(); mTestableLooper = new TestableLooper(mLooper, false); mLooper = new TestableLooper(false); sLoopers.put(test, mLooper); mSetAsMain = setAsMain; } catch (Exception e) { throw new RuntimeException(e); } sLoopers.put(test, mTestableLooper); mHandler = new Handler(mLooper); } public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) { super(base.getMethod()); mLooper = other.mLooper; mTestableLooper = other; mHandler = new Handler(mLooper); } public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) { if (sLoopers.containsKey(test)) { return new LooperFrameworkMethod(sLoopers.get(test), base); } return new LooperFrameworkMethod(base, setAsMain, test); } @Override public Object invokeExplosively(Object target, Object... params) throws Throwable { if (Looper.myLooper() == mLooper) { // Already on the right thread from another statement, just execute then. return super.invokeExplosively(target, params); public void evaluate() throws Throwable { mLooper.setForCurrentThread(); if (mSetAsMain) { mLooper.setAsMainLooper(); } boolean set = mTestableLooper.mQueueWrapper == null; if (set) { mTestableLooper.mQueueWrapper = InstrumentationRegistry.getInstrumentation() .acquireLooperManager(mLooper); } try { Object[] ret = new Object[1]; // Run the execution on the looper thread. Runnable execute = () -> { try { ret[0] = super.invokeExplosively(target, params); } catch (Throwable throwable) { throw new LooperException(throwable); } }; mHandler.post(execute); // Try to wait for the message to be queued. for (int i = 0; i < 10; i++) { if (!mTestableLooper.mQueueWrapper.hasMessages(mHandler, null, execute)) { Thread.sleep(1); } } if (!mTestableLooper.mQueueWrapper.hasMessages(mHandler, null, execute)) { throw new RuntimeException("Message didn't queue..."); } Message m = mTestableLooper.mQueueWrapper.next(); // Parse all other messages until we get to ours. while (m.getTarget() != mHandler) { try { mTestableLooper.mQueueWrapper.execute(m); } catch (LooperException e) { throw e.getSource(); } finally { mTestableLooper.mQueueWrapper.recycle(m); } m = mTestableLooper.mQueueWrapper.next(); } // Dispatch our message. try { mTestableLooper.mQueueWrapper.execute(m); } catch (LooperException e) { throw e.getSource(); } catch (RuntimeException re) { // If the TestLooperManager has to post, it will wrap what it throws in a // RuntimeException, make sure we grab the actual source. if (re.getCause() instanceof LooperException) { throw ((LooperException) re.getCause()).getSource(); } else { throw re.getCause(); } mBase.evaluate(); } finally { mTestableLooper.mQueueWrapper.recycle(m); } return ret[0]; } finally { if (set) { mTestableLooper.mQueueWrapper.release(); mTestableLooper.mQueueWrapper = null; } } } private Looper createLooper() { // TODO: Find way to share these. mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); mHandlerThread.start(); return mHandlerThread.getLooper(); } @Override protected void finalize() throws Throwable { super.finalize(); if (mHandlerThread != null) { mHandlerThread.quit(); } } private static class LooperException extends RuntimeException { private final Throwable mSource; public LooperException(Throwable t) { mSource = t; } public Throwable getSource() { return mSource; mLooper.destroy(); } } } Loading tests/testables/tests/src/android/testing/TestableLooperTest.java +41 −19 Original line number Diff line number Diff line Loading @@ -24,16 +24,17 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.testing.TestableLooper.MessageHandler; import android.testing.TestableLooper.RunWithLooper; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidTestingRunner.class) @RunWithLooper public class TestableLooperTest { Loading @@ -45,6 +46,11 @@ public class TestableLooperTest { mTestableLooper = TestableLooper.get(this); } @After public void tearDown() throws Exception { mTestableLooper.destroy(); } @Test public void testMessageExecuted() throws Exception { Handler h = new Handler(); Loading Loading @@ -127,23 +133,39 @@ public class TestableLooperTest { @Test public void testMainLooper() throws Exception { assertNotEquals(Looper.myLooper(), Looper.getMainLooper()); Looper originalMain = Looper.getMainLooper(); mTestableLooper.setAsMainLooper(); assertEquals(Looper.myLooper(), Looper.getMainLooper()); Runnable r = mock(Runnable.class); Runnable r2 = mock(Runnable.class); TestableLooper testableLooper = new TestableLooper(Looper.getMainLooper()); try { testableLooper.setMessageHandler(m -> { if (m.getCallback() == r) return true; return false; }); new Handler(Looper.getMainLooper()).post(r); testableLooper.processAllMessages(); mTestableLooper.processAllMessages(); verify(r).run(); verify(r2, never()).run(); } finally { testableLooper.destroy(); mTestableLooper.destroy(); assertEquals(originalMain, Looper.getMainLooper()); } @Test public void testNotMyLooper() throws Exception { TestableLooper looper = new TestableLooper(false); assertEquals(Looper.myLooper(), mTestableLooper.getLooper()); assertNotEquals(Looper.myLooper(), looper.getLooper()); Runnable r = mock(Runnable.class); Runnable r2 = mock(Runnable.class); new Handler().post(r); new Handler(looper.getLooper()).post(r2); looper.processAllMessages(); verify(r2).run(); verify(r, never()).run(); mTestableLooper.processAllMessages(); verify(r).run(); } @Test Loading Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +1 −6 Original line number Diff line number Diff line Loading @@ -20,9 +20,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothProfile; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; Loading @@ -33,13 +31,10 @@ import com.android.systemui.SysuiTestCase; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; @RunWith(AndroidTestingRunner.class) @RunWithLooper public class BluetoothControllerImplTest extends SysuiTestCase { private LocalBluetoothManager mMockBluetoothManager; Loading @@ -52,7 +47,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { @Before public void setup() throws Exception { mTestableLooper = TestableLooper.get(this); mTestableLooper = new TestableLooper(); mMockBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager.class); mDevices = new ArrayList<>(); mMockDeviceManager = mock(CachedBluetoothDeviceManager.class); Loading
tests/testables/src/android/testing/AndroidTestingRunner.java +15 −33 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ import android.support.test.internal.runner.junit4.statement.RunAfters; import android.support.test.internal.runner.junit4.statement.RunBefores; import android.support.test.internal.runner.junit4.statement.UiThreadStatement; import android.testing.TestableLooper.LooperFrameworkMethod; import android.testing.TestableLooper.LooperStatement; import android.testing.TestableLooper.RunWithLooper; import org.junit.After; Loading @@ -30,7 +30,6 @@ import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import java.util.ArrayList; import java.util.List; /** Loading @@ -50,21 +49,28 @@ public class AndroidTestingRunner extends BlockJUnit4ClassRunner { @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { method = looperWrap(method, test, method); final Statement statement = super.methodInvoker(method, test); return shouldRunOnUiThread(method) ? new UiThreadStatement(statement, true) : statement; return shouldRunOnUiThread(method) ? new UiThreadStatement( methodInvokerInt(method, test), true) : methodInvokerInt(method, test); } protected Statement methodInvokerInt(FrameworkMethod method, Object test) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = mKlass.getAnnotation(RunWithLooper.class); if (annotation != null) { return new LooperStatement(super.methodInvoker(method, test), annotation.setAsMainLooper(), test); } return super.methodInvoker(method, test); } protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { List befores = looperWrap(method, target, this.getTestClass().getAnnotatedMethods(Before.class)); List befores = this.getTestClass().getAnnotatedMethods(Before.class); return befores.isEmpty() ? statement : new RunBefores(method, statement, befores, target); } protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) { List afters = looperWrap(method, target, this.getTestClass().getAnnotatedMethods(After.class)); List afters = this.getTestClass().getAnnotatedMethods(After.class); return afters.isEmpty() ? statement : new RunAfters(method, statement, afters, target); } Loading @@ -82,30 +88,6 @@ public class AndroidTestingRunner extends BlockJUnit4ClassRunner { return annotation == null ? 0L : annotation.timeout(); } protected List<FrameworkMethod> looperWrap(FrameworkMethod method, Object test, List<FrameworkMethod> methods) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = mKlass.getAnnotation(RunWithLooper.class); if (annotation != null) { methods = new ArrayList<>(methods); for (int i = 0; i < methods.size(); i++) { methods.set(i, LooperFrameworkMethod.get(methods.get(i), annotation.setAsMainLooper(), test)); } } return methods; } protected FrameworkMethod looperWrap(FrameworkMethod method, Object test, FrameworkMethod base) { RunWithLooper annotation = method.getAnnotation(RunWithLooper.class); if (annotation == null) annotation = mKlass.getAnnotation(RunWithLooper.class); if (annotation != null) { return LooperFrameworkMethod.get(base, annotation.setAsMainLooper(), test); } return base; } public boolean shouldRunOnUiThread(FrameworkMethod method) { if (mKlass.getAnnotation(UiThreadTest.class) != null) { return true; Loading
tests/testables/src/android/testing/TestableLooper.java +72 −143 Original line number Diff line number Diff line Loading @@ -15,21 +15,20 @@ package android.testing; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.TestLooperManager; import android.support.test.InstrumentationRegistry; import android.util.ArrayMap; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; /** Loading @@ -39,35 +38,65 @@ import java.util.Map; */ public class TestableLooper { private final Method mNext; private final Method mRecycleUnchecked; private Looper mLooper; private MessageQueue mQueue; private boolean mMain; private Object mOriginalMain; private MessageHandler mMessageHandler; private int mParsedCount; private Handler mHandler; private Message mEmptyMessage; private TestLooperManager mQueueWrapper; public TestableLooper(Looper l) throws Exception { this(InstrumentationRegistry.getInstrumentation().acquireLooperManager(l), l); } private TestableLooper(TestLooperManager wrapper, Looper l) throws Exception { mQueueWrapper = wrapper; setupQueue(l); public TestableLooper() throws Exception { this(true); } private TestableLooper(Looper looper, boolean b) throws Exception { setupQueue(looper); public TestableLooper(boolean setMyLooper) throws Exception { setupQueue(setMyLooper); mNext = mQueue.getClass().getDeclaredMethod("next"); mNext.setAccessible(true); mRecycleUnchecked = Message.class.getDeclaredMethod("recycleUnchecked"); mRecycleUnchecked.setAccessible(true); } public Looper getLooper() { return mLooper; } private void setupQueue(Looper l) throws Exception { mLooper = l; private void clearLooper() throws NoSuchFieldException, IllegalAccessException { Field field = Looper.class.getDeclaredField("sThreadLocal"); field.setAccessible(true); ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null); sThreadLocal.set(null); } private boolean setForCurrentThread() throws NoSuchFieldException, IllegalAccessException { if (Looper.myLooper() != mLooper) { Field field = Looper.class.getDeclaredField("sThreadLocal"); field.setAccessible(true); ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null); sThreadLocal.set(mLooper); return true; } return false; } private void setupQueue(boolean setMyLooper) throws Exception { if (setMyLooper) { clearLooper(); Looper.prepare(); mLooper = Looper.myLooper(); } else { Constructor<Looper> constructor = Looper.class.getDeclaredConstructor( boolean.class); constructor.setAccessible(true); mLooper = constructor.newInstance(true); } mQueue = mLooper.getQueue(); mHandler = new Handler(mLooper); } Loading @@ -92,7 +121,9 @@ public class TestableLooper { * tests. */ public void destroy() throws NoSuchFieldException, IllegalAccessException { mQueueWrapper.release(); if (Looper.myLooper() == mLooper) { clearLooper(); } if (mMain && mOriginalMain != null) { Field field = mLooper.getClass().getDeclaredField("sMainLooper"); field.setAccessible(true); Loading Loading @@ -133,26 +164,26 @@ public class TestableLooper { private boolean parseMessageInt() { try { Message result = mQueueWrapper.next(); Message result = (Message) mNext.invoke(mQueue); if (result != null) { // This is a break message. if (result == mEmptyMessage) { mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); return false; } if (mMessageHandler != null) { if (mMessageHandler.onMessageHandled(result)) { result.getTarget().dispatchMessage(result); mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); } else { mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); // Message handler indicated it doesn't want us to continue. return false; } } else { result.getTarget().dispatchMessage(result); mQueueWrapper.recycle(result); mRecycleUnchecked.invoke(result); } } else { // No messages, don't continue parsing Loading @@ -168,14 +199,10 @@ public class TestableLooper { * Runs an executable with myLooper set and processes all messages added. */ public void runWithLooper(RunnableWithException runnable) throws Exception { new Handler(getLooper()).post(() -> { try { boolean set = setForCurrentThread(); runnable.run(); } catch (Exception e) { throw new RuntimeException(e); } }); processAllMessages(); if (set) clearLooper(); } public interface RunnableWithException { Loading @@ -194,131 +221,33 @@ public class TestableLooper { return sLoopers.get(test); } public static class LooperFrameworkMethod extends FrameworkMethod { private HandlerThread mHandlerThread; public static class LooperStatement extends Statement { private final boolean mSetAsMain; private final Statement mBase; private final TestableLooper mLooper; private final TestableLooper mTestableLooper; private final Looper mLooper; private final Handler mHandler; public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) { super(base.getMethod()); public LooperStatement(Statement base, boolean setAsMain, Object test) { mBase = base; try { mLooper = setAsMain ? Looper.getMainLooper() : createLooper(); mTestableLooper = new TestableLooper(mLooper, false); mLooper = new TestableLooper(false); sLoopers.put(test, mLooper); mSetAsMain = setAsMain; } catch (Exception e) { throw new RuntimeException(e); } sLoopers.put(test, mTestableLooper); mHandler = new Handler(mLooper); } public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) { super(base.getMethod()); mLooper = other.mLooper; mTestableLooper = other; mHandler = new Handler(mLooper); } public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) { if (sLoopers.containsKey(test)) { return new LooperFrameworkMethod(sLoopers.get(test), base); } return new LooperFrameworkMethod(base, setAsMain, test); } @Override public Object invokeExplosively(Object target, Object... params) throws Throwable { if (Looper.myLooper() == mLooper) { // Already on the right thread from another statement, just execute then. return super.invokeExplosively(target, params); public void evaluate() throws Throwable { mLooper.setForCurrentThread(); if (mSetAsMain) { mLooper.setAsMainLooper(); } boolean set = mTestableLooper.mQueueWrapper == null; if (set) { mTestableLooper.mQueueWrapper = InstrumentationRegistry.getInstrumentation() .acquireLooperManager(mLooper); } try { Object[] ret = new Object[1]; // Run the execution on the looper thread. Runnable execute = () -> { try { ret[0] = super.invokeExplosively(target, params); } catch (Throwable throwable) { throw new LooperException(throwable); } }; mHandler.post(execute); // Try to wait for the message to be queued. for (int i = 0; i < 10; i++) { if (!mTestableLooper.mQueueWrapper.hasMessages(mHandler, null, execute)) { Thread.sleep(1); } } if (!mTestableLooper.mQueueWrapper.hasMessages(mHandler, null, execute)) { throw new RuntimeException("Message didn't queue..."); } Message m = mTestableLooper.mQueueWrapper.next(); // Parse all other messages until we get to ours. while (m.getTarget() != mHandler) { try { mTestableLooper.mQueueWrapper.execute(m); } catch (LooperException e) { throw e.getSource(); } finally { mTestableLooper.mQueueWrapper.recycle(m); } m = mTestableLooper.mQueueWrapper.next(); } // Dispatch our message. try { mTestableLooper.mQueueWrapper.execute(m); } catch (LooperException e) { throw e.getSource(); } catch (RuntimeException re) { // If the TestLooperManager has to post, it will wrap what it throws in a // RuntimeException, make sure we grab the actual source. if (re.getCause() instanceof LooperException) { throw ((LooperException) re.getCause()).getSource(); } else { throw re.getCause(); } mBase.evaluate(); } finally { mTestableLooper.mQueueWrapper.recycle(m); } return ret[0]; } finally { if (set) { mTestableLooper.mQueueWrapper.release(); mTestableLooper.mQueueWrapper = null; } } } private Looper createLooper() { // TODO: Find way to share these. mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); mHandlerThread.start(); return mHandlerThread.getLooper(); } @Override protected void finalize() throws Throwable { super.finalize(); if (mHandlerThread != null) { mHandlerThread.quit(); } } private static class LooperException extends RuntimeException { private final Throwable mSource; public LooperException(Throwable t) { mSource = t; } public Throwable getSource() { return mSource; mLooper.destroy(); } } } Loading
tests/testables/tests/src/android/testing/TestableLooperTest.java +41 −19 Original line number Diff line number Diff line Loading @@ -24,16 +24,17 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.testing.TestableLooper.MessageHandler; import android.testing.TestableLooper.RunWithLooper; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidTestingRunner.class) @RunWithLooper public class TestableLooperTest { Loading @@ -45,6 +46,11 @@ public class TestableLooperTest { mTestableLooper = TestableLooper.get(this); } @After public void tearDown() throws Exception { mTestableLooper.destroy(); } @Test public void testMessageExecuted() throws Exception { Handler h = new Handler(); Loading Loading @@ -127,23 +133,39 @@ public class TestableLooperTest { @Test public void testMainLooper() throws Exception { assertNotEquals(Looper.myLooper(), Looper.getMainLooper()); Looper originalMain = Looper.getMainLooper(); mTestableLooper.setAsMainLooper(); assertEquals(Looper.myLooper(), Looper.getMainLooper()); Runnable r = mock(Runnable.class); Runnable r2 = mock(Runnable.class); TestableLooper testableLooper = new TestableLooper(Looper.getMainLooper()); try { testableLooper.setMessageHandler(m -> { if (m.getCallback() == r) return true; return false; }); new Handler(Looper.getMainLooper()).post(r); testableLooper.processAllMessages(); mTestableLooper.processAllMessages(); verify(r).run(); verify(r2, never()).run(); } finally { testableLooper.destroy(); mTestableLooper.destroy(); assertEquals(originalMain, Looper.getMainLooper()); } @Test public void testNotMyLooper() throws Exception { TestableLooper looper = new TestableLooper(false); assertEquals(Looper.myLooper(), mTestableLooper.getLooper()); assertNotEquals(Looper.myLooper(), looper.getLooper()); Runnable r = mock(Runnable.class); Runnable r2 = mock(Runnable.class); new Handler().post(r); new Handler(looper.getLooper()).post(r2); looper.processAllMessages(); verify(r2).run(); verify(r, never()).run(); mTestableLooper.processAllMessages(); verify(r).run(); } @Test Loading