Merge "Create frameworks-base-testutils lib" into nyc-mr1-dev

This commit is contained in:
Christopher Wiley
2016-06-08 20:55:10 +00:00
committed by Android (Google) Code Review
7 changed files with 1116 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
#
# Copyright (C) 2016 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.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := frameworks-base-testutils
LOCAL_MODULE_TAG := tests
LOCAL_SRC_FILES := $(call all-java-files-under,java)
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-test \
mockito-target
include $(BUILD_STATIC_JAVA_LIBRARY)

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 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 android.app.test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* Utilities for creating Answers for mock objects
*/
public class MockAnswerUtil {
/**
* Answer that calls the method in the Answer called "answer" that matches the type signature of
* the method being answered. An error will be thrown at runtime if the signature does not match
* exactly.
*/
public static class AnswerWithArguments implements Answer<Object> {
@Override
public final Object answer(InvocationOnMock invocation) throws Throwable {
Method method = invocation.getMethod();
try {
Method implementation = getClass().getMethod("answer", method.getParameterTypes());
if (!implementation.getReturnType().equals(method.getReturnType())) {
throw new RuntimeException("Found answer method does not have expected return "
+ "type. Expected: " + method.getReturnType() + ", got "
+ implementation.getReturnType());
}
Object[] args = invocation.getArguments();
try {
return implementation.invoke(this, args);
} catch (IllegalAccessException e) {
throw new RuntimeException("Error invoking answer method", e);
} catch (InvocationTargetException e) {
throw e.getCause();
}
} catch (NoSuchMethodException e) {
throw new RuntimeException("Could not find answer method with the expected args "
+ Arrays.toString(method.getParameterTypes()), e);
}
}
}
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright (C) 2015 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 android.app.test;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import android.app.AlarmManager;
import android.app.test.MockAnswerUtil.AnswerWithArguments;
import android.os.Handler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* Creates an AlarmManager whose alarm dispatch can be controlled
* Currently only supports alarm listeners
*
* Alarm listeners will be dispatched to the handler provided or will
* be dispatched immediately if they would have been sent to the main
* looper (handler was null).
*/
public class TestAlarmManager {
private final AlarmManager mAlarmManager;
private final List<PendingAlarm> mPendingAlarms;
public TestAlarmManager() throws Exception {
mPendingAlarms = new ArrayList<>();
mAlarmManager = mock(AlarmManager.class);
doAnswer(new SetListenerAnswer()).when(mAlarmManager).set(anyInt(), anyLong(), anyString(),
any(AlarmManager.OnAlarmListener.class), any(Handler.class));
doAnswer(new SetListenerAnswer()).when(mAlarmManager).setExact(anyInt(), anyLong(),
anyString(), any(AlarmManager.OnAlarmListener.class), any(Handler.class));
doAnswer(new CancelListenerAnswer())
.when(mAlarmManager).cancel(any(AlarmManager.OnAlarmListener.class));
}
public AlarmManager getAlarmManager() {
return mAlarmManager;
}
/**
* Dispatch a pending alarm with the given tag
* @return if any alarm was dispatched
*/
public boolean dispatch(String tag) {
for (int i = 0; i < mPendingAlarms.size(); ++i) {
PendingAlarm alarm = mPendingAlarms.get(i);
if (Objects.equals(tag, alarm.getTag())) {
mPendingAlarms.remove(i);
alarm.dispatch();
return true;
}
}
return false;
}
/**
* @return if an alarm with the given tag is pending
*/
public boolean isPending(String tag) {
for (int i = 0; i < mPendingAlarms.size(); ++i) {
PendingAlarm alarm = mPendingAlarms.get(i);
if (Objects.equals(tag, alarm.getTag())) {
return true;
}
}
return false;
}
/**
* @return trigger time of an pending alarm with the given tag
* -1 if no pending alarm with the given tag
*/
public long getTriggerTimeMillis(String tag) {
for (int i = 0; i < mPendingAlarms.size(); ++i) {
PendingAlarm alarm = mPendingAlarms.get(i);
if (Objects.equals(tag, alarm.getTag())) {
return alarm.getTriggerTimeMillis();
}
}
return -1;
}
private static class PendingAlarm {
private final int mType;
private final long mTriggerAtMillis;
private final String mTag;
private final Runnable mCallback;
public PendingAlarm(int type, long triggerAtMillis, String tag, Runnable callback) {
mType = type;
mTriggerAtMillis = triggerAtMillis;
mTag = tag;
mCallback = callback;
}
public void dispatch() {
if (mCallback != null) {
mCallback.run();
}
}
public Runnable getCallback() {
return mCallback;
}
public String getTag() {
return mTag;
}
public long getTriggerTimeMillis() {
return mTriggerAtMillis;
}
}
private class SetListenerAnswer extends AnswerWithArguments {
public void answer(int type, long triggerAtMillis, String tag,
AlarmManager.OnAlarmListener listener, Handler handler) {
mPendingAlarms.add(new PendingAlarm(type, triggerAtMillis, tag,
new AlarmListenerRunnable(listener, handler)));
}
}
private class CancelListenerAnswer extends AnswerWithArguments {
public void answer(AlarmManager.OnAlarmListener listener) {
Iterator<PendingAlarm> alarmItr = mPendingAlarms.iterator();
while (alarmItr.hasNext()) {
PendingAlarm alarm = alarmItr.next();
if (alarm.getCallback() instanceof AlarmListenerRunnable) {
AlarmListenerRunnable alarmCallback =
(AlarmListenerRunnable) alarm.getCallback();
if (alarmCallback.getListener() == listener) {
alarmItr.remove();
}
}
}
}
}
private static class AlarmListenerRunnable implements Runnable {
private final AlarmManager.OnAlarmListener mListener;
private final Handler mHandler;
public AlarmListenerRunnable(AlarmManager.OnAlarmListener listener, Handler handler) {
mListener = listener;
mHandler = handler;
}
public AlarmManager.OnAlarmListener getListener() {
return mListener;
}
@Override
public void run() {
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mListener.onAlarm();
}
});
} else { // normally gets dispatched in main looper
mListener.onAlarm();
}
}
}
}

