Merge "Add better method test handlers/loopers in sysui"

This commit is contained in:
Jason Monk
2017-02-09 15:43:15 +00:00
committed by Android (Google) Code Review
4 changed files with 537 additions and 1 deletions

View File

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

View File

@@ -0,0 +1,87 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,260 @@
/*
* 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);
}
}

View File

@@ -0,0 +1,184 @@
/*
* 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());
}
}