Ensure that the default timeout in .buckconfig works with @RunWith.
Summary:
The custom logic that we use to support a default timeout for tests in `.buckconfig`
relies on our ability to use a `BuckBlockJUnit4ClassRunner` in place of a
`BlockJUnit4ClassRunner` when running JUnit. Unfortunately, when `@RunWith`
is present, the code path to produce a `BlockJUnit4ClassRunner` is not exercised,
which means our `BuckBlockJUnit4ClassRunner` is not inserted, which means
that our default timeout is not enforced.
This diff modifies the `AllDefaultPossibilitiesBuilder` that is used to select the factory
that is used to produce a `Runner` (which may or may not be a `BlockJUnit4ClassRunner`).
When `@RunWith` is present, we now take the `Runner` that is produced as a result of the
`@RunWith` annotation and wrap it with a `DelegateRunnerWithTimeout` that enforces our
default timeout.
It does this by taking the `RunNotifier` passed by the client and wrapping it in a
`DelegateRunNotifier`, which is passed to the original `Runner`. Our `DelegateRunNotifier`
intercepts events from the original `Runner` and passes them on based on the state of
test execution relative to the default timeout from `.buckconfig`.
Unfortunately, the logic in our `DelegateRunnerWithTimeout`/`DelegateRunNotifier` is not
as airtight as the logic in our `BuckBlockJUnit4ClassRunner`, so we are not going to delete
our `BuckBlockJUnit4ClassRunner` codepath just yet.
Test Plan: RunWithDefaultTimeoutIntegrationTest.
diff --git a/src/com/facebook/buck/junit/BuckBlockJUnit4ClassRunner.java b/src/com/facebook/buck/junit/BuckBlockJUnit4ClassRunner.java
index f6e8962..48ec529 100644
--- a/src/com/facebook/buck/junit/BuckBlockJUnit4ClassRunner.java
+++ b/src/com/facebook/buck/junit/BuckBlockJUnit4ClassRunner.java
@@ -32,6 +32,7 @@
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
import java.lang.reflect.Method;
import java.util.List;
@@ -88,7 +89,7 @@
}
private boolean isNeedingCustomTimeout() {
- return defaultTestTimeoutMillis <= 0 || hasTimeoutRule();
+ return defaultTestTimeoutMillis <= 0 || hasTimeoutRule(getTestClass());
}
/**
@@ -193,13 +194,13 @@
* @return {@code true} if the test class has any fields annotated with {@code Rule} whose type
* is {@link Timeout}.
*/
- private boolean hasTimeoutRule() {
+ static boolean hasTimeoutRule(TestClass testClass) {
// Many protected convenience methods in BlockJUnit4ClassRunner that are available in JUnit 4.11
// such as getTestRules(Object) were not public until
// https://github.com/junit-team/junit/commit/8782efa08abf5d47afdc16740678661443706740,
// which appears to be JUnit 4.9. Because we allow users to use JUnit 4.7, we need to include a
// custom implementation that is backwards compatible to JUnit 4.7.
- List<FrameworkField> fields = getTestClass().getAnnotatedFields(Rule.class);
+ List<FrameworkField> fields = testClass.getAnnotatedFields(Rule.class);
for (FrameworkField field : fields) {
if (field.getField().getType().equals(Timeout.class)) {
return true;
diff --git a/src/com/facebook/buck/junit/DelegateRunNotifier.java b/src/com/facebook/buck/junit/DelegateRunNotifier.java
new file mode 100644
index 0000000..f9edb83
--- /dev/null
+++ b/src/com/facebook/buck/junit/DelegateRunNotifier.java
@@ -0,0 +1,212 @@
+/*
+ * 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();
+ }
+}
diff --git a/src/com/facebook/buck/junit/DelegateRunnerWithTimeout.java b/src/com/facebook/buck/junit/DelegateRunnerWithTimeout.java
new file mode 100644
index 0000000..07c2ec3
--- /dev/null
+++ b/src/com/facebook/buck/junit/DelegateRunnerWithTimeout.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.facebook.buck.util.concurrent.MoreExecutors;
+
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.RunNotifier;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+/**
+ * {@link Runner} that composes a {@link Runner} that enforces a default timeout when running a
+ * test.
+ */
+class DelegateRunnerWithTimeout extends Runner {
+
+ private final Runner delegate;
+ private final long defaultTestTimeoutMillis;
+
+ DelegateRunnerWithTimeout(Runner delegate, long defaultTestTimeoutMillis) {
+ if (defaultTestTimeoutMillis <= 0) {
+ throw new IllegalArgumentException(String.format(
+ "defaultTestTimeoutMillis must be greater than zero but was: %s.",
+ defaultTestTimeoutMillis));
+ }
+ this.delegate = delegate;
+ this.defaultTestTimeoutMillis = defaultTestTimeoutMillis;
+ }
+
+ /**
+ * @return the description from the original {@link Runner} wrapped by this {@link Runner}.
+ */
+ @Override
+ public Description getDescription() {
+ return delegate.getDescription();
+ }
+
+ /**
+ * Runs the tests for this runner, but wraps the specified {@code notifier} with a
+ * {@link DelegateRunNotifier} that intercepts calls to the original {@code notifier}.
+ * The {@link DelegateRunNotifier} is what enables us to impose our default timeout.
+ */
+ @Override
+ public void run(RunNotifier notifier) {
+ final DelegateRunNotifier wrapper = new DelegateRunNotifier(
+ delegate, notifier, defaultTestTimeoutMillis);
+
+ // We run the Runner in an Executor so that we can tear it down if we need to.
+ ExecutorService executor = MoreExecutors.newSingleThreadExecutor();
+ Future<?> future = executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ delegate.run(wrapper);
+ }
+ });
+
+ // We poll the Executor to see if the Runner is complete. In the event that a test has exceeded
+ // the default timeout, we cancel the Runner to protect against the case where the test hangs
+ // forever.
+ while (true) {
+ if (future.isDone()) {
+ // Normal termination: hooray!
+ return;
+ } else if (wrapper.hasTestThatExceededTimeout()) {
+ // The test results that have been reported to the RunNotifier should still be output, but
+ // there may be tests that did not have a chance to run. Unfortunately, we have no way to
+ // tell the Runner to cancel only the runaway test.
+ executor.shutdownNow();
+ return;
+ } else {
+ // Tests are still running, so wait and try again.
+ try {
+ Thread.sleep(/* milliseconds */ 250L);
+ } catch (InterruptedException e) {
+ // Blargh, continue.
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/com/facebook/buck/junit/JUnitRunner.java b/src/com/facebook/buck/junit/JUnitRunner.java
index 4fff79a..0c0c102 100644
--- a/src/com/facebook/buck/junit/JUnitRunner.java
+++ b/src/com/facebook/buck/junit/JUnitRunner.java
@@ -18,6 +18,7 @@
import org.junit.Ignore;
import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
+import org.junit.internal.builders.AnnotatedBuilder;
import org.junit.internal.builders.JUnit4Builder;
import org.junit.runner.Computer;
import org.junit.runner.JUnitCore;
@@ -112,12 +113,29 @@
}
};
- return new AllDefaultPossibilitiesBuilder(
- /* canUseSuiteMethod */ true) {
+ return new AllDefaultPossibilitiesBuilder(/* canUseSuiteMethod */ true) {
@Override
protected JUnit4Builder junit4Builder() {
return jUnit4RunnerBuilder;
}
+
+ @Override
+ protected AnnotatedBuilder annotatedBuilder() {
+ // If there is no default timeout specified in .buckconfig, then use the original behavior
+ // of AllDefaultPossibilitiesBuilder.
+ if (defaultTestTimeoutMillis <= 0) {
+ return super.annotatedBuilder();
+ }
+
+ return new AnnotatedBuilder(this) {
+ @Override
+ public Runner buildRunner(Class<? extends Runner> runnerClass,
+ Class<?> testClass) throws Exception {
+ Runner originalRunner = super.buildRunner(runnerClass, testClass);
+ return new DelegateRunnerWithTimeout(originalRunner, defaultTestTimeoutMillis);
+ }
+ };
+ }
};
}
diff --git a/test/com/facebook/buck/junit/RunWithDefaultTimeoutIntegrationTest.java b/test/com/facebook/buck/junit/RunWithDefaultTimeoutIntegrationTest.java
new file mode 100644
index 0000000..0798174
--- /dev/null
+++ b/test/com/facebook/buck/junit/RunWithDefaultTimeoutIntegrationTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+
+import com.facebook.buck.testutil.integration.DebuggableTemporaryFolder;
+import com.facebook.buck.testutil.integration.ProjectWorkspace;
+import com.facebook.buck.testutil.integration.ProjectWorkspace.ProcessResult;
+import com.facebook.buck.testutil.integration.TestDataHelper;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.internal.builders.AnnotatedBuilder;
+import org.junit.internal.builders.JUnit4Builder;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+
+import java.io.IOException;
+
+public class RunWithDefaultTimeoutIntegrationTest {
+
+ @Rule
+ public DebuggableTemporaryFolder temporaryFolder = new DebuggableTemporaryFolder();
+
+ /**
+ * This test verifies that the default timeout declared in {@code .buckconfig} works in the
+ * presence of the {@link RunWith} annotation.
+ * <p>
+ * We implement support for a default timeout declared in {@code .buckconfig} by implementing our
+ * own {@link BlockJUnit4ClassRunner}, {@link BuckBlockJUnit4ClassRunner}. We have tweaked our
+ * JUnit runner to use a {@link JUnit4Builder} that creates a {@link BuckBlockJUnit4ClassRunner}
+ * whenever a {@link JUnit4Builder} is requested.
+ * <p>
+ * However, we have a problem when the {@link RunWith} annotation is used. When {@link RunWith} is
+ * present, JUnit requests an {@link AnnotatedBuilder} instead of a {@link JUnit4Builder}.
+ * Because Robolectric requires the use of {@link RunWith}, this situation is common in Android
+ * testing.
+ * <p>
+ * To circumvent this issue, when possible, we create our own {@link AnnotatedBuilder} that
+ * delegates to the original {@link AnnotatedBuilder}, but inserts the timeout logic that we need.
+ */
+ @Test
+ public void testRunWithHonorsDefaultTimeoutOnTestThatRunsLong() throws IOException {
+ ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
+ this, "run_with_timeout", temporaryFolder);
+ workspace.setUp();
+
+ ProcessResult testResult = workspace.runBuckCommand("test", "//:TestThatTakesTooLong");
+ testResult.assertExitCode("Should fail due to exceeding timeout.", 1);
+ assertThat(testResult.getStderr(), containsString("timed out after 3000 milliseconds"));
+ }
+
+ @Test
+ public void testRunWithHonorsDefaultTimeoutOnTestThatRunsForever() throws IOException {
+ ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
+ this, "run_with_timeout", temporaryFolder);
+ workspace.setUp();
+
+ ProcessResult testResult = workspace.runBuckCommand("test", "//:TestThatRunsForever");
+ testResult.assertExitCode("Should fail due to exceeding timeout.", 1);
+ assertThat(testResult.getStderr(), containsString("timed out after 3000 milliseconds"));
+ }
+
+ @Test
+ public void testRunWithLetsTimeoutAnnotationOverrideDefaultTimeout() throws IOException {
+ ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
+ this, "run_with_timeout", temporaryFolder);
+ workspace.setUp();
+
+ ProcessResult testResult = workspace.runBuckCommand("test",
+ "//:TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation");
+ testResult.assertExitCode(0);
+ }
+
+ @Test
+ public void testRunWithLetsTimeoutRuleOverrideDefaultTimeout() throws IOException {
+ ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
+ this, "run_with_timeout", temporaryFolder);
+ workspace.setUp();
+
+ ProcessResult testResult = workspace.runBuckCommand("test",
+ "//:TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule");
+ testResult.assertExitCode(0);
+ }
+
+}
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/.buckconfig b/test/com/facebook/buck/junit/testdata/run_with_timeout/.buckconfig
new file mode 100644
index 0000000..ae7d573
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/.buckconfig
@@ -0,0 +1,3 @@
+[test]
+ # Maximum timeout of 3s per test.
+ timeout = 3000
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/BUCK b/test/com/facebook/buck/junit/testdata/run_with_timeout/BUCK
new file mode 100644
index 0000000..e64c201
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/BUCK
@@ -0,0 +1,62 @@
+java_library(
+ name = 'NotBuckBlockJUnit4ClassRunner',
+ srcs = [ 'NotBuckBlockJUnit4ClassRunner.java', ],
+ deps = [
+ ':junit',
+ ],
+)
+
+java_test(
+ name = 'TestThatTakesTooLong',
+ srcs = [ 'TestThatTakesTooLong.java', ],
+ deps = [
+ ':junit',
+ ':NotBuckBlockJUnit4ClassRunner',
+ ],
+)
+
+java_test(
+ name = 'TestThatRunsForever',
+ srcs = [ 'TestThatRunsForever.java', ],
+ deps = [
+ ':junit',
+ ':NotBuckBlockJUnit4ClassRunner',
+ ],
+)
+
+java_test(
+ name = 'TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation',
+ srcs = [ 'TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation.java', ],
+ deps = [
+ ':junit',
+ ':NotBuckBlockJUnit4ClassRunner',
+ ],
+)
+
+java_test(
+ name = 'TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule',
+ srcs = [ 'TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule.java', ],
+ deps = [
+ ':junit',
+ ':NotBuckBlockJUnit4ClassRunner',
+ ],
+)
+
+prebuilt_jar(
+ name = 'junit',
+ binary_jar = 'junit-4.11.jar',
+ deps = [
+ ':hamcrest-core',
+ ':hamcrest-library',
+ ],
+)
+
+prebuilt_jar(
+ name = 'hamcrest-core',
+ binary_jar = 'hamcrest-core-1.3.jar',
+)
+
+prebuilt_jar(
+ name = 'hamcrest-library',
+ binary_jar = 'hamcrest-library-1.3.jar',
+)
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/NotBuckBlockJUnit4ClassRunner.java b/test/com/facebook/buck/junit/testdata/run_with_timeout/NotBuckBlockJUnit4ClassRunner.java
new file mode 100644
index 0000000..7ac2341
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/NotBuckBlockJUnit4ClassRunner.java
@@ -0,0 +1,33 @@
+/*
+ * 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.example;
+
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.InitializationError;
+
+/**
+ * A nominal subclass of {@link BlockJUnit4ClassRunner} that can be used with {@link RunWith}. This
+ * is a good test case because {@code org.robolectric.RobolectricTestRunner} is also a subclass of
+ * {@link BlockJUnit4ClassRunner}.
+ */
+public class NotBuckBlockJUnit4ClassRunner extends BlockJUnit4ClassRunner {
+
+ public NotBuckBlockJUnit4ClassRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+}
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation.java b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation.java
new file mode 100644
index 0000000..ff90b43
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation.java
@@ -0,0 +1,39 @@
+/*
+ * 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.example;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(NotBuckBlockJUnit4ClassRunner.class)
+public class TestThatExceedsDefaultTimeoutButIsLessThanTimeoutAnnotation {
+
+ /**
+ * This test should take 5 seconds, which is longer than the default timeout specified in
+ * {@code .buckconfig}, which is 3 seconds. However, the timeout specified by the annotation
+ * (7 seconds) should take precedence, so this test should pass.
+ */
+ @Test(timeout = 7000)
+ public void testThatRunsForMoreThanThreeSecondsButForLessThanSevenSeconds() {
+ try {
+ Thread.sleep(/* millis */ 5 * 1000);
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+
+}
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule.java b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule.java
new file mode 100644
index 0000000..c532a97
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule.java
@@ -0,0 +1,44 @@
+/*
+ * 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.example;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.Timeout;
+import org.junit.runner.RunWith;
+
+@RunWith(NotBuckBlockJUnit4ClassRunner.class)
+public class TestThatExceedsDefaultTimeoutButIsLessThanTimeoutRule {
+
+ @Rule
+ public Timeout timeout = new Timeout(/* millis */ 7000);
+
+ /**
+ * This test should take 5 seconds, which is longer than the default timeout specified in
+ * {@code .buckconfig}, which is 3 seconds. However, the timeout specified by the {@link Timeout}
+ * (7 seconds) should take precedence, so this test should pass.
+ */
+ @Test
+ public void testThatRunsForMoreThanThreeSecondsButForLessThanSevenSeconds() {
+ try {
+ Thread.sleep(/* millis */ 5 * 1000);
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+
+}
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatRunsForever.java b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatRunsForever.java
new file mode 100644
index 0000000..c011cd8
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatRunsForever.java
@@ -0,0 +1,40 @@
+/*
+ * 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.example;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(NotBuckBlockJUnit4ClassRunner.class)
+public class TestThatRunsForever {
+
+ /**
+ * If the default timeout in {@code .buckconfig} is set to 3 seconds, as expected, then this test
+ * should fail due to a timeout.
+ */
+ @Test
+ public void testThatRunsForever() {
+ while (true) {
+ try {
+ Thread.sleep(/* millis */ 5 * 1000);
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+ }
+
+}
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatTakesTooLong.java b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatTakesTooLong.java
new file mode 100644
index 0000000..d35e96c
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/TestThatTakesTooLong.java
@@ -0,0 +1,38 @@
+/*
+ * 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.example;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(NotBuckBlockJUnit4ClassRunner.class)
+public class TestThatTakesTooLong {
+
+ /**
+ * If the default timeout in {@code .buckconfig} is set to 3 seconds, as expected, then this test
+ * should fail due to a timeout.
+ */
+ @Test
+ public void testShouldBlockForSixSeconds() {
+ try {
+ Thread.sleep(/* millis */ 6 * 1000);
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+
+}
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/hamcrest-core-1.3.jar b/test/com/facebook/buck/junit/testdata/run_with_timeout/hamcrest-core-1.3.jar
new file mode 100644
index 0000000..9d5fe16
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/hamcrest-core-1.3.jar
Binary files differ
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/hamcrest-library-1.3.jar b/test/com/facebook/buck/junit/testdata/run_with_timeout/hamcrest-library-1.3.jar
new file mode 100644
index 0000000..9eac80d
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/hamcrest-library-1.3.jar
Binary files differ
diff --git a/test/com/facebook/buck/junit/testdata/run_with_timeout/junit-4.11.jar b/test/com/facebook/buck/junit/testdata/run_with_timeout/junit-4.11.jar
new file mode 100644
index 0000000..aaf7444
--- /dev/null
+++ b/test/com/facebook/buck/junit/testdata/run_with_timeout/junit-4.11.jar
Binary files differ