package org.junit.internal.runners.statements;
import static java.lang.Long.MAX_VALUE;
import static java.lang.Math.atan;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.sleep;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.internal.runners.statements.FailOnTimeout.builder;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;
/**
* @author Asaf Ary, Stefan Birkner
*/
public class FailOnTimeoutTest {
private static final long TIMEOUT = 100;
private static final long DURATION_THAT_EXCEEDS_TIMEOUT = 60 * 60 * 1000; //1 hour
@Rule
public final ExpectedException thrown = ExpectedException.none();
private final TestStatement statement = new TestStatement();
private final FailOnTimeout failOnTimeout = builder().withTimeout(TIMEOUT, MILLISECONDS).build(statement);
@Test
public void throwsTestTimedOutException() throws Throwable {
thrown.expect(TestTimedOutException.class);
evaluateWithWaitDuration(DURATION_THAT_EXCEEDS_TIMEOUT);
}
@Test
public void throwExceptionWithNiceMessageOnTimeout() throws Throwable {
thrown.expectMessage("test timed out after 100 milliseconds");
evaluateWithWaitDuration(DURATION_THAT_EXCEEDS_TIMEOUT);
}
@Test
public void sendUpExceptionThrownByStatement() throws Throwable {
RuntimeException exception = new RuntimeException();
thrown.expect(is(exception));
evaluateWithException(exception);
}
@Test
public void throwExceptionIfTheSecondCallToEvaluateNeedsTooMuchTime()
throws Throwable {
thrown.expect(TestTimedOutException.class);
evaluateWithWaitDuration(0);
evaluateWithWaitDuration(DURATION_THAT_EXCEEDS_TIMEOUT);
}
@Test
public void throwTimeoutExceptionOnSecondCallAlthoughFirstCallThrowsException()
throws Throwable {
thrown.expectMessage("test timed out after 100 milliseconds");
try {
evaluateWithException(new RuntimeException());
} catch (Throwable expected) {
}
evaluateWithWaitDuration(DURATION_THAT_EXCEEDS_TIMEOUT);
}
@Test
public void throwsExceptionWithTimeoutValueAndTimeUnitSet()
throws Throwable {
try {
evaluateWithWaitDuration(DURATION_THAT_EXCEEDS_TIMEOUT);
fail("No exception was thrown when test timed out");
} catch (TestTimedOutException e) {
assertEquals(TIMEOUT, e.getTimeout());
assertEquals(TimeUnit.MILLISECONDS, e.getTimeUnit());
}
}
private void evaluateWithException(Exception exception) throws Throwable {
statement.nextException = exception;
statement.waitDuration = 0;
failOnTimeout.evaluate();
}
private void evaluateWithWaitDuration(long waitDuration) throws Throwable {
statement.nextException = null;
statement.waitDuration = waitDuration;
failOnTimeout.evaluate();
}
private static final class TestStatement extends Statement {
long waitDuration;
Exception nextException;
@Override
public void evaluate() throws Throwable {
sleep(waitDuration);
if (nextException != null) {
throw nextException;
}
}
}
@Test
public void stopEndlessStatement() throws Throwable {
InfiniteLoopStatement infiniteLoop = new InfiniteLoopStatement();
FailOnTimeout infiniteLoopTimeout = builder().withTimeout(TIMEOUT, MILLISECONDS).build(infiniteLoop);
try {
infiniteLoopTimeout.evaluate();
} catch (Exception timeoutException) {
sleep(20); // time to interrupt the thread
int firstCount = InfiniteLoopStatement.COUNT;
sleep(20); // time to increment the count
assertTrue("Thread has not been stopped.",
firstCount == InfiniteLoopStatement.COUNT);
}
}
private static final class InfiniteLoopStatement extends Statement {
private static int COUNT = 0;
@Override
public void evaluate() throws Throwable {
while (true) {
sleep(10); // sleep in order to enable interrupting thread
++COUNT;
}
}
}
@Test
public void stackTraceContainsRealCauseOfTimeout() throws Throwable {
StuckStatement stuck = new StuckStatement();
FailOnTimeout stuckTimeout = builder().withTimeout(TIMEOUT, MILLISECONDS).build(stuck);
try {
stuckTimeout.evaluate();
// We must not get here, we expect a timeout exception
fail("Expected timeout exception");
} catch (Exception timeoutException) {
StackTraceElement[] stackTrace = timeoutException.getStackTrace();
boolean stackTraceContainsTheRealCauseOfTheTimeout = false;
boolean stackTraceContainsOtherThanTheRealCauseOfTheTimeout = false;
for (StackTraceElement element : stackTrace) {
String methodName = element.getMethodName();
if ("theRealCauseOfTheTimeout".equals(methodName)) {
stackTraceContainsTheRealCauseOfTheTimeout = true;
}
if ("notTheRealCauseOfTheTimeout".equals(methodName)) {
stackTraceContainsOtherThanTheRealCauseOfTheTimeout = true;
}
}
assertTrue(
"Stack trace does not contain the real cause of the timeout",
stackTraceContainsTheRealCauseOfTheTimeout);
assertFalse(
"Stack trace contains other than the real cause of the timeout, which can be very misleading",
stackTraceContainsOtherThanTheRealCauseOfTheTimeout);
}
}
private static final class StuckStatement extends Statement {
@Override
public void evaluate() throws Throwable {
try {
// Must show up in stack trace
theRealCauseOfTheTimeout();
} catch (InterruptedException e) {
} finally {
// Must _not_ show up in stack trace
notTheRealCauseOfTheTimeout();
}
}
private void theRealCauseOfTheTimeout() throws InterruptedException {
sleep(MAX_VALUE);
}
private void notTheRealCauseOfTheTimeout() {
for (long now = currentTimeMillis(), eta = now + 1000L; now < eta; now = currentTimeMillis()) {
// Doesn't matter, just pretend to be busy
atan(now);
}
}
}
}