Skip to content

Commit

Permalink
[Java] Add BeforeAll and AfterAll hooks
Browse files Browse the repository at this point in the history
Work in progress to fix #515.

TODO:
 - [ ] How to deal with failure?
  - [ ] Test with JUnit4
  - [ ] Test with JUnit5
  - [ ] Test with TestNG
  - [ ] Test with CLI
  - [ ] Invoke around semantics?
 - [ ] How to report execution results?
  - [ ] TeamCity Plugin
  - [ ] Pretty formatter
  - [ ] Messages/Events
  • Loading branch information
mpkorstanje committed Jan 26, 2020
1 parent 218dcae commit 2f4e9da
Show file tree
Hide file tree
Showing 17 changed files with 205 additions and 9 deletions.
4 changes: 4 additions & 0 deletions core/src/main/java/io/cucumber/core/backend/Glue.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
@API(status = API.Status.STABLE)
public interface Glue {

void addBeforeAllHook(StaticHookDefinition beforeAllHook);

void addAfterAllHook(StaticHookDefinition afterAllHook);

void addStepDefinition(StepDefinition stepDefinition);

void addBeforeHook(HookDefinition beforeHook);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.cucumber.core.backend;

import org.apiguardian.api.API;

@API(status = API.Status.STABLE)
public interface StaticHookDefinition extends Located {

void execute();

int getOrder();
}
22 changes: 22 additions & 0 deletions core/src/main/java/io/cucumber/core/runner/CachingGlue.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.cucumber.core.backend.HookDefinition;
import io.cucumber.core.backend.ParameterTypeDefinition;
import io.cucumber.core.backend.ScenarioScoped;
import io.cucumber.core.backend.StaticHookDefinition;
import io.cucumber.core.backend.StepDefinition;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.gherkin.Step;
Expand Down Expand Up @@ -45,11 +46,13 @@ final class CachingGlue implements Glue {
private final List<DefaultDataTableCellTransformerDefinition> defaultDataTableCellTransformers = new ArrayList<>();
private final List<DocStringTypeDefinition> docStringTypeDefinitions = new ArrayList<>();

private final List<StaticHookDefinition> beforeAllHooks = new ArrayList<>();
private final List<CoreHookDefinition> beforeHooks = new ArrayList<>();
private final List<CoreHookDefinition> beforeStepHooks = new ArrayList<>();
private final List<StepDefinition> stepDefinitions = new ArrayList<>();
private final List<CoreHookDefinition> afterStepHooks = new ArrayList<>();
private final List<CoreHookDefinition> afterHooks = new ArrayList<>();
private final List<StaticHookDefinition> afterAllHooks = new ArrayList<>();

/*
* Storing the pattern that matches the step text allows us to cache the rather slow
Expand All @@ -67,6 +70,17 @@ final class CachingGlue implements Glue {
this.bus = bus;
}

@Override
public void addBeforeAllHook(StaticHookDefinition beforeAllHook) {
beforeAllHooks.add(beforeAllHook);

}

@Override
public void addAfterAllHook(StaticHookDefinition afterAllHook) {
afterAllHooks.add(afterAllHook);
}

@Override
public void addStepDefinition(StepDefinition stepDefinition) {
stepDefinitions.add(stepDefinition);
Expand Down Expand Up @@ -126,6 +140,10 @@ public void addDocStringType(DocStringTypeDefinition docStringType) {
docStringTypeDefinitions.add(docStringType);
}

List<StaticHookDefinition> getBeforeAllHooks() {
return new ArrayList<>(beforeAllHooks);
}

Collection<CoreHookDefinition> getBeforeHooks() {
return new ArrayList<>(beforeHooks);
}
Expand All @@ -146,6 +164,10 @@ Collection<CoreHookDefinition> getAfterStepHooks() {
return hooks;
}

List<StaticHookDefinition> getAfterAllHooks() {
return new ArrayList<>(afterAllHooks);
}

Collection<ParameterTypeDefinition> getParameterTypeDefinitions() {
return parameterTypeDefinitions;
}
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/java/io/cucumber/core/runner/Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.cucumber.core.api.TypeRegistryConfigurer;
import io.cucumber.core.backend.Backend;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.core.backend.StaticHookDefinition;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.gherkin.Pickle;
import io.cucumber.core.gherkin.Step;
Expand Down Expand Up @@ -70,6 +71,14 @@ public void runPickle(Pickle pickle) {
}
}

public void runBeforeAllHooks(){
glue.getBeforeAllHooks().forEach(StaticHookDefinition::execute);
}

public void runAfterAllHooks(){
glue.getAfterAllHooks().forEach(StaticHookDefinition::execute);
}

private List<SnippetGenerator> createSnippetGeneratorsForPickle(StepTypeRegistry stepTypeRegistry) {
return backends.stream()
.map(Backend::getSnippet)
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/io/cucumber/core/runtime/Runtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public void run() {
for (Feature feature : features) {
bus.send(new TestSourceRead(bus.getInstant(), feature.getUri(), feature.getSource()));
}
runnerSupplier.get().runBeforeAllHooks();

final List<Future<?>> executingPickles = features.stream()
.flatMap(feature -> feature.getPickles().stream())
Expand Down Expand Up @@ -119,7 +120,7 @@ public void run() {
} else if (thrown.size() > 1) {
throw new CompositeCucumberException(thrown);
}

runnerSupplier.get().runAfterAllHooks();
bus.send(new TestRunFinished(bus.getInstant()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.cucumber.examples.java;

import io.cucumber.java.After;
import io.cucumber.java.AfterAll;
import io.cucumber.java.Before;
import io.cucumber.java.BeforeAll;
import io.cucumber.java.Scenario;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
Expand All @@ -14,6 +16,16 @@
public class RpnCalculatorSteps {
private RpnCalculator calc;

@BeforeAll
public static void enable_super_math_engine(){
// System.enableSuperMaths()
}

@AfterAll
public static void disable_super_math_engine(){
// System.disableSuperMaths()
}

@Given("a calculator I just turned on")
public void a_calculator_I_just_turned_on() {
calc = new RpnCalculator();
Expand Down
4 changes: 3 additions & 1 deletion java/src/main/java/io/cucumber/java/After.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
String value() default "";

/**
* @return the order in which this hook should run. Higher numbers are run first.
* The order in which this hook should run. Higher numbers are run first.
* The default order is 10000.
*
* @return the order in which this hook should run.
*/
int order() default 10000;
}
25 changes: 25 additions & 0 deletions java/src/main/java/io/cucumber/java/AfterAll.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.cucumber.java;

import org.apiguardian.api.API;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Executes a method before all scenarios
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@API(status = API.Status.STABLE)
public @interface AfterAll {

/**
* The order in which this hook should run. Higher numbers are run first.
* The default order is 10000.
*
* @return the order in which this hook should run.
*/
int order() default 10000;
}
4 changes: 3 additions & 1 deletion java/src/main/java/io/cucumber/java/Before.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
String value() default "";

/**
* @return the order in which this hook should run. Lower numbers are run first.
* The order in which this hook should run. Lower numbers are run first.
* The default order is 10000.
*
* @return the order in which this hook should run.
*/
int order() default 10000;
}
25 changes: 25 additions & 0 deletions java/src/main/java/io/cucumber/java/BeforeAll.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.cucumber.java;

