Skip to content

Commit

Permalink
Step definitions and hooks can now specify a timeout (milliseconds) a…
Browse files Browse the repository at this point in the history
…fter which a is thrown if the stepdef/hook has not completed. Closes cucumber#343
  • Loading branch information
aslakhellesoy committed Jun 18, 2012
1 parent deae442 commit b1d8b18
Show file tree
Hide file tree
Showing 16 changed files with 235 additions and 101 deletions.
2 changes: 2 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## [Git master](https://github.com/cucumber/cucumber-jvm/compare/v1.0.9...master)

* [Java/Groovy] Step definitions and hooks can now specify a timeout (milliseconds) after which a `TimeoutException` is thrown if the stepdef/hook has not completed.
Please note that for Groovy, `sleep(int)` is not interruptible, so in order for sleeps to work your code must use `Thread.sleep(int)` ([#343](https://github.com/cucumber/cucumber-jvm/issues/343) Aslak Hellesøy)
* [Java] More explanatary exception if a hook is declared with bad parameter types. (Aslak Hellesøy)
* [Core/JUnit] JUnit report has time reported as seconds instead of millis. ([#347](https://github.com/cucumber/cucumber-jvm/issues/347) Aslak Hellesøy)
* [Core] List legal enum values if conversion fails ([#344](https://github.com/cucumber/cucumber-jvm/issues/344) Aslak Hellesøy)
Expand Down
37 changes: 37 additions & 0 deletions core/src/main/java/cucumber/runtime/Timeout.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cucumber.runtime;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

public class Timeout {
public static <T> T timeout(Callback<T> callback, int timeoutMillis) throws Throwable {
if (timeoutMillis == 0) {
return callback.call();
} else {
final Thread executionThread = Thread.currentThread();
final AtomicBoolean done = new AtomicBoolean();
ScheduledFuture<?> timer = Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
@Override
public void run() {
if (!done.get()) {
executionThread.interrupt();
}
}
}, timeoutMillis, TimeUnit.MILLISECONDS);
try {
T result = callback.call();
timer.cancel(true);
return result;
} catch (InterruptedException timeout) {
throw new TimeoutException("Timed out after " + timeoutMillis + "ms.");
}
}
}

public interface Callback<T> {
T call() throws Throwable;
}
}
43 changes: 11 additions & 32 deletions core/src/main/java/cucumber/runtime/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

public class Utils {
public static <T> List<T> listOf(int size, T obj) {
Expand All @@ -35,35 +30,19 @@ public static boolean hasConstructor(Class<?> clazz, Class[] paramTypes) {
}

public static Object invoke(final Object target, final Method method, int timeoutMillis, final Object... args) throws Throwable {
try {
if (timeoutMillis == 0) {
return method.invoke(target, args);
} else {
final Thread executionThread = Thread.currentThread();
final AtomicBoolean done = new AtomicBoolean();
ScheduledFuture<?> timer = Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
@Override
public void run() {
System.out.println("done = " + done);
if (!done.get()) {
executionThread.interrupt();
}
}
}, timeoutMillis, TimeUnit.MILLISECONDS);
return Timeout.timeout(new Timeout.Callback<Object>() {
@Override
public Object call() throws Throwable {
try {
Object result = invoke(target, method, 0, args);
timer.cancel(true);
return result;
} catch (InterruptedException timeout) {
throw new TimeoutException("Timed out after " + timeoutMillis + "ms.");
return method.invoke(target, args);
} catch (IllegalArgumentException e) {
throw new CucumberException("Failed to invoke " + MethodFormat.FULL.format(method), e);
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (IllegalAccessException e) {
throw new CucumberException("Failed to invoke " + MethodFormat.FULL.format(method), e);
}
}
} catch (IllegalArgumentException e) {
throw new CucumberException("Failed to invoke " + MethodFormat.FULL.format(method), e);
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (IllegalAccessException e) {
throw new CucumberException("Failed to invoke " + MethodFormat.FULL.format(method), e);
}
}, timeoutMillis);
}
}
66 changes: 66 additions & 0 deletions core/src/test/java/cucumber/runtime/TimeoutTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cucumber.runtime;

import org.junit.Test;

import java.util.concurrent.TimeoutException;

import static java.lang.Thread.sleep;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

public class TimeoutTest {
@Test
public void doesnt_time_out_if_it_takes_too_long() throws Throwable {
final Slow slow = new Slow();
String what = Timeout.timeout(new Timeout.Callback<String>() {
@Override
public String call() throws Throwable {
return slow.slow();
}
}, 50);
assertEquals("slow", what);
}

@Test(expected = TimeoutException.class)
public void times_out_if_it_takes_too_long() throws Throwable {
final Slow slow = new Slow();
Timeout.timeout(new Timeout.Callback<String>() {
@Override
public String call() throws Throwable {
return slow.slower();
}
}, 50);
fail();
}

@Test(expected = TimeoutException.class)
public void times_out_infinite_loop_if_it_takes_too_long() throws Throwable {
final Slow slow = new Slow();
Timeout.timeout(new Timeout.Callback<Void>() {
@Override
public Void call() throws Throwable {
slow.infinite();
return null;
}
}, 10);
fail();
}

public static class Slow {
public String slow() throws InterruptedException {
sleep(10);
return "slow";
}

public String slower() throws InterruptedException {
sleep(100);
return "slower";
}

public void infinite() throws InterruptedException {
while (true) {
sleep(1);
}
}
}
}
26 changes: 0 additions & 26 deletions core/src/test/java/cucumber/runtime/UtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,4 @@ public class NonStaticInnerClass {

public static class StaticInnerClass {
}

@Test
public void doesnt_time_out_if_it_takes_too_long() throws Throwable {
Slow slow = new Slow();
Object what = Utils.invoke(slow, Slow.class.getMethod("slow"), 50);
assertEquals("slow", what);
}

@Test(expected = TimeoutException.class)
public void times_out_if_it_takes_too_long() throws Throwable {
Slow slow = new Slow();
Utils.invoke(slow, Slow.class.getMethod("slower"), 50);
fail();
}

public static class Slow {
public String slow() throws InterruptedException {
sleep(10);
return "slow";
}

public String slower() throws InterruptedException {
sleep(100);
return "slower";
}
}
}
6 changes: 5 additions & 1 deletion groovy/src/main/code_generator/I18n.groovy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import java.util.regex.Pattern;
public class ${i18n.underscoredIsoCode.toUpperCase()} {
<% i18n.codeKeywords.each { kw -> %>
public static void ${kw}(Pattern regexp, Closure body) throws Throwable {
GroovyBackend.instance.addStepDefinition(regexp, body);
GroovyBackend.instance.addStepDefinition(regexp, 0, body);
}

public static void ${kw}(Pattern regexp, int timeoutMillis, Closure body) throws Throwable {
GroovyBackend.instance.addStepDefinition(regexp, timeoutMillis, body);
}
<% } %>
}
33 changes: 24 additions & 9 deletions groovy/src/main/java/cucumber/runtime/groovy/GroovyBackend.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
import groovy.lang.Closure;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import groovy.transform.ThreadInterrupt;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerInvocationException;

import java.io.IOException;
import java.io.InputStreamReader;
Expand All @@ -38,8 +42,15 @@ public class GroovyBackend implements Backend {
private Object groovyWorld;
private Glue glue;

private static GroovyShell createShell() {
CompilerConfiguration compilerConfig = new CompilerConfiguration();
// Probably not needed:
// compilerConfig.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt.class));
return new GroovyShell(Thread.currentThread().getContextClassLoader(), new Binding(), compilerConfig);
}

public GroovyBackend(ResourceLoader resourceLoader) {
this(new GroovyShell(), resourceLoader);
this(createShell(), resourceLoader);
}

public GroovyBackend(GroovyShell shell, ResourceLoader resourceLoader) {
Expand Down Expand Up @@ -112,25 +123,29 @@ public String getSnippet(Step step) {
return snippetGenerator.getSnippet(step);
}

public void addStepDefinition(Pattern regexp, Closure body) {
glue.addStepDefinition(new GroovyStepDefinition(regexp, body, currentLocation(), instance));
public void addStepDefinition(Pattern regexp, int timeoutMillis, Closure body) {
glue.addStepDefinition(new GroovyStepDefinition(regexp, timeoutMillis, body, currentLocation(), instance));
}

public void registerWorld(Closure closure) {
worldClosure = closure;
}

void addBeforeHook(TagExpression tagExpression, Closure body) {
glue.addBeforeHook(new GroovyHookDefinition(body, tagExpression, currentLocation(), instance));
public void addBeforeHook(TagExpression tagExpression, int timeoutMillis, Closure body) {
glue.addBeforeHook(new GroovyHookDefinition(tagExpression, timeoutMillis, body, currentLocation(), instance));
}

public void addAfterHook(TagExpression tagExpression, Closure body) {
glue.addAfterHook(new GroovyHookDefinition(body, tagExpression, currentLocation(), instance));
public void addAfterHook(TagExpression tagExpression, int timeoutMillis, Closure body) {
glue.addAfterHook(new GroovyHookDefinition(tagExpression, timeoutMillis, body, currentLocation(), instance));
}

public void invoke(Closure body, Object[] args) {
public void invoke(Closure body, Object[] args) throws Throwable {
body.setDelegate(getGroovyWorld());
body.call(args);
try {
body.call(args);
} catch(InvokerInvocationException e) {
throw e.getCause();
}
}

private Object getGroovyWorld() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@

import cucumber.runtime.HookDefinition;
import cucumber.runtime.ScenarioResult;
import cucumber.runtime.Timeout;
import gherkin.TagExpression;
import gherkin.formatter.model.Tag;
import groovy.lang.Closure;

import java.util.Collection;

public class GroovyHookDefinition implements HookDefinition {
private final Closure body;
private final TagExpression tagExpression;
private final int timeoutMillis;
private final Closure body;
private final GroovyBackend backend;
private final StackTraceElement location;

public GroovyHookDefinition(Closure body, TagExpression tagExpression, StackTraceElement location, GroovyBackend backend) {
this.body = body;
public GroovyHookDefinition(TagExpression tagExpression, int timeoutMillis, Closure body, StackTraceElement location, GroovyBackend backend) {
this.tagExpression = tagExpression;
this.timeoutMillis = timeoutMillis;
this.body = body;
this.location = location;
this.backend = backend;
}
Expand All @@ -27,8 +30,14 @@ public String getLocation(boolean detail) {
}

@Override
public void execute(ScenarioResult scenarioResult) throws Throwable {
backend.invoke(body, new Object[]{scenarioResult});
public void execute(final ScenarioResult scenarioResult) throws Throwable {
Timeout.timeout(new Timeout.Callback<Object>() {
@Override
public Object call() throws Throwable {
backend.invoke(body, new Object[]{scenarioResult});
return null;
}
}, timeoutMillis);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import cucumber.runtime.JdkPatternArgumentMatcher;
import cucumber.runtime.ParameterType;
import cucumber.runtime.StepDefinition;
import cucumber.runtime.Timeout;
import gherkin.I18n;
import gherkin.formatter.Argument;
import gherkin.formatter.model.Step;
Expand All @@ -13,14 +14,16 @@
import java.util.regex.Pattern;

public class GroovyStepDefinition implements StepDefinition {
private final Pattern pattern;
private final JdkPatternArgumentMatcher argumentMatcher;
private final int timeoutMillis;
private final Closure body;
private final StackTraceElement location;
private final Pattern pattern;
private GroovyBackend backend;

public GroovyStepDefinition(Pattern pattern, Closure body, StackTraceElement location, GroovyBackend backend) {
public GroovyStepDefinition(Pattern pattern, int timeoutMillis, Closure body, StackTraceElement location, GroovyBackend backend) {
this.pattern = pattern;
this.timeoutMillis = timeoutMillis;
this.backend = backend;
this.argumentMatcher = new JdkPatternArgumentMatcher(pattern);
this.body = body;
Expand All @@ -44,8 +47,14 @@ public List<ParameterType> getParameterTypes() {
return result;
}

public void execute(I18n i18n, Object[] args) throws Throwable {
backend.invoke(body, args);
public void execute(I18n i18n, final Object[] args) throws Throwable {
Timeout.timeout(new Timeout.Callback<Object>() {
@Override
public Object call() throws Throwable {
backend.invoke(body, args);
return null;
}
}, timeoutMillis);
}

public boolean isDefinedAt(StackTraceElement stackTraceElement) {
Expand Down
Loading

0 comments on commit b1d8b18

Please sign in to comment.