diff --git a/tests/utils/testutils/Android.mk b/tests/utils/testutils/Android.mk new file mode 100644 index 0000000000000..d53167f19ebe6 --- /dev/null +++ b/tests/utils/testutils/Android.mk @@ -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) diff --git a/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java new file mode 100644 index 0000000000000..746c77dda4d45 --- /dev/null +++ b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java @@ -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 { + @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); + } + } + } + +} diff --git a/tests/utils/testutils/java/android/app/test/TestAlarmManager.java b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java new file mode 100644 index 0000000000000..e90ea1e648035 --- /dev/null +++ b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java @@ -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 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 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(); + } + } + } +} diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java new file mode 100644 index 0000000000000..e8ceb4a9b02da --- /dev/null +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -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_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 threadLocalLooper = (ThreadLocal) 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."); + } + } +} diff --git a/tests/utils/testutils/java/android/os/test/TestLooperTest.java b/tests/utils/testutils/java/android/os/test/TestLooperTest.java new file mode 100644 index 0000000000000..40d83b5b91717 --- /dev/null +++ b/tests/utils/testutils/java/android/os/test/TestLooperTest.java @@ -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 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. + *

+ * 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 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. + *

+ * 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 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. + *

+ * 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 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. + *

+ * 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 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. + *

+ * 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 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. + *

+ * 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. + *

+ * 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 + *

+ * 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. + *

+ * Expected: After AutoDispatch is stopped, dispatchAll will return 1. + */ + @Test + public void testAutoDispatchStopsCleanlyWithoutDispatchingAMessage() { + final int messageA = 1; + + InOrder inOrder = inOrder(mHandlerSpy); + ArgumentCaptor 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 + *

+ * Expected: Exception is thrown with the stopAutoDispatch call. + */ + @Test(expected = IllegalStateException.class) + public void testAutoDispatchThrowsExceptionWhenNoMessagesDispatched() { + mTestLooper.startAutoDispatch(); + mTestLooper.stopAutoDispatch(); + } +} diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java new file mode 100644 index 0000000000000..25cd5b9b9088e --- /dev/null +++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java @@ -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); + } +} diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java new file mode 100644 index 0000000000000..49c833228b6c6 --- /dev/null +++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java @@ -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 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; + } + +}