Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3dfe492a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Integrate new looper apis into testables"

parents 9817060b f715f417
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -20,7 +20,9 @@ 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;
@@ -31,10 +33,13 @@ 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;
@@ -47,7 +52,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase {

    @Before
    public void setup() throws Exception {
        mTestableLooper = new TestableLooper();
        mTestableLooper = TestableLooper.get(this);
        mMockBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager.class);
        mDevices = new ArrayList<>();
        mMockDeviceManager = mock(CachedBluetoothDeviceManager.class);
+33 −15
Original line number Diff line number Diff line
@@ -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.LooperStatement;
import android.testing.TestableLooper.LooperFrameworkMethod;
import android.testing.TestableLooper.RunWithLooper;

import org.junit.After;
@@ -30,6 +30,7 @@ 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;

/**
@@ -49,28 +50,21 @@ public class AndroidTestingRunner extends BlockJUnit4ClassRunner {

    @Override
    protected Statement methodInvoker(FrameworkMethod method, Object test) {
        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);
        method = looperWrap(method, test, method);
        final Statement statement = super.methodInvoker(method, test);
        return shouldRunOnUiThread(method) ? new UiThreadStatement(statement, true) : statement;
    }

    protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) {
        List befores = this.getTestClass().getAnnotatedMethods(Before.class);
        List befores = looperWrap(method, target,
                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 = this.getTestClass().getAnnotatedMethods(After.class);
        List afters = looperWrap(method, target,
                this.getTestClass().getAnnotatedMethods(After.class));
        return afters.isEmpty() ? statement : new RunAfters(method, statement, afters,
                target);
    }
@@ -88,6 +82,30 @@ 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;
+143 −72
Original line number Diff line number Diff line
@@ -15,20 +15,21 @@
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.Statement;
import org.junit.runners.model.FrameworkMethod;

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;

/**
@@ -38,65 +39,35 @@ 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() throws Exception {
        this(true);
    }

    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 TestableLooper(Looper l) throws Exception {
        this(InstrumentationRegistry.getInstrumentation().acquireLooperManager(l), l);
    }

    public Looper getLooper() {
        return mLooper;
    private TestableLooper(TestLooperManager wrapper, Looper l) throws Exception {
        mQueueWrapper = wrapper;
        setupQueue(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 TestableLooper(Looper looper, boolean b) throws Exception {
        setupQueue(looper);
    }

    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);
    public Looper getLooper() {
        return mLooper;
    }

    private void setupQueue(Looper l) throws Exception {
        mLooper = l;
        mQueue = mLooper.getQueue();
        mHandler = new Handler(mLooper);
    }
@@ -121,9 +92,7 @@ public class TestableLooper {
     * tests.
     */
    public void destroy() throws NoSuchFieldException, IllegalAccessException {
        if (Looper.myLooper() == mLooper) {
            clearLooper();
        }
        mQueueWrapper.release();
        if (mMain && mOriginalMain != null) {
            Field field = mLooper.getClass().getDeclaredField("sMainLooper");
            field.setAccessible(true);
@@ -164,26 +133,26 @@ public class TestableLooper {

    private boolean parseMessageInt() {
        try {
            Message result = (Message) mNext.invoke(mQueue);
            Message result = mQueueWrapper.next();
            if (result != null) {
                // This is a break message.
                if (result == mEmptyMessage) {
                    mRecycleUnchecked.invoke(result);
                    mQueueWrapper.recycle(result);
                    return false;
                }

                if (mMessageHandler != null) {
                    if (mMessageHandler.onMessageHandled(result)) {
                        result.getTarget().dispatchMessage(result);
                        mRecycleUnchecked.invoke(result);
                        mQueueWrapper.recycle(result);
                    } else {
                        mRecycleUnchecked.invoke(result);
                        mQueueWrapper.recycle(result);
                        // Message handler indicated it doesn't want us to continue.
                        return false;
                    }
                } else {
                    result.getTarget().dispatchMessage(result);
                    mRecycleUnchecked.invoke(result);
                    mQueueWrapper.recycle(result);
                }
            } else {
                // No messages, don't continue parsing
@@ -199,10 +168,14 @@ public class TestableLooper {
     * Runs an executable with myLooper set and processes all messages added.
     */
    public void runWithLooper(RunnableWithException runnable) throws Exception {
        boolean set = setForCurrentThread();
        new Handler(getLooper()).post(() -> {
            try {
                runnable.run();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        processAllMessages();
        if (set) clearLooper();
    }

    public interface RunnableWithException {
@@ -221,33 +194,131 @@ public class TestableLooper {
        return sLoopers.get(test);
    }

    public static class LooperStatement extends Statement {
        private final boolean mSetAsMain;
        private final Statement mBase;
        private final TestableLooper mLooper;
    public static class LooperFrameworkMethod extends FrameworkMethod {
        private HandlerThread mHandlerThread;

        public LooperStatement(Statement base, boolean setAsMain, Object test) {
            mBase = base;
        private final TestableLooper mTestableLooper;
        private final Looper mLooper;
        private final Handler mHandler;

        public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
            super(base.getMethod());
            try {
                mLooper = new TestableLooper(false);
                sLoopers.put(test, mLooper);
                mSetAsMain = setAsMain;
                mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
                mTestableLooper = new TestableLooper(mLooper, false);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            sLoopers.put(test, mTestableLooper);
            mHandler = new Handler(mLooper);
        }

        @Override
        public void evaluate() throws Throwable {
            mLooper.setForCurrentThread();
            if (mSetAsMain) {
                mLooper.setAsMainLooper();
        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);
            }
            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 {
                mBase.evaluate();
                        mTestableLooper.mQueueWrapper.execute(m);
                    } catch (LooperException e) {
                        throw e.getSource();
                    } finally {
                mLooper.destroy();
                        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();
                    }
                } 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;
            }
        }
    }
+19 −41
Original line number Diff line number Diff line
@@ -24,17 +24,16 @@ 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 {
@@ -46,11 +45,6 @@ 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();
@@ -133,39 +127,23 @@ 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);
        mTestableLooper.processAllMessages();
            testableLooper.processAllMessages();

            verify(r).run();
        mTestableLooper.destroy();

        assertEquals(originalMain, Looper.getMainLooper());
            verify(r2, never()).run();
        } finally {
            testableLooper.destroy();
        }

    @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