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 onCreate(Bundle arguments)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 finish(int resultCode, Bundle results)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 acquireMain()65 public static void acquireMain() { 66 if (sManager != null) { 67 sManager.acquireMain(); 68 } 69 } 70 releaseMain()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 MainLooperManager()84 public MainLooperManager() { 85 mMainHandler = Handler.createAsync(Looper.getMainLooper()); 86 startManaging(); 87 } 88 89 @Override run()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 checkStack(Throwable t)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 destroy()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 acquireMain()142 public void acquireMain() { 143 synchronized (this) { 144 mStopMessage.sendToTarget(); 145 try { 146 wait(); 147 } catch (InterruptedException e) { 148 } 149 } 150 } 151 releaseMain()152 public void releaseMain() { 153 startManaging(); 154 } 155 startManaging()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