|  | /* | 
|  | * 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(); | 
|  | } | 
|  | } |