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

Commit e7507484 authored by Jason Monk's avatar Jason Monk
Browse files

Add better method test handlers/loopers in sysui

Test: runtest systemui
Change-Id: I94014afab12d385f38a9d1d4a8595bb37cfe0688
parent 2a123242
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -22,8 +22,13 @@ import static org.mockito.Mockito.verify;

import android.os.Looper;

import com.android.systemui.ConfigurationChangedReceiver;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.policy.FlashlightController;

import org.junit.Assert;
import org.junit.Test;

import java.io.PrintWriter;
@@ -34,7 +39,7 @@ public class DependencyTest extends SysuiTestCase {
    public void testClassDependency() {
        FlashlightController f = mock(FlashlightController.class);
        injectTestDependency(FlashlightController.class, f);
        assertEquals(f, Dependency.get(FlashlightController.class));
        Assert.assertEquals(f, Dependency.get(FlashlightController.class));
    }

    @Test
+87 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui;

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 com.android.systemui.utils.TestableLooper.LooperStatement;
import com.android.systemui.utils.TestableLooper.RunWithLooper;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.runners.statements.FailOnTimeout;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

import java.util.List;

public class SysUIRunner extends BlockJUnit4ClassRunner {

    private final long mTimeout;
    private final Class<?> mKlass;

    public SysUIRunner(Class<?> klass) throws InitializationError {
        super(klass);
        mKlass = klass;
        // Can't seem to get reference to timeout parameter from here, so set default to 10 mins.
        mTimeout = 10 * 60 * 1000;
    }

    @Override
    protected Statement methodInvoker(FrameworkMethod method, Object test) {
        return UiThreadStatement.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 = 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);
        return afters.isEmpty() ? statement : new RunAfters(method, statement, afters,
                target);
    }

    protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) {
        long timeout = this.getTimeout(method.getAnnotation(Test.class));
        if (timeout <= 0L && mTimeout > 0L) {
            timeout = mTimeout;
        }

        return timeout <= 0L ? next : new FailOnTimeout(next, timeout);
    }

    private long getTimeout(Test annotation) {
        return annotation == null ? 0L : annotation.timeout();
    }
}
+260 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui.utils;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.util.ArrayMap;

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;

