diff --git a/CHANGELOG.md b/CHANGELOG.md index f0abe4571d..c16e2249e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] (In Git) ### Added + * [Java] Added `BeforeAll` and `AfterAll` hooks ([cucumber/#1876](https://github.com/cucumber/cucumber/pull/1876) M.P. Korstanje) ### Changed * [Core] Updated Cucumber Expressions to v11 ([cucumber/#711](https://github.com/cucumber/cucumber/pull/771) M.P. Korstanje) diff --git a/core/src/main/java/io/cucumber/core/backend/Glue.java b/core/src/main/java/io/cucumber/core/backend/Glue.java index 65423505ee..6bfe758857 100644 --- a/core/src/main/java/io/cucumber/core/backend/Glue.java +++ b/core/src/main/java/io/cucumber/core/backend/Glue.java @@ -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); diff --git a/core/src/main/java/io/cucumber/core/backend/StaticHookDefinition.java b/core/src/main/java/io/cucumber/core/backend/StaticHookDefinition.java new file mode 100644 index 0000000000..ca8c7045ed --- /dev/null +++ b/core/src/main/java/io/cucumber/core/backend/StaticHookDefinition.java @@ -0,0 +1,11 @@ +package io.cucumber.core.backend; + +import org.apiguardian.api.API; + +@API(status = API.Status.EXPERIMENTAL) +public interface StaticHookDefinition extends Located { + + void execute(); + + int getOrder(); +} diff --git a/core/src/main/java/io/cucumber/core/cli/Main.java b/core/src/main/java/io/cucumber/core/cli/Main.java index f866566bef..7f4c398932 100644 --- a/core/src/main/java/io/cucumber/core/cli/Main.java +++ b/core/src/main/java/io/cucumber/core/cli/Main.java @@ -1,7 +1,5 @@ package io.cucumber.core.cli; -import io.cucumber.core.logging.Logger; -import io.cucumber.core.logging.LoggerFactory; import io.cucumber.core.options.CommandlineOptionsParser; import io.cucumber.core.options.Constants; import io.cucumber.core.options.CucumberProperties; @@ -28,8 +26,6 @@ @API(status = API.Status.STABLE) public class Main { - private static final Logger log = LoggerFactory.getLogger(Main.class); - public static void main(String... argv) { byte exitStatus = run(argv, Thread.currentThread().getContextClassLoader()); System.exit(exitStatus); diff --git a/core/src/main/java/io/cucumber/core/exception/CompositeCucumberException.java b/core/src/main/java/io/cucumber/core/exception/CompositeCucumberException.java index 2f0e2aab8d..ef617d9cf7 100644 --- a/core/src/main/java/io/cucumber/core/exception/CompositeCucumberException.java +++ b/core/src/main/java/io/cucumber/core/exception/CompositeCucumberException.java @@ -1,26 +1,12 @@ package io.cucumber.core.exception; -import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; public final class CompositeCucumberException extends CucumberException { - private final List causes; - public CompositeCucumberException(List causes) { - super(String.format("There were %d exceptions:", causes.size())); - this.causes = causes; - } - - public List getCauses() { - return Collections.unmodifiableList(this.causes); - } - - public String getMessage() { - return super.getMessage() + this.causes.stream() - .map(e -> String.format(" %s(%s)", e.getClass().getName(), e.getMessage())) - .collect(Collectors.joining("\n", "\n", "")); + super(String.format("There were %d exceptions. The details are in the stacktrace below.", causes.size())); + causes.forEach(this::addSuppressed); } } diff --git a/core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java b/core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java index c169f2358c..8b2be5c8b9 100644 --- a/core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java +++ b/core/src/main/java/io/cucumber/core/plugin/JsonFormatter.java @@ -354,9 +354,9 @@ private Map createDummyFeatureForFailure(TestRunFinished event) scenario.put("start_timestamp", getDateTimeFromTimeStamp(event.getInstant())); scenario.put("line", 2); - scenario.put("name", "Could not execute Cucumber"); + scenario.put("name", "Failure while executing Cucumber"); scenario.put("description", ""); - scenario.put("id", "failure;could-not-execute-cucumber"); + scenario.put("id", "failure;failure-while-executing-cucumber"); scenario.put("type", "scenario"); scenario.put("keyword", "Scenario"); @@ -372,18 +372,18 @@ private Map createDummyFeatureForFailure(TestRunFinished event) whenResult.put("status", "passed"); } when.put("line", 3); - when.put("name", "Cucumber could not execute"); + when.put("name", "Cucumber failed while executing"); Map whenMatch = new LinkedHashMap<>(); when.put("match", whenMatch); whenMatch.put("arguments", new ArrayList<>()); - whenMatch.put("location", "io.cucumber.core.Failure.cucumber_could_not_execute()"); + whenMatch.put("location", "io.cucumber.core.Failure.failure_while_executing_cucumber()"); when.put("keyword", "When "); { Map thenResult = new LinkedHashMap<>(); then.put("result", thenResult); thenResult.put("duration", 0); - thenResult.put("error_message", exception.getMessage()); + thenResult.put("error_message", printStackTrace(exception)); thenResult.put("status", "failed"); } then.put("line", 4); diff --git a/core/src/main/java/io/cucumber/core/plugin/PrettyFormatter.java b/core/src/main/java/io/cucumber/core/plugin/PrettyFormatter.java index 3eb9689da0..1caed6f449 100644 --- a/core/src/main/java/io/cucumber/core/plugin/PrettyFormatter.java +++ b/core/src/main/java/io/cucumber/core/plugin/PrettyFormatter.java @@ -84,6 +84,7 @@ private void handleEmbed(EmbedEvent event) { } private void handleTestRunFinished(TestRunFinished event) { + printError(event); out.close(); } @@ -132,11 +133,21 @@ private void printStep(TestStepFinished event) { private void printError(TestStepFinished event) { Result result = event.getResult(); + printError(result); + } + + private void printError(TestRunFinished event) { + Result result = event.getResult(); + printError(result); + } + + private void printError(Result result) { Throwable error = result.getError(); if (error != null) { String name = result.getStatus().name().toLowerCase(ROOT); + Format format = formats.get(name); String text = printStackTrace(error); - out.println(" " + formats.get(name).text(text)); + out.println(" " + format.text(text)); } } diff --git a/core/src/main/java/io/cucumber/core/plugin/ProgressFormatter.java b/core/src/main/java/io/cucumber/core/plugin/ProgressFormatter.java index 5351ba2500..44283c9268 100644 --- a/core/src/main/java/io/cucumber/core/plugin/ProgressFormatter.java +++ b/core/src/main/java/io/cucumber/core/plugin/ProgressFormatter.java @@ -7,7 +7,6 @@ import io.cucumber.plugin.event.Status; import io.cucumber.plugin.event.TestRunFinished; import io.cucumber.plugin.event.TestStepFinished; -import io.cucumber.plugin.event.WriteEvent; import java.io.OutputStream; import java.util.HashMap; @@ -51,7 +50,6 @@ public void setMonochrome(boolean monochrome) { @Override public void setEventPublisher(EventPublisher publisher) { publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished); - publisher.registerHandlerFor(WriteEvent.class, this::handleWrite); publisher.registerHandlerFor(TestRunFinished.class, event -> handleTestRunFinished()); } @@ -67,10 +65,6 @@ private void handleTestStepFinished(TestStepFinished event) { } } - private void handleWrite(WriteEvent event) { - out.append(event.getText()); - } - private void handleTestRunFinished() { out.println(); out.close(); diff --git a/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java b/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java index f9929323ce..a1ceee0d27 100644 --- a/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java +++ b/core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java @@ -24,10 +24,8 @@ import io.cucumber.plugin.event.TestStepStarted; import io.cucumber.plugin.event.WriteEvent; -import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -45,6 +43,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static io.cucumber.core.exception.ExceptionUtils.printStackTrace; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.joining; @@ -81,6 +80,13 @@ public class TeamCityPlugin implements EventListener { private static final String TEMPLATE_TEST_IGNORED = TEAMCITY_PREFIX + "[testIgnored timestamp = '%s' duration = '%s' message = '%s' name = '%s']"; + private static final String TEMPLATE_BEFORE_ALL_AFTER_ALL_STARTED = TEAMCITY_PREFIX + + "[testStarted timestamp = '%s' name = '%s']"; + private static final String TEMPLATE_BEFORE_ALL_AFTER_ALL_FAILED = TEAMCITY_PREFIX + + "[testFailed timestamp = '%s' message = '%s' details = '%s' name = '%s']"; + private static final String TEMPLATE_BEFORE_ALL_AFTER_ALL_FINISHED = TEAMCITY_PREFIX + + "[testFinished timestamp = '%s' name = '%s']"; + private static final String TEMPLATE_PROGRESS_COUNTING_STARTED = TEAMCITY_PREFIX + "[customProgressStatus testsCategory = 'Scenarios' count = '0' timestamp = '%s']"; private static final String TEMPLATE_PROGRESS_COUNTING_FINISHED = TEAMCITY_PREFIX @@ -274,7 +280,7 @@ private void printTestStepFinished(TestStepFinished event) { } case AMBIGUOUS: case FAILED: { - String details = extractStackTrace(error); + String details = printStackTrace(error); print(TEMPLATE_TEST_FAILED, timeStamp, duration, "Step failed", details, name); break; } @@ -284,13 +290,6 @@ private void printTestStepFinished(TestStepFinished event) { print(TEMPLATE_TEST_FINISHED, timeStamp, duration, name); } - private String extractStackTrace(Throwable error) { - ByteArrayOutputStream s = new ByteArrayOutputStream(); - PrintStream printStream = new PrintStream(s); - error.printStackTrace(printStream); - return new String(s.toByteArray(), StandardCharsets.UTF_8); - } - private String extractName(TestStep step) { if (step instanceof PickleStepTestStep) { PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) step; @@ -364,9 +363,23 @@ private void printTestRunFinished(TestRunFinished event) { poppedNodes(emptyStack).forEach(node -> finishNode(timestamp, node)); currentStack = emptyStack; + printBeforeAfterAllResult(event, timestamp); print(TEMPLATE_TEST_RUN_FINISHED, timestamp); } + private void printBeforeAfterAllResult(TestRunFinished event, String timestamp) { + Throwable error = event.getResult().getError(); + if (error == null) { + return; + } + // Use dummy test to display before all after all failures + String name = "Before All/After All"; + print(TEMPLATE_BEFORE_ALL_AFTER_ALL_STARTED, timestamp, name); + String details = printStackTrace(error); + print(TEMPLATE_BEFORE_ALL_AFTER_ALL_FAILED, timestamp, "Before All/ After All failed", details, name); + print(TEMPLATE_BEFORE_ALL_AFTER_ALL_FINISHED, timestamp, name); + } + private void handleSnippetSuggested(SnippetsSuggestedEvent event) { suggestions.add(event); } diff --git a/core/src/main/java/io/cucumber/core/runner/CachingGlue.java b/core/src/main/java/io/cucumber/core/runner/CachingGlue.java index 93d97ee56b..599f1eb3d9 100644 --- a/core/src/main/java/io/cucumber/core/runner/CachingGlue.java +++ b/core/src/main/java/io/cucumber/core/runner/CachingGlue.java @@ -11,6 +11,7 @@ import io.cucumber.core.backend.ParameterTypeDefinition; import io.cucumber.core.backend.ScenarioScoped; import io.cucumber.core.backend.StackTraceElementReference; +import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.backend.StepDefinition; import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.gherkin.Step; @@ -48,9 +49,13 @@ final class CachingGlue implements Glue { - private static final Comparator ASCENDING = Comparator + private static final Comparator HOOK_ORDER_ASCENDING = Comparator .comparingInt(CoreHookDefinition::getOrder) .thenComparing(ScenarioScoped.class::isInstance); + + private static final Comparator STATIC_HOOK_ORDER_ASCENDING = Comparator + .comparingInt(StaticHookDefinition::getOrder); + private final List parameterTypeDefinitions = new ArrayList<>(); private final List dataTableTypeDefinitions = new ArrayList<>(); private final List defaultParameterTransformers = new ArrayList<>(); @@ -58,11 +63,13 @@ final class CachingGlue implements Glue { private final List defaultDataTableCellTransformers = new ArrayList<>(); private final List docStringTypeDefinitions = new ArrayList<>(); + private final List beforeAllHooks = new ArrayList<>(); private final List beforeHooks = new ArrayList<>(); private final List beforeStepHooks = new ArrayList<>(); private final List stepDefinitions = new ArrayList<>(); private final List afterStepHooks = new ArrayList<>(); private final List afterHooks = new ArrayList<>(); + private final List afterAllHooks = new ArrayList<>(); /* * Storing the pattern that matches the step text allows us to cache the @@ -79,6 +86,18 @@ final class CachingGlue implements Glue { this.bus = bus; } + @Override + public void addBeforeAllHook(StaticHookDefinition beforeAllHook) { + beforeAllHooks.add(beforeAllHook); + beforeAllHooks.sort(STATIC_HOOK_ORDER_ASCENDING); + } + + @Override + public void addAfterAllHook(StaticHookDefinition afterAllHook) { + afterAllHooks.add(afterAllHook); + afterAllHooks.sort(STATIC_HOOK_ORDER_ASCENDING); + } + @Override public void addStepDefinition(StepDefinition stepDefinition) { stepDefinitions.add(stepDefinition); @@ -87,25 +106,25 @@ public void addStepDefinition(StepDefinition stepDefinition) { @Override public void addBeforeHook(HookDefinition hookDefinition) { beforeHooks.add(CoreHookDefinition.create(hookDefinition)); - beforeHooks.sort(ASCENDING); + beforeHooks.sort(HOOK_ORDER_ASCENDING); } @Override public void addAfterHook(HookDefinition hookDefinition) { afterHooks.add(CoreHookDefinition.create(hookDefinition)); - afterHooks.sort(ASCENDING); + afterHooks.sort(HOOK_ORDER_ASCENDING); } @Override public void addBeforeStepHook(HookDefinition hookDefinition) { beforeStepHooks.add(CoreHookDefinition.create(hookDefinition)); - beforeStepHooks.sort(ASCENDING); + beforeStepHooks.sort(HOOK_ORDER_ASCENDING); } @Override public void addAfterStepHook(HookDefinition hookDefinition) { afterStepHooks.add(CoreHookDefinition.create(hookDefinition)); - afterStepHooks.sort(ASCENDING); + afterStepHooks.sort(HOOK_ORDER_ASCENDING); } @Override @@ -143,6 +162,10 @@ public void addDocStringType(DocStringTypeDefinition docStringType) { docStringTypeDefinitions.add(docStringType); } + List getBeforeAllHooks() { + return new ArrayList<>(beforeAllHooks); + } + Collection getBeforeHooks() { return new ArrayList<>(beforeHooks); } @@ -163,6 +186,12 @@ Collection getAfterStepHooks() { return hooks; } + List getAfterAllHooks() { + ArrayList hooks = new ArrayList<>(afterAllHooks); + Collections.reverse(hooks); + return hooks; + } + Collection getParameterTypeDefinitions() { return parameterTypeDefinitions; } diff --git a/core/src/main/java/io/cucumber/core/runner/Runner.java b/core/src/main/java/io/cucumber/core/runner/Runner.java index 83065ac21a..0a022d74bb 100644 --- a/core/src/main/java/io/cucumber/core/runner/Runner.java +++ b/core/src/main/java/io/cucumber/core/runner/Runner.java @@ -2,8 +2,12 @@ import io.cucumber.core.api.TypeRegistryConfigurer; import io.cucumber.core.backend.Backend; +import io.cucumber.core.backend.CucumberBackendException; +import io.cucumber.core.backend.CucumberInvocationTargetException; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.eventbus.EventBus; +import io.cucumber.core.exception.CucumberException; import io.cucumber.core.gherkin.Pickle; import io.cucumber.core.gherkin.Step; import io.cucumber.core.logging.Logger; @@ -23,6 +27,8 @@ import java.util.Objects; import java.util.stream.Collectors; +import static io.cucumber.core.exception.ExceptionUtils.throwAsUncheckedException; +import static io.cucumber.core.runner.StackManipulation.removeFrameworkFrames; import static java.util.Collections.emptyList; public final class Runner { @@ -65,7 +71,7 @@ public void runPickle(Pickle pickle) { snippetGenerators = createSnippetGeneratorsForPickle(stepTypeRegistry); buildBackendWorlds(); // Java8 step definitions will be added to the - // glue here + // glue here glue.prepareGlue(stepTypeRegistry); @@ -87,6 +93,32 @@ private StepTypeRegistry createTypeRegistryForPickle(Pickle pickle) { return stepTypeRegistry; } + public void runBeforeAllHooks() { + glue.getBeforeAllHooks().forEach(this::executeHook); + } + + public void runAfterAllHooks() { + glue.getAfterAllHooks().forEach(this::executeHook); + } + + private void executeHook(StaticHookDefinition hookDefinition) { + if (runnerOptions.isDryRun()) { + return; + } + try { + hookDefinition.execute(); + } catch (CucumberBackendException e) { + CucumberException exception = new CucumberException(String.format("" + + "Could not invoke hook defined at '%s'.\n" + + "It appears there was a problem with the hook definition.", + hookDefinition.getLocation()), e); + throwAsUncheckedException(exception); + } catch (CucumberInvocationTargetException e) { + Throwable throwable = removeFrameworkFrames(e); + throwAsUncheckedException(throwable); + } + } + private List createSnippetGeneratorsForPickle(StepTypeRegistry stepTypeRegistry) { return backends.stream() .map(Backend::getSnippet) diff --git a/core/src/main/java/io/cucumber/core/runtime/CucumberExecutionContext.java b/core/src/main/java/io/cucumber/core/runtime/CucumberExecutionContext.java index 846e41115a..b1667c0b0c 100644 --- a/core/src/main/java/io/cucumber/core/runtime/CucumberExecutionContext.java +++ b/core/src/main/java/io/cucumber/core/runtime/CucumberExecutionContext.java @@ -2,7 +2,6 @@ import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.exception.CompositeCucumberException; -import io.cucumber.core.exception.CucumberException; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; @@ -23,6 +22,7 @@ import java.util.ResourceBundle; import java.util.function.Consumer; +import static io.cucumber.core.exception.ExceptionUtils.printStackTrace; import static io.cucumber.core.exception.ExceptionUtils.throwAsUncheckedException; import static io.cucumber.createmeta.CreateMeta.createMeta; import static io.cucumber.messages.TimeConversion.javaInstantToTimestamp; @@ -68,36 +68,54 @@ private void emitTestRunStarted() { .build()); } + public void runBeforeAllHooks() { + try { + runnerSupplier.get().runBeforeAllHooks(); + } catch (Throwable e) { + thrown.add(e); + throw e; + } + } + + public void runAfterAllHooks() { + try { + runnerSupplier.get().runAfterAllHooks(); + } catch (Throwable e) { + thrown.add(e); + throw e; + } + } + public void finishTestRun() { log.debug(() -> "Sending test run finished event"); - CucumberException cucumberException = getException(); + Throwable cucumberException = getException(); emitTestRunFinished(cucumberException); } - public CucumberException getException() { + public Throwable getException() { if (thrown.isEmpty()) { return null; } if (thrown.size() == 1) { - return new CucumberException(thrown.get(0)); + return thrown.get(0); } return new CompositeCucumberException(thrown); } - private void emitTestRunFinished(CucumberException cucumberException) { + private void emitTestRunFinished(Throwable exception) { Instant instant = bus.getInstant(); Result result = new Result( - cucumberException != null ? Status.FAILED : exitStatus.getStatus(), + exception != null ? Status.FAILED : exitStatus.getStatus(), Duration.between(start, instant), - cucumberException); + exception); bus.send(new TestRunFinished(instant, result)); Messages.TestRunFinished.Builder testRunFinished = Messages.TestRunFinished.newBuilder() - .setSuccess(exitStatus.isSuccess()) + .setSuccess(exception == null && exitStatus.isSuccess()) .setTimestamp(javaInstantToTimestamp(instant)); - if (cucumberException != null) { - testRunFinished.setMessage(cucumberException.getMessage()); + if (exception != null) { + testRunFinished.setMessage(printStackTrace(exception)); } bus.send(Envelope.newBuilder() .setTestRunFinished(testRunFinished) @@ -127,7 +145,6 @@ private Runner getRunner() { try { return runnerSupplier.get(); } catch (Throwable e) { - log.error(e, () -> "Unable to start Cucumber"); thrown.add(e); throw e; } diff --git a/core/src/main/java/io/cucumber/core/runtime/Runtime.java b/core/src/main/java/io/cucumber/core/runtime/Runtime.java index 2e9630be39..37a999f63b 100644 --- a/core/src/main/java/io/cucumber/core/runtime/Runtime.java +++ b/core/src/main/java/io/cucumber/core/runtime/Runtime.java @@ -1,7 +1,7 @@ package io.cucumber.core.runtime; import io.cucumber.core.eventbus.EventBus; -import io.cucumber.core.exception.CucumberException; +import io.cucumber.core.exception.ExceptionUtils; import io.cucumber.core.feature.FeatureParser; import io.cucumber.core.filter.Filters; import io.cucumber.core.gherkin.Feature; @@ -75,9 +75,27 @@ public static Builder builder() { public void run() { context.startTestRun(); - final List features = featureSupplier.get(); + try { + context.runBeforeAllHooks(); + runFeatures(); + } finally { + try { + context.runAfterAllHooks(); + } finally { + context.finishTestRun(); + } + } + + Throwable exception = context.getException(); + if (exception != null) { + ExceptionUtils.throwAsUncheckedException(exception); + } + } + + private void runFeatures() { + List features = featureSupplier.get(); features.forEach(context::beforeFeature); - final List> executingPickles = features.stream() + List> executingPickles = features.stream() .flatMap(feature -> feature.getPickles().stream()) .filter(filter) .collect(collectingAndThen(toList(), @@ -98,12 +116,6 @@ public void run() { log.debug(e, () -> "Interrupted while executing pickle"); } } - context.finishTestRun(); - - CucumberException exception = context.getException(); - if (exception != null) { - throw exception; - } } private Runnable execute(Pickle pickle) { diff --git a/core/src/test/java/io/cucumber/core/backend/StubStaticHookDefinition.java b/core/src/test/java/io/cucumber/core/backend/StubStaticHookDefinition.java new file mode 100644 index 0000000000..361f74e90b --- /dev/null +++ b/core/src/test/java/io/cucumber/core/backend/StubStaticHookDefinition.java @@ -0,0 +1,61 @@ +package io.cucumber.core.backend; + +public class StubStaticHookDefinition implements StaticHookDefinition { + + private static final String STUBBED_LOCATION_WITH_DETAILS = "{stubbed location with details}"; + private final String location; + private final RuntimeException exception; + private final Runnable action; + + public StubStaticHookDefinition(String location, RuntimeException exception, Runnable action) { + this.location = location; + this.exception = exception; + this.action = action; + } + + public StubStaticHookDefinition(String location, Runnable action) { + this(location, null, action); + } + + public StubStaticHookDefinition() { + this(STUBBED_LOCATION_WITH_DETAILS, null, null); + } + + public StubStaticHookDefinition(Runnable action) { + this(STUBBED_LOCATION_WITH_DETAILS, null, action); + } + + public StubStaticHookDefinition(RuntimeException exception) { + this(STUBBED_LOCATION_WITH_DETAILS, exception, null); + } + + public StubStaticHookDefinition(String location) { + this(location, null, null); + } + + @Override + public void execute() { + if (action != null) { + action.run(); + } + if (exception != null) { + throw exception; + } + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public boolean isDefinedAt(StackTraceElement stackTraceElement) { + return false; + } + + @Override + public String getLocation() { + return location; + } + +} diff --git a/core/src/test/java/io/cucumber/core/exception/CompositeCucumberExceptionTest.java b/core/src/test/java/io/cucumber/core/exception/CompositeCucumberExceptionTest.java index e29952b0b0..01d57539df 100644 --- a/core/src/test/java/io/cucumber/core/exception/CompositeCucumberExceptionTest.java +++ b/core/src/test/java/io/cucumber/core/exception/CompositeCucumberExceptionTest.java @@ -7,6 +7,7 @@ import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsNull.nullValue; @@ -19,9 +20,10 @@ void throws_for_zero_exceptions() { final List causes = Collections.emptyList(); CompositeCucumberException expectedThrown = new CompositeCucumberException(causes); assertAll( - () -> assertThat(expectedThrown.getMessage(), is(equalTo("There were 0 exceptions:\n"))), + () -> assertThat(expectedThrown.getMessage(), + is(equalTo("There were 0 exceptions. The details are in the stacktrace below."))), () -> assertThat(expectedThrown.getCause(), is(nullValue())), - () -> assertThat(expectedThrown.getCauses(), is(equalTo(causes)))); + () -> assertThat(expectedThrown.getSuppressed(), is(arrayWithSize(0)))); } @Test @@ -30,9 +32,9 @@ void throws_for_one_exception() { CompositeCucumberException expectedThrown = new CompositeCucumberException(causes); assertAll( () -> assertThat(expectedThrown.getMessage(), - is(equalTo("There were 1 exceptions:\n java.lang.IllegalArgumentException(null)"))), + is(equalTo("There were 1 exceptions. The details are in the stacktrace below."))), () -> assertThat(expectedThrown.getCause(), is(nullValue())), - () -> assertThat(expectedThrown.getCauses(), is(equalTo(causes)))); + () -> assertThat(expectedThrown.getSuppressed(), is(arrayWithSize(1)))); } @Test @@ -40,10 +42,10 @@ void throws_for_two_exceptions() { final List causes = Arrays.asList(new IllegalArgumentException(), new RuntimeException()); CompositeCucumberException expectedThrown = new CompositeCucumberException(causes); assertAll( - () -> assertThat(expectedThrown.getMessage(), is(equalTo( - "There were 2 exceptions:\n java.lang.IllegalArgumentException(null)\n java.lang.RuntimeException(null)"))), + () -> assertThat(expectedThrown.getMessage(), + is(equalTo("There were 2 exceptions. The details are in the stacktrace below."))), () -> assertThat(expectedThrown.getCause(), is(nullValue())), - () -> assertThat(expectedThrown.getCauses(), is(equalTo(causes)))); + () -> assertThat(expectedThrown.getSuppressed(), is(arrayWithSize(2)))); } } diff --git a/core/src/test/java/io/cucumber/core/plugin/PrettyFormatterTest.java b/core/src/test/java/io/cucumber/core/plugin/PrettyFormatterTest.java index 2a2e188a42..c1f032c15f 100755 --- a/core/src/test/java/io/cucumber/core/plugin/PrettyFormatterTest.java +++ b/core/src/test/java/io/cucumber/core/plugin/PrettyFormatterTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.StepDefinition; import io.cucumber.core.backend.StubHookDefinition; +import io.cucumber.core.backend.StubStaticHookDefinition; import io.cucumber.core.backend.StubStepDefinition; import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.feature.TestFeatureParser; @@ -30,6 +31,7 @@ import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; class PrettyFormatterTest { @@ -452,4 +454,30 @@ void should_mark_nested_arguments_as_part_of_enclosing_argument() { AnsiEscapes.GREEN + AnsiEscapes.INTENSITY_BOLD + " and not yet confirmed" + AnsiEscapes.RESET)); } + @Test + void should_print_system_failure_for_failed_hooks() { + Feature feature = TestFeatureParser.parse("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThrows(StubException.class, () -> Runtime.builder() + .withFeatureSupplier(new StubFeatureSupplier(feature)) + .withAdditionalPlugins(new PrettyFormatter(out)) + .withBackendSupplier(new StubBackendSupplier( + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + singletonList(new StubStaticHookDefinition(new StubException("Hook failed", "the stack trace"))))) + .build() + .run()); + + assertThat(out, bytesContainsString("" + + " " + AnsiEscapes.RED + "the stack trace" + AnsiEscapes.RESET + "\n")); + } + } diff --git a/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java b/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java index 159f866b77..36f72cd5e9 100755 --- a/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java +++ b/core/src/test/java/io/cucumber/core/plugin/TeamCityPluginTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.StubHookDefinition; import io.cucumber.core.backend.StubPendingException; +import io.cucumber.core.backend.StubStaticHookDefinition; import io.cucumber.core.backend.StubStepDefinition; import io.cucumber.core.backend.TestCaseState; import io.cucumber.core.feature.TestFeatureParser; @@ -26,6 +27,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; @DisabledOnOs(OS.WINDOWS) class TeamCityPluginTest { @@ -309,4 +311,35 @@ void should_print_location_hint_for_lambda_hooks() { "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://com.example.HookDefinition/HookDefinition' captureStandardOutput = 'true' name = 'Before']\n")); } + @Test + void should_print_system_failure_for_failed_hooks() { + Feature feature = TestFeatureParser.parse("path/test.feature", "" + + "Feature: feature name\n" + + " Scenario: scenario name\n" + + " Given first step\n"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + assertThrows(StubException.class, () -> Runtime.builder() + .withFeatureSupplier(new StubFeatureSupplier(feature)) + .withAdditionalPlugins(new TeamCityPlugin(new PrintStream(out))) + .withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID)) + .withBackendSupplier(new StubBackendSupplier( + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + singletonList(new StubStaticHookDefinition(new StubException("Hook failed", "the stack trace"))))) + .build() + .run()); + + assertThat(out, bytesContainsString("" + + "##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' name = 'Before All/After All']\n" + + "##teamcity[testFailed timestamp = '1970-01-01T12:00:00.000+0000' message = 'Before All/ After All failed' details = 'the stack trace' name = 'Before All/After All']\n" + + + "##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Before All/After All']")); + } + } diff --git a/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java b/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java index dba36306fb..1b2ce6ea60 100644 --- a/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java +++ b/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java @@ -56,7 +56,8 @@ void tearDown() { @Test void scanForSubClassesInPackage() { - List> classes = scanner.scanForSubClassesInPackage("io.cucumber", + List> classes = scanner.scanForSubClassesInPackage( + "io.cucumber.core.resource.test", ExampleInterface.class); assertThat(classes, contains(ExampleClass.class)); diff --git a/core/src/test/java/io/cucumber/core/runner/RunnerTest.java b/core/src/test/java/io/cucumber/core/runner/RunnerTest.java index c7c95c6552..b74477cfb1 100644 --- a/core/src/test/java/io/cucumber/core/runner/RunnerTest.java +++ b/core/src/test/java/io/cucumber/core/runner/RunnerTest.java @@ -5,6 +5,7 @@ import io.cucumber.core.backend.Glue; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.feature.TestFeatureParser; import io.cucumber.core.gherkin.Feature; @@ -46,36 +47,36 @@ class RunnerTest { }; @Test - void hooks_execute_when_world_exist() { - final HookDefinition beforeHook = addBeforeHook(); - final HookDefinition afterHook = addAfterHook(); + void hooks_execute_inside_world_and_around_world() { + StaticHookDefinition beforeAllHook = createStaticHook(); + StaticHookDefinition afterAllHook = createStaticHook(); + HookDefinition beforeHook = createHook(); + HookDefinition afterHook = createHook(); Backend backend = mock(Backend.class); when(backend.getSnippet()).thenReturn(new TestSnippet()); ObjectFactory objectFactory = mock(ObjectFactory.class); doAnswer(invocation -> { Glue glue = invocation.getArgument(0); - glue.addAfterHook(afterHook); + glue.addBeforeAllHook(beforeAllHook); + glue.addAfterAllHook(afterAllHook); glue.addBeforeHook(beforeHook); + glue.addAfterHook(afterHook); return null; }).when(backend).loadGlue(any(Glue.class), ArgumentMatchers.anyList()); - new Runner(bus, singletonList(backend), objectFactory, typeRegistryConfigurer, runtimeOptions) - .runPickle(createPicklesWithSteps()); + Runner runner = new Runner(bus, singletonList(backend), objectFactory, typeRegistryConfigurer, runtimeOptions); + runner.runBeforeAllHooks(); + runner.runPickle(createPicklesWithSteps()); + runner.runAfterAllHooks(); - InOrder inOrder = inOrder(beforeHook, afterHook, backend); + InOrder inOrder = inOrder(beforeAllHook, afterAllHook, beforeHook, afterHook, backend); + inOrder.verify(beforeAllHook).execute(); inOrder.verify(backend).buildWorld(); inOrder.verify(beforeHook).execute(any(TestCaseState.class)); inOrder.verify(afterHook).execute(any(TestCaseState.class)); inOrder.verify(backend).disposeWorld(); - } - - private HookDefinition addBeforeHook() { - return addHook(); - } - - private HookDefinition addAfterHook() { - return addHook(); + inOrder.verify(afterAllHook).execute(); } private Pickle createPicklesWithSteps() { @@ -86,7 +87,13 @@ private Pickle createPicklesWithSteps() { return feature.getPickles().get(0); } - private HookDefinition addHook() { + private StaticHookDefinition createStaticHook() { + StaticHookDefinition hook = mock(StaticHookDefinition.class); + when(hook.getLocation()).thenReturn(""); + return hook; + } + + private HookDefinition createHook() { HookDefinition hook = mock(HookDefinition.class); when(hook.getTagExpression()).thenReturn(""); when(hook.getLocation()).thenReturn(""); @@ -98,7 +105,7 @@ void steps_are_skipped_after_failure() { StubStepDefinition stepDefinition = spy(new StubStepDefinition("some step")); Pickle pickleMatchingStepDefinitions = createPickleMatchingStepDefinitions(stepDefinition); - final HookDefinition failingBeforeHook = addBeforeHook(); + final HookDefinition failingBeforeHook = createHook(); doThrow(new RuntimeException("Boom")).when(failingBeforeHook).execute(ArgumentMatchers.any()); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override @@ -137,7 +144,7 @@ public void execute(Object[] args) { Pickle pickleMatchingStepDefinitions = createPickleMatchingStepDefinitions(stepDefinition); - final HookDefinition afterStepHook = addAfterStepHook(); + final HookDefinition afterStepHook = createHook(); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override @@ -154,17 +161,13 @@ public void loadGlue(Glue glue, List gluePaths) { inOrder.verify(afterStepHook).execute(any(TestCaseState.class)); } - private HookDefinition addAfterStepHook() { - return addHook(); - } - @Test void aftersteps_executed_for_passed_step() { StubStepDefinition stepDefinition = spy(new StubStepDefinition("some step")); Pickle pickle = createPickleMatchingStepDefinitions(stepDefinition); - HookDefinition afteStepHook1 = addAfterStepHook(); - HookDefinition afteStepHook2 = addAfterStepHook(); + HookDefinition afteStepHook1 = createHook(); + HookDefinition afteStepHook2 = createHook(); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override @@ -185,10 +188,11 @@ public void loadGlue(Glue glue, List gluePaths) { @Test void hooks_execute_also_after_failure() { - final HookDefinition failingBeforeHook = addBeforeHook(); + HookDefinition beforeHook = createHook(); + HookDefinition afterHook = createHook(); + + HookDefinition failingBeforeHook = createHook(); doThrow(new RuntimeException("boom")).when(failingBeforeHook).execute(any(TestCaseState.class)); - final HookDefinition beforeHook = addBeforeHook(); - final HookDefinition afterHook = addAfterHook(); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override @@ -241,38 +245,51 @@ public void loadGlue(Glue glue, List gluePaths) { void hooks_not_executed_in_dry_run_mode() { RuntimeOptions runtimeOptions = new RuntimeOptionsBuilder().setDryRun().build(); - final HookDefinition beforeHook = addBeforeHook(); - final HookDefinition afterHook = addAfterHook(); - final HookDefinition afterStepHook = addAfterStepHook(); + StaticHookDefinition beforeAllHook = createStaticHook(); + StaticHookDefinition afterAllHook = createStaticHook(); + HookDefinition beforeHook = createHook(); + HookDefinition afterHook = createHook(); + HookDefinition beforeStepHook = createHook(); + HookDefinition afterStepHook = createHook(); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override public void loadGlue(Glue glue, List gluePaths) { + glue.addBeforeAllHook(beforeAllHook); + glue.addAfterAllHook(afterAllHook); glue.addBeforeHook(beforeHook); - glue.addBeforeHook(afterHook); + glue.addAfterHook(afterHook); + glue.addBeforeStepHook(beforeStepHook); glue.addAfterStepHook(afterStepHook); } }; + runnerSupplier.get().runBeforeAllHooks(); runnerSupplier.get().runPickle(createPicklesWithSteps()); + runnerSupplier.get().runAfterAllHooks(); + verify(beforeAllHook, never()).execute(); + verify(afterAllHook, never()).execute(); verify(beforeHook, never()).execute(any(TestCaseState.class)); - verify(afterStepHook, never()).execute(any(TestCaseState.class)); verify(afterHook, never()).execute(any(TestCaseState.class)); + verify(beforeStepHook, never()).execute(any(TestCaseState.class)); + verify(afterStepHook, never()).execute(any(TestCaseState.class)); } @Test - void hooks_not_executed_for_empty_pickles() { - final HookDefinition beforeHook = addBeforeHook(); - final HookDefinition afterHook = addAfterHook(); - final HookDefinition afterStepHook = addAfterStepHook(); + void scenario_hooks_not_executed_for_empty_pickles() { + HookDefinition beforeHook = createHook(); + HookDefinition afterHook = createHook(); + HookDefinition beforeStepHook = createHook(); + HookDefinition afterStepHook = createHook(); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override public void loadGlue(Glue glue, List gluePaths) { glue.addBeforeHook(beforeHook); - glue.addBeforeHook(afterHook); + glue.addAfterHook(afterHook); + glue.addBeforeStepHook(beforeStepHook); glue.addAfterStepHook(afterStepHook); } }; diff --git a/core/src/test/java/io/cucumber/core/runtime/CucumberExecutionContextTest.java b/core/src/test/java/io/cucumber/core/runtime/CucumberExecutionContextTest.java index 0ae75cc898..8c4615ff9f 100644 --- a/core/src/test/java/io/cucumber/core/runtime/CucumberExecutionContextTest.java +++ b/core/src/test/java/io/cucumber/core/runtime/CucumberExecutionContextTest.java @@ -1,8 +1,8 @@ package io.cucumber.core.runtime; import io.cucumber.core.eventbus.EventBus; +import io.cucumber.core.options.RuntimeOptions; import io.cucumber.core.options.RuntimeOptionsBuilder; -import io.cucumber.core.plugin.Options; import io.cucumber.plugin.event.Result; import io.cucumber.plugin.event.Status; import io.cucumber.plugin.event.TestCase; @@ -17,6 +17,7 @@ import java.util.List; import java.util.UUID; import java.util.function.Function; +import java.util.function.Supplier; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -28,11 +29,20 @@ class CucumberExecutionContextTest { private final EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); - private final Options options = new RuntimeOptionsBuilder().build(); + private final RuntimeOptions options = new RuntimeOptionsBuilder().build(); private final ExitStatus exitStatus = new ExitStatus(options); private final RuntimeException failure = new IllegalStateException("failure runner"); - private final CucumberExecutionContext context = new CucumberExecutionContext(bus, exitStatus, - mock(RunnerSupplier.class)); + private final BackendSupplier backendSupplier = new StubBackendSupplier(); + private final Supplier classLoader = CucumberExecutionContext.class::getClassLoader; + private final ObjectFactoryServiceLoader objectFactoryServiceLoader = new ObjectFactoryServiceLoader(classLoader, + options); + private final ObjectFactorySupplier objectFactorySupplier = new SingletonObjectFactorySupplier( + objectFactoryServiceLoader); + private final TypeRegistryConfigurerSupplier typeRegistryConfigurerSupplier = new ScanningTypeRegistryConfigurerSupplier( + classLoader, options); + private final RunnerSupplier runnerSupplier = new SingletonRunnerSupplier(options, bus, backendSupplier, + objectFactorySupplier, typeRegistryConfigurerSupplier); + private final CucumberExecutionContext context = new CucumberExecutionContext(bus, exitStatus, runnerSupplier); @Test public void collects_and_rethrows_failures_in_runner() { @@ -40,7 +50,7 @@ public void collects_and_rethrows_failures_in_runner() { throw failure; })); assertThat(thrown, is(failure)); - assertThat(context.getException().getCause(), is(failure)); + assertThat(context.getException(), is(failure)); } @Test @@ -77,7 +87,7 @@ public void emits_failures_in_events() { assertThat(testRunStarted.get(0), notNullValue()); Result result = testRunFinished.get(0).getResult(); assertThat(result.getStatus(), is(Status.FAILED)); - assertThat(result.getError().getCause(), is(failure)); + assertThat(result.getError(), is(failure)); } } diff --git a/core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java b/core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java index 9650d7710e..56b2255fd1 100644 --- a/core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java +++ b/core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java @@ -36,21 +36,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.mockito.ArgumentCaptor; -import org.opentest4j.TestAbortedException; -import java.io.ByteArrayOutputStream; import java.net.URI; import java.time.Clock; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.CountDownLatch; import static java.time.Clock.fixed; import static java.time.Duration.ZERO; -import static java.time.Duration.ofMillis; import static java.time.Instant.EPOCH; import static java.time.ZoneId.of; import static java.util.Arrays.asList; @@ -59,6 +55,7 @@ import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; @@ -361,11 +358,9 @@ void should_fail_on_event_listener_exception_when_running_in_parallel() { .build() .run(); CompositeCucumberException actualThrown = assertThrows(CompositeCucumberException.class, testMethod); - assertThat(actualThrown.getMessage(), is(equalTo( - "There were 3 exceptions:\n" + - " java.lang.RuntimeException(This exception is expected)\n" + - " java.lang.RuntimeException(This exception is expected)\n" + - " java.lang.RuntimeException(This exception is expected)"))); + assertThat(actualThrown.getMessage(), + is(equalTo("There were 3 exceptions. The details are in the stacktrace below."))); + assertThat(actualThrown.getSuppressed(), is(arrayWithSize(3))); } @Test diff --git a/core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java b/core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java index 68d096a6a9..b8a8ae5c10 100644 --- a/core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java +++ b/core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java @@ -4,8 +4,8 @@ import io.cucumber.core.backend.Glue; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.Snippet; +import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.backend.StepDefinition; -import io.cucumber.core.runtime.BackendSupplier; import io.cucumber.core.snippets.TestSnippet; import java.net.URI; @@ -16,11 +16,13 @@ public class StubBackendSupplier implements BackendSupplier { + private final List beforeAll; private final List before; private final List beforeStep; private final List steps; private final List afterStep; private final List after; + private final List afterAll; public StubBackendSupplier(StepDefinition... steps) { this(Collections.emptyList(), Arrays.asList(steps), Collections.emptyList()); @@ -33,11 +35,25 @@ public StubBackendSupplier( List afterStep, List after ) { + this(Collections.emptyList(), before, beforeStep, steps, afterStep, after, Collections.emptyList()); + } + + public StubBackendSupplier( + List beforeAll, + List before, + List beforeStep, + List steps, + List afterStep, + List after, + List afterAll + ) { + this.beforeAll = beforeAll; this.before = before; this.beforeStep = beforeStep; this.steps = steps; this.afterStep = afterStep; this.after = after; + this.afterAll = afterAll; } public StubBackendSupplier( @@ -53,11 +69,13 @@ public Collection get() { return Collections.singletonList(new Backend() { @Override public void loadGlue(Glue glue, List gluePaths) { + beforeAll.forEach(glue::addBeforeAllHook); before.forEach(glue::addBeforeHook); beforeStep.forEach(glue::addBeforeStepHook); steps.forEach(glue::addStepDefinition); afterStep.forEach(glue::addAfterStepHook); after.forEach(glue::addAfterHook); + afterAll.forEach(glue::addAfterAllHook); } @Override diff --git a/examples/calculator-java-cli/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java b/examples/calculator-java-cli/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java index 82c2f5939c..64a4d09404 100644 --- a/examples/calculator-java-cli/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java +++ b/examples/calculator-java-cli/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java @@ -1,7 +1,9 @@ package io.cucumber.examples.calculator; 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; @@ -16,6 +18,26 @@ public class RpnCalculatorSteps { private RpnCalculator calc; + @BeforeAll + public static void beforeAll() { + // Runs before all scenarios + } + + @AfterAll + public static void afterAll() { + // Runs after all scenarios + } + + @Before("not @foo") + public void before(Scenario scenario) { + scenario.log("Runs before each scenarios *not* tagged with @foo"); + } + + @After + public void after(Scenario scenario) { + scenario.log("Runs after each scenarios"); + } + @Given("a calculator I just turned on") public void a_calculator_I_just_turned_on() { calc = new RpnCalculator(); @@ -38,16 +60,6 @@ public void the_result_is(double expected) { assertThat(calc.value(), equalTo(expected)); } - @Before("not @foo") - public void before(Scenario scenario) { - scenario.log("Runs before scenarios *not* tagged with @foo"); - } - - @After - public void after(Scenario scenario) { - // scenario.write("HELLLLOO"); - } - @Given("the previous entries:") public void thePreviousEntries(List entries) { for (Entry entry : entries) { diff --git a/examples/calculator-java-junit4/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java b/examples/calculator-java-junit4/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java index 0bcf252ced..e618366ccc 100644 --- a/examples/calculator-java-junit4/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java +++ b/examples/calculator-java-junit4/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java @@ -1,7 +1,9 @@ package io.cucumber.examples.calculator; 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; @@ -15,6 +17,26 @@ public class RpnCalculatorSteps { private RpnCalculator calc; + @BeforeAll + public static void beforeAll() { + // Runs before all scenarios + } + + @AfterAll + public static void afterAll() { + // Runs after all scenarios + } + + @Before("not @foo") + public void before(Scenario scenario) { + scenario.log("Runs before each scenarios *not* tagged with @foo"); + } + + @After + public void after(Scenario scenario) { + scenario.log("Runs after each scenarios"); + } + @Given("a calculator I just turned on") public void a_calculator_I_just_turned_on() { calc = new RpnCalculator(); @@ -37,16 +59,6 @@ public void the_result_is(double expected) { assertEquals(expected, calc.value()); } - @Before("not @foo") - public void before(Scenario scenario) { - scenario.log("Runs before scenarios *not* tagged with @foo"); - } - - @After - public void after(Scenario scenario) { - // scenario.write("HELLLLOO"); - } - @Given("the previous entries:") public void thePreviousEntries(List entries) { for (Entry entry : entries) { diff --git a/examples/calculator-java-junit5/src/test/java/io/cucumber/examples/calculator/RpnCalculatorStepDefinitions.java b/examples/calculator-java-junit5/src/test/java/io/cucumber/examples/calculator/RpnCalculatorStepDefinitions.java index 87e9034de9..0da95ed9b5 100644 --- a/examples/calculator-java-junit5/src/test/java/io/cucumber/examples/calculator/RpnCalculatorStepDefinitions.java +++ b/examples/calculator-java-junit5/src/test/java/io/cucumber/examples/calculator/RpnCalculatorStepDefinitions.java @@ -1,7 +1,9 @@ package io.cucumber.examples.calculator; 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; @@ -15,6 +17,26 @@ public class RpnCalculatorStepDefinitions { private RpnCalculator calc; + @BeforeAll + public static void beforeAll() { + // Runs before all scenarios + } + + @AfterAll + public static void afterAll() { + // Runs after all scenarios + } + + @Before("not @foo") + public void before(Scenario scenario) { + scenario.log("Runs before each scenarios *not* tagged with @foo"); + } + + @After + public void after(Scenario scenario) { + scenario.log("Runs after each scenarios"); + } + @Given("a calculator I just turned on") public void a_calculator_I_just_turned_on() { calc = new RpnCalculator(); @@ -37,16 +59,6 @@ public void the_result_is(double expected) { assertEquals(expected, calc.value()); } - @Before("not @foo") - public void before(Scenario scenario) { - scenario.log("Runs before scenarios *not* tagged with @foo"); - } - - @After - public void after(Scenario scenario) { - // result.write("HELLLLOO"); - } - @Given("the previous entries:") public void thePreviousEntries(List entries) { for (Entry entry : entries) { diff --git a/examples/calculator-java-testng/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java b/examples/calculator-java-testng/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java index 65ecf5ab89..f2fcf8722d 100644 --- a/examples/calculator-java-testng/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java +++ b/examples/calculator-java-testng/src/test/java/io/cucumber/examples/calculator/RpnCalculatorSteps.java @@ -1,7 +1,9 @@ package io.cucumber.examples.calculator; import io.cucumber.java.After; +import io.cucumber.java.AfterAll; import io.cucumber.java.Before; +import io.cucumber.java.BeforeAll; import io.cucumber.java.DataTableType; import io.cucumber.java.Scenario; import io.cucumber.java.en.Given; @@ -17,6 +19,26 @@ public class RpnCalculatorSteps { private RpnCalculator calc; + @BeforeAll + public static void beforeAll() { + // Runs before all scenarios + } + + @AfterAll + public static void afterAll() { + // Runs after all scenarios + } + + @Before("not @foo") + public void before(Scenario scenario) { + scenario.log("Runs before each scenarios *not* tagged with @foo"); + } + + @After + public void after(Scenario scenario) { + scenario.log("Runs after each scenarios"); + } + @Given("a calculator I just turned on") public void a_calculator_I_just_turned_on() { calc = new RpnCalculator(); @@ -39,16 +61,6 @@ public void the_result_is(double expected) { assertEquals(expected, calc.value()); } - @Before("not @foo") - public void before() { - System.out.println("Runs before scenarios *not* tagged with @foo"); - } - - @After - public void after(Scenario scenario) { - // result.write("HELLLLOO"); - } - @Given("the previous entries:") public void thePreviousEntries(List entries) { for (Entry entry : entries) { diff --git a/examples/spring-java-junit5/pom.xml b/examples/spring-java-junit5/pom.xml index 63c61163f8..82f49785ee 100644 --- a/examples/spring-java-junit5/pom.xml +++ b/examples/spring-java-junit5/pom.xml @@ -8,7 +8,7 @@ spring-java-junit5 - Examples: Spring Transactions + Examples: Spring Transactions - Java - Junit 5 io.cucumber.examples.spring.application diff --git a/java/README.md b/java/README.md index da58ee72a1..62c0806b66 100644 --- a/java/README.md +++ b/java/README.md @@ -119,15 +119,100 @@ public class StepDefinitions { ## Hooks -Declare hooks that will be executed before/after each scenario/step by -annotating a method. The method may declare an argument of type -`io.cucumber.java.Scenario`. - - * `@Before` - * `@After` - * `@BeforeStep` - * `@AfterStep` - +Hooks are executed before or after all scenarios/each scenario/each step. A hook +is declared by annotating a method. + +Hooks are global, all hooks declared in any step definition class will be +executed. The order in which hooks are executed is not defined. An explicit +order can be provided by using the `order` property in the annotation. + +### BeforeAll / AfterAll + +`BeforeAll` and `AfterAll` hooks are executed before all scenarios are executed and +after all scenarios have been executed. A hook is declared by annotating a method. +This methods must be static and do not take any arguments. + +```java +package io.cucumber.example; + +import io.cucumber.java.AfterAll; +import io.cucumber.java.BeforeAll; + +public class StepDefinitions { + + @BeforeAll + public static void beforeAll() { + // Runs before all scenarios + } + + @AfterAll + public static void afterAll() { + // Runs after all scenarios + } +} +``` + +Notes: + + 1. When used in combination with Junit 5, Maven Surefire, and/or Failsafe use + version `3.0.0-M5` or later. + 2. When used in combination with Junit 5 and InteliJ IDEA failures in before + all and after all hooks do not fail a test run. + +### Before / After + +`Before` and `After` hooks are executed before and after each scenario is executed. +A hook is declared by annotating a method. This method may take an argument of +`io.cucumber.java.Scenario`. A tag-expression can be used to execute a hook +conditionally. + +```java +package io.cucumber.example; + +import io.cucumber.java.After; +import io.cucumber.java.Before; + +public class StepDefinitions { + + @Before("not @zukini") + public void before(Scenario scenario) { + scenario.log("Runs before each scenarios *not* tagged with @zukini"); + } + + @After + public void after(Scenario scenario) { + scenario.log("Runs after each scenarios"); + } +} +``` + +### BeforeStep / AfterStep + +`BeforeStep` and `AfterStep` hooks are executed before and after each step is +executed. A hook is declared by annotating a method. This method may take an +argument of `io.cucumber.java.Scenario`. A tag-expression can be used to execute +a hook conditionally. + +```java +package io.cucumber.example; + +import io.cucumber.java.AfterStep; +import io.cucumber.java.BeforeStep; + +public class StepDefinitions { + + @BeforeStep("not @zukini") + public void before(Scenario scenario) { + scenario.log("Runs before each step in scenarios *not* tagged with @zukini"); + } + + @AfterStep + public void after(Scenario scenario) { + scenario.log("Runs after each step"); + } +} +``` + ## Transformers Cucumber expression parameters, data tables and docs strings can be transformed diff --git a/java/src/main/java/io/cucumber/java/After.java b/java/src/main/java/io/cucumber/java/After.java index 44fd3043fa..8d4e05226b 100644 --- a/java/src/main/java/io/cucumber/java/After.java +++ b/java/src/main/java/io/cucumber/java/After.java @@ -24,8 +24,10 @@ String value() default ""; /** - * @return the order in which this hook should run. Higher numbers are run - * first. The default order is 10000. + * 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; diff --git a/java/src/main/java/io/cucumber/java/AfterAll.java b/java/src/main/java/io/cucumber/java/AfterAll.java new file mode 100644 index 0000000000..938bc757dd --- /dev/null +++ b/java/src/main/java/io/cucumber/java/AfterAll.java @@ -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.EXPERIMENTAL) +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; +} diff --git a/java/src/main/java/io/cucumber/java/Before.java b/java/src/main/java/io/cucumber/java/Before.java index 4c171a5c32..5fd462cad9 100644 --- a/java/src/main/java/io/cucumber/java/Before.java +++ b/java/src/main/java/io/cucumber/java/Before.java @@ -24,8 +24,10 @@ String value() default ""; /** - * @return the order in which this hook should run. Lower numbers are run - * first. The default order is 10000. + * 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; diff --git a/java/src/main/java/io/cucumber/java/BeforeAll.java b/java/src/main/java/io/cucumber/java/BeforeAll.java new file mode 100644 index 0000000000..7bc6e679a8 --- /dev/null +++ b/java/src/main/java/io/cucumber/java/BeforeAll.java @@ -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.EXPERIMENTAL) +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; +} diff --git a/java/src/main/java/io/cucumber/java/GlueAdaptor.java b/java/src/main/java/io/cucumber/java/GlueAdaptor.java index 89f5f1d94e..2bb64f32ae 100644 --- a/java/src/main/java/io/cucumber/java/GlueAdaptor.java +++ b/java/src/main/java/io/cucumber/java/GlueAdaptor.java @@ -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(); diff --git a/java/src/main/java/io/cucumber/java/JavaHookDefinition.java b/java/src/main/java/io/cucumber/java/JavaHookDefinition.java index 825daf46c7..3fcc2e1e52 100644 --- a/java/src/main/java/io/cucumber/java/JavaHookDefinition.java +++ b/java/src/main/java/io/cucumber/java/JavaHookDefinition.java @@ -5,6 +5,7 @@ import io.cucumber.core.backend.TestCaseState; import java.lang.reflect.Method; +import java.lang.reflect.Type; import static io.cucumber.java.InvalidMethodSignatureException.builder; import static java.util.Objects.requireNonNull; @@ -33,6 +34,10 @@ private static Method requireValidMethod(Method method) { } } + Type returnType = method.getGenericReturnType(); + if (!Void.class.equals(returnType) && !void.class.equals(returnType)) { + throw createInvalidSignatureException(method); + } return method; } diff --git a/java/src/main/java/io/cucumber/java/JavaStaticHookDefinition.java b/java/src/main/java/io/cucumber/java/JavaStaticHookDefinition.java new file mode 100644 index 0000000000..1fc61c8f02 --- /dev/null +++ b/java/src/main/java/io/cucumber/java/JavaStaticHookDefinition.java @@ -0,0 +1,56 @@ +package io.cucumber.java; + +import io.cucumber.core.backend.Lookup; +import io.cucumber.core.backend.StaticHookDefinition; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +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; + + JavaStaticHookDefinition(Method method, int order, Lookup lookup) { + super(requireValidMethod(method), lookup); + this.order = order; + } + + private static Method requireValidMethod(Method method) { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 0) { + throw createInvalidSignatureException(method); + } + + if (!isStatic(method.getModifiers())) { + throw createInvalidSignatureException(method); + } + + Type returnType = method.getGenericReturnType(); + if (!Void.class.equals(returnType) && !void.class.equals(returnType)) { + 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() { + invokeMethod(); + } + + @Override + public int getOrder() { + return order; + } +} diff --git a/java/src/main/java/io/cucumber/java/MethodScanner.java b/java/src/main/java/io/cucumber/java/MethodScanner.java index 3834dfb38c..e79e72fd66 100644 --- a/java/src/main/java/io/cucumber/java/MethodScanner.java +++ b/java/src/main/java/io/cucumber/java/MethodScanner.java @@ -5,11 +5,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.function.BiConsumer; import static io.cucumber.core.resource.ClasspathSupport.classPathScanningExplanation; 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 { @@ -43,9 +45,9 @@ private static Method[] safelyGetMethods(Class aClass) { } 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 consumer, Class aClass, Method method) { @@ -78,7 +80,9 @@ private static void validateMethod(Class glueCodeClass, Method method) { private static boolean isHookAnnotation(Annotation annotation) { Class 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) diff --git a/java/src/test/java/io/cucumber/java/GlueAdaptorTest.java b/java/src/test/java/io/cucumber/java/GlueAdaptorTest.java index ea413c8700..aa2b3b4938 100644 --- a/java/src/test/java/io/cucumber/java/GlueAdaptorTest.java +++ b/java/src/test/java/io/cucumber/java/GlueAdaptorTest.java @@ -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; @@ -59,8 +60,20 @@ protected boolean matchesSafely(StepDefinition item) { private HookDefinition beforeStepHook; private HookDefinition afterHook; private HookDefinition beforeHook; + private StaticHookDefinition afterAllHook; + private StaticHookDefinition beforeAllHook; private DocStringTypeDefinition docStringTypeDefinition; private final Glue container = new Glue() { + @Override + public void addBeforeAllHook(StaticHookDefinition beforeAllHook) { + GlueAdaptorTest.this.beforeAllHook = beforeAllHook; + } + + @Override + public void addAfterAllHook(StaticHookDefinition afterAllHook) { + GlueAdaptorTest.this.afterAllHook = afterAllHook; + } + @Override public void addStepDefinition(StepDefinition stepDefinition) { GlueAdaptorTest.this.stepDefinitions.add(stepDefinition); @@ -150,6 +163,8 @@ void creates_all_glue_steps() { () -> assertThat(beforeStepHook, notNullValue()), () -> assertThat(afterHook, notNullValue()), () -> assertThat(beforeHook, notNullValue()), + () -> assertThat(beforeAllHook, notNullValue()), + () -> assertThat(afterAllHook, notNullValue()), () -> assertThat(docStringTypeDefinition, notNullValue())); } @@ -209,6 +224,16 @@ public void before() { } + @AfterAll + public static void afterAll() { + + } + + @BeforeAll + public static void beforeAll() { + + } + @DocStringType public Object json(String docString) { return null; diff --git a/java/src/test/java/io/cucumber/java/JavaHookDefinitionTest.java b/java/src/test/java/io/cucumber/java/JavaHookDefinitionTest.java index 15f7829527..e1616cd003 100644 --- a/java/src/test/java/io/cucumber/java/JavaHookDefinitionTest.java +++ b/java/src/test/java/io/cucumber/java/JavaHookDefinitionTest.java @@ -103,4 +103,23 @@ public void too_many_parameters(Scenario arg1, String arg2) { } + @Test + void fails_with_non_void_return_type() throws Throwable { + Method method = JavaHookDefinitionTest.class.getMethod("string_return_type"); + InvalidMethodSignatureException exception = assertThrows( + InvalidMethodSignatureException.class, + () -> new JavaHookDefinition(method, "", 0, lookup)); + assertThat(exception.getMessage(), startsWith("" + + "A method annotated with Before, After, BeforeStep or AfterStep must have one of these signatures:\n" + + " * public void before_or_after(io.cucumber.java.Scenario scenario)\n" + + " * public void before_or_after()\n" + + "at io.cucumber.java.JavaHookDefinitionTest.string_return_type()\n")); + } + + @Before + public String string_return_type() { + invoked = true; + return ""; + } + } diff --git a/java/src/test/java/io/cucumber/java/JavaStaticHookDefinitionTest.java b/java/src/test/java/io/cucumber/java/JavaStaticHookDefinitionTest.java new file mode 100644 index 0000000000..a4e4f633a0 --- /dev/null +++ b/java/src/test/java/io/cucumber/java/JavaStaticHookDefinitionTest.java @@ -0,0 +1,81 @@ +package io.cucumber.java; + +import io.cucumber.core.backend.Lookup; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings({ "WeakerAccess" }) +public class JavaStaticHookDefinitionTest { + + private final Lookup lookup = new Lookup() { + + @Override + @SuppressWarnings("unchecked") + public T getInstance(Class glueClass) { + return (T) JavaStaticHookDefinitionTest.this; + } + }; + + private static boolean invoked; + + @BeforeEach + void reset() { + invoked = false; + } + + @Test + void can_create_with_no_argument() throws Throwable { + Method method = JavaStaticHookDefinitionTest.class.getMethod("no_arguments"); + JavaStaticHookDefinition definition = new JavaStaticHookDefinition(method, 0, lookup); + definition.execute(); + assertTrue(invoked); + } + + @BeforeAll + public static void no_arguments() { + invoked = true; + } + + @Test + void fails_with_arguments() throws Throwable { + Method method = JavaStaticHookDefinitionTest.class.getMethod("single_argument", Scenario.class); + InvalidMethodSignatureException exception = assertThrows( + InvalidMethodSignatureException.class, + () -> new JavaStaticHookDefinition(method, 0, lookup)); + assertThat(exception.getMessage(), startsWith("" + + "A method annotated with BeforeAll or AfterAll must have one of these signatures:\n" + + " * public static void before_or_after_all()\n" + + "at io.cucumber.java.JavaStaticHookDefinitionTest.single_argument(io.cucumber.java.Scenario)\n")); + } + + @Before + public void single_argument(Scenario scenario) { + invoked = true; + } + + @Test + void fails_with_non_void_return_type() throws Throwable { + Method method = JavaStaticHookDefinitionTest.class.getMethod("string_return_type"); + InvalidMethodSignatureException exception = assertThrows( + InvalidMethodSignatureException.class, + () -> new JavaStaticHookDefinition(method, 0, lookup)); + assertThat(exception.getMessage(), startsWith("" + + "A method annotated with BeforeAll or AfterAll must have one of these signatures:\n" + + " * public static void before_or_after_all()\n" + + "at io.cucumber.java.JavaStaticHookDefinitionTest.string_return_type()\n")); + } + + @Before + public String string_return_type() { + invoked = true; + return ""; + } + +} diff --git a/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java b/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java index 3a471046f0..f689e0abbd 100644 --- a/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java +++ b/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java @@ -27,13 +27,24 @@ private static void recursivelyMerge(TestDescriptor descriptor, TestDescriptor p } @Override - public CucumberEngineExecutionContext before(CucumberEngineExecutionContext context) { + public CucumberEngineExecutionContext prepare(CucumberEngineExecutionContext context) { context.startTestRun(); return context; } + @Override + public CucumberEngineExecutionContext before(CucumberEngineExecutionContext context) { + context.runBeforeAllHooks(); + return context; + } + @Override public void after(CucumberEngineExecutionContext context) { + context.runAfterAllHooks(); + } + + @Override + public void cleanUp(CucumberEngineExecutionContext context) { context.finishTestRun(); } diff --git a/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java b/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java index 4810a9dba7..3ec51705b0 100644 --- a/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java +++ b/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java @@ -79,6 +79,10 @@ void startTestRun() { context.startTestRun(); } + public void runBeforeAllHooks() { + context.runBeforeAllHooks(); + } + public void beforeFeature(Feature feature) { context.beforeFeature(feature); } @@ -94,6 +98,10 @@ void runTestCase(Pickle pickle) { }); } + public void runAfterAllHooks() { + context.runAfterAllHooks(); + } + public void finishTestRun() { context.finishTestRun(); } diff --git a/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java b/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java index 7afe0f44b0..f5a0914478 100644 --- a/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java +++ b/junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java @@ -13,6 +13,7 @@ import static io.cucumber.junit.platform.engine.Constants.FILTER_NAME_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FILTER_TAGS_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.CucumberEngineDescriptor.ENGINE_ID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -50,6 +51,7 @@ void createExecutionContext() { @Test void selectAndExecuteSingleScenario() { EngineTestKit.engine(ENGINE_ID) + .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") .selectors(selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature")) .execute() .testEvents() @@ -61,6 +63,7 @@ void selectAndExecuteSingleScenario() { @Test void selectAndSkipDisabledScenarioByTags() { EngineTestKit.engine(ENGINE_ID) + .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") .configurationParameter(FILTER_TAGS_PROPERTY_NAME, "@Integration and not @Disabled") .selectors(selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature")) .execute() @@ -74,6 +77,7 @@ void selectAndSkipDisabledScenarioByTags() { @Test void selectAndSkipDisabledScenarioByName() { EngineTestKit.engine(ENGINE_ID) + .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") .configurationParameter(FILTER_NAME_PROPERTY_NAME, "^Nothing$") .selectors(selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature")) .execute() diff --git a/junit/src/main/java/io/cucumber/junit/Cucumber.java b/junit/src/main/java/io/cucumber/junit/Cucumber.java index 807417fe8c..3e58946015 100644 --- a/junit/src/main/java/io/cucumber/junit/Cucumber.java +++ b/junit/src/main/java/io/cucumber/junit/Cucumber.java @@ -92,7 +92,6 @@ public final class Cucumber extends ParentRunner> { private final List> children; private final EventBus bus; - private final List features; private final Plugins plugins; private final CucumberExecutionContext context; @@ -155,7 +154,7 @@ public Cucumber(Class clazz) throws InitializationError { Supplier classLoader = ClassLoaders::getDefaultClassLoader; FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions, parser); - this.features = featureSupplier.get(); + List features = featureSupplier.get(); // Create plugins after feature parsing to avoid the creation of empty // files on lexer errors. @@ -179,7 +178,7 @@ public Cucumber(Class clazz) throws InitializationError { this.children = features.stream() .map(feature -> { Integer uniqueSuffix = uniqueSuffix(groupedByName, feature, Feature::getName); - return FeatureRunner.create(feature, uniqueSuffix, filters, runnerSupplier, junitOptions); + return FeatureRunner.create(feature, uniqueSuffix, filters, context, junitOptions); }) .filter(runner -> !runner.isEmpty()) .collect(toList()); @@ -202,8 +201,15 @@ protected void runChild(ParentRunner child, RunNotifier notifier) { @Override protected Statement childrenInvoker(RunNotifier notifier) { - Statement runFeatures = super.childrenInvoker(notifier); - return new RunCucumber(runFeatures); + Statement statement = super.childrenInvoker(notifier); + + statement = new RunBeforeAllHooks(statement); + statement = new RunAfterAllHooks(statement); + + statement = new StartTestRun(statement); + statement = new FinishTestRun(statement); + + return statement; } @Override @@ -212,12 +218,11 @@ public void setScheduler(RunnerScheduler scheduler) { multiThreadingAssumed = true; } - class RunCucumber extends Statement { + private class StartTestRun extends Statement { + private final Statement next; - private final Statement runFeatures; - - RunCucumber(Statement runFeatures) { - this.runFeatures = runFeatures; + public StartTestRun(Statement next) { + this.next = next; } @Override @@ -227,12 +232,23 @@ public void evaluate() throws Throwable { } else { plugins.setEventBusOnEventListenerPlugins(bus); } - context.startTestRun(); - features.forEach(context::beforeFeature); + next.evaluate(); + } + + } + + private class FinishTestRun extends Statement { + private final Statement next; + public FinishTestRun(Statement next) { + this.next = next; + } + + @Override + public void evaluate() throws Throwable { try { - runFeatures.evaluate(); + next.evaluate(); } finally { context.finishTestRun(); } @@ -240,4 +256,37 @@ public void evaluate() throws Throwable { } + private class RunBeforeAllHooks extends Statement { + private final Statement next; + + public RunBeforeAllHooks(Statement next) { + this.next = next; + } + + @Override + public void evaluate() throws Throwable { + context.runBeforeAllHooks(); + next.evaluate(); + } + + } + + private class RunAfterAllHooks extends Statement { + private final Statement next; + + public RunAfterAllHooks(Statement next) { + this.next = next; + } + + @Override + public void evaluate() throws Throwable { + try { + next.evaluate(); + } finally { + context.runAfterAllHooks(); + } + } + + } + } diff --git a/junit/src/main/java/io/cucumber/junit/FeatureRunner.java b/junit/src/main/java/io/cucumber/junit/FeatureRunner.java index 0911fe0e20..4fd134e573 100644 --- a/junit/src/main/java/io/cucumber/junit/FeatureRunner.java +++ b/junit/src/main/java/io/cucumber/junit/FeatureRunner.java @@ -3,7 +3,7 @@ import io.cucumber.core.exception.CucumberException; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.gherkin.Pickle; -import io.cucumber.core.runtime.RunnerSupplier; +import io.cucumber.core.runtime.CucumberExecutionContext; import io.cucumber.junit.PickleRunners.PickleRunner; import org.junit.runner.Description; import org.junit.runner.notification.Failure; @@ -30,10 +30,11 @@ final class FeatureRunner extends ParentRunner { private final Feature feature; private final JUnitOptions options; private final Integer uniqueSuffix; + private final CucumberExecutionContext context; private Description description; private FeatureRunner( - Feature feature, Integer uniqueSuffix, Predicate filter, RunnerSupplier runners, + Feature feature, Integer uniqueSuffix, Predicate filter, CucumberExecutionContext context, JUnitOptions options ) throws InitializationError { @@ -41,6 +42,7 @@ private FeatureRunner( this.feature = feature; this.uniqueSuffix = uniqueSuffix; this.options = options; + this.context = context; Map> groupedByName = feature.getPickles().stream() .collect(groupingBy(Pickle::getName)); @@ -51,18 +53,18 @@ private FeatureRunner( String featureName = getName(); Integer exampleId = uniqueSuffix(groupedByName, pickle, Pickle::getName); return options.stepNotifications() - ? withStepDescriptions(runners, pickle, exampleId, options) - : withNoStepDescriptions(featureName, runners, pickle, exampleId, options); + ? withStepDescriptions(context, pickle, exampleId, options) + : withNoStepDescriptions(featureName, context, pickle, exampleId, options); }) .collect(toList()); } static FeatureRunner create( - Feature feature, Integer uniqueSuffix, Predicate filter, RunnerSupplier runners, + Feature feature, Integer uniqueSuffix, Predicate filter, CucumberExecutionContext context, JUnitOptions options ) { try { - return new FeatureRunner(feature, uniqueSuffix, filter, runners, options); + return new FeatureRunner(feature, uniqueSuffix, filter, context, options); } catch (InitializationError e) { throw new CucumberException("Failed to create scenario runner", e); } @@ -128,6 +130,12 @@ protected Description describeChild(PickleRunner child) { return child.getDescription(); } + @Override + public void run(RunNotifier notifier) { + context.beforeFeature(feature); + super.run(notifier); + } + @Override protected void runChild(PickleRunner child, RunNotifier notifier) { notifier.fireTestStarted(describeChild(child)); diff --git a/junit/src/main/java/io/cucumber/junit/PickleRunners.java b/junit/src/main/java/io/cucumber/junit/PickleRunners.java index 11074ed685..c3935f59ec 100644 --- a/junit/src/main/java/io/cucumber/junit/PickleRunners.java +++ b/junit/src/main/java/io/cucumber/junit/PickleRunners.java @@ -2,8 +2,7 @@ import io.cucumber.core.exception.CucumberException; import io.cucumber.core.gherkin.Pickle; -import io.cucumber.core.runner.Runner; -import io.cucumber.core.runtime.RunnerSupplier; +import io.cucumber.core.runtime.CucumberExecutionContext; import io.cucumber.plugin.event.Step; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; @@ -22,20 +21,20 @@ final class PickleRunners { static PickleRunner withStepDescriptions( - RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix, JUnitOptions options + CucumberExecutionContext context, Pickle pickle, Integer uniqueSuffix, JUnitOptions options ) { try { - return new WithStepDescriptions(runnerSupplier, pickle, uniqueSuffix, options); + return new WithStepDescriptions(context, pickle, uniqueSuffix, options); } catch (InitializationError e) { throw new CucumberException("Failed to create scenario runner", e); } } static PickleRunner withNoStepDescriptions( - String featureName, RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix, + String featureName, CucumberExecutionContext context, Pickle pickle, Integer uniqueSuffix, JUnitOptions jUnitOptions ) { - return new NoStepDescriptions(featureName, runnerSupplier, pickle, uniqueSuffix, jUnitOptions); + return new NoStepDescriptions(featureName, context, pickle, uniqueSuffix, jUnitOptions); } interface PickleRunner { @@ -50,7 +49,7 @@ interface PickleRunner { static class WithStepDescriptions extends ParentRunner implements PickleRunner { - private final RunnerSupplier runnerSupplier; + private final CucumberExecutionContext context; private final Pickle pickle; private final JUnitOptions jUnitOptions; private final Map stepDescriptions = new HashMap<>(); @@ -58,11 +57,11 @@ static class WithStepDescriptions extends ParentRunner implements PickleRu private Description description; WithStepDescriptions( - RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix, JUnitOptions jUnitOptions + CucumberExecutionContext context, Pickle pickle, Integer uniqueSuffix, JUnitOptions jUnitOptions ) throws InitializationError { super((Class) null); - this.runnerSupplier = runnerSupplier; + this.context = context; this.pickle = pickle; this.jUnitOptions = jUnitOptions; this.uniqueSuffix = uniqueSuffix; @@ -104,11 +103,12 @@ public Description describeChild(Step step) { @Override public void run(final RunNotifier notifier) { // Possibly invoked by a thread other then the creating thread - Runner runner = runnerSupplier.get(); - JUnitReporter jUnitReporter = new JUnitReporter(runner.getBus(), jUnitOptions); - jUnitReporter.startExecutionUnit(this, notifier); - runner.runPickle(pickle); - jUnitReporter.finishExecutionUnit(); + context.runTestCase(runner -> { + JUnitReporter jUnitReporter = new JUnitReporter(runner.getBus(), jUnitOptions); + jUnitReporter.startExecutionUnit(this, notifier); + runner.runPickle(pickle); + jUnitReporter.finishExecutionUnit(); + }); } @Override @@ -125,18 +125,18 @@ protected void runChild(Step step, RunNotifier notifier) { static final class NoStepDescriptions implements PickleRunner { private final String featureName; - private final RunnerSupplier runnerSupplier; + private final CucumberExecutionContext context; private final Pickle pickle; private final JUnitOptions jUnitOptions; private final Integer uniqueSuffix; private Description description; NoStepDescriptions( - String featureName, RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix, + String featureName, CucumberExecutionContext context, Pickle pickle, Integer uniqueSuffix, JUnitOptions jUnitOptions ) { this.featureName = featureName; - this.runnerSupplier = runnerSupplier; + this.context = context; this.pickle = pickle; this.jUnitOptions = jUnitOptions; this.uniqueSuffix = uniqueSuffix; @@ -145,11 +145,12 @@ static final class NoStepDescriptions implements PickleRunner { @Override public void run(final RunNotifier notifier) { // Possibly invoked by a thread other then the creating thread - Runner runner = runnerSupplier.get(); - JUnitReporter jUnitReporter = new JUnitReporter(runner.getBus(), jUnitOptions); - jUnitReporter.startExecutionUnit(this, notifier); - runner.runPickle(pickle); - jUnitReporter.finishExecutionUnit(); + context.runTestCase(runner -> { + JUnitReporter jUnitReporter = new JUnitReporter(runner.getBus(), jUnitOptions); + jUnitReporter.startExecutionUnit(this, notifier); + runner.runPickle(pickle); + jUnitReporter.finishExecutionUnit(); + }); } @Override diff --git a/junit/src/test/java/io/cucumber/junit/FeatureRunnerTest.java b/junit/src/test/java/io/cucumber/junit/FeatureRunnerTest.java index e62a9af605..33997263f5 100644 --- a/junit/src/test/java/io/cucumber/junit/FeatureRunnerTest.java +++ b/junit/src/test/java/io/cucumber/junit/FeatureRunnerTest.java @@ -6,6 +6,8 @@ import io.cucumber.core.options.RuntimeOptions; import io.cucumber.core.options.RuntimeOptionsBuilder; import io.cucumber.core.runtime.BackendSupplier; +import io.cucumber.core.runtime.CucumberExecutionContext; +import io.cucumber.core.runtime.ExitStatus; import io.cucumber.core.runtime.ObjectFactoryServiceLoader; import io.cucumber.core.runtime.ObjectFactorySupplier; import io.cucumber.core.runtime.RunnerSupplier; @@ -123,7 +125,9 @@ public Instant instant() { classLoader, runtimeOptions); ThreadLocalRunnerSupplier runnerSupplier = new ThreadLocalRunnerSupplier(runtimeOptions, bus, backendSupplier, objectFactory, typeRegistrySupplier); - return FeatureRunner.create(feature, null, filters, runnerSupplier, junitOption); + CucumberExecutionContext context = new CucumberExecutionContext(bus, new ExitStatus(runtimeOptions), + runnerSupplier); + return FeatureRunner.create(feature, null, filters, context, junitOption); } @Test @@ -366,8 +370,10 @@ void should_notify_of_failure_to_create_runners_and_request_test_execution_to_st RunnerSupplier runnerSupplier = () -> { throw illegalStateException; }; - - FeatureRunner featureRunner = FeatureRunner.create(feature, null, filters, runnerSupplier, new JUnitOptions()); + TimeServiceEventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + RuntimeOptions options = RuntimeOptions.defaultOptions(); + CucumberExecutionContext context = new CucumberExecutionContext(bus, new ExitStatus(options), runnerSupplier); + FeatureRunner featureRunner = FeatureRunner.create(feature, null, filters, context, new JUnitOptions()); RunNotifier notifier = mock(RunNotifier.class); PickleRunners.PickleRunner pickleRunner = featureRunner.getChildren().get(0); @@ -407,7 +413,9 @@ void should_filter_pickles() { throw illegalStateException; }; - FeatureRunner featureRunner = FeatureRunner.create(feature, null, filters, runnerSupplier, new JUnitOptions()); + EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + CucumberExecutionContext context = new CucumberExecutionContext(bus, new ExitStatus(options), runnerSupplier); + FeatureRunner featureRunner = FeatureRunner.create(feature, null, filters, context, new JUnitOptions()); assertThat(featureRunner.getChildren().size(), is(1)); assertThat(featureRunner.getChildren().get(0).getDescription().getDisplayName(), is("scenario_2 name(feature name)")); diff --git a/junit/src/test/java/io/cucumber/junit/InvokeMethodsAroundEventsTest.java b/junit/src/test/java/io/cucumber/junit/InvokeMethodsAroundEventsTest.java index ae619b3e5f..c21ddff72e 100644 --- a/junit/src/test/java/io/cucumber/junit/InvokeMethodsAroundEventsTest.java +++ b/junit/src/test/java/io/cucumber/junit/InvokeMethodsAroundEventsTest.java @@ -2,18 +2,24 @@ import io.cucumber.plugin.ConcurrentEventListener; import io.cucumber.plugin.event.EventPublisher; +import io.cucumber.plugin.event.TestCaseFinished; +import io.cucumber.plugin.event.TestCaseStarted; import io.cucumber.plugin.event.TestRunFinished; import io.cucumber.plugin.event.TestRunStarted; +import io.cucumber.plugin.event.TestSourceRead; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.InitializationError; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; +import static io.cucumber.junit.StubBackendProviderService.callbacks; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsIterableContainingInOrder.contains; @@ -21,19 +27,58 @@ class InvokeMethodsAroundEventsTest { private static final List events = new ArrayList<>(); + private final Consumer callback = events::add; + + @BeforeEach + void before() { + callbacks.add(callback); + } + @AfterEach - void afterClass() { + void after() { events.clear(); + callbacks.remove(callback); } @Test void invoke_methods_around_events() throws InitializationError { Cucumber cucumber = new Cucumber(BeforeAfterClass.class); cucumber.run(new RunNotifier()); - assertThat(events, contains("BeforeClass", "TestRunStarted", "TestRunFinished", "AfterClass")); + assertThat(events, contains( + "BeforeClass", + "TestRunStarted", + "BeforeAll", + "TestSourceRead", + "TestCaseStarted", + "Before", + "Step", + "Step", + "Step", + "After", + "TestCaseFinished", + "TestCaseStarted", + "Before", + "Step", + "Step", + "Step", + "After", + "TestCaseFinished", + "TestSourceRead", + "TestCaseStarted", + "Before", + "Step", + "Step", + "Step", + "After", + "TestCaseFinished", + "AfterAll", + "TestRunFinished", + "AfterClass")); } - @CucumberOptions(plugin = "io.cucumber.junit.InvokeMethodsAroundEventsTest$TestRunStartedFinishedListener") + @CucumberOptions( + plugin = "io.cucumber.junit.InvokeMethodsAroundEventsTest$TestRunStartedFinishedListener", + features = { "classpath:io/cucumber/junit/rule.feature", "classpath:io/cucumber/junit/single.feature" }) public static class BeforeAfterClass { @BeforeClass @@ -55,6 +100,9 @@ public static class TestRunStartedFinishedListener implements ConcurrentEventLis public void setEventPublisher(EventPublisher publisher) { publisher.registerHandlerFor(TestRunStarted.class, event -> events.add("TestRunStarted")); publisher.registerHandlerFor(TestRunFinished.class, event -> events.add("TestRunFinished")); + publisher.registerHandlerFor(TestSourceRead.class, event -> events.add("TestSourceRead")); + publisher.registerHandlerFor(TestCaseStarted.class, event -> events.add("TestCaseStarted")); + publisher.registerHandlerFor(TestCaseFinished.class, event -> events.add("TestCaseFinished")); } } diff --git a/junit/src/test/java/io/cucumber/junit/PickleRunnerWithNoStepDescriptionsTest.java b/junit/src/test/java/io/cucumber/junit/PickleRunnerWithNoStepDescriptionsTest.java index 2bcbe63157..cca3622051 100644 --- a/junit/src/test/java/io/cucumber/junit/PickleRunnerWithNoStepDescriptionsTest.java +++ b/junit/src/test/java/io/cucumber/junit/PickleRunnerWithNoStepDescriptionsTest.java @@ -1,11 +1,19 @@ package io.cucumber.junit; +import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.gherkin.Pickle; +import io.cucumber.core.options.RuntimeOptions; +import io.cucumber.core.plugin.Options; +import io.cucumber.core.runtime.CucumberExecutionContext; +import io.cucumber.core.runtime.ExitStatus; import io.cucumber.core.runtime.RunnerSupplier; +import io.cucumber.core.runtime.TimeServiceEventBus; import io.cucumber.junit.PickleRunners.PickleRunner; import org.junit.jupiter.api.Test; +import java.time.Clock; import java.util.List; +import java.util.UUID; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -14,6 +22,11 @@ class PickleRunnerWithNoStepDescriptionsTest { + final EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + final Options options = RuntimeOptions.defaultOptions(); + final RunnerSupplier runnerSupplier = mock(RunnerSupplier.class); + final CucumberExecutionContext context = new CucumberExecutionContext(bus, new ExitStatus(options), runnerSupplier); + @Test void shouldUseScenarioNameWithFeatureNameAsClassNameForDisplayName() { List pickles = TestPickleBuilder.picklesFromFeature("featurePath", "" + @@ -23,7 +36,7 @@ void shouldUseScenarioNameWithFeatureNameAsClassNameForDisplayName() { PickleRunner runner = PickleRunners.withNoStepDescriptions( "feature name", - mock(RunnerSupplier.class), + context, pickles.get(0), null, createJunitOptions()); @@ -44,7 +57,7 @@ void shouldConvertTextFromFeatureFileForNamesWithFilenameCompatibleNameOption() PickleRunner runner = PickleRunners.withNoStepDescriptions( "feature name", - mock(RunnerSupplier.class), + context, pickles.get(0), null, createFileNameCompatibleJUnitOptions()); @@ -66,7 +79,7 @@ void shouldConvertTextFromFeatureFileWithRussianLanguage() { PickleRunner runner = PickleRunners.withNoStepDescriptions( "имя функции", - mock(RunnerSupplier.class), + context, pickles.get(0), null, createFileNameCompatibleJUnitOptions()); diff --git a/junit/src/test/java/io/cucumber/junit/PickleRunnerWithStepDescriptionsTest.java b/junit/src/test/java/io/cucumber/junit/PickleRunnerWithStepDescriptionsTest.java index 85b71b931a..83f3cd4ff1 100644 --- a/junit/src/test/java/io/cucumber/junit/PickleRunnerWithStepDescriptionsTest.java +++ b/junit/src/test/java/io/cucumber/junit/PickleRunnerWithStepDescriptionsTest.java @@ -1,15 +1,23 @@ package io.cucumber.junit; +import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.gherkin.Pickle; +import io.cucumber.core.options.RuntimeOptions; +import io.cucumber.core.plugin.Options; +import io.cucumber.core.runtime.CucumberExecutionContext; +import io.cucumber.core.runtime.ExitStatus; import io.cucumber.core.runtime.RunnerSupplier; +import io.cucumber.core.runtime.TimeServiceEventBus; import io.cucumber.junit.PickleRunners.PickleRunner; import io.cucumber.junit.PickleRunners.WithStepDescriptions; import io.cucumber.plugin.event.Step; import org.junit.jupiter.api.Test; import org.junit.runner.Description; +import java.time.Clock; import java.util.List; +import java.util.UUID; import static io.cucumber.junit.TestPickleBuilder.picklesFromFeature; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -18,6 +26,11 @@ class PickleRunnerWithStepDescriptionsTest { + final EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID); + final Options options = RuntimeOptions.defaultOptions(); + final RunnerSupplier runnerSupplier = mock(RunnerSupplier.class); + final CucumberExecutionContext context = new CucumberExecutionContext(bus, new ExitStatus(options), runnerSupplier); + @Test void shouldAssignUnequalDescriptionsToDifferentOccurrencesOfSameStepInAScenario() { List pickles = picklesFromFeature("path/test.feature", "" + @@ -32,7 +45,7 @@ void shouldAssignUnequalDescriptionsToDifferentOccurrencesOfSameStepInAScenario( " Then baz\n"); WithStepDescriptions runner = (WithStepDescriptions) PickleRunners.withStepDescriptions( - mock(RunnerSupplier.class), + context, pickles.get(0), null, createJunitOptions()); @@ -68,7 +81,7 @@ void shouldAssignUnequalDescriptionsToDifferentStepsInAScenarioOutline() { " | a1 | r1 |\n"); WithStepDescriptions runner = (WithStepDescriptions) PickleRunners.withStepDescriptions( - mock(RunnerSupplier.class), + context, features.getPickles().get(0), null, createJunitOptions()); @@ -93,7 +106,7 @@ void shouldIncludeScenarioNameAsClassNameInStepDescriptions() { " Then another step\n"); PickleRunner runner = PickleRunners.withStepDescriptions( - mock(RunnerSupplier.class), + context, features.getPickles().get(0), null, createJunitOptions()); @@ -116,7 +129,7 @@ void shouldUseScenarioNameForDisplayName() { " Then it works\n"); PickleRunner runner = PickleRunners.withStepDescriptions( - mock(RunnerSupplier.class), + context, pickles.get(0), null, createJunitOptions()); @@ -132,7 +145,7 @@ void shouldUseStepKeyworkAndNameForChildName() { " Then it works\n"); PickleRunner runner = PickleRunners.withStepDescriptions( - mock(RunnerSupplier.class), + context, pickles.get(0), null, createJunitOptions()); @@ -148,7 +161,7 @@ void shouldConvertTextFromFeatureFileForNamesWithFilenameCompatibleNameOption() " Then it works\n"); PickleRunner runner = PickleRunners.withStepDescriptions( - mock(RunnerSupplier.class), + context, pickles.get(0), null, createFileNameCompatibleJunitOptions()); diff --git a/junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java b/junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java index bf250b852f..cb4ad19255 100644 --- a/junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java +++ b/junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java @@ -4,21 +4,28 @@ import io.cucumber.core.backend.BackendProviderService; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.ParameterInfo; import io.cucumber.core.backend.Snippet; +import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.backend.StepDefinition; +import io.cucumber.core.backend.TestCaseState; import java.lang.reflect.Type; import java.net.URI; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; public class StubBackendProviderService implements BackendProviderService { + static final List> callbacks = new ArrayList<>(); + @Override public Backend create(Lookup lookup, Container container, Supplier classLoader) { return new StubBackend(); @@ -57,6 +64,64 @@ public void loadGlue(Glue glue, List gluePaths) { glue.addStepDefinition(createStepDefinition("C is used")); glue.addStepDefinition(createStepDefinition("D is used")); + glue.addBeforeAllHook(createStaticHook("BeforeAll")); + glue.addAfterAllHook(createStaticHook("AfterAll")); + glue.addBeforeHook(createHook("Before")); + glue.addAfterHook(createHook("After")); + + } + + private HookDefinition createHook(String event) { + return new HookDefinition() { + @Override + public void execute(TestCaseState state) { + callbacks.forEach(consumer -> consumer.accept(event)); + } + + @Override + public String getTagExpression() { + return ""; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public boolean isDefinedAt(StackTraceElement stackTraceElement) { + return false; + } + + @Override + public String getLocation() { + return "stubbed location"; + } + }; + } + + private StaticHookDefinition createStaticHook(String event) { + return new StaticHookDefinition() { + @Override + public void execute() { + callbacks.forEach(consumer -> consumer.accept(event)); + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public boolean isDefinedAt(StackTraceElement stackTraceElement) { + return false; + } + + @Override + public String getLocation() { + return "stubbed location"; + } + }; } private StepDefinition createStepDefinition(final String pattern) { @@ -64,7 +129,7 @@ private StepDefinition createStepDefinition(final String pattern) { @Override public void execute(Object[] args) { - + callbacks.forEach(consumer -> consumer.accept("Step")); } @Override diff --git a/picocontainer/pom.xml b/picocontainer/pom.xml index 70f110f945..1b4063c3ec 100644 --- a/picocontainer/pom.xml +++ b/picocontainer/pom.xml @@ -59,7 +59,7 @@ maven-antrun-plugin - CLI-test + cli-test integration-test run diff --git a/pom.xml b/pom.xml index 6d4bfec0c2..f5b9ae8af5 100644 --- a/pom.xml +++ b/pom.xml @@ -289,6 +289,17 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M5 + diff --git a/testng/src/main/java/io/cucumber/testng/TestNGCucumberRunner.java b/testng/src/main/java/io/cucumber/testng/TestNGCucumberRunner.java index ca6fd4701f..80c44f7529 100644 --- a/testng/src/main/java/io/cucumber/testng/TestNGCucumberRunner.java +++ b/testng/src/main/java/io/cucumber/testng/TestNGCucumberRunner.java @@ -107,6 +107,7 @@ public TestNGCucumberRunner(Class clazz) { plugins.setSerialEventBusOnEventListenerPlugins(bus); features = featureSupplier.get(); context.startTestRun(); + context.runBeforeAllHooks(); features.forEach(context::beforeFeature); } @@ -124,7 +125,11 @@ public void runScenario(io.cucumber.testng.Pickle pickle) { * Finishes test execution by Cucumber. */ public void finish() { - context.finishTestRun(); + try { + context.runAfterAllHooks(); + } finally { + context.finishTestRun(); + } } /**