summaryrefslogtreecommitdiff
path: root/tests/testables/src
diff options
context:
space:
mode:
authorJason Monk <jmonk@google.com>2017-06-27 13:13:49 -0400
committerJason Monk <jmonk@google.com>2017-06-27 14:48:45 -0400
commitc429f690e54db1bdc4631aab7c0191e30ccf1d51 (patch)
treedee65e4c2b392474bb16df051ef9be5bed37b0c4 /tests/testables/src
parent21c7490f207c60541935497b0df58604eb578bd5 (diff)
Add system to avoid a crash stopping tests from running
Add a system such that when a crash happens on the main thread, it is delayed until the tests complete, then thrown. Also mark some old flaky tests as not flaky as they should be good now. Test: runtest systemui Change-Id: Ic18a38daf4a93dab63549d834fa00f295644fbf1 Fixes: 62935720, 62251903, 62529569
Diffstat (limited to 'tests/testables/src')
-rw-r--r--tests/testables/src/android/testing/TestableInstrumentation.java181
-rw-r--r--tests/testables/src/android/testing/TestableLooper.java19
2 files changed, 196 insertions, 4 deletions
diff --git a/tests/testables/src/android/testing/TestableInstrumentation.java b/tests/testables/src/android/testing/TestableInstrumentation.java
new file mode 100644
index 000000000000..93fed859cf39
--- /dev/null
+++ b/tests/testables/src/android/testing/TestableInstrumentation.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package android.testing;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.TestLooperManager;
+import android.support.test.runner.AndroidJUnitRunner;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Wrapper around instrumentation that spins up a TestLooperManager around
+ * the main looper whenever a test is not using it to attempt to stop crashes
+ * from stopping other tests from running.
+ */
+public class TestableInstrumentation extends AndroidJUnitRunner {
+
+ private static final String TAG = "TestableInstrumentation";
+
+ private static final int MAX_CRASHES = 5;
+ private static MainLooperManager sManager;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ sManager = new MainLooperManager();
+ Log.setWtfHandler((tag, what, system) -> {
+ if (system) {
+ Log.e(TAG, "WTF!!", what);
+ } else {
+ // These normally kill the app, but we don't want that in a test, instead we want
+ // it to throw.
+ throw new RuntimeException(what);
+ }
+ });
+ super.onCreate(arguments);
+ }
+
+ @Override
+ public void finish(int resultCode, Bundle results) {
+ sManager.destroy();
+ super.finish(resultCode, results);
+ }
+
+ public static void acquireMain() {
+ if (sManager != null) {
+ sManager.acquireMain();
+ }
+ }
+
+ public static void releaseMain() {
+ if (sManager != null) {
+ sManager.releaseMain();
+ }
+ }
+
+ public class MainLooperManager implements Runnable {
+
+ private final ArrayList<Throwable> mExceptions = new ArrayList<>();
+ private Message mStopMessage;
+ private final Handler mMainHandler;
+ private TestLooperManager mManager;
+
+ public MainLooperManager() {
+ mMainHandler = new Handler(Looper.getMainLooper());
+ startManaging();
+ }
+
+ @Override
+ public void run() {
+ try {
+ synchronized (this) {
+ // Let the thing starting us know we are up and ready to run.
+ notify();
+ }
+ while (true) {
+ Message m = mManager.next();
+ if (m == mStopMessage) {
+ mManager.recycle(m);
+ return;
+ }
+ try {
+ mManager.execute(m);
+ } catch (Throwable t) {
+ if (!checkStack(t) || (mExceptions.size() == MAX_CRASHES)) {
+ throw t;
+ }
+ mExceptions.add(t);
+ Log.d(TAG, "Ignoring exception to run more tests", t);
+ }
+ mManager.recycle(m);
+ }
+ } finally {
+ mManager.release();
+ synchronized (this) {
+ // Let the caller know we are done managing the main thread.
+ notify();
+ }
+ }
+ }
+
+ private boolean checkStack(Throwable t) {
+ StackTraceElement topStack = t.getStackTrace()[0];
+ String className = topStack.getClassName();
+ if (className.equals(TestLooperManager.class.getName())) {
+ topStack = t.getCause().getStackTrace()[0];
+ className = topStack.getClassName();
+ }
+ // Only interested in blocking exceptions from the app itself, not from android
+ // framework.
+ return !className.startsWith("android.")
+ && !className.startsWith("com.android.internal");
+ }
+
+ public void destroy() {
+ mStopMessage.sendToTarget();
+ if (mExceptions.size() != 0) {
+ throw new RuntimeException("Exception caught during tests", mExceptions.get(0));
+ }
+ }
+
+ public void acquireMain() {
+ synchronized (this) {
+ mStopMessage.sendToTarget();
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ public void releaseMain() {
+ startManaging();
+ }
+
+ private void startManaging() {
+ mStopMessage = mMainHandler.obtainMessage();
+ synchronized (this) {
+ mManager = acquireLooperManager(Looper.getMainLooper());
+ // This bit needs to happen on a background thread or it will hang if called
+ // from the same thread we are looking to block.
+ new Thread(() -> {
+ // Post a message to the main handler that will manage executing all future
+ // messages.
+ mMainHandler.post(this);
+ while (!mManager.hasMessages(mMainHandler, null, this));
+ // Lastly run the message that executes this so it can manage the main thread.
+ Message next = mManager.next();
+ // Run through messages until we reach ours.
+ while (next.getCallback() != this) {
+ mManager.execute(next);
+ mManager.recycle(next);
+ next = mManager.next();
+ }
+ mManager.execute(next);
+ }).start();
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index f6c3cb3ec498..f1a70921a469 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -29,7 +29,6 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import java.lang.reflect.Field;
import java.util.Map;
/**
@@ -49,7 +48,7 @@ public class TestableLooper {
private TestLooperManager mQueueWrapper;
public TestableLooper(Looper l) throws Exception {
- this(InstrumentationRegistry.getInstrumentation().acquireLooperManager(l), l);
+ this(acquireLooperManager(l), l);
}
private TestableLooper(TestLooperManager wrapper, Looper l) throws Exception {
@@ -78,6 +77,9 @@ public class TestableLooper {
*/
public void destroy() throws NoSuchFieldException, IllegalAccessException {
mQueueWrapper.release();
+ if (mLooper == Looper.getMainLooper()) {
+ TestableInstrumentation.releaseMain();
+ }
}
/**
@@ -196,6 +198,13 @@ public class TestableLooper {
}
}
+ private static TestLooperManager acquireLooperManager(Looper l) {
+ if (l == Looper.getMainLooper()) {
+ TestableInstrumentation.acquireMain();
+ }
+ return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
+ }
+
private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
/**
@@ -247,8 +256,7 @@ public class TestableLooper {
}
boolean set = mTestableLooper.mQueueWrapper == null;
if (set) {
- mTestableLooper.mQueueWrapper = InstrumentationRegistry.getInstrumentation()
- .acquireLooperManager(mLooper);
+ mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
}
try {
Object[] ret = new Object[1];
@@ -283,6 +291,9 @@ public class TestableLooper {
if (set) {
mTestableLooper.mQueueWrapper.release();
mTestableLooper.mQueueWrapper = null;
+ if (mLooper == Looper.getMainLooper()) {
+ TestableInstrumentation.releaseMain();
+ }
}
}
}