From 3bc80b964971f9a61f6d22d6fe772499edbb8933 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 3 Apr 2021 23:28:11 +0200 Subject: [PATCH] [Java] Add BeforeAll and AfterAll hooks (#1876) `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. 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. ```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. Fixes: #515 --- CHANGELOG.md | 1 + .../java/io/cucumber/core/backend/Glue.java | 4 + .../core/backend/StaticHookDefinition.java | 11 ++ .../main/java/io/cucumber/core/cli/Main.java | 4 - .../exception/CompositeCucumberException.java | 18 +-- .../cucumber/core/plugin/JsonFormatter.java | 10 +- .../cucumber/core/plugin/PrettyFormatter.java | 13 ++- .../core/plugin/ProgressFormatter.java | 6 - .../cucumber/core/plugin/TeamCityPlugin.java | 33 ++++-- .../io/cucumber/core/runner/CachingGlue.java | 39 ++++++- .../java/io/cucumber/core/runner/Runner.java | 34 +++++- .../runtime/CucumberExecutionContext.java | 39 +++++-- .../io/cucumber/core/runtime/Runtime.java | 30 +++-- .../backend/StubStaticHookDefinition.java | 61 +++++++++++ .../CompositeCucumberExceptionTest.java | 16 +-- .../core/plugin/PrettyFormatterTest.java | 28 +++++ .../core/plugin/TeamCityPluginTest.java | 33 ++++++ .../core/resource/ClasspathScannerTest.java | 3 +- .../io/cucumber/core/runner/RunnerTest.java | 91 +++++++++------- .../runtime/CucumberExecutionContextTest.java | 22 +++- .../io/cucumber/core/runtime/RuntimeTest.java | 9 +- .../core/runtime/StubBackendSupplier.java | 20 +++- .../calculator/RpnCalculatorSteps.java | 32 ++++-- .../calculator/RpnCalculatorSteps.java | 32 ++++-- .../RpnCalculatorStepDefinitions.java | 32 ++++-- .../calculator/RpnCalculatorSteps.java | 32 ++++-- examples/spring-java-junit5/pom.xml | 2 +- java/README.md | 103 ++++++++++++++++-- .../src/main/java/io/cucumber/java/After.java | 6 +- .../main/java/io/cucumber/java/AfterAll.java | 25 +++++ .../main/java/io/cucumber/java/Before.java | 6 +- .../main/java/io/cucumber/java/BeforeAll.java | 25 +++++ .../java/io/cucumber/java/GlueAdaptor.java | 6 + .../io/cucumber/java/JavaHookDefinition.java | 5 + .../java/JavaStaticHookDefinition.java | 56 ++++++++++ .../java/io/cucumber/java/MethodScanner.java | 12 +- .../io/cucumber/java/GlueAdaptorTest.java | 25 +++++ .../cucumber/java/JavaHookDefinitionTest.java | 19 ++++ .../java/JavaStaticHookDefinitionTest.java | 81 ++++++++++++++ .../engine/CucumberEngineDescriptor.java | 13 ++- .../CucumberEngineExecutionContext.java | 8 ++ .../engine/CucumberTestEngineTest.java | 4 + .../main/java/io/cucumber/junit/Cucumber.java | 75 ++++++++++--- .../java/io/cucumber/junit/FeatureRunner.java | 20 +++- .../java/io/cucumber/junit/PickleRunners.java | 45 ++++---- .../io/cucumber/junit/FeatureRunnerTest.java | 16 ++- .../junit/InvokeMethodsAroundEventsTest.java | 54 ++++++++- ...ickleRunnerWithNoStepDescriptionsTest.java | 19 +++- .../PickleRunnerWithStepDescriptionsTest.java | 25 ++++- .../junit/StubBackendProviderService.java | 67 +++++++++++- picocontainer/pom.xml | 2 +- pom.xml | 11 ++ .../cucumber/testng/TestNGCucumberRunner.java | 7 +- 53 files changed, 1146 insertions(+), 244 deletions(-) create mode 100644 core/src/main/java/io/cucumber/core/backend/StaticHookDefinition.java create mode 100644 core/src/test/java/io/cucumber/core/backend/StubStaticHookDefinition.java create mode 100644 java/src/main/java/io/cucumber/java/AfterAll.java create mode 100644 java/src/main/java/io/cucumber/java/BeforeAll.java create mode 100644 java/src/main/java/io/cucumber/java/JavaStaticHookDefinition.java create mode 100644 java/src/test/java/io/cucumber/java/JavaStaticHookDefinitionTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf7e98465..81440578ea 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 b0d28b86ba..dd574a9964 100644 --- a/core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java +++ b/core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java @@ -51,6 +51,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; @@ -347,11 +348,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(); + } } /**