blob: ed867c1331417a0b34c1ec3f431b88530bd84427 [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;
Jason Monkc429f692017-06-27 13:13:49 -040022import android.util.Log;
23
Brett Chabot84151d92019-02-27 15:37:59 -080024import androidx.test.runner.AndroidJUnitRunner;
25
Jason Monkc429f692017-06-27 13:13:49 -040026import 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 */
33public 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
41 public void onCreate(Bundle arguments) {
Jason Monk759e9122018-07-20 14:52:22 -040042 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 }
Jason Monkc429f692017-06-27 13:13:49 -040054 super.onCreate(arguments);
55 }
56
57 @Override
58 public void finish(int resultCode, Bundle results) {
Jason Monk759e9122018-07-20 14:52:22 -040059 if (TestableLooper.HOLD_MAIN_THREAD) {
60 sManager.destroy();
61 }
Jason Monkc429f692017-06-27 13:13:49 -040062 super.finish(resultCode, results);
63 }
64
65 public static void acquireMain() {
66 if (sManager != null) {
67 sManager.acquireMain();
68 }
69 }
70
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
84 public MainLooperManager() {
Jason Monk1e352f42018-05-16 10:15:33 -040085 mMainHandler = Handler.createAsync(Looper.getMainLooper());
Jason Monkc429f692017-06-27 13:13:49 -040086 startManaging();
87 }
88
89 @Override
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
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
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
142 public void acquireMain() {
143 synchronized (this) {
144 mStopMessage.sendToTarget();
145 try {
146 wait();
147 } catch (InterruptedException e) {
148 }
149 }
150 }
151
152 public void releaseMain() {
153 startManaging();
154 }
155
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}