Home | History | Annotate | Download | only in testing
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package android.testing;
     16 
     17 import android.os.Handler;
     18 import android.os.HandlerThread;
     19 import android.os.Looper;
     20 import android.os.Message;
     21 import android.os.MessageQueue;
     22 import android.os.TestLooperManager;
     23 import android.support.test.InstrumentationRegistry;
     24 import android.util.ArrayMap;
     25 
     26 import org.junit.runners.model.FrameworkMethod;
     27 
     28 import java.lang.annotation.ElementType;
     29 import java.lang.annotation.Retention;
     30 import java.lang.annotation.RetentionPolicy;
     31 import java.lang.annotation.Target;
     32 import java.util.Map;
     33 
     34 /**
     35  * This is a wrapper around {@link TestLooperManager} to make it easier to manage
     36  * and provide an easy annotation for use with tests.
     37  *
     38  * @see TestableLooperTest TestableLooperTest for examples.
     39  */
     40 public class TestableLooper {
     41 
     42     private Looper mLooper;
     43     private MessageQueue mQueue;
     44     private MessageHandler mMessageHandler;
     45 
     46     private Handler mHandler;
     47     private Runnable mEmptyMessage;
     48     private TestLooperManager mQueueWrapper;
     49 
     50     public TestableLooper(Looper l) throws Exception {
     51         this(acquireLooperManager(l), l);
     52     }
     53 
     54     private TestableLooper(TestLooperManager wrapper, Looper l) {
     55         mQueueWrapper = wrapper;
     56         setupQueue(l);
     57     }
     58 
     59     private TestableLooper(Looper looper, boolean b) {
     60         setupQueue(looper);
     61     }
     62 
     63     public Looper getLooper() {
     64         return mLooper;
     65     }
     66 
     67     private void setupQueue(Looper l) {
     68         mLooper = l;
     69         mQueue = mLooper.getQueue();
     70         mHandler = new Handler(mLooper);
     71     }
     72 
     73     /**
     74      * Must be called to release the looper when the test is complete, otherwise
     75      * the looper will not be available for any subsequent tests. This is
     76      * automatically handled for tests using {@link RunWithLooper}.
     77      */
     78     public void destroy() {
     79         mQueueWrapper.release();
     80         if (mLooper == Looper.getMainLooper()) {
     81             TestableInstrumentation.releaseMain();
     82         }
     83     }
     84 
     85     /**
     86      * Sets a callback for all messages processed on this TestableLooper.
     87      *
     88      * @see {@link MessageHandler}
     89      */
     90     public void setMessageHandler(MessageHandler handler) {
     91         mMessageHandler = handler;
     92     }
     93 
     94     /**
     95      * Parse num messages from the message queue.
     96      *
     97      * @param num Number of messages to parse
     98      */
     99     public int processMessages(int num) {
    100         for (int i = 0; i < num; i++) {
    101             if (!parseMessageInt()) {
    102                 return i + 1;
    103             }
    104         }
    105         return num;
    106     }
    107 
    108     /**
    109      * Process messages in the queue until no more are found.
    110      */
    111     public void processAllMessages() {
    112         while (processQueuedMessages() != 0) ;
    113     }
    114 
    115     private int processQueuedMessages() {
    116         int count = 0;
    117         mEmptyMessage = () -> { };
    118         mHandler.post(mEmptyMessage);
    119         waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
    120         while (parseMessageInt()) count++;
    121         return count;
    122     }
    123 
    124     private boolean parseMessageInt() {
    125         try {
    126             Message result = mQueueWrapper.next();
    127             if (result != null) {
    128                 // This is a break message.
    129                 if (result.getCallback() == mEmptyMessage) {
    130                     mQueueWrapper.recycle(result);
    131                     return false;
    132                 }
    133 
    134                 if (mMessageHandler != null) {
    135                     if (mMessageHandler.onMessageHandled(result)) {
    136                         mQueueWrapper.execute(result);
    137                         mQueueWrapper.recycle(result);
    138                     } else {
    139                         mQueueWrapper.recycle(result);
    140                         // Message handler indicated it doesn't want us to continue.
    141                         return false;
    142                     }
    143                 } else {
    144                     mQueueWrapper.execute(result);
    145                     mQueueWrapper.recycle(result);
    146                 }
    147             } else {
    148                 // No messages, don't continue parsing
    149                 return false;
    150             }
    151         } catch (Exception e) {
    152             throw new RuntimeException(e);
    153         }
    154         return true;
    155     }
    156 
    157     /**
    158      * Runs an executable with myLooper set and processes all messages added.
    159      */
    160     public void runWithLooper(RunnableWithException runnable) throws Exception {
    161         new Handler(getLooper()).post(() -> {
    162             try {
    163                 runnable.run();
    164             } catch (Exception e) {
    165                 throw new RuntimeException(e);
    166             }
    167         });
    168         processAllMessages();
    169     }
    170 
    171     public interface RunnableWithException {
    172         void run() throws Exception;
    173     }
    174 
    175     /**
    176      * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
    177      * run this test/class on that thread. The {@link TestableLooper} can be acquired using
    178      * {@link #get(Object)}.
    179      */
    180     @Retention(RetentionPolicy.RUNTIME)
    181     @Target({ElementType.METHOD, ElementType.TYPE})
    182     public @interface RunWithLooper {
    183         boolean setAsMainLooper() default false;
    184     }
    185 
    186     private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
    187             Runnable execute) {
    188         for (int i = 0; i < 10; i++) {
    189             if (!queueWrapper.hasMessages(handler, null, execute)) {
    190                 try {
    191                     Thread.sleep(1);
    192                 } catch (InterruptedException e) {
    193                 }
    194             }
    195         }
    196         if (!queueWrapper.hasMessages(handler, null, execute)) {
    197             throw new RuntimeException("Message didn't queue...");
    198         }
    199     }
    200 
    201     private static TestLooperManager acquireLooperManager(Looper l) {
    202         if (l == Looper.getMainLooper()) {
    203             TestableInstrumentation.acquireMain();
    204         }
    205         return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
    206     }
    207 
    208     private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
    209 
    210     /**
    211      * For use with {@link RunWithLooper}, used to get the TestableLooper that was
    212      * automatically created for this test.
    213      */
    214     public static TestableLooper get(Object test) {
    215         return sLoopers.get(test);
    216     }
    217 
    218     static class LooperFrameworkMethod extends FrameworkMethod {
    219         private HandlerThread mHandlerThread;
    220 
    221         private final TestableLooper mTestableLooper;
    222         private final Looper mLooper;
    223         private final Handler mHandler;
    224 
    225         public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
    226             super(base.getMethod());
    227             try {
    228                 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
    229                 mTestableLooper = new TestableLooper(mLooper, false);
    230             } catch (Exception e) {
    231                 throw new RuntimeException(e);
    232             }
    233             sLoopers.put(test, mTestableLooper);
    234             mHandler = new Handler(mLooper);
    235         }
    236 
    237         public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
    238             super(base.getMethod());
    239             mLooper = other.mLooper;
    240             mTestableLooper = other;
    241             mHandler = Handler.createAsync(mLooper);
    242         }
    243 
    244         public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
    245             if (sLoopers.containsKey(test)) {
    246                 return new LooperFrameworkMethod(sLoopers.get(test), base);
    247             }
    248             return new LooperFrameworkMethod(base, setAsMain, test);
    249         }
    250 
    251         @Override
    252         public Object invokeExplosively(Object target, Object... params) throws Throwable {
    253             if (Looper.myLooper() == mLooper) {
    254                 // Already on the right thread from another statement, just execute then.
    255                 return super.invokeExplosively(target, params);
    256             }
    257             boolean set = mTestableLooper.mQueueWrapper == null;
    258             if (set) {
    259                 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
    260             }
    261             try {
    262                 Object[] ret = new Object[1];
    263                 // Run the execution on the looper thread.
    264                 Runnable execute = () -> {
    265                     try {
    266                         ret[0] = super.invokeExplosively(target, params);
    267                     } catch (Throwable throwable) {
    268                         throw new LooperException(throwable);
    269                     }
    270                 };
    271                 Message m = Message.obtain(mHandler, execute);
    272 
    273                 // Dispatch our message.
    274                 try {
    275                     mTestableLooper.mQueueWrapper.execute(m);
    276                 } catch (LooperException e) {
    277                     throw e.getSource();
    278                 } catch (RuntimeException re) {
    279                     // If the TestLooperManager has to post, it will wrap what it throws in a
    280                     // RuntimeException, make sure we grab the actual source.
    281                     if (re.getCause() instanceof LooperException) {
    282                         throw ((LooperException) re.getCause()).getSource();
    283                     } else {
    284                         throw re.getCause();
    285                     }
    286                 } finally {
    287                     m.recycle();
    288                 }
    289                 return ret[0];
    290             } finally {
    291                 if (set) {
    292                     mTestableLooper.mQueueWrapper.release();
    293                     mTestableLooper.mQueueWrapper = null;
    294                     if (mLooper == Looper.getMainLooper()) {
    295                         TestableInstrumentation.releaseMain();
    296                     }
    297                 }
    298             }
    299         }
    300 
    301         private Looper createLooper() {
    302             // TODO: Find way to share these.
    303             mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
    304             mHandlerThread.start();
    305             return mHandlerThread.getLooper();
    306         }
    307 
    308         @Override
    309         protected void finalize() throws Throwable {
    310             super.finalize();
    311             if (mHandlerThread != null) {
    312                 mHandlerThread.quit();
    313             }
    314         }
    315 
    316         private static class LooperException extends RuntimeException {
    317             private final Throwable mSource;
    318 
    319             public LooperException(Throwable t) {
    320                 mSource = t;
    321             }
    322 
    323             public Throwable getSource() {
    324                 return mSource;
    325             }
    326         }
    327     }
    328 
    329     /**
    330      * Callback to control the execution of messages on the looper, when set with
    331      * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
    332      * will get called back for every message processed on the {@link TestableLooper}.
    333      */
    334     public interface MessageHandler {
    335         /**
    336          * Return true to have the message executed and delivered to target.
    337          * Return false to not execute the message and stop executing messages.
    338          */
    339         boolean onMessageHandled(Message m);
    340     }
    341 }
    342