Home | History | Annotate | Download | only in test
      1 /*
      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 
     17 package android.os.test;
     18 
     19 import static org.junit.Assert.assertTrue;
     20 
     21 import android.os.Handler;
     22 import android.os.HandlerExecutor;
     23 import android.os.Looper;
     24 import android.os.Message;
     25 import android.os.MessageQueue;
     26 import android.os.SystemClock;
     27 import android.util.Log;
     28 
     29 import java.lang.reflect.Constructor;
     30 import java.lang.reflect.Field;
     31 import java.lang.reflect.InvocationTargetException;
     32 import java.lang.reflect.Method;
     33 import java.util.concurrent.Executor;
     34 
     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  */
     40 public 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 
     51     private AutoDispatchThread mAutoDispatchThread;
     52 
     53     static {
     54         try {
     55             LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
     56             LOOPER_CONSTRUCTOR.setAccessible(true);
     57             THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
     58             THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
     59             MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
     60             MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
     61             MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
     62             MESSAGE_NEXT_FIELD.setAccessible(true);
     63             MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
     64             MESSAGE_WHEN_FIELD.setAccessible(true);
     65             MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
     66             MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
     67         } catch (NoSuchFieldException | NoSuchMethodException e) {
     68             throw new RuntimeException("Failed to initialize TestLooper", e);
     69         }
     70     }
     71 
     72 
     73     public TestLooper() {
     74         try {
     75             mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
     76 
     77             ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
     78                     .get(null);
     79             threadLocalLooper.set(mLooper);
     80         } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
     81             throw new RuntimeException("Reflection error constructing or accessing looper", e);
     82         }
     83     }
     84 
     85     public Looper getLooper() {
     86         return mLooper;
     87     }
     88 
     89     public Executor getNewExecutor() {
     90         return new HandlerExecutor(new Handler(getLooper()));
     91     }
     92 
     93     private Message getMessageLinkedList() {
     94         try {
     95             MessageQueue queue = mLooper.getQueue();
     96             return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
     97         } catch (IllegalAccessException e) {
     98             throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
     99                     e);
    100         }
    101     }
    102 
    103     public void moveTimeForward(long milliSeconds) {
    104         try {
    105             Message msg = getMessageLinkedList();
    106             while (msg != null) {
    107                 long updatedWhen = msg.getWhen() - milliSeconds;
    108                 if (updatedWhen < 0) {
    109                     updatedWhen = 0;
    110                 }
    111                 MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
    112                 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
    113             }
    114         } catch (IllegalAccessException e) {
    115             throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
    116         }
    117     }
    118 
    119     private Message messageQueueNext() {
    120         try {
    121             long now = SystemClock.uptimeMillis();
    122 
    123             Message prevMsg = null;
    124             Message msg = getMessageLinkedList();
    125             if (msg != null && msg.getTarget() == null) {
    126                 // Stalled by a barrier. Find the next asynchronous message in
    127                 // the queue.
    128                 do {
    129                     prevMsg = msg;
    130                     msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
    131                 } while (msg != null && !msg.isAsynchronous());
    132             }
    133             if (msg != null) {
    134                 if (now >= msg.getWhen()) {
    135                     // Got a message.
    136                     if (prevMsg != null) {
    137                         MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
    138                     } else {
    139                         MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
    140                                 MESSAGE_NEXT_FIELD.get(msg));
    141                     }
    142                     MESSAGE_NEXT_FIELD.set(msg, null);
    143                     MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
    144                     return msg;
    145                 }
    146             }
    147         } catch (IllegalAccessException | InvocationTargetException e) {
    148             throw new RuntimeException("Access failed in TestLooper", e);
    149         }
    150 
    151         return null;
    152     }
    153 
    154     /**
    155      * @return true if there are pending messages in the message queue
    156      */
    157     public synchronized boolean isIdle() {
    158         Message messageList = getMessageLinkedList();
    159 
    160         return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
    161     }
    162 
    163     /**
    164      * @return the next message in the Looper's message queue or null if there is none
    165      */
    166     public synchronized Message nextMessage() {
    167         if (isIdle()) {
    168             return messageQueueNext();
    169         } else {
    170             return null;
    171         }
    172     }
    173 
    174     /**
    175      * Dispatch the next message in the queue
    176      * Asserts that there is a message in the queue
    177      */
    178     public synchronized void dispatchNext() {
    179         assertTrue(isIdle());
    180         Message msg = messageQueueNext();
    181         if (msg == null) {
    182             return;
    183         }
    184         msg.getTarget().dispatchMessage(msg);
    185     }
    186 
    187     /**
    188      * Dispatch all messages currently in the queue
    189      * Will not fail if there are no messages pending
    190      * @return the number of messages dispatched
    191      */
    192     public synchronized int dispatchAll() {
    193         int count = 0;
    194         while (isIdle()) {
    195             dispatchNext();
    196             ++count;
    197         }
    198         return count;
    199     }
    200 
    201     /**
    202      * Thread used to dispatch messages when the main thread is blocked waiting for a response.
    203      */
    204     private class AutoDispatchThread extends Thread {
    205         private static final int MAX_LOOPS = 100;
    206         private static final int LOOP_SLEEP_TIME_MS = 10;
    207 
    208         private RuntimeException mAutoDispatchException = null;
    209 
    210         /**
    211          * Run method for the auto dispatch thread.
    212          * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
    213          * The thread continues looping and attempting to dispatch all messages until at
    214          * least one message has been dispatched.
    215          */
    216         @Override
    217         public void run() {
    218             int dispatchCount = 0;
    219             for (int i = 0; i < MAX_LOOPS; i++) {
    220                 try {
    221                     dispatchCount = dispatchAll();
    222                 } catch (RuntimeException e) {
    223                     mAutoDispatchException = e;
    224                 }
    225                 Log.d(TAG, "dispatched " + dispatchCount + " messages");
    226                 if (dispatchCount > 0) {
    227                     return;
    228                 }
    229                 try {
    230                     Thread.sleep(LOOP_SLEEP_TIME_MS);
    231                 } catch (InterruptedException e) {
    232                     mAutoDispatchException = new IllegalStateException(
    233                             "stopAutoDispatch called before any messages were dispatched.");
    234                     return;
    235                 }
    236             }
    237             Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
    238             mAutoDispatchException = new IllegalStateException(
    239                     "TestLooper did not dispatch any messages before exiting.");
    240         }
    241 
    242         /**
    243          * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
    244          * to the main thread.
    245          *
    246          * @return RuntimeException Exception created by stopping without dispatching a message
    247          */
    248         public RuntimeException getException() {
    249             return mAutoDispatchException;
    250         }
    251     }
    252 
    253     /**
    254      * Create and start a new AutoDispatchThread if one is not already running.
    255      */
    256     public void startAutoDispatch() {
    257         if (mAutoDispatchThread != null) {
    258             throw new IllegalStateException(
    259                     "startAutoDispatch called with the AutoDispatchThread already running.");
    260         }
    261         mAutoDispatchThread = new AutoDispatchThread();
    262         mAutoDispatchThread.start();
    263     }
    264 
    265     /**
    266      * If an AutoDispatchThread is currently running, stop and clean up.
    267      */
    268     public void stopAutoDispatch() {
    269         if (mAutoDispatchThread != null) {
    270             if (mAutoDispatchThread.isAlive()) {
    271                 mAutoDispatchThread.interrupt();
    272             }
    273             try {
    274                 mAutoDispatchThread.join();
    275             } catch (InterruptedException e) {
    276                 // Catch exception from join.
    277             }
    278 
    279             RuntimeException e = mAutoDispatchThread.getException();
    280             mAutoDispatchThread = null;
    281             if (e != null) {
    282                 throw e;
    283             }
    284         } else {
    285             // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
    286             throw new IllegalStateException(
    287                     "stopAutoDispatch called without startAutoDispatch.");
    288         }
    289     }
    290 }
    291