View File

@@ -0,0 +1,283 @@
/*
* Copyright (C) 2015 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 android.os.test;
import static org.junit.Assert.assertTrue;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;
import android.util.Log;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Creates a looper whose message queue can be manipulated
* This allows testing code that uses a looper to dispatch messages in a deterministic manner
* Creating a TestLooper will also install it as the looper for the current thread
*/
public class TestLooper {
protected final Looper mLooper;
private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
private static final Field THREAD_LOCAL_LOOPER_FIELD;
private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
private static final Field MESSAGE_NEXT_FIELD;
private static final Field MESSAGE_WHEN_FIELD;
private static final Method MESSAGE_MARK_IN_USE_METHOD;
private static final String TAG = "TestLooper";
private AutoDispatchThread mAutoDispatchThread;
static {
try {
LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
LOOPER_CONSTRUCTOR.setAccessible(true);
THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
MESSAGE_NEXT_FIELD.setAccessible(true);
MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
MESSAGE_WHEN_FIELD.setAccessible(true);
MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
} catch (NoSuchFieldException | NoSuchMethodException e) {
throw new RuntimeException("Failed to initialize TestLooper", e);
}
}
public TestLooper() {
try {
mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
.get(null);
threadLocalLooper.set(mLooper);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
throw new RuntimeException("Reflection error constructing or accessing looper", e);
}
}
public Looper getLooper() {
return mLooper;
}
private Message getMessageLinkedList() {
try {
MessageQueue queue = mLooper.getQueue();
return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
} catch (IllegalAccessException e) {
throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
e);
}
}
public void moveTimeForward(long milliSeconds) {
try {
Message msg = getMessageLinkedList();
while (msg != null) {
long updatedWhen = msg.getWhen() - milliSeconds;
if (updatedWhen < 0) {
updatedWhen = 0;
}
MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
}
}
private Message messageQueueNext() {
try {
long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = getMessageLinkedList();
if (msg != null && msg.getTarget() == null) {
// Stalled by a barrier. Find the next asynchronous message in
// the queue.
do {
prevMsg = msg;
msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now >= msg.getWhen()) {
// Got a message.
if (prevMsg != null) {
MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
} else {
MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
MESSAGE_NEXT_FIELD.get(msg));
}
MESSAGE_NEXT_FIELD.set(msg, null);
MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
return msg;
}
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Access failed in TestLooper", e);
}
return null;
}
/**
* @return true if there are pending messages in the message queue
*/
public synchronized boolean isIdle() {
Message messageList = getMessageLinkedList();
return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
}
/**
* @return the next message in the Looper's message queue or null if there is none
*/
public synchronized Message nextMessage() {
if (isIdle()) {
return messageQueueNext();
} else {
return null;
}
}
/**
* Dispatch the next message in the queue
* Asserts that there is a message in the queue
*/
public synchronized void dispatchNext() {
assertTrue(isIdle());
Message msg = messageQueueNext();
if (msg == null) {
return;
}
msg.getTarget().dispatchMessage(msg);
}
/**
* Dispatch all messages currently in the queue
* Will not fail if there are no messages pending
* @return the number of messages dispatched
*/
public synchronized int dispatchAll() {
int count = 0;
while (isIdle()) {
dispatchNext();
++count;
}
return count;
}
/**
* Thread used to dispatch messages when the main thread is blocked waiting for a response.
*/
private class AutoDispatchThread extends Thread {
private static final int MAX_LOOPS = 100;
private static final int LOOP_SLEEP_TIME_MS = 10;
private RuntimeException mAutoDispatchException = null;
/**
* Run method for the auto dispatch thread.
* The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
* The thread continues looping and attempting to dispatch all messages until at
* least one message has been dispatched.
*/
@Override
public void run() {
int dispatchCount = 0;
for (int i = 0; i < MAX_LOOPS; i++) {
try {
dispatchCount = dispatchAll();
} catch (RuntimeException e) {
mAutoDispatchException = e;
}
Log.d(TAG, "dispatched " + dispatchCount + " messages");
if (dispatchCount > 0) {
return;
}
try {
Thread.sleep(LOOP_SLEEP_TIME_MS);
} catch (InterruptedException e) {
mAutoDispatchException = new IllegalStateException(
"stopAutoDispatch called before any messages were dispatched.");
return;
}
}
Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
mAutoDispatchException = new IllegalStateException(
"TestLooper did not dispatch any messages before exiting.");
}
/**
* Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
* to the main thread.
*
* @return RuntimeException Exception created by stopping without dispatching a message
*/
public RuntimeException getException() {
return mAutoDispatchException;
}
}
/**
* Create and start a new AutoDispatchThread if one is not already running.
*/
public void startAutoDispatch() {
if (mAutoDispatchThread != null) {
throw new IllegalStateException(
"startAutoDispatch called with the AutoDispatchThread already running.");
}
mAutoDispatchThread = new AutoDispatchThread();
mAutoDispatchThread.start();
}
/**
* If an AutoDispatchThread is currently running, stop and clean up.
*/
public void stopAutoDispatch() {
if (mAutoDispatchThread != null) {
if (mAutoDispatchThread.isAlive()) {
mAutoDispatchThread.interrupt();
}
try {
mAutoDispatchThread.join();
} catch (InterruptedException e) {
// Catch exception from join.
}
RuntimeException e = mAutoDispatchThread.getException();
mAutoDispatchThread = null;
if (e != null) {
throw e;
}
} else {
// stopAutoDispatch was called when startAutoDispatch has not created a new thread.
throw new IllegalStateException(
"stopAutoDispatch called without startAutoDispatch.");
}
}
}

