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