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