View File

@@ -0,0 +1,371 @@
/*
* Copyright (C) 2016 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 android.os.test;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.test.suitebuilder.annotation.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.MockitoAnnotations;
/**
* Test TestLooperAbstractTime which provides control over "time". Note that
* real-time is being used as well. Therefore small time increments are NOT
* reliable. All tests are in "K" units (i.e. *1000).
*/
@SmallTest
public class TestLooperTest {
private TestLooper mTestLooper;
private Handler mHandler;
private Handler mHandlerSpy;
@Rule
public ErrorCollector collector = new ErrorCollector();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mTestLooper = new TestLooper();
mHandler = new Handler(mTestLooper.getLooper());
mHandlerSpy = spy(mHandler);
}
/**
* Basic test with no time stamps: dispatch 4 messages, check that all 4
* delivered (in correct order).
*/
@Test
public void testNoTimeMovement() {
final int messageA = 1;
final int messageB = 2;
final int messageC = 3;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageC));
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("2: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("3: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("4: messageC", messageC, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
}
/**
* Test message sequence: A, B, C@5K, A@10K. Don't move time.
* <p>
* Expected: only get A, B
*/
@Test
public void testDelayedDispatchNoTimeMove() {
final int messageA = 1;
final int messageB = 2;
final int messageC = 3;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
}
/**
* Test message sequence: A, B, C@5K, A@10K, Advance time by 5K.
* <p>
* Expected: only get A, B, C
*/
@Test
public void testDelayedDispatchAdvanceTimeOnce() {
final int messageA = 1;
final int messageB = 2;
final int messageC = 3;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
mTestLooper.moveTimeForward(5000);
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
}
/**
* Test message sequence: A, B, C@5K, Advance time by 4K, A@1K, B@2K Advance
* time by 1K.
* <p>
* Expected: get A, B, C, A
*/
@Test
public void testDelayedDispatchAdvanceTimeTwice() {
final int messageA = 1;
final int messageB = 2;
final int messageC = 3;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
mTestLooper.moveTimeForward(4000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 1000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
mTestLooper.moveTimeForward(1000);
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("4: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
}
/**
* Test message sequence: A, B, C@5K, Advance time by 4K, A@5K, B@2K Advance
* time by 3K.
* <p>
* Expected: get A, B, C, B
*/
@Test
public void testDelayedDispatchReverseOrder() {
final int messageA = 1;
final int messageB = 2;
final int messageC = 3;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
mTestLooper.moveTimeForward(4000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
mTestLooper.moveTimeForward(3000);
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
}
/**
* Test message sequence: A, B, C@5K, Advance time by 4K, dispatch all,
* A@5K, B@2K Advance time by 3K, dispatch all.
* <p>
* Expected: get A, B after first dispatch; then C, B after second dispatch
*/
@Test
public void testDelayedDispatchAllMultipleTimes() {
final int messageA = 1;
final int messageB = 2;
final int messageC = 3;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
mTestLooper.moveTimeForward(4000);
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
mTestLooper.moveTimeForward(3000);
mTestLooper.dispatchAll();
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
}
/**
* Test AutoDispatch for a single message.
* This test would ideally use the Channel sendMessageSynchronously. At this time, the setup to
* get a working test channel is cumbersome. Until this is fixed, we substitute with a
* sendMessage followed by a blocking call. The main test thread blocks until the test handler
* receives the test message (messageA) and sets a boolean true. Once the boolean is true, the
* main thread will exit the busy wait loop, stop autoDispatch and check the assert.
*
* Enable AutoDispatch, add message, block on message being handled and stop AutoDispatch.
* <p>
* Expected: handleMessage is called for messageA and stopAutoDispatch is called.
*/
@Test
public void testAutoDispatchWithSingleMessage() {
final int mLoopSleepTimeMs = 5;
final int messageA = 1;
TestLooper mockLooper = new TestLooper();
class TestHandler extends Handler {
public volatile boolean handledMessage = false;
TestHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == messageA) {
handledMessage = true;
}
}
}
TestHandler testHandler = new TestHandler(mockLooper.getLooper());
mockLooper.startAutoDispatch();
testHandler.sendMessage(testHandler.obtainMessage(messageA));
while (!testHandler.handledMessage) {
// Block until message is handled
try {
Thread.sleep(mLoopSleepTimeMs);
} catch (InterruptedException e) {
// Interrupted while sleeping.
}
}
mockLooper.stopAutoDispatch();
assertTrue("TestHandler should have received messageA", testHandler.handledMessage);
}
/**
* Test starting AutoDispatch while already running throws IllegalStateException
* Enable AutoDispatch two times in a row.
* <p>
* Expected: catch IllegalStateException on second call.
*/
@Test(expected = IllegalStateException.class)
public void testRepeatedStartAutoDispatchThrowsException() {
mTestLooper.startAutoDispatch();
mTestLooper.startAutoDispatch();
}
/**
* Test stopping AutoDispatch without previously starting throws IllegalStateException
* Stop AutoDispatch
* <p>
* Expected: catch IllegalStateException on second call.
*/
@Test(expected = IllegalStateException.class)
public void testStopAutoDispatchWithoutStartThrowsException() {
mTestLooper.stopAutoDispatch();
}
/**
* Test AutoDispatch exits and does not dispatch a later message.
* Start and stop AutoDispatch then add a message.
* <p>
* Expected: After AutoDispatch is stopped, dispatchAll will return 1.
*/
@Test
public void testAutoDispatchStopsCleanlyWithoutDispatchingAMessage() {
final int messageA = 1;
InOrder inOrder = inOrder(mHandlerSpy);
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
mTestLooper.startAutoDispatch();
try {
mTestLooper.stopAutoDispatch();
} catch (IllegalStateException e) {
// Stopping without a dispatch will throw an exception.
}
mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
assertEquals("One message should be dispatched", 1, mTestLooper.dispatchAll());
}
/**
* Test AutoDispatch throws an exception when no messages are dispatched.
* Start and stop AutoDispatch
* <p>
* Expected: Exception is thrown with the stopAutoDispatch call.
*/
@Test(expected = IllegalStateException.class)
public void testAutoDispatchThrowsExceptionWhenNoMessagesDispatched() {
mTestLooper.startAutoDispatch();
mTestLooper.stopAutoDispatch();
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2016 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.internal.util.test;
import static org.junit.Assert.assertEquals;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import com.android.internal.util.AsyncChannel;
/**
* Provides an AsyncChannel interface that implements the connection initiating half of a
* bidirectional channel as described in {@link com.android.internal.util.AsyncChannel}.
*/
public class BidirectionalAsyncChannel {
private static final String TAG = "BidirectionalAsyncChannel";
private AsyncChannel mChannel;
public enum ChannelState { DISCONNECTED, HALF_CONNECTED, CONNECTED, FAILURE };
private ChannelState mState = ChannelState.DISCONNECTED;
public void assertConnected() {
assertEquals("AsyncChannel was not fully connected", ChannelState.CONNECTED, mState);
}
public void connect(final Looper looper, final Messenger messenger,
final Handler incomingMessageHandler) {
assertEquals("AsyncChannel must be disconnected to connect",
ChannelState.DISCONNECTED, mState);
mChannel = new AsyncChannel();
Handler rawMessageHandler = new Handler(looper) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
Log.d(TAG, "Successfully half connected " + this);
mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
mState = ChannelState.HALF_CONNECTED;
} else {
Log.d(TAG, "Failed to connect channel " + this);
mState = ChannelState.FAILURE;
mChannel = null;
}
break;
case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
mState = ChannelState.CONNECTED;
Log.d(TAG, "Channel fully connected" + this);
break;
case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
mState = ChannelState.DISCONNECTED;
mChannel = null;
Log.d(TAG, "Channel disconnected" + this);
break;
default:
incomingMessageHandler.handleMessage(msg);
break;
}
}
};
mChannel.connect(null, rawMessageHandler, messenger);
}
public void disconnect() {
assertEquals("AsyncChannel must be connected to disconnect",
ChannelState.CONNECTED, mState);
mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_DISCONNECT);
mState = ChannelState.DISCONNECTED;
mChannel = null;
}
public void sendMessage(Message msg) {
assertEquals("AsyncChannel must be connected to send messages",
ChannelState.CONNECTED, mState);
mChannel.sendMessage(msg);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 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.internal.util.test;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import com.android.internal.util.AsyncChannel;
import java.util.HashMap;
import java.util.Map;
/**
* Provides an interface for the server side implementation of a bidirectional channel as described
* in {@link com.android.internal.util.AsyncChannel}.
*/
public class BidirectionalAsyncChannelServer {
private static final String TAG = "BidirectionalAsyncChannelServer";
// Keeps track of incoming clients, which are identifiable by their messengers.
private final Map<Messenger, AsyncChannel> mClients = new HashMap<>();
private Messenger mMessenger;
public BidirectionalAsyncChannelServer(final Context context, final Looper looper,
final Handler messageHandler) {
Handler handler = new Handler(looper) {
@Override
public void handleMessage(Message msg) {
AsyncChannel channel = mClients.get(msg.replyTo);
switch (msg.what) {
case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
if (channel != null) {
Log.d(TAG, "duplicate client connection: " + msg.sendingUid);
channel.replyToMessage(msg,
AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED);
} else {
channel = new AsyncChannel();
mClients.put(msg.replyTo, channel);
channel.connected(context, this, msg.replyTo);
channel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
AsyncChannel.STATUS_SUCCESSFUL);
}
break;
case AsyncChannel.CMD_CHANNEL_DISCONNECT:
channel.disconnect();
break;
case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
mClients.remove(msg.replyTo);
break;
default:
messageHandler.handleMessage(msg);
break;
}
}
};
mMessenger = new Messenger(handler);
}
public Messenger getMessenger() {
return mMessenger;
}
}