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 = Handler.createAsync(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