blob: a826646f69f3fa2cb1f933669daf2acf3fc5500b [file] [log] [blame]
Christopher Wiley07630f62016-05-18 17:09:56 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.os.test;
18
19import static org.junit.Assert.assertTrue;
20
Etan Cohencff2fe42018-02-14 11:44:13 -080021import android.os.Handler;
22import android.os.HandlerExecutor;
Christopher Wiley07630f62016-05-18 17:09:56 -070023import android.os.Looper;
24import android.os.Message;
25import android.os.MessageQueue;
26import android.os.SystemClock;
27import android.util.Log;
28
29import java.lang.reflect.Constructor;
30import java.lang.reflect.Field;
31import java.lang.reflect.InvocationTargetException;
32import java.lang.reflect.Method;
Etan Cohencff2fe42018-02-14 11:44:13 -080033import java.util.concurrent.Executor;
Christopher Wiley07630f62016-05-18 17:09:56 -070034
35/**
36 * Creates a looper whose message queue can be manipulated
37 * This allows testing code that uses a looper to dispatch messages in a deterministic manner
38 * Creating a TestLooper will also install it as the looper for the current thread
39 */
40public class TestLooper {
41 protected final Looper mLooper;
42
43 private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
44 private static final Field THREAD_LOCAL_LOOPER_FIELD;
45 private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
46 private static final Field MESSAGE_NEXT_FIELD;
47 private static final Field MESSAGE_WHEN_FIELD;
48 private static final Method MESSAGE_MARK_IN_USE_METHOD;
49 private static final String TAG = "TestLooper";
50
Robert Horvathe75cf0a2020-03-23 14:46:25 +010051 private final Clock mClock;
52
Christopher Wiley07630f62016-05-18 17:09:56 -070053 private AutoDispatchThread mAutoDispatchThread;
54
55 static {
56 try {
57 LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
58 LOOPER_CONSTRUCTOR.setAccessible(true);
59 THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
60 THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
61 MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
62 MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
63 MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
64 MESSAGE_NEXT_FIELD.setAccessible(true);
65 MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
66 MESSAGE_WHEN_FIELD.setAccessible(true);
67 MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
68 MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
69 } catch (NoSuchFieldException | NoSuchMethodException e) {
70 throw new RuntimeException("Failed to initialize TestLooper", e);
71 }
72 }
73
Robert Horvathe75cf0a2020-03-23 14:46:25 +010074 /**
75 * Creates a TestLooper and installs it as the looper for the current thread.
76 */
Christopher Wiley07630f62016-05-18 17:09:56 -070077 public TestLooper() {
Robert Horvathe75cf0a2020-03-23 14:46:25 +010078 this(SystemClock::uptimeMillis);
79 }
80
81 /**
82 * Creates a TestLooper with a custom clock and installs it as the looper for the current
83 * thread.
84 *
85 * Messages are dispatched when their {@link Message#when} is before or at {@link
86 * Clock#uptimeMillis()}.
87 * Use a custom clock with care. When using an offsettable clock like {@link
88 * com.android.server.testutils.OffsettableClock} be sure not to double offset messages by
89 * offsetting the clock and calling {@link #moveTimeForward(long)}. Instead, offset the clock
90 * and call {@link #dispatchAll()}.
91 */
92 public TestLooper(Clock clock) {
Christopher Wiley07630f62016-05-18 17:09:56 -070093 try {
94 mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
95
96 ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
97 .get(null);
98 threadLocalLooper.set(mLooper);
99 } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
100 throw new RuntimeException("Reflection error constructing or accessing looper", e);
101 }
Robert Horvathe75cf0a2020-03-23 14:46:25 +0100102
103 mClock = clock;
Christopher Wiley07630f62016-05-18 17:09:56 -0700104 }
105
106 public Looper getLooper() {
107 return mLooper;
108 }
109
Etan Cohencff2fe42018-02-14 11:44:13 -0800110 public Executor getNewExecutor() {
111 return new HandlerExecutor(new Handler(getLooper()));
112 }
113
Christopher Wiley07630f62016-05-18 17:09:56 -0700114 private Message getMessageLinkedList() {
115 try {
116 MessageQueue queue = mLooper.getQueue();
117 return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
118 } catch (IllegalAccessException e) {
119 throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
120 e);
121 }
122 }
123
124 public void moveTimeForward(long milliSeconds) {
125 try {
126 Message msg = getMessageLinkedList();
127 while (msg != null) {
128 long updatedWhen = msg.getWhen() - milliSeconds;
129 if (updatedWhen < 0) {
130 updatedWhen = 0;
131 }
132 MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
133 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
134 }
135 } catch (IllegalAccessException e) {
136 throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
137 }
138 }
139
Robert Horvathe75cf0a2020-03-23 14:46:25 +0100140 private long currentTime() {
141 return mClock.uptimeMillis();
142 }
143
Christopher Wiley07630f62016-05-18 17:09:56 -0700144 private Message messageQueueNext() {
145 try {
Robert Horvathe75cf0a2020-03-23 14:46:25 +0100146 long now = currentTime();
Christopher Wiley07630f62016-05-18 17:09:56 -0700147
148 Message prevMsg = null;
149 Message msg = getMessageLinkedList();
150 if (msg != null && msg.getTarget() == null) {
151 // Stalled by a barrier. Find the next asynchronous message in
152 // the queue.
153 do {
154 prevMsg = msg;
155 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
156 } while (msg != null && !msg.isAsynchronous());
157 }
158 if (msg != null) {
159 if (now >= msg.getWhen()) {
160 // Got a message.
161 if (prevMsg != null) {
162 MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
163 } else {
164 MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
165 MESSAGE_NEXT_FIELD.get(msg));
166 }
167 MESSAGE_NEXT_FIELD.set(msg, null);
168 MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
169 return msg;
170 }
171 }
172 } catch (IllegalAccessException | InvocationTargetException e) {
173 throw new RuntimeException("Access failed in TestLooper", e);
174 }
175
176 return null;
177 }
178
179 /**
180 * @return true if there are pending messages in the message queue
181 */
182 public synchronized boolean isIdle() {
183 Message messageList = getMessageLinkedList();
184
Robert Horvathe75cf0a2020-03-23 14:46:25 +0100185 return messageList != null && currentTime() >= messageList.getWhen();
Christopher Wiley07630f62016-05-18 17:09:56 -0700186 }
187
188 /**
189 * @return the next message in the Looper's message queue or null if there is none
190 */
191 public synchronized Message nextMessage() {
192 if (isIdle()) {
193 return messageQueueNext();
194 } else {
195 return null;
196 }
197 }
198
199 /**
200 * Dispatch the next message in the queue
201 * Asserts that there is a message in the queue
202 */
203 public synchronized void dispatchNext() {
204 assertTrue(isIdle());
205 Message msg = messageQueueNext();
206 if (msg == null) {
207 return;
208 }
209 msg.getTarget().dispatchMessage(msg);
210 }
211
212 /**
213 * Dispatch all messages currently in the queue
214 * Will not fail if there are no messages pending
Robert Horvathe75cf0a2020-03-23 14:46:25 +0100215 *
Christopher Wiley07630f62016-05-18 17:09:56 -0700216 * @return the number of messages dispatched
217 */
218 public synchronized int dispatchAll() {
219 int count = 0;
220 while (isIdle()) {
221 dispatchNext();
222 ++count;
223 }
224 return count;
225 }
226
Robert Horvathe75cf0a2020-03-23 14:46:25 +0100227 public interface Clock {
228 long uptimeMillis();
229 }
230
Christopher Wiley07630f62016-05-18 17:09:56 -0700231 /**
232 * Thread used to dispatch messages when the main thread is blocked waiting for a response.
233 */
234 private class AutoDispatchThread extends Thread {
235 private static final int MAX_LOOPS = 100;
236 private static final int LOOP_SLEEP_TIME_MS = 10;
237
238 private RuntimeException mAutoDispatchException = null;
239
240 /**
241 * Run method for the auto dispatch thread.
242 * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
Roshan Piusf145e172019-09-17 14:51:09 -0700243 * The thread continues looping and attempting to dispatch all messages until
244 * {@link #stopAutoDispatch()} has been invoked.
Christopher Wiley07630f62016-05-18 17:09:56 -0700245 */
246 @Override
247 public void run() {
248 int dispatchCount = 0;
249 for (int i = 0; i < MAX_LOOPS; i++) {
250 try {
Roshan Piusf145e172019-09-17 14:51:09 -0700251 dispatchCount += dispatchAll();
Christopher Wiley07630f62016-05-18 17:09:56 -0700252 } catch (RuntimeException e) {
253 mAutoDispatchException = e;
Christopher Wiley07630f62016-05-18 17:09:56 -0700254 return;
255 }
Roshan Piusf145e172019-09-17 14:51:09 -0700256 Log.d(TAG, "dispatched " + dispatchCount + " messages");
Christopher Wiley07630f62016-05-18 17:09:56 -0700257 try {
258 Thread.sleep(LOOP_SLEEP_TIME_MS);
259 } catch (InterruptedException e) {
Roshan Piusf145e172019-09-17 14:51:09 -0700260 if (dispatchCount == 0) {
261 Log.e(TAG, "stopAutoDispatch called before any messages were dispatched.");
262 mAutoDispatchException = new IllegalStateException(
263 "stopAutoDispatch called before any messages were dispatched.");
264 }
Christopher Wiley07630f62016-05-18 17:09:56 -0700265 return;
266 }
267 }
Roshan Piusf145e172019-09-17 14:51:09 -0700268 if (dispatchCount == 0) {
269 Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
270 mAutoDispatchException = new IllegalStateException(
271 "TestLooper did not dispatch any messages before exiting.");
272 }
Christopher Wiley07630f62016-05-18 17:09:56 -0700273 }
274
275 /**
276 * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
277 * to the main thread.
278 *
279 * @return RuntimeException Exception created by stopping without dispatching a message
280 */
281 public RuntimeException getException() {
282 return mAutoDispatchException;
283 }
284 }
285
286 /**
287 * Create and start a new AutoDispatchThread if one is not already running.
288 */
289 public void startAutoDispatch() {
290 if (mAutoDispatchThread != null) {
291 throw new IllegalStateException(
292 "startAutoDispatch called with the AutoDispatchThread already running.");
293 }
294 mAutoDispatchThread = new AutoDispatchThread();
295 mAutoDispatchThread.start();
296 }
297
298 /**
299 * If an AutoDispatchThread is currently running, stop and clean up.
300 */
301 public void stopAutoDispatch() {
302 if (mAutoDispatchThread != null) {
303 if (mAutoDispatchThread.isAlive()) {
304 mAutoDispatchThread.interrupt();
305 }
306 try {
307 mAutoDispatchThread.join();
308 } catch (InterruptedException e) {
309 // Catch exception from join.
310 }
311
312 RuntimeException e = mAutoDispatchThread.getException();
313 mAutoDispatchThread = null;
314 if (e != null) {
315 throw e;
316 }
317 } else {
318 // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
319 throw new IllegalStateException(
320 "stopAutoDispatch called without startAutoDispatch.");
321 }
322 }
Roshan Piusf145e172019-09-17 14:51:09 -0700323
324 /**
325 * If an AutoDispatchThread is currently running, stop and clean up.
326 * This method ignores exceptions raised for indicating that no messages were dispatched.
327 */
328 public void stopAutoDispatchAndIgnoreExceptions() {
329 try {
330 stopAutoDispatch();
331 } catch (IllegalStateException e) {
332 Log.e(TAG, "stopAutoDispatch", e);
333 }
334
335 }
Christopher Wiley07630f62016-05-18 17:09:56 -0700336}