Use JUnitCore to run a list of VogarTest instances
Moved functionality for running tests with a timeout into
VogarTestRunner and added VmIsUnstableException which extends
StoppedByUserException and is used to abort the test run when a
test times out and so leaves the VM in an unstable state.
Bug: 27940141
Change-Id: I99f4c44eaf9c8e4303bdd4def5bd287bf7bdafdb
diff --git a/src/vogar/target/junit/JUnitTargetRunner.java b/src/vogar/target/junit/JUnitTargetRunner.java
index bf4ed55..58104a9 100644
--- a/src/vogar/target/junit/JUnitTargetRunner.java
+++ b/src/vogar/target/junit/JUnitTargetRunner.java
@@ -16,25 +16,17 @@
package vogar.target.junit;
-import java.util.Collections;
import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
+import org.junit.runner.JUnitCore;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runners.model.InitializationError;
-import vogar.Result;
import vogar.monitor.TargetMonitor;
import vogar.target.Profiler;
import vogar.target.SkipPastFilter;
import vogar.target.TargetRunner;
import vogar.target.TestEnvironment;
-import vogar.util.Threads;
/**
* Adapts a JUnit3 test for use by vogar.
@@ -46,12 +38,8 @@
private final TestEnvironment testEnvironment;
private final int timeoutSeconds;
- private boolean vmIsUnstable;
private final List<VogarTest> tests;
- private final ExecutorService executor = Executors.newCachedThreadPool(
- Threads.daemonThreadFactory("testrunner"));
-
public JUnitTargetRunner(TargetMonitor monitor, AtomicReference<String> skipPastReference,
TestEnvironment testEnvironment, int timeoutSeconds, List<VogarTest> tests) {
this.monitor = monitor;
@@ -62,150 +50,33 @@
}
public boolean run(Profiler profiler) {
- for (VogarTest test : tests) {
+ // Use JUnit infrastructure to run the tests.
+ Runner runner;
+ try {
+ runner = new VogarTestRunner(tests, monitor, testEnvironment, timeoutSeconds, profiler);
+ } catch (InitializationError e) {
+ throw new IllegalStateException("Could not create VogarTestRunner", e);
+ }
- // Use JUnit infrastructure to run the tests.
- Runner runner;
+ String skipPast = skipPastReference.get();
+ if (skipPast != null) {
try {
- runner = new VogarTestRunner(Collections.singletonList(test));
- } catch (InitializationError e) {
- throw new IllegalStateException("Could not create VogarTestRunner", e);
+ new SkipPastFilter(skipPastReference).apply(runner);
+ } catch (NoTestsRemainException ignored) {
+ return true;
}
+ }
- final String skipPast = skipPastReference.get();
- if (skipPast != null) {
- try {
- new SkipPastFilter(skipPastReference).apply(runner);
- } catch (NoTestsRemainException ignored) {
- continue;
- }
- }
-
- runWithTimeout(profiler, test);
-
- if (vmIsUnstable) {
- return false;
- }
+ try {
+ JUnitCore core = new JUnitCore();
+ core.run(runner);
+ } catch (VmIsUnstableException e) {
+ // If a test reports that the VM is unstable then inform the caller so that the
+ // current process can be exited abnormally which will trigger the vogar main process
+ // to rerun the tests from after the timing out test.
+ return false;
}
return true;
}
-
- /**
- * Runs the test on another thread. If the test completes before the
- * timeout, this reports the result normally. But if the test times out,
- * this reports the timeout stack trace and begins the process of killing
- * this no-longer-trustworthy process.
- */
- private void runWithTimeout(final Profiler profiler, final VogarTest test) {
- testEnvironment.reset();
- monitor.outcomeStarted(test.toString());
-
- // Start the test on a background thread.
- final AtomicReference<Thread> executingThreadReference = new AtomicReference<Thread>();
- Future<Throwable> result = executor.submit(new Callable<Throwable>() {
- public Throwable call() throws Exception {
- executingThreadReference.set(Thread.currentThread());
- try {
- if (profiler != null) {
- profiler.start();
- }
- test.run();
- return null;
- } catch (Throwable throwable) {
- return throwable;
- } finally {
- if (profiler != null) {
- profiler.stop();
- }
- }
- }
- });
-
- // Wait until either the result arrives or the test times out.
- Throwable thrown;
- try {
- thrown = timeoutSeconds == 0
- ? result.get()
- : result.get(timeoutSeconds, TimeUnit.SECONDS);
- } catch (TimeoutException e) {
- vmIsUnstable = true;
- Thread executingThread = executingThreadReference.get();
- if (executingThread != null) {
- executingThread.interrupt();
- e.setStackTrace(executingThread.getStackTrace());
- }
- thrown = e;
- } catch (Exception e) {
- thrown = e;
- }
-
- if (thrown != null) {
- prepareForDisplay(thrown);
- thrown.printStackTrace(System.out);
- monitor.outcomeFinished(Result.EXEC_FAILED);
- } else {
- monitor.outcomeFinished(Result.SUCCESS);
- }
- }
-
- /**
- * Strip vogar's lines from the stack trace. For example, we'd strip the
- * first two Assert lines and everything after the testFoo() line in this
- * stack trace:
- *
- * at junit.framework.Assert.fail(Assert.java:198)
- * at junit.framework.Assert.assertEquals(Assert.java:56)
- * at junit.framework.Assert.assertEquals(Assert.java:61)
- * at libcore.java.net.FooTest.testFoo(FooTest.java:124)
- * at java.lang.reflect.Method.invokeNative(Native Method)
- * at java.lang.reflect.Method.invoke(Method.java:491)
- * at vogar.target.junit.Junit$JUnitTest.run(Junit.java:214)
- * at vogar.target.junit.JUnitRunner$1.call(JUnitRunner.java:112)
- * at vogar.target.junit.JUnitRunner$1.call(JUnitRunner.java:105)
- * at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
- * at java.util.concurrent.FutureTask.run(FutureTask.java:137)
- * at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
- * at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
- * at java.lang.Thread.run(Thread.java:863)
- */
- private void prepareForDisplay(Throwable t) {
- StackTraceElement[] stackTraceElements = t.getStackTrace();
- boolean foundVogar = false;
-
- int last = stackTraceElements.length - 1;
- for (; last >= 0; last--) {
- String className = stackTraceElements[last].getClassName();
- if (className.startsWith("vogar.target")) {
- foundVogar = true;
- } else if (foundVogar
- && !className.startsWith("java.lang.reflect")
- && !className.startsWith("sun.reflect")
- && !className.startsWith("junit.framework")) {
- if (last < stackTraceElements.length) {
- last++;
- }
- break;
- }
- }
-
- int first = 0;
- for (; first < last; first++) {
- String className = stackTraceElements[first].getClassName();
- if (!className.startsWith("junit.framework")) {
- break;
- }
- }
-
- if (first > 0) {
- first--; // retain one assertSomething() line in the trace
- }
-
- if (first < last) {
- // Arrays.copyOfRange() didn't exist on Froyo
- StackTraceElement[] copyOfRange = new StackTraceElement[last - first];
- System.arraycopy(stackTraceElements, first, copyOfRange, 0, last - first);
- t.setStackTrace(copyOfRange);
- }
- }
}
diff --git a/src/vogar/target/junit/VmIsUnstableException.java b/src/vogar/target/junit/VmIsUnstableException.java
new file mode 100644
index 0000000..99a1728
--- /dev/null
+++ b/src/vogar/target/junit/VmIsUnstableException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 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 vogar.target.junit;
+
+import org.junit.runner.notification.StoppedByUserException;
+
+/**
+ * A special exception used to abort a test run due to the VM becoming unstable.
+ */
+public class VmIsUnstableException extends StoppedByUserException {
+}
diff --git a/src/vogar/target/junit/VogarTestRunner.java b/src/vogar/target/junit/VogarTestRunner.java
index b5c8a24..e9a6169 100644
--- a/src/vogar/target/junit/VogarTestRunner.java
+++ b/src/vogar/target/junit/VogarTestRunner.java
@@ -17,11 +17,24 @@
package vogar.target.junit;
import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
+import vogar.Result;
+import vogar.monitor.TargetMonitor;
+import vogar.target.Profiler;
+import vogar.target.TestEnvironment;
+import vogar.util.Threads;
/**
* A {@link org.junit.runner.Runner} that can run a list of {@link VogarTest} instances.
@@ -29,10 +42,27 @@
public class VogarTestRunner extends ParentRunner<VogarTest> {
private final List<VogarTest> children;
+ private final TargetMonitor monitor;
- public VogarTestRunner(List<VogarTest> children) throws InitializationError {
+ private final TestEnvironment testEnvironment;
+ private final int timeoutSeconds;
+
+ private final ExecutorService executor = Executors.newCachedThreadPool(
+ Threads.daemonThreadFactory("testrunner"));
+ private final Profiler profiler;
+
+ private boolean vmIsUnstable;
+
+ public VogarTestRunner(List<VogarTest> children, TargetMonitor monitor,
+ TestEnvironment testEnvironment, int timeoutSeconds,
+ Profiler profiler)
+ throws InitializationError {
super(VogarTestRunner.class);
this.children = children;
+ this.monitor = monitor;
+ this.testEnvironment = testEnvironment;
+ this.timeoutSeconds = timeoutSeconds;
+ this.profiler = profiler;
}
@Override
@@ -50,8 +80,140 @@
runLeaf(new Statement() {
@Override
public void evaluate() throws Throwable {
- child.run();
+ runWithTimeout(child);
}
}, describeChild(child), notifier);
+
+ // Abort the test run if the VM is deemed unstable, i.e. the previous test timed out.
+ // Throw this after the results of the previous test have been reported.
+ if (vmIsUnstable) {
+ throw new VmIsUnstableException();
+ }
+ }
+
+ /**
+ * Runs the test on another thread. If the test completes before the
+ * timeout, this reports the result normally. But if the test times out,
+ * this reports the timeout stack trace and begins the process of killing
+ * this no-longer-trustworthy process.
+ */
+ private void runWithTimeout(final VogarTest test) {
+ testEnvironment.reset();
+ monitor.outcomeStarted(test.toString());
+
+ // Start the test on a background thread.
+ final AtomicReference<Thread> executingThreadReference = new AtomicReference<>();
+ Future<Throwable> result = executor.submit(new Callable<Throwable>() {
+ public Throwable call() throws Exception {
+ executingThreadReference.set(Thread.currentThread());
+ try {
+ if (profiler != null) {
+ profiler.start();
+ }
+ test.run();
+ return null;
+ } catch (Throwable throwable) {
+ return throwable;
+ } finally {
+ if (profiler != null) {
+ profiler.stop();
+ }
+ }
+ }
+ });
+
+ // Wait until either the result arrives or the test times out.
+ Throwable thrown;
+ try {
+ thrown = getThrowable(result);
+ } catch (TimeoutException e) {
+ vmIsUnstable = true;
+ Thread executingThread = executingThreadReference.get();
+ if (executingThread != null) {
+ executingThread.interrupt();
+ e.setStackTrace(executingThread.getStackTrace());
+ }
+ thrown = e;
+ } catch (Exception e) {
+ thrown = e;
+ }
+
+ if (thrown != null) {
+ prepareForDisplay(thrown);
+ thrown.printStackTrace(System.out);
+ monitor.outcomeFinished(Result.EXEC_FAILED);
+ } else {
+ monitor.outcomeFinished(Result.SUCCESS);
+ }
+ }
+
+ @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
+ private Throwable getThrowable(Future<Throwable> result)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ Throwable thrown;
+ thrown = timeoutSeconds == 0
+ ? result.get()
+ : result.get(timeoutSeconds, TimeUnit.SECONDS);
+ return thrown;
+ }
+
+ /**
+ * Strip vogar's lines from the stack trace. For example, we'd strip the
+ * first two Assert lines and everything after the testFoo() line in this
+ * stack trace:
+ *
+ * at junit.framework.Assert.fail(Assert.java:198)
+ * at junit.framework.Assert.assertEquals(Assert.java:56)
+ * at junit.framework.Assert.assertEquals(Assert.java:61)
+ * at libcore.java.net.FooTest.testFoo(FooTest.java:124)
+ * at java.lang.reflect.Method.invokeNative(Native Method)
+ * at java.lang.reflect.Method.invoke(Method.java:491)
+ * at vogar.target.junit.Junit$JUnitTest.run(Junit.java:214)
+ * at vogar.target.junit.JUnitRunner$1.call(JUnitRunner.java:112)
+ * at vogar.target.junit.JUnitRunner$1.call(JUnitRunner.java:105)
+ * at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
+ * at java.util.concurrent.FutureTask.run(FutureTask.java:137)
+ * at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
+ * at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
+ * at java.lang.Thread.run(Thread.java:863)
+ */
+ public void prepareForDisplay(Throwable t) {
+ StackTraceElement[] stackTraceElements = t.getStackTrace();
+ boolean foundVogar = false;
+
+ int last = stackTraceElements.length - 1;
+ for (; last >= 0; last--) {
+ String className = stackTraceElements[last].getClassName();
+ if (className.startsWith("vogar.target")) {
+ foundVogar = true;
+ } else if (foundVogar
+ && !className.startsWith("java.lang.reflect")
+ && !className.startsWith("sun.reflect")
+ && !className.startsWith("junit.framework")) {
+ if (last < stackTraceElements.length) {
+ last++;
+ }
+ break;
+ }
+ }
+
+ int first = 0;
+ for (; first < last; first++) {
+ String className = stackTraceElements[first].getClassName();
+ if (!className.startsWith("junit.framework")) {
+ break;
+ }
+ }
+
+ if (first > 0) {
+ first--; // retain one assertSomething() line in the trace
+ }
+
+ if (first < last) {
+ // Arrays.copyOfRange() didn't exist on Froyo
+ StackTraceElement[] copyOfRange = new StackTraceElement[last - first];
+ System.arraycopy(stackTraceElements, first, copyOfRange, 0, last - first);
+ t.setStackTrace(copyOfRange);
+ }
}
}