Skip to content

Commit

Permalink
implemented timeout decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
maciejmikosik committed Aug 23, 2018
1 parent 343c8e4 commit 91aa23a
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 0 deletions.
4 changes: 4 additions & 0 deletions main/doc/tutorial.md
Expand Up @@ -276,6 +276,10 @@ Sometimes your project's production code (the code you test) loads bytecode dyna

Using `ThreadLocal` is popular way to avoid synchronization issues for static resources that don't need to be global (cache, network connections pool, etc.). If `ThreadLocal` reference is static, then running 2 tests using the same thread makes one test affecting the other. To isolate them use `threadScoped(test)` which makes each `Case` to be run in different thread. This does not make them run concurrently, because original thread joins new thread (blocks until new thread finishes).

### timeout

Tests can take a long time to finish. Sometimes they can take forever because of buggy code. You can limit maximum time they have using `timeout(time, test)`. If `Case` takes longer than specified `time` in seconds, then `Case` is interrupted. Tested code is responsive to interruption if it blocks on method throwing `InterruptedException` or if it checks interruption flag `Thread.interrupted()` manually. If code is responsive to interruption, then `Case.run()` is aborted and `InterruptedException` is propagated as test result. If code is not responsive to interruption then `Case.run()` call has to block until test finishes. However result of this finished test is ignored and `InterruptedException` is being thrown instead.

# reporting

Once you run the test and cache results, you are ready to present report.
Expand Down
39 changes: 39 additions & 0 deletions main/java/org/quackery/run/Runners.java
Expand Up @@ -10,6 +10,9 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -80,6 +83,42 @@ public void run() throws Throwable {
};
}

public static Test timeout(final double time, Test test) {
check(time >= 0);
check(test != null);
return new TraversingDecorator() {
protected Case decorateCase(Case cas) {
return timeout(time, cas);
}
}.decorate(test);
}

private static Case timeout(final double time, final Case cas) {
return new Case(cas.name) {
public void run() throws Throwable {
final Thread caller = Thread.currentThread();
ScheduledFuture<?> alarm = timeoutScheduler.schedule(
new Runnable() {
public void run() {
caller.interrupt();
}
},
(long) (time * 1e9),
NANOSECONDS);
try {
cas.run();
} finally {
alarm.cancel(true);
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
}
};
}

private static final ScheduledExecutorService timeoutScheduler = new ScheduledThreadPoolExecutor(0);

public static Test threadScoped(Test root) {
check(root != null);
return new TraversingDecorator() {
Expand Down
2 changes: 2 additions & 0 deletions test/java/org/quackery/TestAll.java
Expand Up @@ -13,6 +13,7 @@
import static org.quackery.run.TestRunnersRunConcurrent.test_runners_run_concurrent;
import static org.quackery.run.TestRunnersRunIn.test_runners_run_in;
import static org.quackery.run.TestRunnersThreadScoped.test_runners_thread_scoped;
import static org.quackery.run.TestRunnersTimeout.test_runners_timeout;

public class TestAll {
public static void main(String[] args) throws Throwable {
Expand All @@ -26,6 +27,7 @@ public static void main(String[] args) throws Throwable {
test_runners_run();
test_runners_run_in();
test_runners_run_concurrent();
test_runners_timeout();
test_runners_thread_scoped();
test_runners_class_loader_scoped();

Expand Down
96 changes: 96 additions & 0 deletions test/java/org/quackery/run/TestRunnersTimeout.java
@@ -0,0 +1,96 @@
package org.quackery.run;

import static org.quackery.report.AssertException.assertTrue;
import static org.quackery.run.Runners.timeout;
import static org.quackery.run.TestingDecorators.decorator_preserves_case_result;
import static org.quackery.run.TestingDecorators.decorator_preserves_names_and_structure;
import static org.quackery.run.TestingDecorators.decorator_runs_cases_lazily;
import static org.quackery.run.TestingDecorators.decorator_validates_arguments;
import static org.quackery.testing.Testing.fail;
import static org.quackery.testing.Testing.mockCase;
import static org.quackery.testing.Testing.sleep;
import static org.quackery.testing.Testing.sleepBusy;

import java.util.concurrent.atomic.AtomicBoolean;

import org.quackery.Case;
import org.quackery.QuackeryException;
import org.quackery.Test;
import org.quackery.help.Decorator;

public class TestRunnersTimeout {
public static void test_runners_timeout() throws Throwable {
Decorator decorator = new Decorator() {
public Test decorate(Test test) {
return timeout(1, test);
}
};

decorator_preserves_names_and_structure(decorator);
decorator_preserves_case_result(decorator);
decorator_validates_arguments(decorator);
decorator_runs_cases_lazily(decorator);

interrupts_interruptible_case();
interrupts_uninterruptible_successful_case();
interrupts_uninterruptible_failing_case();
validates_arguments();
}

private static void interrupts_interruptible_case() throws Throwable {
final AtomicBoolean interrupted = new AtomicBoolean(false);

Test test = timeout(0.01, new Case("case") {
public void run() throws InterruptedException {
try {
sleep(0.02);
interrupted.set(false);
} catch (InterruptedException e) {
interrupted.set(true);
throw e;
}
}
});

try {
((Case) test).run();
fail();
} catch (InterruptedException e) {}
assertTrue(interrupted.get());
}

private static void interrupts_uninterruptible_successful_case() throws Throwable {
Test test = timeout(0.01, new Case("case") {
public void run() {
sleepBusy(0.02);
}
});

try {
((Case) test).run();
fail();
} catch (InterruptedException e) {}
}

private static void interrupts_uninterruptible_failing_case() throws Throwable {
Test test = timeout(0.01, new Case("case") {
public void run() {
sleepBusy(0.02);
throw new RuntimeException();
}
});

try {
((Case) test).run();
fail();
} catch (InterruptedException e) {}
}

private static void validates_arguments() {
Case test = mockCase("case");
try {
timeout(-0.001, test);
fail();
} catch (QuackeryException e) {}
}
}
6 changes: 6 additions & 0 deletions test/java/org/quackery/testing/Testing.java
Expand Up @@ -133,4 +133,10 @@ public void run() {
public static void sleep(double time) throws InterruptedException {
Thread.sleep((long) (time * 1e3));
}

public static void sleepBusy(double time) {
long nanos = (long) (time * 1e9);
long start = System.nanoTime();
while (System.nanoTime() - start < nanos) {}
}
}

0 comments on commit 91aa23a

Please sign in to comment.