import org.apiguardian.api.API;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Executes a method after all scenarios
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@API(status = API.Status.STABLE)
public @interface BeforeAll {

/**
* The order in which this hook should run. Lower numbers are run first.
* The default order is 10000.
*
* @return the order in which this hook should run.
*/
int order() default 10000;
}
8 changes: 7 additions & 1 deletion java/src/main/java/io/cucumber/java/GlueAdaptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ void addDefinition(Method method, Annotation annotation) {
Before before = (Before) annotation;
String tagExpression = before.value();
glue.addBeforeHook(new JavaHookDefinition(method, tagExpression, before.order(), lookup));
} else if (annotationType.equals(BeforeAll.class)) {
BeforeAll beforeAll = (BeforeAll) annotation;
glue.addBeforeAllHook(new JavaStaticHookDefinition(method, beforeAll.order(), lookup));
} else if (annotationType.equals(After.class)) {
After after = (After) annotation;
String tagExpression = after.value();
glue.addAfterHook(new JavaHookDefinition(method, tagExpression, after.order(), lookup));
} else if (annotationType.equals(AfterAll.class)) {
AfterAll afterAll = (AfterAll) annotation;
glue.addAfterAllHook(new JavaStaticHookDefinition(method, afterAll.order(), lookup));
} else if (annotationType.equals(BeforeStep.class)) {
BeforeStep beforeStep = (BeforeStep) annotation;
String tagExpression = beforeStep.value();
Expand Down Expand Up @@ -58,7 +64,7 @@ void addDefinition(Method method, Annotation annotation) {
DefaultDataTableCellTransformer cellTransformer = (DefaultDataTableCellTransformer) annotation;
String[] emptyPatterns = cellTransformer.replaceWithEmptyString();
glue.addDefaultDataTableCellTransformer(new JavaDefaultDataTableCellTransformerDefinition(method, lookup, emptyPatterns));
} else if (annotationType.equals(DocStringType.class)){
} else if (annotationType.equals(DocStringType.class)) {
DocStringType docStringType = (DocStringType) annotation;
String contentType = docStringType.contentType();
glue.addDocStringType(new JavaDocStringTypeDefinition(contentType, method, lookup));
Expand Down
52 changes: 52 additions & 0 deletions java/src/main/java/io/cucumber/java/JavaStaticHookDefinition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.cucumber.java;

import io.cucumber.core.backend.Lookup;
import io.cucumber.core.backend.StaticHookDefinition;

import java.lang.reflect.Method;

import static io.cucumber.java.InvalidMethodSignatureException.builder;
import static java.lang.reflect.Modifier.isStatic;

final class JavaStaticHookDefinition extends AbstractGlueDefinition implements StaticHookDefinition {

private final int order;
private final Lookup lookup;

JavaStaticHookDefinition(Method method, int order, Lookup lookup) {
super(requireValidMethod(method), lookup);
this.order = order;
this.lookup = lookup;
}

private static Method requireValidMethod(Method method) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 0) {
throw createInvalidSignatureException(method);
}

if (!isStatic(method.getModifiers())) {
throw createInvalidSignatureException(method);
}

return method;
}

private static InvalidMethodSignatureException createInvalidSignatureException(Method method) {
return builder(method)
.addAnnotation(BeforeAll.class)
.addAnnotation(AfterAll.class)
.addSignature("public static void before_or_after_all()")
.build();
}

@Override
public void execute() {
Invoker.invoke(this, lookup.getInstance(method.getDeclaringClass()), method);
}

@Override
public int getOrder() {
return order;
}
}
11 changes: 8 additions & 3 deletions java/src/main/java/io/cucumber/java/MethodScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.function.BiConsumer;

