blob: f9edb8340fbae3e5915d0f252d61173264d2f945 [file] [log] [blame]
/*
* Copyright 2013-present Facebook, Inc.
*
* 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 com.facebook.buck.junit;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.TestClass;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* {@link RunNotifier} that sets a timer when a test starts. The default timeout specified in
* {@code .buckconfig} is the length of the timer. When the timer goes off, it checks if the test
* has finished. If it has not finished, the test is flagged as a failure, and all future updates to
* the test status are ignored.
*/
class DelegateRunNotifier extends RunNotifier {
private final Runner runner;
private final RunNotifier delegate;
private final Set<Description> finishedTests;
private final long defaultTestTimeoutMillis;
private final Timer timer;
/** Flag that will be set if a test exceeds {@link #defaultTestTimeoutMillis}. */
private final AtomicBoolean hasTestThatExceededTimeout;
DelegateRunNotifier(Runner runner, RunNotifier delegate, long defaultTestTimeoutMillis) {
this.runner = runner;
this.delegate = delegate;
this.finishedTests = new HashSet<Description>();
this.defaultTestTimeoutMillis = defaultTestTimeoutMillis;
this.timer = new Timer();
this.hasTestThatExceededTimeout = new AtomicBoolean(false);
// Because our fireTestRunFinished() does not seem to get invoked, we listen for the
// delegate to fire a testRunFinished event so we can dispose of the timer.
delegate.addListener(new RunListener() {
@Override
public void testRunFinished(Result result) throws Exception {
onTestRunFinished();
}
});
}
/** Performs any cleanup that we need to do as a result of the test run being complete. */
private void onTestRunFinished() {
timer.cancel();
}
/**
* Method that can be polled to see whether a test has exceeded its default timeout. If a test
* hangs forever, then the Runner will never start the next test, even if it was the last test and
* we invoked fireTestFinished() on the Runner's RunNotifier. For this reason, an external process
* should monitor the state of this method and cancel the Runner, if appropriate.
*/
public boolean hasTestThatExceededTimeout() {
return hasTestThatExceededTimeout.get();
}
@Override
public void addFirstListener(RunListener listener) {
delegate.addFirstListener(listener);
}
@Override
public void addListener(RunListener listener) {
delegate.addListener(listener);
}
@Override
public void removeListener(RunListener listener) {
delegate.removeListener(listener);
}
@Override
public void fireTestRunStarted(Description description) {
// This method does not appear to be invoked. Presumably whoever has a reference to the original
// delegate is invoking its fireTestRunStarted(Description) method directly.
delegate.fireTestRunStarted(description);
}
@Override
public void fireTestRunFinished(Result result) {
// This method does not appear to be invoked. Presumably whoever has a reference to the original
// delegate is invoking its fireTestRunFinished(Description) method directly.
delegate.fireTestRunFinished(result);
}
@Override
public void fireTestStarted(final Description description) throws StoppedByUserException {
delegate.fireTestStarted(description);
// Do not do apply the default timeout if the test has its own @Test(timeout).
Test testAnnotation = description.getAnnotation(Test.class);
if (testAnnotation != null && testAnnotation.timeout() > 0) {
return;
}
// Do not do apply the default timeout if the test has its own @Rule Timeout.
TestClass testClass = getTestClass(description);
if (BuckBlockJUnit4ClassRunner.hasTimeoutRule(testClass)) {
return;
}
// Schedule a timer that verifies that the test completed within the specified timeout.
TimerTask task = new TimerTask() {
@Override
public void run() {
synchronized (finishedTests) {
// If the test already finished, then do nothing.
if (finishedTests.contains(description)) {
return;
}
// Should report the failure. The Exception is modeled after the one created by
// org.junit.internal.runners.statements.FailOnTimeout#createTimeoutException(Thread).
Exception exception = new Exception(String.format(
"test timed out after %d milliseconds", defaultTestTimeoutMillis));
Failure failure = new Failure(description, exception);
fireTestFailure(failure);
fireTestFinished(description);
if (!finishedTests.contains(description)) {
throw new IllegalStateException("fireTestFinished() should update finishedTests.");
}
onTestRunFinished();
hasTestThatExceededTimeout.set(true);
}
}
};
timer.schedule(task, defaultTestTimeoutMillis);
}
private TestClass getTestClass(Description description) {
if (runner instanceof ParentRunner) {
return ((ParentRunner<?>) runner).getTestClass();
} else {
Class<?> testClass = description.getTestClass();
return new TestClass(testClass);
}
}
@Override
public void fireTestFailure(Failure failure) {
synchronized (finishedTests) {
if (!finishedTests.contains(failure.getDescription())) {
delegate.fireTestFailure(failure);
}
}
}
@Override
public void fireTestAssumptionFailed(Failure failure) {
// This is fired when there is a failure for a org.junit.Assume.assumeXXX() method.
synchronized (finishedTests) {
if (!finishedTests.contains(failure.getDescription())) {
delegate.fireTestAssumptionFailed(failure);
}
}
}
@Override
public void fireTestIgnored(Description description) {
synchronized (finishedTests) {
if (!finishedTests.contains(description)) {
delegate.fireTestIgnored(description);
}
}
}
@Override
public void fireTestFinished(Description description) {
synchronized (finishedTests) {
if (!finishedTests.contains(description)) {
delegate.fireTestFinished(description);
finishedTests.add(description);
}
}
}
@Override
public void pleaseStop() {
delegate.pleaseStop();
}
}