Skip to content

Commit

Permalink
Create TimeoutRule as replacement for junit Timeout issue#3829
Browse files Browse the repository at this point in the history
org.junit.rules.Timeout is incompatible with Robolectric's scheduler because it spawns a new thread for the test.
This commit introduces TimeoutRule which behaves like `@Test(timeout = )` but for all tests in the file
  • Loading branch information
fknives authored and utzcoz committed May 3, 2024
1 parent 6da154c commit 3ab3afe
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import org.junit.runners.model.TestTimedOutException;

/**
* Similar to JUnit's {@link org.junit.internal.runners.statements.FailOnTimeout}, but runs the
* test on the current thread (with a timer on a new thread) rather than the other way around.
* Similar to JUnit's {@link org.junit.internal.runners.statements.FailOnTimeout}, but runs the test
* on the current thread (with a timer on a new thread) rather than the other way around.
*/
class TimeLimitedStatement extends Statement {
public class TimeLimitedStatement extends Statement {

private final long timeout;
private final Statement delegate;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.robolectric.junit.rules;

import androidx.annotation.NonNull;
import java.util.concurrent.TimeUnit;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.robolectric.internal.TimeLimitedStatement;

/**
* Robolectric's replacement for JUnit's {@link org.junit.rules.Timeout Timeout}.
*
* <p>{@link org.junit.rules.Timeout Timeout} spawns a new thread, which is not compatible with
* Robolectric's Scheduler. Instead this Rule uses {@link
* org.robolectric.internal.TimeLimitedStatement TimeLimitedStatement} like {@code @Test(timeout=)}
* does.
*
* <p>Example usage:
*
* <pre>
* {@literal @}Rule public final TimeoutRule timeoutRule = TimeoutRule.seconds(40);
*
* {@literal @}Test
* public void testWhichShouldFinishIn40Seconds() {
* // ...
* }
* </pre>
*/
public class TimeoutRule implements TestRule {

private final long timeout;
@NonNull private final TimeUnit timeUnit;

/**
* Create a {@code TimeoutRule} instance with the timeout specified at the timeUnit of granularity
* of the provided {@code TimeUnit}.
*
* @param timeout the maximum time to allow the test to run before it should timeout
* @param timeUnit the time unit for the {@code timeout}
*/
public TimeoutRule(long timeout, @NonNull TimeUnit timeUnit) {
this.timeout = timeout;
this.timeUnit = timeUnit;
}

@Override
public Statement apply(Statement base, Description description) {
return new TimeLimitedStatement(timeUnit.toMillis(timeout), base);
}

/**
* Creates a {@link org.robolectric.junit.rules.TimeoutRule TimeoutRule} that will timeout a test
* after the given duration, in milliseconds.
*/
public static TimeoutRule millis(long millis) {
return new TimeoutRule(millis, TimeUnit.MILLISECONDS);
}

/**
* Creates a {@link org.robolectric.junit.rules.TimeoutRule TimeoutRule} that will timeout a test
* after the given duration, in seconds.
*/
public static TimeoutRule seconds(long seconds) {
return new TimeoutRule(seconds, TimeUnit.SECONDS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.robolectric.junit.rules;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;

/** Tests for {@link TimeoutRule}. */
@RunWith(AndroidJUnit4.class)
public final class TimeoutRuleTest {

private final TimeoutRule rule = TimeoutRule.millis(200);

@Rule public RuleChain chain = RuleChain.outerRule(rule);

@Test
public void testNotTimingOutFinishes() throws InterruptedException {
Thread.sleep(50);
}

@Test
public void testTimingOutIsInterrupted() {
Assert.assertThrows(
InterruptedException.class,
() -> {
Thread.sleep(1000);
throw new IllegalArgumentException();
});
}

@Test
public void verifyErrorMessage() {
final TimeoutRule timeoutRule = TimeoutRule.millis(50);
final Statement threadSleepStatement =
new Statement() {
@Override
public void evaluate() throws Throwable {
Thread.sleep(1000);
}
};
final Statement wrappedStatement = timeoutRule.apply(threadSleepStatement, Description.EMPTY);

final TestTimedOutException exception =
Assert.assertThrows(TestTimedOutException.class, wrappedStatement::evaluate);
Assert.assertEquals("test timed out after 50 milliseconds", exception.getMessage());
}
}

0 comments on commit 3ab3afe

Please sign in to comment.