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