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.Bundle;
     18 import android.os.Handler;
     19 import android.os.Looper;
     20 import android.os.Message;
     21 import android.os.TestLooperManager;
     22 import android.util.Log;
     23 
     24 import androidx.test.runner.AndroidJUnitRunner;
     25 
     26 import java.util.ArrayList;
     27 
     28 /**
     29  * Wrapper around instrumentation that spins up a TestLooperManager around
     30  * the main looper whenever a test is not using it to attempt to stop crashes
     31  * from stopping other tests from running.
     32  */
     33 public class TestableInstrumentation extends AndroidJUnitRunner {
     34 
     35     private static final String TAG = "TestableInstrumentation";
     36 
     37     private static final int MAX_CRASHES = 5;
     38     private static MainLooperManager sManager;
     39 
     40     @Override
     41     public void onCreate(Bundle arguments) {
     42         if (TestableLooper.HOLD_MAIN_THREAD) {
     43             sManager = new MainLooperManager();
     44             Log.setWtfHandler((tag, what, system) -> {
     45                 if (system) {
     46                     Log.e(TAG, "WTF!!", what);
     47                 } else {
     48                     // These normally kill the app, but we don't want that in a test, instead we want
     49                     // it to throw.
     50                     throw new RuntimeException(what);
     51                 }
     52             });
     53         }
     54         super.onCreate(arguments);
     55     }
     56 
     57     @Override
     58     public void finish(int resultCode, Bundle results) {
     59         if (TestableLooper.HOLD_MAIN_THREAD) {
     60             sManager.destroy();
     61         }
     62         super.finish(resultCode, results);
     63     }
     64 
     65     public static void acquireMain() {
     66         if (sManager != null) {
     67             sManager.acquireMain();
     68         }
     69     }
     70 
     71     public static void releaseMain() {
     72         if (sManager != null) {
     73             sManager.releaseMain();
     74         }
     75     }
     76 
     77     public class MainLooperManager implements Runnable {
     78 
     79         private final ArrayList<Throwable> mExceptions = new ArrayList<>();
     80         private Message mStopMessage;
     81         private final Handler mMainHandler;
     82         private TestLooperManager mManager;
     83 
     84         public MainLooperManager() {
     85             mMainHandler = Handler.createAsync(Looper.getMainLooper());
     86             startManaging();
     87         }
     88 
     89         @Override
     90         public void run() {
     91             try {
     92                 synchronized (this) {
     93                     // Let the thing starting us know we are up and ready to run.
     94                     notify();
     95                 }
     96                 while (true) {
     97                     Message m = mManager.next();
     98                     if (m == mStopMessage) {
     99                         mManager.recycle(m);
    100                         return;
    101                     }
    102                     try {
    103                         mManager.execute(m);
    104                     } catch (Throwable t) {
    105                         if (!checkStack(t) || (mExceptions.size() == MAX_CRASHES)) {
    106                             throw t;
    107                         }
    108                         mExceptions.add(t);
    109                         Log.d(TAG, "Ignoring exception to run more tests", t);
    110                     }
    111                     mManager.recycle(m);
    112                 }
    113             } finally {
    114                 mManager.release();
    115                 synchronized (this) {
    116                     // Let the caller know we are done managing the main thread.
    117                     notify();
    118                 }
    119             }
    120         }
    121 
    122         private boolean checkStack(Throwable t) {
    123             StackTraceElement topStack = t.getStackTrace()[0];
    124             String className = topStack.getClassName();
    125             if (className.equals(TestLooperManager.class.getName())) {
    126                 topStack = t.getCause().getStackTrace()[0];
    127                 className = topStack.getClassName();
    128             }
    129             // Only interested in blocking exceptions from the app itself, not from android
    130             // framework.
    131             return !className.startsWith("android.")
    132                     && !className.startsWith("com.android.internal");
    133         }
    134 
    135         public void destroy() {
    136             mStopMessage.sendToTarget();
    137             if (mExceptions.size() != 0) {
    138                 throw new RuntimeException("Exception caught during tests", mExceptions.get(0));
    139             }
    140         }
    141 
    142         public void acquireMain() {
    143             synchronized (this) {
    144                 mStopMessage.sendToTarget();
    145                 try {
    146                     wait();
    147                 } catch (InterruptedException e) {
    148                 }
    149             }
    150         }
    151 
    152         public void releaseMain() {
    153             startManaging();
    154         }
    155 
    156         private void startManaging() {
    157             mStopMessage = mMainHandler.obtainMessage();
    158             synchronized (this) {
    159                 mManager = acquireLooperManager(Looper.getMainLooper());
    160                 // This bit needs to happen on a background thread or it will hang if called
    161                 // from the same thread we are looking to block.
    162                 new Thread(() -> {
    163                     // Post a message to the main handler that will manage executing all future
    164                     // messages.
    165                     mMainHandler.post(this);
    166                     while (!mManager.hasMessages(mMainHandler, null, this));
    167                     // Lastly run the message that executes this so it can manage the main thread.
    168                     Message next = mManager.next();
    169                     // Run through messages until we reach ours.
    170                     while (next.getCallback() != this) {
    171                         mManager.execute(next);
    172                         mManager.recycle(next);
    173                         next = mManager.next();
    174                     }
    175                     mManager.execute(next);
    176                 }).start();
    177                 if (Looper.myLooper() != Looper.getMainLooper()) {
    178                     try {
    179                         wait();
    180                     } catch (InterruptedException e) {
    181                     }
    182                 }
    183             }
    184         }
    185     }
    186 }
    187