import static io.cucumber.java.InvalidMethodException.createInvalidMethodException;
import static java.lang.reflect.Modifier.isAbstract;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;

final class MethodScanner {

Expand All @@ -27,8 +29,9 @@ static void scan(Class<?> aClass, BiConsumer<Method, Annotation> consumer) {
}

private static boolean isInstantiable(Class<?> clazz) {
boolean isNonStaticInnerClass = !Modifier.isStatic(clazz.getModifiers()) && clazz.getEnclosingClass() != null;
return Modifier.isPublic(clazz.getModifiers()) && !Modifier.isAbstract(clazz.getModifiers()) && !isNonStaticInnerClass;
return isPublic(clazz.getModifiers())
&& !isAbstract(clazz.getModifiers())
&& (isStatic(clazz.getModifiers()) || clazz.getEnclosingClass() == null);
}

private static void scan(BiConsumer<Method, Annotation> consumer, Class<?> aClass, Method method) {
Expand Down Expand Up @@ -59,7 +62,9 @@ private static void validateMethod(Class<?> glueCodeClass, Method method) {
private static boolean isHookAnnotation(Annotation annotation) {
Class<? extends Annotation> annotationClass = annotation.annotationType();
return annotationClass.equals(Before.class)
|| annotationClass.equals(BeforeAll.class)
|| annotationClass.equals(After.class)
|| annotationClass.equals(AfterAll.class)
|| annotationClass.equals(BeforeStep.class)
|| annotationClass.equals(AfterStep.class)
|| annotationClass.equals(ParameterType.class)
Expand Down
11 changes: 11 additions & 0 deletions java/src/test/java/io/cucumber/java/GlueAdaptorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.cucumber.core.backend.HookDefinition;
import io.cucumber.core.backend.Lookup;
import io.cucumber.core.backend.ParameterTypeDefinition;
import io.cucumber.core.backend.StaticHookDefinition;
import io.cucumber.core.backend.StepDefinition;
import io.cucumber.java.en.Given;
import org.hamcrest.CustomTypeSafeMatcher;
Expand Down Expand Up @@ -61,6 +62,16 @@ protected boolean matchesSafely(StepDefinition item) {
private HookDefinition beforeHook;
private DocStringTypeDefinition docStringTypeDefinition;
private final Glue container = new Glue() {
@Override
public void addBeforeAllHook(StaticHookDefinition beforeAllHook) {
//TODO
}

@Override
public void addAfterAllHook(StaticHookDefinition afterAllHook) {
//TODO
}

@Override
public void addStepDefinition(StepDefinition stepDefinition) {
GlueAdaptorTest.this.stepDefinitions.add(stepDefinition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ public final class CucumberEngineExecutionContext implements EngineExecutionCont
}

void startTestRun() {
logger.debug(() -> "Sending run test started event");
logger.debug(() -> "running before all hooks");
bus.send(new TestRunStarted(bus.getInstant()));
logger.debug(() -> "Sending run test started event");
runnerSupplier.get().runBeforeAllHooks();
}

void beforeFeature(Feature feature) {
Expand All @@ -83,6 +85,8 @@ void runTestCase(Pickle pickle) {
}

void finishTestRun() {
logger.debug(() -> "running after all hooks");
runnerSupplier.get().runAfterAllHooks();
logger.debug(() -> "Sending test run finished event");
bus.send(new TestRunFinished(bus.getInstant()));
}
Expand Down
Loading

0 comments on commit 2f4e9da

Please # to comment.