See go/jetpack-test-android-migration This is the internal version of AOSP change aosp/908373 Test: m -j TestablesTests NetworkStackTests FrameworksServicesTests SystemUITests FrameworksUiServicesTests ExtServicesUnitTests WmTests FrameworksCoreTests Exempt-From-Owner-Approval: automated package name refactoring; already reviewed on AOSP Change-Id: Ib04d80954bd8536914d88c66b28e6632e60a6245
187 lines
6.4 KiB
Java
187 lines
6.4 KiB
Java
/*
|
|
* 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.util.Log;
|
|
|
|
import androidx.test.runner.AndroidJUnitRunner;
|
|
|
|
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) {
|
|
if (TestableLooper.HOLD_MAIN_THREAD) {
|
|
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) {
|
|
if (TestableLooper.HOLD_MAIN_THREAD) {
|
|
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 = Handler.createAsync(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) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|