/**
 * Creates a looper on the current thread with control over if/when messages are
 * executed. Warning: This class works through some reflection and may break/need
 * to be updated from time to time.
 */
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;

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

    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);
    }

    public void setAsMainLooper() throws NoSuchFieldException, IllegalAccessException {
        mMain = true;
        setAsMainInt();
    }

    private void setAsMainInt() throws NoSuchFieldException, IllegalAccessException {
        Field field = mLooper.getClass().getDeclaredField("sMainLooper");
        field.setAccessible(true);
        if (mOriginalMain == null) {
            mOriginalMain = field.get(null);
        }
        field.set(null, mLooper);
    }

    /**
     * Must be called if setAsMainLooper is called to restore the main looper when the
     * test is complete, otherwise the main looper will not be available for any subsequent
     * tests.
     */
    public void destroy() throws NoSuchFieldException, IllegalAccessException {
        if (Looper.myLooper() == mLooper) {
            clearLooper();
        }
        if (mMain && mOriginalMain != null) {
            Field field = mLooper.getClass().getDeclaredField("sMainLooper");
            field.setAccessible(true);
            field.set(null, mOriginalMain);
            mOriginalMain = null;
        }
    }

    public void setMessageHandler(MessageHandler handler) {
        mMessageHandler = handler;
    }

    /**
     * Parse num messages from the message queue.
     *
     * @param num Number of messages to parse
     */
    public int processMessages(int num) {
        for (int i = 0; i < num; i++) {
            if (!parseMessageInt()) {
                return i + 1;
            }
        }
        return num;
    }

    public void processAllMessages() {
        while (processQueuedMessages() != 0) ;
    }

    private int processQueuedMessages() {
        int count = 0;
        mEmptyMessage = mHandler.obtainMessage(1);
        mHandler.sendMessageDelayed(mEmptyMessage, 1);
        while (parseMessageInt()) count++;
        return count;
    }

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

                if (mMessageHandler != null) {
                    if (mMessageHandler.onMessageHandled(result)) {
                        result.getTarget().dispatchMessage(result);
                        mRecycleUnchecked.invoke(result);
                    } else {
                        mRecycleUnchecked.invoke(result);
                        // Message handler indicated it doesn't want us to continue.
                        return false;
                    }
                } else {
                    result.getTarget().dispatchMessage(result);
                    mRecycleUnchecked.invoke(result);
                }
            } else {
                // No messages, don't continue parsing
                return false;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return true;
    }

    /**
     * Runs an executable with myLooper set and processes all messages added.
     */
    public void runWithLooper(RunnableWithException runnable) throws Exception {
        boolean set = setForCurrentThread();
        runnable.run();
        processAllMessages();
        if (set) clearLooper();
    }

    public interface RunnableWithException {
        void run() throws Exception;
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.TYPE})
    public @interface RunWithLooper {
        boolean setAsMainLooper() default false;
    }

    private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();

    public static TestableLooper get(Object test) {
        return sLoopers.get(test);
    }

    public static class LooperStatement extends Statement {
        private final boolean mSetAsMain;
        private final Statement mBase;
        private final TestableLooper mLooper;

        public LooperStatement(Statement base, boolean setAsMain, Object test) {
            mBase = base;
            try {
                mLooper = new TestableLooper(false);
                sLoopers.put(test, mLooper);
                mSetAsMain = setAsMain;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void evaluate() throws Throwable {
            mLooper.setForCurrentThread();
            if (mSetAsMain) {
                mLooper.setAsMainLooper();
            }

            mBase.evaluate();

            mLooper.destroy();
        }
    }

    public interface MessageHandler {
        /**
         * Return true to have the message executed and delivered to target.
         * Return false to not execute the message and stop executing messages.
         */
        boolean onMessageHandled(Message m);
    }
}
+184 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui.utils;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import com.android.systemui.SysUIRunner;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.utils.TestableLooper.MessageHandler;
import com.android.systemui.utils.TestableLooper.RunWithLooper;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(SysUIRunner.class)
@RunWithLooper
public class TestableLooperTest extends SysuiTestCase {

    private TestableLooper mTestableLooper;

    @Before
    public void setup() throws Exception {
        mTestableLooper = TestableLooper.get(this);
    }

    @After
    public void tearDown() throws Exception {
        mTestableLooper.destroy();
    }

    @Test
    public void testMessageExecuted() throws Exception {
        Handler h = new Handler();
        Runnable r = mock(Runnable.class);
        h.post(r);
        verify(r, never()).run();
        mTestableLooper.processAllMessages();
        verify(r).run();
    }

    @Test
    public void testMessageCallback() throws Exception {
        Handler h = new Handler();
        Message m = h.obtainMessage(3);
        Runnable r = mock(Runnable.class);
        MessageHandler messageHandler = mock(MessageHandler.class);
        when(messageHandler.onMessageHandled(any())).thenReturn(false);
        mTestableLooper.setMessageHandler(messageHandler);

        m.sendToTarget();
        h.post(r);

        mTestableLooper.processAllMessages();

        verify(messageHandler).onMessageHandled(eq(m));
        // This should never be run becaus the mock returns false on the first message, and
        // the second will get skipped.
        verify(r, never()).run();
    }

    @Test
    public void testProcessNumberOfMessages() throws Exception {
        Handler h = new Handler();
        Runnable r = mock(Runnable.class);
        h.post(r);
        h.post(r);
        h.post(r);

        mTestableLooper.processMessages(2);

        verify(r, times(2)).run();
    }

    @Test
    public void testProcessAllMessages() throws Exception {
        Handler h = new Handler();
        Runnable r = mock(Runnable.class);
        Runnable poster = () -> h.post(r);
        h.post(poster);

        mTestableLooper.processAllMessages();
        verify(r).run();
    }

    @Test
    public void test3Chain() throws Exception {
        Handler h = new Handler();
        Runnable r = mock(Runnable.class);
        Runnable poster = () -> h.post(r);
        Runnable poster2 = () -> h.post(poster);
        h.post(poster2);

        mTestableLooper.processAllMessages();
        verify(r).run();
    }

    @Test
    public void testProcessAllMessages_2Messages() throws Exception {
        Handler h = new Handler();
        Runnable r = mock(Runnable.class);
        Runnable r2 = mock(Runnable.class);
        h.post(r);
        h.post(r2);

        mTestableLooper.processAllMessages();
        verify(r).run();
        verify(r2).run();
    }

    @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);

        new Handler(Looper.getMainLooper()).post(r);
        mTestableLooper.processAllMessages();

        verify(r).run();
        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
    public void testNonMainLooperAnnotation() {
        assertNotEquals(Looper.myLooper(), Looper.getMainLooper());
    }

    @Test
    @RunWithLooper(setAsMainLooper = true)
    public void testMainLooperAnnotation() {
        assertEquals(Looper.myLooper(), Looper.getMainLooper());
    }
}