Merge "Add better method test handlers/loopers in sysui"
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user