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