blob: 3207b486b3294b9f7f63954b409a169ca3ed1cef [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) {
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
55 public void finish(int resultCode, Bundle results) {
56 sManager.destroy();
57 super.finish(resultCode, results);
58 }
59
60 public static void acquireMain() {
61 if (sManager != null) {
62 sManager.acquireMain();
63 }
64 }
65
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
79 public MainLooperManager() {
Jason Monk1e352f42018-05-16 10:15:33 -040080 mMainHandler = Handler.createAsync(Looper.getMainLooper());
Jason Monkc429f692017-06-27 13:13:49 -040081 startManaging();
82 }
83
84 @Override
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
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
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
137 public void acquireMain() {
138 synchronized (this) {
139 mStopMessage.sendToTarget();
140 try {
141 wait();
142 } catch (InterruptedException e) {
143 }
144 }
145 }
146
147 public void releaseMain() {
148 startManaging();
149 }
150
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}