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