diff --git a/.github/workflows/test-java.yml b/.github/workflows/test-java.yml index 578863b795..e7c2121a67 100644 --- a/.github/workflows/test-java.yml +++ b/.github/workflows/test-java.yml @@ -33,8 +33,6 @@ jobs: run: ./mvnw install -Pinclude-extra-modules -DskipTests=true -DskipITs=true -D"archetype.test.skip=true" -D"maven.javadoc.skip=true" --batch-mode -D"style.color=always" --show-version - name: Test run: ./mvnw verify -Pinclude-extra-modules -D"style.color=always" - env: - CUCUMBER_PUBLISH_TOKEN: ${{ secrets.CUCUMBER_PUBLISH_TOKEN }} javadoc: name: 'Javadoc' diff --git a/CHANGELOG.md b/CHANGELOG.md index ab8549ac40..b1623f23e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- [JUnit Platform Engine] Option to include a parameterized scenario name only if the scenario is parameterized ([#2835](https://github.com/cucumber/cucumber-jvm/pull/2835) M.P. Korstanje) +- [JUnit Platform Engine] Option to order features and scenarios ([#2835](https://github.com/cucumber/cucumber-jvm/pull/2835) M.P. Korstanje) +- [JUnit Platform Engine] Log discovery issues when a classpath resource selector is (e.g. `@SelectClasspathResource`) is used to select a directory. ([#2835](https://github.com/cucumber/cucumber-jvm/pull/2835) M.P. Korstanje) + ### Changed +- [JUnit Platform Engine] Use JUnit's `EngineDiscoveryRequestResolver` to resolve classpath based resources. ([#2835](https://github.com/cucumber/cucumber-jvm/pull/2835) M.P. Korstanje) - [JUnit Platform Engine] Use JUnit Platform 1.13.1 (JUnit Jupiter 5.13.1) +### Fixed +- [JUnit Platform Engine] Log discovery issues for feature files with parse errors. ([#2835](https://github.com/cucumber/cucumber-jvm/pull/2835) M.P. Korstanje) + + ## [7.23.0] - 2025-05-29 ### Added - [JUnit Platform Engine, TestNG] Remove framework elements from `UndefinedStepException` stacktrace ([#3002](https://github.com/cucumber/cucumber-jvm/pull/3002) M.P. Korstanje) diff --git a/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureIdentifier.java b/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureIdentifier.java index 113c8cf7d6..84d7391f33 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureIdentifier.java +++ b/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureIdentifier.java @@ -32,11 +32,15 @@ public static URI parse(URI featureIdentifier) { } public static boolean isFeature(URI featureIdentifier) { - return featureIdentifier.getSchemeSpecificPart().endsWith(FEATURE_FILE_SUFFIX); + return isFeature(featureIdentifier.getSchemeSpecificPart()); } public static boolean isFeature(Path path) { - return path.getFileName().toString().endsWith(FEATURE_FILE_SUFFIX); + return isFeature(path.getFileName().toString()); + } + + public static boolean isFeature(String fileName) { + return fileName.endsWith(FEATURE_FILE_SUFFIX); } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java index 6f314e4eea..637c79edea 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java @@ -42,7 +42,8 @@ public final class Constants { * Valid values are {@code lexical}, {@code reverse}, {@code random} or * {@code random:[seed]}. *

- * By default features are executed in lexical file name order + * By default, features are executed in lexical file name order and + * scenarios in a feature from top to bottom. */ public static final String EXECUTION_ORDER_PROPERTY_NAME = "cucumber.execution.order"; diff --git a/cucumber-core/src/main/java/io/cucumber/core/order/StandardPickleOrders.java b/cucumber-core/src/main/java/io/cucumber/core/order/StandardPickleOrders.java index 95cdb6d501..c5216edfa0 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/order/StandardPickleOrders.java +++ b/cucumber-core/src/main/java/io/cucumber/core/order/StandardPickleOrders.java @@ -8,8 +8,9 @@ public final class StandardPickleOrders { - private static final Comparator pickleUriComparator = Comparator.comparing(Pickle::getUri) - .thenComparing(pickle -> pickle.getLocation().getLine()); + private static final Comparator pickleUriComparator = Comparator + .comparing(Pickle::getUri) + .thenComparing(Pickle::getLocation); private StandardPickleOrders() { diff --git a/cucumber-core/src/main/java/io/cucumber/core/resource/PathScanner.java b/cucumber-core/src/main/java/io/cucumber/core/resource/PathScanner.java index ea12bf21c7..a4ec320864 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/resource/PathScanner.java +++ b/cucumber-core/src/main/java/io/cucumber/core/resource/PathScanner.java @@ -2,6 +2,7 @@ import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; +import org.apiguardian.api.API; import java.io.IOException; import java.net.URI; @@ -20,8 +21,10 @@ import static java.nio.file.FileVisitResult.CONTINUE; import static java.nio.file.Files.exists; import static java.nio.file.Files.walkFileTree; +import static org.apiguardian.api.API.Status.INTERNAL; -class PathScanner { +@API(status = INTERNAL) +public class PathScanner { private static final Logger log = LoggerFactory.getLogger(PathScanner.class); @@ -48,10 +51,14 @@ void findResourcesForPath(Path path, Predicate filter, Function filter, Consumer consumer) { try { - walkFileTree(path, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, - new ResourceFileVisitor(filter, consumer.apply(path))); + EnumSet options = EnumSet.of(FileVisitOption.FOLLOW_LINKS); + ResourceFileVisitor visitor = new ResourceFileVisitor(filter, consumer); + walkFileTree(path, options, Integer.MAX_VALUE, visitor); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExample.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExample.java index 5a38fa1bd7..2f64d03bda 100644 --- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExample.java +++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExample.java @@ -4,6 +4,7 @@ import io.cucumber.plugin.event.Location; import io.cucumber.plugin.event.Node; +import java.net.URI; import java.util.Optional; final class GherkinMessagesExample implements Node.Example { @@ -20,6 +21,11 @@ final class GherkinMessagesExample implements Node.Example { this.rowIndex = rowIndex; } + @Override + public URI getUri() { + return parent.getUri(); + } + @Override public Location getLocation() { return GherkinMessagesLocation.from(tableRow.getLocation()); diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java index fda9d54964..e7c2265bb0 100644 --- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java +++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesExamples.java @@ -3,6 +3,7 @@ import io.cucumber.plugin.event.Location; import io.cucumber.plugin.event.Node; +import java.net.URI; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -31,6 +32,11 @@ public Collection elements() { return children; } + @Override + public URI getUri() { + return parent.getUri(); + } + @Override public Location getLocation() { return location; diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java index c42c378317..e34cde6f26 100644 --- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java +++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesRule.java @@ -4,6 +4,7 @@ import io.cucumber.plugin.event.Location; import io.cucumber.plugin.event.Node; +import java.net.URI; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -42,6 +43,11 @@ public Collection elements() { return children; } + @Override + public URI getUri() { + return parent.getUri(); + } + @Override public Location getLocation() { return GherkinMessagesLocation.from(rule.getLocation()); diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenario.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenario.java index f35cce97b8..55684eae2f 100644 --- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenario.java +++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenario.java @@ -3,6 +3,7 @@ import io.cucumber.plugin.event.Location; import io.cucumber.plugin.event.Node; +import java.net.URI; import java.util.Optional; final class GherkinMessagesScenario implements Node.Scenario { @@ -20,6 +21,11 @@ public Optional getParent() { return Optional.of(parent); } + @Override + public URI getUri() { + return parent.getUri(); + } + @Override public Location getLocation() { return GherkinMessagesLocation.from(scenario.getLocation()); diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java index b34180c013..8349f9419a 100644 --- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java +++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesScenarioOutline.java @@ -3,6 +3,7 @@ import io.cucumber.plugin.event.Location; import io.cucumber.plugin.event.Node; +import java.net.URI; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -34,6 +35,11 @@ public Collection elements() { return children; } + @Override + public URI getUri() { + return parent.getUri(); + } + @Override public Location getLocation() { return GherkinMessagesLocation.from(scenario.getLocation()); diff --git a/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Feature.java b/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Feature.java index e46951dff8..a8cb5b3b52 100644 --- a/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Feature.java +++ b/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Feature.java @@ -2,7 +2,6 @@ import io.cucumber.plugin.event.Node; -import java.net.URI; import java.util.List; public interface Feature extends Node.Feature { @@ -11,8 +10,6 @@ public interface Feature extends Node.Feature { List getPickles(); - URI getUri(); - String getSource(); Iterable getParseEvents(); diff --git a/cucumber-junit-platform-engine/README.md b/cucumber-junit-platform-engine/README.md index c5a8cfb0aa..b673dee36f 100644 --- a/cucumber-junit-platform-engine/README.md +++ b/cucumber-junit-platform-engine/README.md @@ -26,7 +26,8 @@ like this: ```mermaid erDiagram - "IDE, Maven, Gradle or Console Launcher" ||--|{ "JUnit Platform" : "requests discovery and execution" + "IDE" ||--|{ "JUnit Platform" : "requests discovery and execution" + "Maven or Gradle" ||--|{ "JUnit Platform" : "requests discovery and execution" "Console Launcher" ||--|{ "JUnit Platform" : "requests discovery and execution" "JUnit Platform" ||--|{ "Cucumber Test Engine": "forwards request" "JUnit Platform" ||--|{ "Jupiter Test Engine": "forwards request" @@ -211,11 +212,12 @@ different configurations. Conceptually this looks like this: ```mermaid erDiagram - "IDE, Maven, Gradle or Console Launcher" ||--|{ "JUnit Platform" : "requests discovery and execution" + "IDE" ||--|{ "JUnit Platform" : "requests discovery and execution" + "Maven or Gradle" ||--|{ "JUnit Platform" : "requests discovery and execution" + "Console Launcher" ||--|{ "JUnit Platform" : "requests discovery and execution" "JUnit Platform" ||--|{ "Suite Test Engine": "forwards request" "Suite Test Engine" ||--|{ "@Suite annotated class A" : "discovers and executes" "Suite Test Engine" ||--|{ "@Suite annotated class B" : "discovers and executes" - "@Suite annotated class A" ||--|{ "JUnit Platform (A)" : "requests discovery and execution" "@Suite annotated class B" ||--|{ "JUnit Platform (B)" : "requests discovery and execution" "JUnit Platform (A)" ||--|{ "Cucumber Test Engine (A)": "forwards request" @@ -424,12 +426,12 @@ cucumber.junit-platform.naming-strategy= # long, short or cucumber.junit-platform.naming-strategy.short.example-name= # number, number-and-pickle-if-parameterized or pickle. # default: number-and-pickle-if-parameterized - # Use example number or pickle name for examples when + # Use example number and/or pickle name for examples when # short naming strategy is used cucumber.junit-platform.naming-strategy.long.example-name= # number, number-and-pickle-if-parameterized or pickle. # default: number-and-pickle-if-parameterized - # Use example number or pickle name for examples when + # Use example number and/or pickle name for examples when # long naming strategy is used cucumber.junit-platform.naming-strategy.surefire.example-name= # number or pickle. @@ -471,6 +473,16 @@ cucumber.execution.execution-mode.feature= # same_thread or # concurrent - executes scenarios concurrently on any # available thread +cucumber.execution.order= # lexical, reverse or random + # default: lexical + # lexical - executes features in lexical uri order, scenarios and examples from top to bottom + # reverse - as lexical, but with the elements of each container reversed + # random - executes scenarios and examples in a random order within their parent container + +cucumber.execution.order.random.seed= # any long + # example: 20090120 + # enables deterministic random execution + cucumber.execution.parallel.enabled= # true or false. # default: false diff --git a/cucumber-junit-platform-engine/pom.xml b/cucumber-junit-platform-engine/pom.xml index ca741a096a..3b70cca899 100644 --- a/cucumber-junit-platform-engine/pom.xml +++ b/cucumber-junit-platform-engine/pom.xml @@ -65,6 +65,11 @@ junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-params + test + org.junit.platform junit-platform-testkit diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CachingFeatureParser.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CachingFeatureParser.java deleted file mode 100644 index 621cda5fe3..0000000000 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CachingFeatureParser.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import io.cucumber.core.feature.FeatureParser; -import io.cucumber.core.gherkin.Feature; -import io.cucumber.core.resource.Resource; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -class CachingFeatureParser { - - private final Map> cache = new HashMap<>(); - private final FeatureParser delegate; - - CachingFeatureParser(FeatureParser delegate) { - this.delegate = delegate; - } - - Optional parseResource(Resource resource) { - return cache.computeIfAbsent(resource.getUri(), uri -> delegate.parseResource(resource)); - } -} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java index 0349498808..1cacae44cb 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java @@ -291,6 +291,27 @@ public final class Constants { */ public static final String EXECUTION_MODE_FEATURE_PROPERTY_NAME = "cucumber.execution.execution-mode.feature"; + /** + * Property name used to set execution order: {@value} + *

+ * Valid values are {@code lexical}, {@code reverse} or {@code random}. + *

+ * By default, features are executed in lexical file name order and + * scenarios in a feature from top to bottom. + */ + public static final String EXECUTION_ORDER_PROPERTY_NAME = io.cucumber.core.options.Constants.EXECUTION_ORDER_PROPERTY_NAME; + + /** + * Property name used to set the seed for random execution order: {@value} + *

+ * Valid values are any value understood by {@link Long#decode(String)}. If + * omitted a random seed is used instead. The exact value can be obtained by + * + * listening for discovery issues. + */ + public static final String EXECUTION_ORDER_RANDOM_SEED_PROPERTY_NAME = "cucumber.execution.order.random.seed"; + /** * Property name used to enable parallel test execution: {@value} *

diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Cucumber.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Cucumber.java index 41ea03f685..4684e78719 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Cucumber.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Cucumber.java @@ -24,7 +24,9 @@ *

* * @deprecated Please use the JUnit Platform Suite to run Cucumber in - * combination with Surefire or Gradle. E.g:

{@code
+ *             combination with Surefire or Gradle. E.g:
+ * 
+ *             
{@code
  *package com.example;
  *
  *import org.junit.platform.suite.api.ConfigurationParameter;
@@ -33,15 +35,15 @@
  *
  *import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
  *
- *@Suite
- *@SelectPackages("com.example")
- *@ConfigurationParameter(
- *   key = GLUE_PROPERTY_NAME,
- *   value = "com.example"
- *)
- *public class RunCucumberTest {
- *}
- *}
+ * @Suite + * @SelectPackages("com.example") + * @ConfigurationParameter( + * key = GLUE_PROPERTY_NAME, + * value = "com.example") + * public class RunCucumberTest { + * } + * }
+ * * @see CucumberTestEngine */ @API(status = Status.DEPRECATED) diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java similarity index 83% rename from cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java rename to cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java index f12b963b12..3c0a8909de 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineOptions.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java @@ -11,23 +11,29 @@ import io.cucumber.core.plugin.NoPublishFormatter; import io.cucumber.core.plugin.PublishFormatter; import io.cucumber.core.snippets.SnippetType; +import io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureWithLinesSelector; import io.cucumber.tagexpressions.Expression; import io.cucumber.tagexpressions.TagExpressionParser; import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.support.config.PrefixedConfigurationParameters; +import org.junit.platform.engine.support.hierarchical.Node.ExecutionMode; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME_PREFIX; import static io.cucumber.junit.platform.engine.Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.EXECUTION_DRY_RUN_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_EXCLUSIVE_RESOURCES_PREFIX; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_MODE_FEATURE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FILTER_NAME_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FILTER_TAGS_PROPERTY_NAME; @@ -41,8 +47,9 @@ import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.SNIPPET_TYPE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.UUID_GENERATOR_PROPERTY_NAME; +import static java.util.Objects.requireNonNull; -class CucumberEngineOptions implements +class CucumberConfiguration implements io.cucumber.core.plugin.Options, io.cucumber.core.runner.Options, io.cucumber.core.backend.Options, @@ -50,8 +57,8 @@ class CucumberEngineOptions implements private final ConfigurationParameters configurationParameters; - CucumberEngineOptions(ConfigurationParameters configurationParameters) { - this.configurationParameters = configurationParameters; + CucumberConfiguration(ConfigurationParameters configurationParameters) { + this.configurationParameters = requireNonNull(configurationParameters); } @Override @@ -177,14 +184,28 @@ NamingStrategy namingStrategy() { .create(configurationParameters); } - List featuresWithLines() { + Set featuresWithLines() { return configurationParameters.get(FEATURES_PROPERTY_NAME, s -> Arrays.stream(s.split(",")) .map(String::trim) .map(FeatureWithLines::parse) - .sorted(Comparator.comparing(FeatureWithLines::uri)) - .distinct() - .collect(Collectors.toList())) - .orElse(Collections.emptyList()); + .map(FeatureWithLinesSelector::from) + .collect(Collectors.toSet())) + .orElse(Collections.emptySet()); } + + ExecutionMode getExecutionModeFeature() { + return configurationParameters.get(EXECUTION_MODE_FEATURE_PROPERTY_NAME, + value -> ExecutionMode.valueOf(value.toUpperCase(Locale.US))) + .orElse(ExecutionMode.CONCURRENT); + } + + ExclusiveResourceConfiguration getExclusiveResourceConfiguration(String tag) { + requireNonNull(tag); + return new ExclusiveResourceConfiguration(new PrefixedConfigurationParameters( + configurationParameters, + EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + tag)); + + } + } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberDiscoverySelectors.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberDiscoverySelectors.java new file mode 100644 index 0000000000..f82e1e83ec --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberDiscoverySelectors.java @@ -0,0 +1,159 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.core.feature.FeatureWithLines; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.plugin.event.Node; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.FilePosition; + +import java.net.URI; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toSet; + +class CucumberDiscoverySelectors { + + static final class FeatureWithLinesSelector implements DiscoverySelector { + private final URI uri; + private final Set filePositions; + + private FeatureWithLinesSelector(URI uri, Set filePositions) { + this.uri = requireNonNull(uri); + this.filePositions = requireNonNull(filePositions); + } + + static FeatureWithLinesSelector from(FeatureWithLines featureWithLines) { + Set lines = featureWithLines.lines().stream() + .map(FilePosition::from) + .collect(Collectors.toSet()); + return new FeatureWithLinesSelector(featureWithLines.uri(), lines); + } + + static Set from(UniqueId uniqueId) { + return uniqueId.getSegments() + .stream() + .filter(FeatureOrigin::isFeatureSegment) + .map(featureSegment -> { + URI uri = URI.create(featureSegment.getValue()); + Set filePosition = getFilePosition(uniqueId.getLastSegment()); + return new FeatureWithLinesSelector(uri, filePosition); + }) + .collect(Collectors.toSet()); + } + + static FeatureWithLinesSelector from(URI uri) { + Set positions = FilePosition.fromQuery(uri.getQuery()) + .map(Collections::singleton) + .orElseGet(Collections::emptySet); + return new FeatureWithLinesSelector(stripQuery(uri), positions); + } + + private static URI stripQuery(URI uri) { + if (uri.getQuery() == null) { + return uri; + } + String uriString = uri.toString(); + return URI.create(uriString.substring(0, uriString.indexOf('?'))); + } + + private static Set getFilePosition(UniqueId.Segment segment) { + if (FeatureOrigin.isFeatureSegment(segment)) { + return Collections.emptySet(); + } + + int line = Integer.parseInt(segment.getValue()); + return Collections.singleton(FilePosition.from(line)); + } + + URI getUri() { + return uri; + } + + Optional> getFilePositions() { + return filePositions.isEmpty() ? Optional.empty() : Optional.of(filePositions); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + FeatureWithLinesSelector that = (FeatureWithLinesSelector) o; + return uri.equals(that.uri) && filePositions.equals(that.filePositions); + } + + @Override + public int hashCode() { + return Objects.hash(uri, filePositions); + } + } + + static class FeatureElementSelector implements DiscoverySelector { + + private final Feature feature; + private final Node element; + + private FeatureElementSelector(Feature feature) { + this(feature, feature); + } + + private FeatureElementSelector(Feature feature, Node element) { + this.feature = requireNonNull(feature); + this.element = requireNonNull(element); + } + + static FeatureElementSelector selectFeature(Feature feature) { + return new FeatureElementSelector(feature); + } + + static FeatureElementSelector selectElement(Feature feature, Node element) { + return new FeatureElementSelector(feature, element); + } + + static Optional selectElementAt(Feature feature, FilePosition filePosition) { + return feature.findPathTo(candidate -> candidate.getLocation().getLine() == filePosition.getLine()) + .map(nodes -> nodes.get(nodes.size() - 1)) + .map(node -> new FeatureElementSelector(feature, node)); + } + + static Set selectElementsOf(Feature feature, Node selected) { + if (selected instanceof Node.Container) { + Node.Container container = (Node.Container) selected; + return container.elements().stream() + .map(element -> new FeatureElementSelector(feature, element)) + .collect(toSet()); + } + return Collections.emptySet(); + } + + Feature getFeature() { + return feature; + } + + Node getElement() { + return element; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + FeatureElementSelector that = (FeatureElementSelector) o; + return feature.equals(that.feature) && element.equals(that.element); + } + + @Override + public int hashCode() { + return Objects.hash(feature, element); + } + } +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java index 84b0e733e8..2986c670ff 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineDescriptor.java @@ -1,6 +1,5 @@ package io.cucumber.junit.platform.engine; -import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.EngineDescriptor; @@ -9,20 +8,28 @@ import java.util.Optional; import java.util.function.Consumer; +import static java.util.Objects.requireNonNull; + class CucumberEngineDescriptor extends EngineDescriptor implements Node { static final String ENGINE_ID = "cucumber"; + private final CucumberConfiguration configuration; private final TestSource source; - CucumberEngineDescriptor(UniqueId uniqueId) { - this(uniqueId, null); + CucumberEngineDescriptor(UniqueId uniqueId, CucumberConfiguration configuration) { + this(uniqueId, configuration, null); } - CucumberEngineDescriptor(UniqueId uniqueId, TestSource source) { + CucumberEngineDescriptor(UniqueId uniqueId, CucumberConfiguration configuration, TestSource source) { super(uniqueId, "Cucumber"); + this.configuration = requireNonNull(configuration); this.source = source; } + public CucumberConfiguration getConfiguration() { + return configuration; + } + @Override public Optional getSource() { return Optional.ofNullable(this.source); @@ -65,18 +72,4 @@ private CucumberEngineExecutionContext ifChildren( return context; } - void mergeFeature(FeatureDescriptor descriptor) { - recursivelyMerge(descriptor, this); - } - - private static void recursivelyMerge(TestDescriptor descriptor, TestDescriptor parent) { - Optional byUniqueId = parent.findByUniqueId(descriptor.getUniqueId()); - if (!byUniqueId.isPresent()) { - parent.addChild(descriptor); - } else { - byUniqueId.ifPresent( - existingParent -> descriptor.getChildren() - .forEach(child -> recursivelyMerge(child, existingParent))); - } - } } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java index 447b98d279..2609add2bd 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberEngineExecutionContext.java @@ -21,7 +21,6 @@ import io.cucumber.core.runtime.TimeServiceEventBus; import io.cucumber.core.runtime.UuidGeneratorServiceLoader; import org.apiguardian.api.API; -import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.support.hierarchical.EngineExecutionContext; import java.time.Clock; @@ -34,41 +33,43 @@ public final class CucumberEngineExecutionContext implements EngineExecutionContext { private static final Logger log = LoggerFactory.getLogger(CucumberEngineExecutionContext.class); - private final CucumberEngineOptions options; + private final CucumberConfiguration configuration; private CucumberExecutionContext context; - CucumberEngineExecutionContext(ConfigurationParameters configurationParameters) { - options = new CucumberEngineOptions(configurationParameters); + CucumberEngineExecutionContext(CucumberConfiguration configuration) { + this.configuration = configuration; } - CucumberEngineOptions getOptions() { - return options; + CucumberConfiguration getConfiguration() { + return configuration; } private CucumberExecutionContext createCucumberExecutionContext() { Supplier classLoader = CucumberEngineExecutionContext.class::getClassLoader; - UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader, options); + UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader, + configuration); EventBus bus = synchronize( new TimeServiceEventBus(Clock.systemUTC(), uuidGeneratorServiceLoader.loadUuidGenerator())); - ObjectFactoryServiceLoader objectFactoryServiceLoader = new ObjectFactoryServiceLoader(classLoader, options); - Plugins plugins = new Plugins(new PluginFactory(), options); - ExitStatus exitStatus = new ExitStatus(options); + ObjectFactoryServiceLoader objectFactoryServiceLoader = new ObjectFactoryServiceLoader(classLoader, + configuration); + Plugins plugins = new Plugins(new PluginFactory(), configuration); + ExitStatus exitStatus = new ExitStatus(configuration); plugins.addPlugin(exitStatus); RunnerSupplier runnerSupplier; - if (options.isParallelExecutionEnabled()) { + if (configuration.isParallelExecutionEnabled()) { plugins.setSerialEventBusOnEventListenerPlugins(bus); ObjectFactorySupplier objectFactorySupplier = new ThreadLocalObjectFactorySupplier( objectFactoryServiceLoader); BackendSupplier backendSupplier = new BackendServiceLoader(classLoader, objectFactorySupplier); - runnerSupplier = new ThreadLocalRunnerSupplier(options, bus, backendSupplier, objectFactorySupplier); + runnerSupplier = new ThreadLocalRunnerSupplier(configuration, bus, backendSupplier, objectFactorySupplier); } else { plugins.setEventBusOnEventListenerPlugins(bus); ObjectFactorySupplier objectFactorySupplier = new SingletonObjectFactorySupplier( objectFactoryServiceLoader); BackendSupplier backendSupplier = new BackendServiceLoader(classLoader, objectFactorySupplier); - runnerSupplier = new SingletonRunnerSupplier(options, bus, backendSupplier, objectFactorySupplier); + runnerSupplier = new SingletonRunnerSupplier(configuration, bus, backendSupplier, objectFactorySupplier); } return new CucumberExecutionContext(bus, exitStatus, runnerSupplier); } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestDescriptor.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestDescriptor.java new file mode 100644 index 0000000000..4adecda831 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestDescriptor.java @@ -0,0 +1,253 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.gherkin.Pickle; +import io.cucumber.plugin.event.Location; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; +import org.junit.platform.engine.support.hierarchical.Node; + +import java.net.URI; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toSet; + +abstract class CucumberTestDescriptor extends AbstractTestDescriptor { + + protected CucumberTestDescriptor(UniqueId uniqueId, String displayName, TestSource source) { + super(uniqueId, displayName, source); + } + + protected abstract URI getUri(); + + protected abstract Location getLocation(); + + static class FeatureDescriptor extends CucumberTestDescriptor implements Node { + + private final Feature feature; + + FeatureDescriptor(UniqueId uniqueId, String name, TestSource source, Feature feature) { + super(uniqueId, name, source); + this.feature = feature; + } + + Feature getFeature() { + return feature; + } + + @Override + public CucumberEngineExecutionContext prepare(CucumberEngineExecutionContext context) { + context.beforeFeature(feature); + return context; + } + + @Override + public Type getType() { + return Type.CONTAINER; + } + + @Override + protected URI getUri() { + return feature.getUri(); + } + + @Override + protected Location getLocation() { + return feature.getLocation(); + } + } + + abstract static class FeatureElementDescriptor extends CucumberTestDescriptor + implements Node { + + private final CucumberConfiguration configuration; + private final io.cucumber.plugin.event.Node element; + + FeatureElementDescriptor( + CucumberConfiguration configuration, UniqueId uniqueId, String name, TestSource source, + io.cucumber.plugin.event.Node element + ) { + super(uniqueId, name, source); + this.configuration = configuration; + this.element = element; + } + + @Override + public ExecutionMode getExecutionMode() { + return configuration.getExecutionModeFeature(); + } + + @Override + protected Location getLocation() { + return element.getLocation(); + } + + @Override + protected URI getUri() { + return element.getUri(); + } + + static final class ExamplesDescriptor extends FeatureElementDescriptor { + + ExamplesDescriptor( + CucumberConfiguration configuration, UniqueId uniqueId, String name, TestSource source, + io.cucumber.plugin.event.Node element + ) { + super(configuration, uniqueId, name, source, element); + } + + @Override + public Type getType() { + return Type.CONTAINER; + } + + } + + static final class RuleDescriptor extends FeatureElementDescriptor { + + RuleDescriptor( + CucumberConfiguration configuration, UniqueId uniqueId, String name, TestSource source, + io.cucumber.plugin.event.Node element + ) { + super(configuration, uniqueId, name, source, element); + } + + @Override + public Type getType() { + return Type.CONTAINER; + } + + } + + static final class ScenarioOutlineDescriptor extends FeatureElementDescriptor { + + ScenarioOutlineDescriptor( + CucumberConfiguration configuration, UniqueId uniqueId, String name, + TestSource source, io.cucumber.plugin.event.Node element + ) { + super(configuration, uniqueId, name, source, element); + } + + @Override + public Type getType() { + return Type.CONTAINER; + } + + } + } + + static final class PickleDescriptor extends CucumberTestDescriptor implements Node { + + private final Pickle pickle; + private final CucumberConfiguration configuration; + + PickleDescriptor( + CucumberConfiguration configuration, UniqueId uniqueId, String name, TestSource source, + Pickle pickle + ) { + super(uniqueId, name, source); + this.configuration = configuration; + this.pickle = pickle; + } + + Pickle getPickle() { + return pickle; + } + + @Override + public Type getType() { + return Type.TEST; + } + + @Override + public SkipResult shouldBeSkipped(CucumberEngineExecutionContext context) { + return Stream.of(shouldBeSkippedByTagFilter(context), shouldBeSkippedByNameFilter(context)) + .flatMap(skipResult -> skipResult.map(Stream::of).orElseGet(Stream::empty)) + .filter(SkipResult::isSkipped) + .findFirst() + .orElseGet(SkipResult::doNotSkip); + } + + private Optional shouldBeSkippedByTagFilter(CucumberEngineExecutionContext context) { + return context.getConfiguration().tagFilter().map(expression -> { + if (expression.evaluate(pickle.getTags())) { + return SkipResult.doNotSkip(); + } + return SkipResult + .skip( + "'" + Constants.FILTER_TAGS_PROPERTY_NAME + "=" + expression + + "' did not match this scenario"); + }); + } + + private Optional shouldBeSkippedByNameFilter(CucumberEngineExecutionContext context) { + return context.getConfiguration().nameFilter().map(pattern -> { + if (pattern.matcher(pickle.getName()).matches()) { + return SkipResult.doNotSkip(); + } + return SkipResult + .skip("'" + Constants.FILTER_NAME_PROPERTY_NAME + "=" + pattern + + "' did not match this scenario"); + }); + } + + @Override + public CucumberEngineExecutionContext execute( + CucumberEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor + ) { + context.runTestCase(pickle); + return context; + } + + @Override + public Set getExclusiveResources() { + return getTags().stream() + .map(tag -> configuration.getExclusiveResourceConfiguration(tag.getName())) + .flatMap(ExclusiveResourceConfiguration::getExclusiveResources) + .collect(toSet()); + } + + /** + * Returns the set of {@linkplain TestTag tags} for a pickle. + *

+ * Note that Cucumber will remove the {code @} symbol from all Gherkin + * tags. So a scenario tagged with {@code @Smoke} becomes a test tagged + * with {@code Smoke}. + * + * @return the set of tags + */ + @Override + public Set getTags() { + return pickle.getTags().stream() + .map(tag -> tag.substring(1)) + .filter(TestTag::isValid) + .map(TestTag::create) + // Retain input order + .collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); + } + + @Override + protected URI getUri() { + return pickle.getUri(); + } + + @Override + protected Location getLocation() { + return pickle.getLocation(); + } + + @Override + public ExecutionMode getExecutionMode() { + return configuration.getExecutionModeFeature(); + } + } +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestEngine.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestEngine.java index 7d5d8f2108..b3b302ad90 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestEngine.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberTestEngine.java @@ -9,13 +9,15 @@ import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.config.PrefixedConfigurationParameters; import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.hierarchical.ForkJoinPoolHierarchicalTestExecutorService; import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine; import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService; import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PARALLEL_CONFIG_PREFIX; -import static io.cucumber.junit.platform.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; +import static org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.deduplicating; +import static org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.forwarding; /** * The Cucumber {@link org.junit.platform.engine.TestEngine TestEngine}. @@ -43,8 +45,16 @@ public String getId() { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { TestSource testSource = createEngineTestSource(discoveryRequest); - CucumberEngineDescriptor engineDescriptor = new CucumberEngineDescriptor(uniqueId, testSource); - new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor); + CucumberConfiguration configuration = new CucumberConfiguration(discoveryRequest.getConfigurationParameters()); + CucumberEngineDescriptor engineDescriptor = new CucumberEngineDescriptor(uniqueId, configuration, testSource); + + DiscoveryIssueReporter issueReporter = deduplicating(forwarding( // + discoveryRequest.getDiscoveryListener(), // + engineDescriptor.getUniqueId() // + )); + + FeaturesPropertyResolver resolver = new FeaturesPropertyResolver(new DiscoverySelectorResolver()); + resolver.resolveSelectors(discoveryRequest, engineDescriptor, issueReporter); return engineDescriptor; } @@ -63,17 +73,23 @@ private static TestSource createEngineTestSource(EngineDiscoveryRequest discover @Override protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest request) { - ConfigurationParameters config = request.getConfigurationParameters(); - if (config.getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME).orElse(false)) { + CucumberConfiguration configuration = getCucumberConfiguration(request); + if (configuration.isParallelExecutionEnabled()) { return new ForkJoinPoolHierarchicalTestExecutorService( - new PrefixedConfigurationParameters(config, PARALLEL_CONFIG_PREFIX)); + new PrefixedConfigurationParameters(request.getConfigurationParameters(), PARALLEL_CONFIG_PREFIX)); } return super.createExecutorService(request); } @Override protected CucumberEngineExecutionContext createExecutionContext(ExecutionRequest request) { - return new CucumberEngineExecutionContext(request.getConfigurationParameters()); + CucumberConfiguration configuration = getCucumberConfiguration(request); + return new CucumberEngineExecutionContext(configuration); + } + + private CucumberConfiguration getCucumberConfiguration(ExecutionRequest request) { + CucumberEngineDescriptor engineDescriptor = (CucumberEngineDescriptor) request.getRootTestDescriptor(); + return engineDescriptor.getConfiguration(); } } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultDescriptorOrderingStrategy.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultDescriptorOrderingStrategy.java new file mode 100644 index 0000000000..c0f6124393 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultDescriptorOrderingStrategy.java @@ -0,0 +1,72 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.core.logging.Logger; +import io.cucumber.core.logging.LoggerFactory; +import org.junit.platform.engine.ConfigurationParameters; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.function.UnaryOperator; + +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_ORDER_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_ORDER_RANDOM_SEED_PROPERTY_NAME; + +enum DefaultDescriptorOrderingStrategy implements DescriptorOrderingStrategy { + + LEXICAL { + @Override + public UnaryOperator> create(ConfigurationParameters configuration) { + return pickles -> { + pickles.sort(lexical); + return pickles; + }; + } + }, + REVERSE { + @Override + public UnaryOperator> create(ConfigurationParameters configuration) { + return pickles -> { + pickles.sort(lexical.reversed()); + return pickles; + }; + } + }, + RANDOM { + @Override + public UnaryOperator> create(ConfigurationParameters configuration) { + long seed = configuration + .get(EXECUTION_ORDER_RANDOM_SEED_PROPERTY_NAME, Long::decode) + .orElseGet(this::createRandomSeed); + // Invoked multiple times, keep state outside of closure. + Random random = new Random(seed); + return testDescriptors -> { + // Sort in expected order first to remove arbitrary initial + // ordering before applying a deterministic shuffle. + testDescriptors.sort(lexical); + Collections.shuffle(testDescriptors, random); + return testDescriptors; + }; + + } + + private long createRandomSeed() { + long generatedSeed = Math.abs(new Random().nextLong()); + log.config(() -> String.format("Using generated seed for configuration parameter '%s' with value '%s'.", + EXECUTION_ORDER_RANDOM_SEED_PROPERTY_NAME, generatedSeed)); + return generatedSeed; + } + }; + private static final Logger log = LoggerFactory.getLogger(DefaultDescriptorOrderingStrategy.class); + + private static final Comparator lexical = Comparator + .comparing(CucumberTestDescriptor::getUri) + .thenComparing(CucumberTestDescriptor::getLocation); + + static DefaultDescriptorOrderingStrategy getStrategy(ConfigurationParameters configurationParameters) { + return valueOf( + configurationParameters.get(EXECUTION_ORDER_PROPERTY_NAME).orElse("lexical").toUpperCase(Locale.ROOT)); + } +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultNamingStrategyProvider.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultNamingStrategyProvider.java index 55508153b4..0af1c0f709 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultNamingStrategyProvider.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DefaultNamingStrategyProvider.java @@ -108,7 +108,7 @@ public String name(Node node) { } @Override - public String nameExample(Node.Example node, Pickle pickle) { + public String nameExample(Node node, Pickle pickle) { return exampleNameFunction.apply(node, pickle); } }; diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DescriptorOrderingStrategy.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DescriptorOrderingStrategy.java new file mode 100644 index 0000000000..222c0caaaa --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DescriptorOrderingStrategy.java @@ -0,0 +1,20 @@ +package io.cucumber.junit.platform.engine; + +import org.junit.platform.engine.ConfigurationParameters; + +import java.util.List; +import java.util.function.UnaryOperator; + +interface DescriptorOrderingStrategy { + + /** + * Creates a unary operator used by + * {@link org.junit.platform.engine.TestDescriptor#orderChildren(UnaryOperator)}. + * + * @param configuration to pull configuration values from, never + * {@code null}. + * @return an operator, never {@code null}. + */ + UnaryOperator> create(ConfigurationParameters configuration); + +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolver.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolver.java index 6088759f4c..abbc75d11f 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolver.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolver.java @@ -1,115 +1,35 @@ package io.cucumber.junit.platform.engine; -import io.cucumber.core.feature.FeatureWithLines; -import io.cucumber.core.logging.Logger; -import io.cucumber.core.logging.LoggerFactory; -import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor; -import org.junit.platform.engine.ConfigurationParameters; +import io.cucumber.core.feature.FeatureIdentifier; import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.Filter; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.discovery.ClassSelector; -import org.junit.platform.engine.discovery.ClasspathResourceSelector; -import org.junit.platform.engine.discovery.ClasspathRootSelector; -import org.junit.platform.engine.discovery.DirectorySelector; -import org.junit.platform.engine.discovery.FileSelector; -import org.junit.platform.engine.discovery.PackageNameFilter; -import org.junit.platform.engine.discovery.PackageSelector; -import org.junit.platform.engine.discovery.UniqueIdSelector; -import org.junit.platform.engine.discovery.UriSelector; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver; -import java.util.List; -import java.util.function.Predicate; - -import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.FeatureResolver.create; -import static org.junit.platform.engine.Filter.composeFilters; +import static io.cucumber.core.feature.FeatureIdentifier.isFeature; class DiscoverySelectorResolver { - private static final Logger log = LoggerFactory.getLogger(DiscoverySelectorResolver.class); - - private static boolean warnedWhenCucumberFeaturesPropertyIsUsed = false; - - private static void warnWhenCucumberFeaturesPropertyIsUsed() { - if (warnedWhenCucumberFeaturesPropertyIsUsed) { - return; - } - warnedWhenCucumberFeaturesPropertyIsUsed = true; - log.warn( - () -> "Discovering tests using the " + FEATURES_PROPERTY_NAME + " property. Other discovery " + - "selectors are ignored!\n" + - "\n" + - "This is a work around for the limited JUnit 5 support in Maven and Gradle. " + - "Please request/upvote/sponsor/ect better support for JUnit 5 discovery selectors. " + - "For details see: https://github.com/cucumber/cucumber-jvm/pull/2498\n" + - "\n" + - "If you are using the JUnit 5 Suite Engine, Platform Launcher API or Console Launcher you " + - "should not use this property. Please consult the JUnit 5 documentation on test selection."); - } - - void resolveSelectors(EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor) { - Predicate packageFilter = buildPackageFilter(request); - resolve(request, engineDescriptor, packageFilter); - filter(engineDescriptor, packageFilter); - pruneTree(engineDescriptor); - } - - private Predicate buildPackageFilter(EngineDiscoveryRequest request) { - Filter packageFilter = composeFilters(request.getFiltersByType(PackageNameFilter.class)); - return packageFilter.toPredicate(); - } - - private void resolve( - EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor, Predicate packageFilter + private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver + . builder() + .addSelectorResolver(context -> new FileContainerSelectorResolver( // + FeatureIdentifier::isFeature // + )) + .addResourceContainerSelectorResolver(resource -> isFeature(resource.getName())) + .addSelectorResolver(context -> new FeatureResolver( + context.getEngineDescriptor().getConfiguration(), // + context.getPackageFilter(), // + context.getIssueReporter() // + )) + .addTestDescriptorVisitor(context -> new OrderingVisitor( + context.getDiscoveryRequest().getConfigurationParameters() // + )) + .build(); + + void resolveSelectors( + EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor, + DiscoveryIssueReporter issueReporter ) { - ConfigurationParameters configuration = request.getConfigurationParameters(); - FeatureResolver featureResolver = create( - configuration, - engineDescriptor, - packageFilter); - - CucumberEngineOptions options = new CucumberEngineOptions(configuration); - List featureWithLines = options.featuresWithLines(); - if (!featureWithLines.isEmpty()) { - warnWhenCucumberFeaturesPropertyIsUsed(); - featureWithLines.forEach(featureResolver::resolveFeatureWithLines); - return; - } - - request.getSelectorsByType(ClasspathRootSelector.class).forEach(featureResolver::resolveClasspathRoot); - request.getSelectorsByType(ClasspathResourceSelector.class).forEach(featureResolver::resolveClasspathResource); - request.getSelectorsByType(ClassSelector.class).forEach(featureResolver::resolveClass); - request.getSelectorsByType(PackageSelector.class).forEach(featureResolver::resolvePackageResource); - request.getSelectorsByType(FileSelector.class).forEach(featureResolver::resolveFile); - request.getSelectorsByType(DirectorySelector.class).forEach(featureResolver::resolveDirectory); - request.getSelectorsByType(UniqueIdSelector.class).forEach(featureResolver::resolveUniqueId); - request.getSelectorsByType(UriSelector.class).forEach(featureResolver::resolveUri); - } - - private void filter(TestDescriptor engineDescriptor, Predicate packageFilter) { - applyPackagePredicate(packageFilter, engineDescriptor); - } - - private void pruneTree(TestDescriptor rootDescriptor) { - rootDescriptor.accept(TestDescriptor::prune); - } - - private void applyPackagePredicate(Predicate packageFilter, TestDescriptor engineDescriptor) { - engineDescriptor.accept(descriptor -> { - if (descriptor instanceof PickleDescriptor) { - PickleDescriptor pickleDescriptor = (PickleDescriptor) descriptor; - if (!includePickle(pickleDescriptor, packageFilter)) { - descriptor.removeFromHierarchy(); - } - } - }); - } - - private boolean includePickle(PickleDescriptor pickleDescriptor, Predicate packageFilter) { - return pickleDescriptor.getPackage() - .map(packageFilter::test) - .orElse(true); + resolver.resolve(request, engineDescriptor, issueReporter); } } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/ExclusiveResourceConfiguration.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/ExclusiveResourceConfiguration.java new file mode 100644 index 0000000000..2ef7655737 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/ExclusiveResourceConfiguration.java @@ -0,0 +1,43 @@ +package io.cucumber.junit.platform.engine; + +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static io.cucumber.junit.platform.engine.Constants.READ_SUFFIX; +import static io.cucumber.junit.platform.engine.Constants.READ_WRITE_SUFFIX; +import static java.util.Objects.requireNonNull; +import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ; +import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ_WRITE; + +final class ExclusiveResourceConfiguration { + + private final ConfigurationParameters configuration; + + ExclusiveResourceConfiguration(ConfigurationParameters configuration) { + this.configuration = requireNonNull(configuration); + } + + private Stream exclusiveReadWriteResource() { + return configuration.get(READ_WRITE_SUFFIX, s -> Arrays.stream(s.split(",")) + .map(String::trim)) + .orElse(Stream.empty()); + } + + private Stream exclusiveReadResource() { + return configuration.get(READ_SUFFIX, s -> Arrays.stream(s.split(",")) + .map(String::trim)) + .orElse(Stream.empty()); + } + + Stream getExclusiveResources() { + Stream readWrite = exclusiveReadWriteResource() + .map(resource -> new ExclusiveResource(resource, READ_WRITE)); + Stream read = exclusiveReadResource() + .map(resource -> new ExclusiveResource(resource, READ)); + return Stream.concat(readWrite, read); + } + +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureDescriptor.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureDescriptor.java deleted file mode 100644 index b8d9d003ab..0000000000 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureDescriptor.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import io.cucumber.core.gherkin.Feature; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; -import org.junit.platform.engine.support.hierarchical.Node; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; - -class FeatureDescriptor extends AbstractTestDescriptor implements Node { - - private final Feature feature; - - FeatureDescriptor(UniqueId uniqueId, String name, TestSource source, Feature feature) { - super(uniqueId, name, source); - this.feature = feature; - } - - Feature getFeature() { - return feature; - } - - private static void pruneRecursively(TestDescriptor descriptor, Predicate toKeep) { - if (!toKeep.test(descriptor)) { - if (descriptor.isTest()) { - descriptor.removeFromHierarchy(); - } - List children = new ArrayList<>(descriptor.getChildren()); - children.forEach(child -> pruneRecursively(child, toKeep)); - } - } - - void prune(Predicate toKeep) { - pruneRecursively(this, toKeep); - } - - @Override - public CucumberEngineExecutionContext prepare(CucumberEngineExecutionContext context) { - context.beforeFeature(feature); - return context; - } - - @Override - public Type getType() { - return Type.CONTAINER; - } - -} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureOrigin.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureOrigin.java index 128e75bb87..9d6b648a38 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureOrigin.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureOrigin.java @@ -1,6 +1,5 @@ package io.cucumber.junit.platform.engine; -import io.cucumber.core.gherkin.Feature; import io.cucumber.plugin.event.Location; import io.cucumber.plugin.event.Node; import org.junit.platform.engine.TestSource; @@ -16,11 +15,11 @@ abstract class FeatureOrigin { - private static final String RULE_SEGMENT_TYPE = "rule"; - private static final String FEATURE_SEGMENT_TYPE = "feature"; - private static final String SCENARIO_SEGMENT_TYPE = "scenario"; - private static final String EXAMPLES_SEGMENT_TYPE = "examples"; - private static final String EXAMPLE_SEGMENT_TYPE = "example"; + static final String RULE_SEGMENT_TYPE = "rule"; + static final String FEATURE_SEGMENT_TYPE = "feature"; + static final String SCENARIO_SEGMENT_TYPE = "scenario"; + static final String EXAMPLES_SEGMENT_TYPE = "examples"; + static final String EXAMPLE_SEGMENT_TYPE = "example"; private static FilePosition createFilePosition(Location location) { return FilePosition.from(location.getLine(), location.getColumn()); @@ -50,27 +49,9 @@ static boolean isFeatureSegment(UniqueId.Segment segment) { return FEATURE_SEGMENT_TYPE.equals(segment.getType()); } - abstract TestSource featureSource(); - abstract TestSource nodeSource(Node node); - abstract UniqueId featureSegment(UniqueId parent, Feature feature); - - UniqueId ruleSegment(UniqueId parent, Node rule) { - return parent.append(RULE_SEGMENT_TYPE, String.valueOf(rule.getLocation().getLine())); - } - - UniqueId scenarioSegment(UniqueId parent, Node scenarioDefinition) { - return parent.append(SCENARIO_SEGMENT_TYPE, String.valueOf(scenarioDefinition.getLocation().getLine())); - } - - UniqueId examplesSegment(UniqueId parent, Node examples) { - return parent.append(EXAMPLES_SEGMENT_TYPE, String.valueOf(examples.getLocation().getLine())); - } - - UniqueId exampleSegment(UniqueId parent, Node tableRow) { - return parent.append(EXAMPLE_SEGMENT_TYPE, String.valueOf(tableRow.getLocation().getLine())); - } + abstract TestSource source(); private static class FileFeatureOrigin extends FeatureOrigin { @@ -80,19 +61,14 @@ private static class FileFeatureOrigin extends FeatureOrigin { this.source = source; } - @Override - TestSource featureSource() { - return source; - } - @Override TestSource nodeSource(Node node) { return FileSource.from(source.getFile(), createFilePosition(node.getLocation())); } @Override - UniqueId featureSegment(UniqueId parent, Feature feature) { - return parent.append(FEATURE_SEGMENT_TYPE, source.getUri().toString()); + TestSource source() { + return source; } } @@ -105,21 +81,15 @@ private static class UriFeatureOrigin extends FeatureOrigin { this.source = source; } - @Override - TestSource featureSource() { - return source; - } - @Override TestSource nodeSource(Node node) { return source; } @Override - UniqueId featureSegment(UniqueId parent, Feature feature) { - return parent.append(FEATURE_SEGMENT_TYPE, source.getUri().toString()); + TestSource source() { + return source; } - } private static class ClasspathFeatureOrigin extends FeatureOrigin { @@ -130,11 +100,6 @@ private static class ClasspathFeatureOrigin extends FeatureOrigin { this.source = source; } - @Override - TestSource featureSource() { - return source; - } - @Override TestSource nodeSource(Node node) { return ClasspathResourceSource.from(source.getClasspathResourceName(), @@ -142,10 +107,9 @@ TestSource nodeSource(Node node) { } @Override - UniqueId featureSegment(UniqueId parent, Feature feature) { - return parent.append(FEATURE_SEGMENT_TYPE, feature.getUri().toString()); + TestSource source() { + return source; } - } } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureParserWithCaching.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureParserWithCaching.java new file mode 100644 index 0000000000..65616e1480 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureParserWithCaching.java @@ -0,0 +1,80 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.core.exception.CucumberException; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.resource.Resource; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +class FeatureParserWithCaching { + + private final Map> cache = new HashMap<>(); + private final FeatureParserWithIssueReporting delegate; + + FeatureParserWithCaching(FeatureParserWithIssueReporting delegate) { + this.delegate = delegate; + } + + Optional parseResource(Resource resource) { + return cache.computeIfAbsent(resource.getUri(), uri -> delegate.parseResource(resource)); + } + + Optional parseResource(Path resource) { + return parseResource(new PathAdapter(resource)); + } + + Optional parseResource(org.junit.platform.commons.support.Resource resource) { + return parseResource(new ResourceAdapter(resource)); + } + + private static class ResourceAdapter implements Resource { + private final org.junit.platform.commons.support.Resource resource; + + public ResourceAdapter(org.junit.platform.commons.support.Resource resource) { + this.resource = resource; + } + + @Override + public URI getUri() { + String name = resource.getName(); + try { + return new URI("classpath", name, null); + } catch (URISyntaxException e) { + String message = String.format("Could not create classpath uri for resource '%s'", name); + throw new CucumberException(message, e); + } + } + + @Override + public InputStream getInputStream() throws IOException { + return resource.getInputStream(); + } + } + + private static class PathAdapter implements Resource { + private final Path resource; + + public PathAdapter(Path resource) { + this.resource = resource; + } + + @Override + public URI getUri() { + return resource.toUri(); + } + + @Override + public InputStream getInputStream() throws IOException { + return Files.newInputStream(resource); + } + } + +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureParserWithIssueReporting.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureParserWithIssueReporting.java new file mode 100644 index 0000000000..fd056910b6 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureParserWithIssueReporting.java @@ -0,0 +1,38 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.core.feature.FeatureParser; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.gherkin.FeatureParserException; +import io.cucumber.core.resource.Resource; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; + +import java.util.Optional; + +import static org.junit.platform.engine.DiscoveryIssue.Severity.ERROR; + +class FeatureParserWithIssueReporting { + + private final FeatureParser delegate; + private final DiscoveryIssueReporter issueReporter; + + FeatureParserWithIssueReporting(FeatureParser delegate, DiscoveryIssueReporter issueReporter) { + this.delegate = delegate; + this.issueReporter = issueReporter; + } + + Optional parseResource(Resource resource) { + try { + return delegate.parseResource(resource); + } catch (FeatureParserException e) { + FeatureOrigin featureOrigin = FeatureOrigin.fromUri(resource.getUri()); + issueReporter.reportIssue(DiscoveryIssue + // TODO: Improve parse exception to separate out source uri + // and individual errors. + .builder(ERROR, e.getMessage()) + .cause(e.getCause()) + .source(featureOrigin.source())); + return Optional.empty(); + } + } +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureResolver.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureResolver.java index 5003a04873..46c665630e 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureResolver.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureResolver.java @@ -3,275 +3,307 @@ import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureIdentifier; import io.cucumber.core.feature.FeatureParser; -import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.core.gherkin.Feature; import io.cucumber.core.gherkin.Pickle; -import io.cucumber.core.logging.Logger; -import io.cucumber.core.logging.LoggerFactory; import io.cucumber.core.resource.ClassLoaders; import io.cucumber.core.resource.ResourceScanner; import io.cucumber.core.runtime.UuidGeneratorServiceLoader; -import io.cucumber.junit.platform.engine.NodeDescriptor.ExamplesDescriptor; -import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor; -import io.cucumber.junit.platform.engine.NodeDescriptor.RuleDescriptor; -import io.cucumber.junit.platform.engine.NodeDescriptor.ScenarioOutlineDescriptor; +import io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureElementSelector; +import io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureWithLinesSelector; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.FeatureDescriptor; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.FeatureElementDescriptor.ExamplesDescriptor; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.FeatureElementDescriptor.RuleDescriptor; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.FeatureElementDescriptor.ScenarioOutlineDescriptor; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.PickleDescriptor; import io.cucumber.plugin.event.Node; -import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.commons.support.Resource; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.ClasspathResourceSelector; -import org.junit.platform.engine.discovery.ClasspathRootSelector; -import org.junit.platform.engine.discovery.DirectorySelector; import org.junit.platform.engine.discovery.FileSelector; -import org.junit.platform.engine.discovery.PackageSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.engine.discovery.UriSelector; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.discovery.SelectorResolver; import java.net.URI; -import java.util.List; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; -import static java.util.Comparator.comparing; - -final class FeatureResolver { - - private static final Logger log = LoggerFactory.getLogger(FeatureResolver.class); - +import static io.cucumber.core.feature.FeatureIdentifier.isFeature; +import static io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureElementSelector.selectElement; +import static io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureElementSelector.selectElementAt; +import static io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureElementSelector.selectElementsOf; +import static io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureElementSelector.selectFeature; +import static io.cucumber.junit.platform.engine.FeatureOrigin.EXAMPLES_SEGMENT_TYPE; +import static io.cucumber.junit.platform.engine.FeatureOrigin.EXAMPLE_SEGMENT_TYPE; +import static io.cucumber.junit.platform.engine.FeatureOrigin.FEATURE_SEGMENT_TYPE; +import static io.cucumber.junit.platform.engine.FeatureOrigin.RULE_SEGMENT_TYPE; +import static io.cucumber.junit.platform.engine.FeatureOrigin.SCENARIO_SEGMENT_TYPE; +import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.junit.platform.engine.DiscoveryIssue.Severity.WARNING; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; + +final class FeatureResolver implements SelectorResolver { private final ResourceScanner featureScanner; - private final CucumberEngineDescriptor engineDescriptor; + private final CucumberConfiguration configuration; + private final FeatureParserWithCaching featureParser; private final Predicate packageFilter; - private final ConfigurationParameters parameters; - private final NamingStrategy namingStrategy; + private final DiscoveryIssueReporter issueReporter; - private FeatureResolver( - ConfigurationParameters parameters, CucumberEngineDescriptor engineDescriptor, - Predicate packageFilter + FeatureResolver( + CucumberConfiguration configuration, Predicate packageFilter, DiscoveryIssueReporter issueReporter ) { - this.parameters = parameters; - this.engineDescriptor = engineDescriptor; + this.configuration = configuration; this.packageFilter = packageFilter; - CucumberEngineOptions options = new CucumberEngineOptions(parameters); - this.namingStrategy = options.namingStrategy(); - CachingFeatureParser featureParser = createFeatureParser(options); + this.issueReporter = issueReporter; + this.featureParser = createFeatureParser(configuration, issueReporter); this.featureScanner = new ResourceScanner<>( ClassLoaders::getDefaultClassLoader, FeatureIdentifier::isFeature, featureParser::parseResource); } - private static CachingFeatureParser createFeatureParser(CucumberEngineOptions options) { + private static FeatureParserWithCaching createFeatureParser( + CucumberConfiguration options, DiscoveryIssueReporter issueReporter + ) { Supplier classLoader = FeatureResolver.class::getClassLoader; UuidGeneratorServiceLoader uuidGeneratorServiceLoader = new UuidGeneratorServiceLoader(classLoader, options); UuidGenerator uuidGenerator = uuidGeneratorServiceLoader.loadUuidGenerator(); FeatureParser featureParser = new FeatureParser(uuidGenerator::generateId); - return new CachingFeatureParser(featureParser); + FeatureParserWithIssueReporting featureParserWithIssueReporting = new FeatureParserWithIssueReporting( + featureParser, issueReporter); + return new FeatureParserWithCaching(featureParserWithIssueReporting); } - static FeatureResolver create( - ConfigurationParameters parameters, CucumberEngineDescriptor engineDescriptor, - Predicate packageFilter - ) { - return new FeatureResolver(parameters, engineDescriptor, packageFilter); + @Override + public Resolution resolve(DiscoverySelector selector, Context context) { + if (selector instanceof FeatureElementSelector) { + return resolve((FeatureElementSelector) selector, context); + } + if (selector instanceof FeatureWithLinesSelector) { + return resolve((FeatureWithLinesSelector) selector); + } + return SelectorResolver.super.resolve(selector, context); } - void resolveFile(FileSelector selector) { - featureScanner - .scanForResourcesPath(selector.getPath()) - .stream() - .sorted(comparing(Feature::getUri)) - .map(this::createFeatureDescriptor) - .forEach(featureDescriptor -> { - featureDescriptor.prune(TestDescriptorOnLine.from(selector)); - engineDescriptor.mergeFeature(featureDescriptor); - }); + public Resolution resolve(FeatureElementSelector selector, Context context) { + Feature feature = selector.getFeature(); + Node selected = selector.getElement(); + return selected.getParent() + .map(parent -> context.addToParent(() -> selectElement(feature, parent), + createTestDescriptor(feature, selected))) + .orElseGet(() -> context.addToParent(createTestDescriptor(feature, selected))) + .map(descriptor -> Match.exact(descriptor, () -> selectElementsOf(feature, selected))) + .map(Resolution::match) + .orElseGet(Resolution::unresolved); } - private FeatureDescriptor createFeatureDescriptor(Feature feature) { - FeatureOrigin source = FeatureOrigin.fromUri(feature.getUri()); - - return (FeatureDescriptor) feature.map( - engineDescriptor, - (Node.Feature self, TestDescriptor parent) -> new FeatureDescriptor( - source.featureSegment(parent.getUniqueId(), feature), - namingStrategy.name(self), - source.featureSource(), - feature), - (Node.Rule node, TestDescriptor parent) -> { - TestDescriptor descriptor = new RuleDescriptor( - parameters, - source.ruleSegment(parent.getUniqueId(), node), - namingStrategy.name(node), - source.nodeSource(node)); - parent.addChild(descriptor); - return descriptor; - }, (Node.Scenario node, TestDescriptor parent) -> { - Pickle pickle = feature.getPickleAt(node); - TestDescriptor descriptor = new PickleDescriptor( - parameters, - source.scenarioSegment(parent.getUniqueId(), node), - namingStrategy.name(node), - source.nodeSource(node), - pickle); - parent.addChild(descriptor); - return descriptor; - }, - (Node.ScenarioOutline node, TestDescriptor parent) -> { - TestDescriptor descriptor = new ScenarioOutlineDescriptor( - parameters, - source.scenarioSegment(parent.getUniqueId(), node), - namingStrategy.name(node), - source.nodeSource(node)); - parent.addChild(descriptor); - return descriptor; - }, - (Node.Examples node, TestDescriptor parent) -> { - NodeDescriptor descriptor = new ExamplesDescriptor( - parameters, - source.examplesSegment(parent.getUniqueId(), node), - namingStrategy.name(node), - source.nodeSource(node)); - parent.addChild(descriptor); - return descriptor; - }, - (Node.Example node, TestDescriptor parent) -> { - Pickle pickle = feature.getPickleAt(node); - PickleDescriptor descriptor = new PickleDescriptor( - parameters, - source.exampleSegment(parent.getUniqueId(), node), - namingStrategy.nameExample(node, pickle), - source.nodeSource(node), - pickle); - parent.addChild(descriptor); - return descriptor; - }); + public Resolution resolve(FeatureWithLinesSelector selector) { + URI uri = selector.getUri(); + Set selectors = featureScanner + .scanForResourcesUri(uri) + .stream() + .flatMap(feature -> selector.getFilePositions() + .map(filePositions -> filePositions.stream() + .map(position -> selectElementAt(feature, position)) + .filter(Optional::isPresent) + .map(Optional::get)) + .orElseGet(() -> Stream.of(selectFeature(feature)))) + .collect(toSet()); + + return toResolution(selectors); } - void resolveDirectory(DirectorySelector selector) { - featureScanner - .scanForResourcesPath(selector.getPath()) + @Override + public Resolution resolve(FileSelector selector, Context context) { + Set selectors = featureParser.parseResource(selector.getPath()) .stream() - .sorted(comparing(Feature::getUri)) - .map(this::createFeatureDescriptor) - .forEach(engineDescriptor::mergeFeature); + .map(feature -> selector.getPosition() + .map(position -> selectElementAt(feature, position)) + .orElseGet(() -> Optional.of(selectFeature(feature)))) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toSet()); + return toResolution(selectors); } - void resolvePackageResource(PackageSelector selector) { - resolvePackageResource(selector.getPackageName()); + @Override + public Resolution resolve(ClasspathResourceSelector selector, Context context) { + Set resources = selector.getClasspathResources(); + if (!resources.stream().allMatch(resource -> isFeature(resource.getName()))) { + return resolveClasspathResourceSelectorAsPackageSelector(selector); + } + if (resources.size() > 1) { + throw new IllegalArgumentException(String.format( + "Found %s resources named %s on the classpath %s.", + resources.size(), selector.getClasspathResourceName(), + resources.stream().map(Resource::getUri).collect(toList()))); + } + return resources.stream() + .findFirst() + .flatMap(featureParser::parseResource) + .map(feature -> selector.getPosition() + .map(position -> selectElementAt(feature, position)) + .orElseGet(() -> Optional.of(selectFeature(feature)))) + .filter(Optional::isPresent) + .map(Optional::get) + .map(Collections::singleton) + .map(FeatureResolver::toResolution) + .orElseGet(Resolution::unresolved); } - private List resolvePackageResource(String packageName) { - List features = featureScanner - .scanForResourcesInPackage(packageName, packageFilter); - - features + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + private Resolution resolveClasspathResourceSelectorAsPackageSelector(ClasspathResourceSelector selector) { + Set selectors = featureScanner + .scanForClasspathResource(selector.getClasspathResourceName(), packageFilter) .stream() - .sorted(comparing(Feature::getUri)) - .map(this::createFeatureDescriptor) - .forEach(engineDescriptor::mergeFeature); - - return features; - } + .map(feature -> selector.getPosition() + .map(position -> selectElementAt(feature, position)) + .orElseGet(() -> Optional.of(selectFeature(feature)))) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toSet()); - void resolveClass(ClassSelector classSelector) { - Class javaClass = classSelector.getJavaClass(); - Cucumber annotation = javaClass.getAnnotation(Cucumber.class); - if (annotation != null) { - // We know now the intention is to run feature files in the - // package of the annotated class. - resolvePackageResourceWarnIfNone(javaClass.getPackage().getName()); - } - } + warnClasspathResourceSelectorUsedForPackage(selector); - private void resolvePackageResourceWarnIfNone(String packageName) { - List features = resolvePackageResource(packageName); - if (features.isEmpty()) { - log.warn(() -> "No features found in package '" + packageName + "'"); - } + return toResolution(selectors); } - void resolveClasspathResource(ClasspathResourceSelector selector) { + private void warnClasspathResourceSelectorUsedForPackage(ClasspathResourceSelector selector) { String classpathResourceName = selector.getClasspathResourceName(); - - featureScanner - .scanForClasspathResource(classpathResourceName, packageFilter) - .stream() - .sorted(comparing(Feature::getUri)) - .map(this::createFeatureDescriptor) - .forEach(featureDescriptor -> { - featureDescriptor.prune(TestDescriptorOnLine.from(selector)); - engineDescriptor.mergeFeature(featureDescriptor); - }); + String packageName = classpathResourceName.replaceAll("/", "."); + String message = String.format( + "The classpath resource selector '%s' should not be used to select features in a package. Use the package selector with '%s' instead", + classpathResourceName, + packageName); + issueReporter.reportIssue(DiscoveryIssue.builder(WARNING, message)); } - void resolveClasspathRoot(ClasspathRootSelector selector) { - featureScanner - .scanForResourcesInClasspathRoot(selector.getClasspathRoot(), packageFilter) - .stream() - .sorted(comparing(Feature::getUri)) - .map(this::createFeatureDescriptor) - .forEach(engineDescriptor::mergeFeature); + @Override + public Resolution resolve(UriSelector selector, Context context) { + URI uri = selector.getUri(); + Set selectors = singleton(FeatureWithLinesSelector.from(uri)); + return toResolution(selectors); } - void resolveUniqueId(UniqueIdSelector uniqueIdSelector) { - UniqueId uniqueId = uniqueIdSelector.getUniqueId(); - // Ignore any ids not from our own engine - if (!uniqueId.hasPrefix(engineDescriptor.getUniqueId())) { - return; + @SuppressWarnings("deprecation") + @Override + public Resolution resolve(ClassSelector selector, Context context) { + Class javaClass = selector.getJavaClass(); + Cucumber annotation = javaClass.getAnnotation(Cucumber.class); + if (annotation != null) { + warnAboutDeprecatedCucumberClass(javaClass); + String packageName = javaClass.getPackage().getName(); + Set selectors = singleton(selectPackage(packageName)); + return toResolution(selectors); } - - Predicate keepTestWithSelectedId = testDescriptor -> uniqueId - .equals(testDescriptor.getUniqueId()); - - List resolvedSegments = engineDescriptor.getUniqueId().getSegments(); - - uniqueId.getSegments() - .stream() - .skip(resolvedSegments.size()) - .findFirst() - .filter(FeatureOrigin::isFeatureSegment) - .map(UniqueId.Segment::getValue) - .map(URI::create) - .map(this::resolveUri) - .ifPresent(featureDescriptors -> featureDescriptors.forEach(featureDescriptor -> { - featureDescriptor.prune(keepTestWithSelectedId); - engineDescriptor.mergeFeature(featureDescriptor); - })); + return Resolution.unresolved(); } - private Stream resolveUri(URI uri) { - return featureScanner - .scanForResourcesUri(uri) - .stream() - .sorted(comparing(Feature::getUri)) - .map(this::createFeatureDescriptor); + private void warnAboutDeprecatedCucumberClass(Class javaClass) { + String message = "The @Cucumber annotation has been deprecated. See the Javadoc for more details."; + DiscoveryIssue issue = DiscoveryIssue.builder(WARNING, message) + .source(ClassSource.from(javaClass)) + .build(); + issueReporter.reportIssue(issue); } - void resolveUri(UriSelector selector) { - resolveUri(stripQuery(selector.getUri())) - .forEach(featureDescriptor -> { - featureDescriptor.prune(TestDescriptorOnLine.from(selector)); - engineDescriptor.mergeFeature(featureDescriptor); - }); + @Override + public Resolution resolve(UniqueIdSelector selector, Context context) { + UniqueId uniqueId = selector.getUniqueId(); + Set selectors = FeatureWithLinesSelector.from(uniqueId); + return toResolution(selectors); } - void resolveFeatureWithLines(FeatureWithLines selector) { - resolveUri(selector.uri()) - .forEach(featureDescriptor -> { - featureDescriptor.prune(TestDescriptorOnLine.from(selector)); - engineDescriptor.mergeFeature(featureDescriptor); - }); + private Function> createTestDescriptor(Feature feature, Node node) { + return parent -> { + NamingStrategy namingStrategy = configuration.namingStrategy(); + FeatureOrigin source = FeatureOrigin.fromUri(feature.getUri()); + String name = namingStrategy.name(node); + TestSource testSource = source.nodeSource(node); + if (node instanceof Node.Feature) { + return Optional.of(new FeatureDescriptor( + parent.getUniqueId().append(FEATURE_SEGMENT_TYPE, feature.getUri().toString()), + name, + testSource, + feature)); + } + + int line = node.getLocation().getLine(); + + if (node instanceof Node.Rule) { + return Optional.of(new RuleDescriptor( + configuration, + parent.getUniqueId().append(RULE_SEGMENT_TYPE, + String.valueOf(line)), + name, + testSource, + node)); + } + + if (node instanceof Node.Scenario) { + return Optional.of(new PickleDescriptor( + configuration, + parent.getUniqueId().append(SCENARIO_SEGMENT_TYPE, + String.valueOf(line)), + name, + testSource, + feature.getPickleAt(node))); + } + + if (node instanceof Node.ScenarioOutline) { + return Optional.of(new ScenarioOutlineDescriptor( + configuration, + parent.getUniqueId().append(SCENARIO_SEGMENT_TYPE, + String.valueOf(line)), + name, + testSource, + node)); + } + + if (node instanceof Node.Examples) { + return Optional.of(new ExamplesDescriptor( + configuration, + parent.getUniqueId().append(EXAMPLES_SEGMENT_TYPE, + String.valueOf(line)), + name, + testSource, + node)); + } + + if (node instanceof Node.Example) { + Pickle pickle = feature.getPickleAt(node); + return Optional.of(new PickleDescriptor( + configuration, + parent.getUniqueId().append(EXAMPLE_SEGMENT_TYPE, + String.valueOf(line)), + namingStrategy.nameExample(node, pickle), + testSource, + pickle)); + } + throw new IllegalStateException("Got a " + node.getClass() + " but didn't have a case to handle it"); + }; } - private static URI stripQuery(URI uri) { - if (uri.getQuery() == null) { - return uri; + private static Resolution toResolution(Set selectors) { + if (selectors.isEmpty()) { + return Resolution.unresolved(); } - String uriString = uri.toString(); - return URI.create(uriString.substring(0, uriString.indexOf('?'))); + return Resolution.selectors(selectors); } - } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeaturesPropertyResolver.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeaturesPropertyResolver.java new file mode 100644 index 0000000000..34c9d3e6b7 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeaturesPropertyResolver.java @@ -0,0 +1,103 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.junit.platform.engine.CucumberDiscoverySelectors.FeatureWithLinesSelector; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryFilter; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; + +import java.util.List; +import java.util.Set; + +import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; +import static org.junit.platform.engine.DiscoveryIssue.Severity.WARNING; + +/** + * Decorator to support resolving the + * {@value io.cucumber.junit.platform.engine.Constants#FEATURES_PROPERTY_NAME} + * property. + *

+ * The JUnit Platform provides various discovery selectors to select feature + * files. Unfortunately, these do not yet receive support from IDEs, Maven or + * Gradle. Resolving this property allows uses to target a single feature, + * scenario or example from the commandline. + *

+ * This class decorates the {@link DiscoverySelectorResolver}. When the features + * property is provided it replaces the discovery request. + *

+ * Note: This effectively causes Cucumber to ignore any requests from the JUnit + * Platform. So features will be discovered even when none are expected to be. + */ +class FeaturesPropertyResolver { + + private final DiscoverySelectorResolver delegate; + + FeaturesPropertyResolver(DiscoverySelectorResolver delegate) { + this.delegate = delegate; + } + + void resolveSelectors( + EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor, + DiscoveryIssueReporter issueReporter + ) { + ConfigurationParameters configuration = request.getConfigurationParameters(); + CucumberConfiguration options = new CucumberConfiguration(configuration); + Set selectors = options.featuresWithLines(); + + if (selectors.isEmpty()) { + delegate.resolveSelectors(request, engineDescriptor, issueReporter); + return; + } + issueReporter.reportIssue(createCucumberFeaturesPropertyIsUsedIssue()); + EngineDiscoveryRequest replacement = new FeaturesPropertyDiscoveryRequest(request, selectors); + delegate.resolveSelectors(replacement, engineDescriptor, issueReporter); + } + + private static DiscoveryIssue createCucumberFeaturesPropertyIsUsedIssue() { + return DiscoveryIssue.create(WARNING, + "Discovering tests using the " + FEATURES_PROPERTY_NAME + " property. Other discovery " + + "selectors are ignored!\n" + + "\n" + + "This is a work around for the limited JUnit 5 support in Maven and Gradle. " + + "Please request/upvote/sponsor/ect better support for JUnit 5 discovery selectors. " + + "For details see: https://github.com/cucumber/cucumber-jvm/pull/2498\n" + + "\n" + + "If you are using the JUnit 5 Suite Engine, Platform Launcher API or Console Launcher you " + + "should not use this property. Please consult the JUnit 5 documentation on test selection."); + } + + private static class FeaturesPropertyDiscoveryRequest implements EngineDiscoveryRequest { + + private final EngineDiscoveryRequest delegate; + private final Set selectors; + + public FeaturesPropertyDiscoveryRequest( + EngineDiscoveryRequest delegate, + Set selectors + ) { + this.delegate = delegate; + this.selectors = selectors; + } + + @Override + public List getSelectorsByType(Class selectorType) { + requireNonNull(selectorType); + return this.selectors.stream().filter(selectorType::isInstance).map(selectorType::cast).collect(toList()); + } + + @Override + public > List getFiltersByType(Class filterType) { + return delegate.getFiltersByType(filterType); + } + + @Override + public ConfigurationParameters getConfigurationParameters() { + return delegate.getConfigurationParameters(); + } + } + +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FileContainerSelectorResolver.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FileContainerSelectorResolver.java new file mode 100644 index 0000000000..7749dd6ad6 --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FileContainerSelectorResolver.java @@ -0,0 +1,33 @@ +package io.cucumber.junit.platform.engine; + +import io.cucumber.core.resource.PathScanner; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.discovery.DirectorySelector; +import org.junit.platform.engine.support.discovery.SelectorResolver; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectFile; + +class FileContainerSelectorResolver implements SelectorResolver { + + private final PathScanner pathScanner = new PathScanner(); + private final Predicate filter; + + FileContainerSelectorResolver(Predicate filter) { + this.filter = filter; + } + + @Override + public Resolution resolve(DirectorySelector selector, Context context) { + Set selectors = new HashSet<>(); + pathScanner.findResourcesForPath(selector.getPath(), filter, path -> selectors.add(selectFile(path.toFile()))); + if (selectors.isEmpty()) { + return Resolution.unresolved(); + } + return Resolution.selectors(selectors); + } +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NamingStrategy.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NamingStrategy.java index 81d3b81bf4..4a45773791 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NamingStrategy.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NamingStrategy.java @@ -7,5 +7,5 @@ interface NamingStrategy { String name(Node node); - String nameExample(Node.Example node, Pickle pickle); + String nameExample(Node node, Pickle pickle); } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java deleted file mode 100644 index 5312941d2e..0000000000 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java +++ /dev/null @@ -1,225 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import io.cucumber.core.gherkin.Pickle; -import io.cucumber.core.resource.ClasspathSupport; -import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.TestTag; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.config.PrefixedConfigurationParameters; -import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; -import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; -import org.junit.platform.engine.support.hierarchical.ExclusiveResource; -import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode; -import org.junit.platform.engine.support.hierarchical.Node; - -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; - -import static io.cucumber.junit.platform.engine.Constants.EXECUTION_EXCLUSIVE_RESOURCES_PREFIX; -import static io.cucumber.junit.platform.engine.Constants.EXECUTION_MODE_FEATURE_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.READ_SUFFIX; -import static io.cucumber.junit.platform.engine.Constants.READ_WRITE_SUFFIX; -import static java.util.stream.Collectors.collectingAndThen; -import static java.util.stream.Collectors.toCollection; - -abstract class NodeDescriptor extends AbstractTestDescriptor implements Node { - - private final ExecutionMode executionMode; - - NodeDescriptor(ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source) { - super(uniqueId, name, source); - this.executionMode = parameters - .get(EXECUTION_MODE_FEATURE_PROPERTY_NAME, - value -> ExecutionMode.valueOf(value.toUpperCase(Locale.US))) - .orElse(ExecutionMode.CONCURRENT); - } - - @Override - public ExecutionMode getExecutionMode() { - return executionMode; - } - - static final class ExamplesDescriptor extends NodeDescriptor { - - ExamplesDescriptor(ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source) { - super(parameters, uniqueId, name, source); - } - - @Override - public Type getType() { - return Type.CONTAINER; - } - - } - - static final class RuleDescriptor extends NodeDescriptor { - - RuleDescriptor(ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source) { - super(parameters, uniqueId, name, source); - } - - @Override - public Type getType() { - return Type.CONTAINER; - } - - } - - static final class ScenarioOutlineDescriptor extends NodeDescriptor { - - ScenarioOutlineDescriptor( - ConfigurationParameters parameters, UniqueId uniqueId, String name, - TestSource source - ) { - super(parameters, uniqueId, name, source); - } - - @Override - public Type getType() { - return Type.CONTAINER; - } - - } - - static final class PickleDescriptor extends NodeDescriptor { - - private final Pickle pickle; - private final Set tags; - private final Set exclusiveResources = new LinkedHashSet<>(0); - - PickleDescriptor( - ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source, - Pickle pickle - ) { - super(parameters, uniqueId, name, source); - this.pickle = pickle; - this.tags = getTags(pickle); - this.tags.forEach(tag -> { - ExclusiveResourceOptions exclusiveResourceOptions = new ExclusiveResourceOptions(parameters, tag); - exclusiveResourceOptions.exclusiveReadWriteResource() - .map(resource -> new ExclusiveResource(resource, LockMode.READ_WRITE)) - .forEach(exclusiveResources::add); - exclusiveResourceOptions.exclusiveReadResource() - .map(resource -> new ExclusiveResource(resource, LockMode.READ)) - .forEach(exclusiveResources::add); - }); - } - - Pickle getPickle() { - return pickle; - } - - private Set getTags(Pickle pickleEvent) { - return pickleEvent.getTags().stream() - .map(tag -> tag.substring(1)) - .filter(TestTag::isValid) - .map(TestTag::create) - // Retain input order - .collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); - } - - @Override - public Type getType() { - return Type.TEST; - } - - @Override - public SkipResult shouldBeSkipped(CucumberEngineExecutionContext context) { - return Stream.of(shouldBeSkippedByTagFilter(context), shouldBeSkippedByNameFilter(context)) - .flatMap(skipResult -> skipResult.map(Stream::of).orElseGet(Stream::empty)) - .filter(SkipResult::isSkipped) - .findFirst() - .orElseGet(SkipResult::doNotSkip); - } - - private Optional shouldBeSkippedByTagFilter(CucumberEngineExecutionContext context) { - return context.getOptions().tagFilter().map(expression -> { - if (expression.evaluate(pickle.getTags())) { - return SkipResult.doNotSkip(); - } - return SkipResult - .skip( - "'" + Constants.FILTER_TAGS_PROPERTY_NAME + "=" + expression - + "' did not match this scenario"); - }); - } - - private Optional shouldBeSkippedByNameFilter(CucumberEngineExecutionContext context) { - return context.getOptions().nameFilter().map(pattern -> { - if (pattern.matcher(pickle.getName()).matches()) { - return SkipResult.doNotSkip(); - } - return SkipResult - .skip("'" + Constants.FILTER_NAME_PROPERTY_NAME + "=" + pattern - + "' did not match this scenario"); - }); - } - - @Override - public CucumberEngineExecutionContext execute( - CucumberEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor - ) { - context.runTestCase(pickle); - return context; - } - - @Override - public Set getExclusiveResources() { - return exclusiveResources; - } - - /** - * Returns the set of {@linkplain TestTag tags} for a pickle. - *

- * Note that Cucumber will remove the {code @} symbol from all Gherkin - * tags. So a scenario tagged with {@code @Smoke} becomes a test tagged - * with {@code Smoke}. - * - * @return the set of tags - */ - @Override - public Set getTags() { - return tags; - } - - Optional getPackage() { - return getSource() - .filter(ClasspathResourceSource.class::isInstance) - .map(ClasspathResourceSource.class::cast) - .map(ClasspathResourceSource::getClasspathResourceName) - .map(ClasspathSupport::packageNameOfResource); - } - - private static final class ExclusiveResourceOptions { - - private final ConfigurationParameters parameters; - - ExclusiveResourceOptions(ConfigurationParameters parameters, TestTag tag) { - this.parameters = new PrefixedConfigurationParameters( - parameters, - EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + tag.getName()); - } - - public Stream exclusiveReadWriteResource() { - return parameters.get(READ_WRITE_SUFFIX, s -> Arrays.stream(s.split(",")) - .map(String::trim)) - .orElse(Stream.empty()); - } - - public Stream exclusiveReadResource() { - return parameters.get(READ_SUFFIX, s -> Arrays.stream(s.split(",")) - .map(String::trim)) - .orElse(Stream.empty()); - } - - } - - } - -} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/OrderingVisitor.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/OrderingVisitor.java new file mode 100644 index 0000000000..3bb32cca9c --- /dev/null +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/OrderingVisitor.java @@ -0,0 +1,35 @@ +package io.cucumber.junit.platform.engine; + +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.TestDescriptor; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static io.cucumber.junit.platform.engine.DefaultDescriptorOrderingStrategy.getStrategy; + +class OrderingVisitor implements TestDescriptor.Visitor { + + private final UnaryOperator> orderer; + + OrderingVisitor(ConfigurationParameters configuration) { + this(getStrategy(configuration).create(configuration)); + } + + private OrderingVisitor(UnaryOperator> orderer) { + this.orderer = orderer; + } + + @SuppressWarnings("unchecked") + @Override + public void visit(TestDescriptor descriptor) { + descriptor.orderChildren(children -> { + // Ok. All TestDescriptors are AbstractCucumberTestDescriptor + @SuppressWarnings("rawtypes") + List cucumberDescriptors = (List) children; + orderer.apply(cucumberDescriptors); + return children; + }); + } + +} diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/TestDescriptorOnLine.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/TestDescriptorOnLine.java deleted file mode 100644 index 3725891788..0000000000 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/TestDescriptorOnLine.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import io.cucumber.core.feature.FeatureWithLines; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.discovery.ClasspathResourceSelector; -import org.junit.platform.engine.discovery.FileSelector; -import org.junit.platform.engine.discovery.UriSelector; -import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; -import org.junit.platform.engine.support.descriptor.FileSource; - -import java.util.Optional; -import java.util.function.Predicate; - -import static org.junit.platform.engine.support.descriptor.FilePosition.fromQuery; - -@SuppressWarnings("Convert2MethodRef") -class TestDescriptorOnLine { - - static Predicate testDescriptorOnLine(int line) { - return descriptor -> descriptor.getSource() - .flatMap(testSource -> { - if (testSource instanceof FileSource) { - FileSource fileSystemSource = (FileSource) testSource; - return fileSystemSource.getPosition(); - } - if (testSource instanceof ClasspathResourceSource) { - ClasspathResourceSource classpathResourceSource = (ClasspathResourceSource) testSource; - return classpathResourceSource.getPosition(); - } - return Optional.empty(); - }) - .map(filePosition -> filePosition.getLine()) - .map(testSourceLine -> line == testSourceLine) - .orElse(false); - } - - private static boolean anyTestDescriptor(TestDescriptor testDescriptor) { - return true; - } - - private static Predicate eitherTestDescriptor( - Predicate a, Predicate b - ) { - return a.or(b); - } - - static Predicate from(FeatureWithLines selector) { - return selector.lines().stream() - .map(TestDescriptorOnLine::testDescriptorOnLine) - .reduce(TestDescriptorOnLine::eitherTestDescriptor) - .orElse(TestDescriptorOnLine::anyTestDescriptor); - } - - static Predicate from(UriSelector selector) { - String query = selector.getUri().getQuery(); - return fromQuery(query) - .map(filePosition -> filePosition.getLine()) - .map(TestDescriptorOnLine::testDescriptorOnLine) - .orElse(TestDescriptorOnLine::anyTestDescriptor); - } - - static Predicate from(ClasspathResourceSelector selector) { - return selector.getPosition() - .map(filePosition -> filePosition.getLine()) - .map(TestDescriptorOnLine::testDescriptorOnLine) - .orElse(TestDescriptorOnLine::anyTestDescriptor); - } - - static Predicate from(FileSelector selector) { - return selector.getPosition() - .map(filePosition -> filePosition.getLine()) - .map(TestDescriptorOnLine::testDescriptorOnLine) - .orElse(TestDescriptorOnLine::anyTestDescriptor); - } - -} diff --git a/cucumber-junit-platform-engine/src/test/bad-features/parse-error.feature b/cucumber-junit-platform-engine/src/test/bad-features/parse-error.feature new file mode 100644 index 0000000000..b5b77f48e3 --- /dev/null +++ b/cucumber-junit-platform-engine/src/test/bad-features/parse-error.feature @@ -0,0 +1,5 @@ +Feature: A feature with a parse error + + Scenario: A single scenario + Given a single scenario + AndAStep with an invalid keyword diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberConfigurationTest.java similarity index 83% rename from cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java rename to cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberConfigurationTest.java index 271bcd1d9c..155e1dde3b 100644 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberConfigurationTest.java @@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -class CucumberEngineOptionsTest { +class CucumberConfigurationTest { @Test void getPluginNames() { @@ -28,12 +28,12 @@ void getPluginNames() { Constants.PLUGIN_PROPERTY_NAME, "html:path/to/report.html"); - assertThat(new CucumberEngineOptions(config).plugins().stream() + assertThat(new CucumberConfiguration(config).plugins().stream() .map(Options.Plugin::pluginString) .collect(toList()), hasItem("html:path/to/report.html")); - CucumberEngineOptions htmlAndJson = new CucumberEngineOptions( + CucumberConfiguration htmlAndJson = new CucumberConfiguration( new MapConfigurationParameters(Constants.PLUGIN_PROPERTY_NAME, "html:path/with spaces/to/report.html, message:path/with spaces/to/report.ndjson")); @@ -48,7 +48,7 @@ void getPluginNamesWithPublishToken() { ConfigurationParameters config = new MapConfigurationParameters( Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME, "some/token"); - assertThat(new CucumberEngineOptions(config).plugins().stream() + assertThat(new CucumberConfiguration(config).plugins().stream() .map(Options.Plugin::pluginString) .collect(toList()), hasItem("io.cucumber.core.plugin.PublishFormatter:some/token")); @@ -58,7 +58,7 @@ void getPluginNamesWithPublishToken() { void getPluginNamesWithNothingEnabled() { ConfigurationParameters config = new EmptyConfigurationParameters(); - assertThat(new CucumberEngineOptions(config).plugins().stream() + assertThat(new CucumberConfiguration(config).plugins().stream() .map(Options.Plugin::pluginString) .collect(toList()), empty()); @@ -69,7 +69,7 @@ void getPluginNamesWithPublishQuiteEnabled() { ConfigurationParameters config = new MapConfigurationParameters( Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true"); - assertThat(new CucumberEngineOptions(config).plugins().stream() + assertThat(new CucumberConfiguration(config).plugins().stream() .map(Options.Plugin::pluginString) .collect(toList()), empty()); @@ -80,7 +80,7 @@ void getPluginNamesWithPublishEnabled() { ConfigurationParameters config = new MapConfigurationParameters( Constants.PLUGIN_PUBLISH_ENABLED_PROPERTY_NAME, "true"); - assertThat(new CucumberEngineOptions(config).plugins().stream() + assertThat(new CucumberConfiguration(config).plugins().stream() .map(Options.Plugin::pluginString) .collect(toList()), hasItem("io.cucumber.core.plugin.PublishFormatter")); @@ -92,7 +92,7 @@ void getPluginNamesWithPublishDisabledAndPublishToken() { Constants.PLUGIN_PUBLISH_ENABLED_PROPERTY_NAME, "false", Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME, "some/token")); - assertThat(new CucumberEngineOptions(config).plugins().stream() + assertThat(new CucumberConfiguration(config).plugins().stream() .map(Options.Plugin::pluginString) .collect(toList()), empty()); @@ -103,12 +103,12 @@ void isMonochrome() { MapConfigurationParameters ansiColors = new MapConfigurationParameters( Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME, "true"); - assertTrue(new CucumberEngineOptions(ansiColors).isMonochrome()); + assertTrue(new CucumberConfiguration(ansiColors).isMonochrome()); MapConfigurationParameters noAnsiColors = new MapConfigurationParameters( Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME, "false"); - assertFalse(new CucumberEngineOptions(noAnsiColors).isMonochrome()); + assertFalse(new CucumberConfiguration(noAnsiColors).isMonochrome()); } @Test @@ -117,7 +117,7 @@ void getGlue() { Constants.GLUE_PROPERTY_NAME, "com.example.app, com.example.glue"); - assertThat(new CucumberEngineOptions(config).getGlue(), + assertThat(new CucumberConfiguration(config).getGlue(), contains( URI.create("classpath:/com/example/app"), URI.create("classpath:/com/example/glue"))); @@ -128,12 +128,12 @@ void isDryRun() { ConfigurationParameters dryRun = new MapConfigurationParameters( Constants.EXECUTION_DRY_RUN_PROPERTY_NAME, "true"); - assertTrue(new CucumberEngineOptions(dryRun).isDryRun()); + assertTrue(new CucumberConfiguration(dryRun).isDryRun()); ConfigurationParameters noDryRun = new MapConfigurationParameters( Constants.EXECUTION_DRY_RUN_PROPERTY_NAME, "false"); - assertFalse(new CucumberEngineOptions(noDryRun).isDryRun()); + assertFalse(new CucumberConfiguration(noDryRun).isDryRun()); } @Test @@ -142,12 +142,12 @@ void getSnippetType() { Constants.SNIPPET_TYPE_PROPERTY_NAME, "underscore"); - assertThat(new CucumberEngineOptions(underscore).getSnippetType(), is(SnippetType.UNDERSCORE)); + assertThat(new CucumberConfiguration(underscore).getSnippetType(), is(SnippetType.UNDERSCORE)); ConfigurationParameters camelcase = new MapConfigurationParameters( Constants.SNIPPET_TYPE_PROPERTY_NAME, "camelcase"); - assertThat(new CucumberEngineOptions(camelcase).getSnippetType(), is(SnippetType.CAMELCASE)); + assertThat(new CucumberConfiguration(camelcase).getSnippetType(), is(SnippetType.CAMELCASE)); } @Test @@ -155,15 +155,15 @@ void isParallelExecutionEnabled() { ConfigurationParameters enabled = new MapConfigurationParameters( Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, "true"); - assertTrue(new CucumberEngineOptions(enabled).isParallelExecutionEnabled()); + assertTrue(new CucumberConfiguration(enabled).isParallelExecutionEnabled()); ConfigurationParameters disabled = new MapConfigurationParameters( Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, "false"); - assertFalse(new CucumberEngineOptions(disabled).isParallelExecutionEnabled()); + assertFalse(new CucumberConfiguration(disabled).isParallelExecutionEnabled()); ConfigurationParameters absent = new MapConfigurationParameters( "some key", "some value"); - assertFalse(new CucumberEngineOptions(absent).isParallelExecutionEnabled()); + assertFalse(new CucumberConfiguration(absent).isParallelExecutionEnabled()); } @Test @@ -172,7 +172,7 @@ void objectFactory() { Constants.OBJECT_FACTORY_PROPERTY_NAME, DefaultObjectFactory.class.getName()); - assertThat(new CucumberEngineOptions(configurationParameters).getObjectFactoryClass(), + assertThat(new CucumberConfiguration(configurationParameters).getObjectFactoryClass(), is(DefaultObjectFactory.class)); } @@ -182,7 +182,7 @@ void uuidGenerator() { Constants.UUID_GENERATOR_PROPERTY_NAME, IncrementingUuidGenerator.class.getName()); - assertThat(new CucumberEngineOptions(configurationParameters).getUuidGeneratorClass(), + assertThat(new CucumberConfiguration(configurationParameters).getUuidGeneratorClass(), is(IncrementingUuidGenerator.class)); } } diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEventConditions.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEventConditions.java new file mode 100644 index 0000000000..73842a773e --- /dev/null +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEventConditions.java @@ -0,0 +1,134 @@ +package io.cucumber.junit.platform.engine; + +import org.assertj.core.api.Condition; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.testkit.engine.Event; +import org.junit.platform.testkit.engine.EventConditions; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.allOf; +import static org.junit.platform.commons.util.FunctionUtils.where; +import static org.junit.platform.testkit.engine.Event.byTestDescriptor; +import static org.junit.platform.testkit.engine.EventConditions.displayName; +import static org.junit.platform.testkit.engine.EventConditions.uniqueIdSubstring; + +class CucumberEventConditions { + + static Condition engine(Condition condition) { + return allOf(EventConditions.engine(), condition); + } + + static Condition examples() { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getUniqueId, lastSegmentTYpe("examples"))), + "examples descriptor"); + } + + static Condition example(String uniqueIdSubstring, String displayName) { + return allOf(example(), uniqueIdSubstring(uniqueIdSubstring), displayName(displayName)); + } + + static Condition example(String uniqueIdSubstring) { + return allOf(example(), uniqueIdSubstring(uniqueIdSubstring)); + } + + static Condition example() { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getUniqueId, lastSegmentTYpe("example"))), + "examples descriptor"); + } + + static Condition examples(String uniqueIdSubstring) { + return allOf(examples(), uniqueIdSubstring(uniqueIdSubstring)); + } + + static Condition examples(String uniqueIdSubstring, String displayName) { + return allOf(examples(), uniqueIdSubstring(uniqueIdSubstring), displayName(displayName)); + } + + static Condition feature() { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getUniqueId, lastSegmentTYpe("feature"))), + "feature descriptor"); + } + + static Condition tags(Set tags) { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getTags, hasTags(tags))), + "has tags " + tags); + } + + static Condition tags(String... tags) { + return tags(new HashSet<>(Arrays.asList(tags))); + } + + private static Predicate> hasTags(Set expected) { + return testTags -> { + Set actual = testTags.stream().map(TestTag::getName).collect(Collectors.toSet()); + return expected.equals(actual); + }; + } + + static Condition feature(String uniqueIdSubstring, String displayName) { + return allOf(feature(), uniqueIdSubstring(uniqueIdSubstring), displayName(displayName)); + } + + static Condition feature(String uniqueIdSubstring) { + return allOf(feature(), uniqueIdSubstring(uniqueIdSubstring)); + } + + static Condition rule() { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getUniqueId, lastSegmentTYpe("rule"))), + "rule descriptor"); + } + + static Condition rule(String uniqueIdSubstring, String displayName) { + return allOf(rule(), uniqueIdSubstring(uniqueIdSubstring), displayName(displayName)); + } + + static Condition scenario() { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getUniqueId, lastSegmentTYpe("scenario"))), + "feature descriptor"); + } + + static Condition scenario(String uniqueIdSubstring, String displayName) { + return allOf(scenario(), uniqueIdSubstring(uniqueIdSubstring), displayName(displayName)); + } + + static Condition scenario(Condition condition) { + return allOf(scenario(), condition); + } + + static Condition scenario(String uniqueIdSubstring) { + return allOf(scenario(), uniqueIdSubstring(uniqueIdSubstring)); + } + + static Condition source(TestSource testSource) { + return new Condition<>(event -> event.getTestDescriptor().getSource().filter(testSource::equals).isPresent(), + "test descriptor with test source '%s'", testSource); + } + + static Condition emptySource() { + return new Condition<>(event -> !event.getTestDescriptor().getSource().isPresent(), "without a test source"); + } + + private static Predicate lastSegmentTYpe(String type) { + return uniqueId -> uniqueId.getLastSegment().getType().equals(type); + } + + static Condition prefix(UniqueId uniqueId) { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getUniqueId, candidate -> candidate.hasPrefix(uniqueId))), + "test descriptor with prefix " + uniqueId); + } +} diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java index c86af75258..abc5590131 100644 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberTestEngineTest.java @@ -1,35 +1,94 @@ package io.cucumber.junit.platform.engine; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.Condition; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.gherkin.Pickle; +import io.cucumber.core.logging.LogRecordListener; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.FeatureDescriptor; +import io.cucumber.junit.platform.engine.CucumberTestDescriptor.PickleDescriptor; import org.junit.jupiter.api.Test; -import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.ExecutionRequest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.commons.support.Resource; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.discovery.FilePosition; import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; +import org.junit.platform.engine.support.descriptor.FileSource; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; +import org.junit.platform.engine.support.hierarchical.Node; +import org.junit.platform.testkit.engine.EngineDiscoveryResults; import org.junit.platform.testkit.engine.EngineTestKit; import org.junit.platform.testkit.engine.Event; -import org.junit.platform.testkit.engine.EventConditions; +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_EXCLUSIVE_RESOURCES_PREFIX; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_MODE_FEATURE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_ORDER_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.EXECUTION_ORDER_RANDOM_SEED_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; 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.Constants.JUNIT_PLATFORM_LONG_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.READ_SUFFIX; +import static io.cucumber.junit.platform.engine.Constants.READ_WRITE_SUFFIX; import static io.cucumber.junit.platform.engine.CucumberEngineDescriptor.ENGINE_ID; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.emptySource; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.engine; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.example; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.examples; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.feature; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.prefix; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.rule; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.scenario; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.source; +import static io.cucumber.junit.platform.engine.CucumberEventConditions.tags; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.platform.engine.UniqueId.forEngine; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathResource; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathRoots; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectDirectory; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectFile; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUri; +import static org.junit.platform.engine.discovery.PackageNameFilter.includePackageNames; +import static org.junit.platform.engine.support.descriptor.FilePosition.from; +import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.CONCURRENT; +import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD; +import static org.junit.platform.testkit.engine.EventConditions.displayName; import static org.junit.platform.testkit.engine.EventConditions.event; import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason; import static org.junit.platform.testkit.engine.EventConditions.test; +// TODO: Split out tests to multiple classes, but do use EngineTestKit everywhere + +@WithLogRecordListener class CucumberTestEngineTest { private final CucumberTestEngine engine = new CucumberTestEngine(); @@ -45,42 +104,472 @@ void version() { } @Test - void createExecutionContext() { - EngineExecutionListener listener = new EmptyEngineExecutionListener(); - ConfigurationParameters configuration = new EmptyConfigurationParameters(); - EngineDiscoveryRequest discoveryRequest = new EmptyEngineDiscoveryRequest(configuration); - UniqueId id = UniqueId.forEngine(engine.getId()); - TestDescriptor testDescriptor = engine.discover(discoveryRequest, id); - ExecutionRequest execution = new ExecutionRequest(testDescriptor, listener, configuration); - assertNotNull(engine.createExecutionContext(execution)); + void empty() { + EngineTestKit.engine(ENGINE_ID) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(0, event(test())); } @Test - void selectAndExecuteNoScenario() { + void notCucumber() { EngineTestKit.engine(ENGINE_ID) - .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") + .selectors(selectUniqueId(forEngine("not-cucumber"))) .execute() - .testEvents() + .allEvents() .assertThatEvents() .haveExactly(0, event(test())); } @Test - void selectAndExecuteSingleScenario() { + void supportsClassSelector() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(RunCucumberTest.class)) + .execute() + .containerEvents() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature")); + } + + @Test + void warnsAboutClassSelector() { + EngineDiscoveryResults results = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(RunCucumberTest.class)) + .discover(); + + DiscoveryIssue discoveryIssue = results.getDiscoveryIssues().get(0); + assertThat(discoveryIssue.message()) + .isEqualTo("The @Cucumber annotation has been deprecated. See the Javadoc for more details."); + } + + @Test + void supportsClasspathResourceSelector() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/single.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + scenario("scenario:3", "A single scenario"), // + finishedSuccessfully())); + } + + @Test + void warnWhenResourceSelectorIsUsedToSelectAPackage() { + EngineTestKit.Builder selectors = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine")); + + EngineDiscoveryResults discoveryResults = selectors.discover(); + DiscoveryIssue discoveryIssue = discoveryResults.getDiscoveryIssues().get(0); + assertThat(discoveryIssue.message()) + .isEqualTo( + "The classpath resource selector 'io/cucumber/junit/platform/engine' should not be " + + "used to select features in a package. Use the package selector with " + + "'io.cucumber.junit.platform.engine' instead"); + + // It should also still work + selectors + .execute() + .allEvents() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature")); + + } + + @Test + void classpathResourceSelectorThrowIfDuplicateResources() { + class TestResource implements Resource { + + private final String name; + private final File source; + + TestResource(String name, File source) { + this.name = name; + this.source = source; + } + + @Override + public String getName() { + return name; + } + + @Override + public URI getUri() { + return source.toURI(); + } + } + Set resources = new LinkedHashSet<>(Arrays.asList( + new TestResource("io/cucumber/junit/platform/engine/single.feature", + new File("src/test/resources/io/cucumber/junit/platform/engine/single.feature")), + new TestResource("io/cucumber/junit/platform/engine/single.feature", + new File("src/test/resources/io/cucumber/junit/platform/engine/single.feature")), + new TestResource("io/cucumber/junit/platform/engine/single.feature", + new File("src/test/resources/io/cucumber/junit/platform/engine/single.feature")))); + + Throwable exception = EngineTestKit.engine(ENGINE_ID) // + .selectors(selectClasspathResource(resources)) // + .discover() // + .getDiscoveryIssues() // + .get(0) // + .cause() // + .orElseThrow(); + + assertThat(exception) // + .isInstanceOf(IllegalArgumentException.class) // + .hasMessage( // + "Found %s resources named %s on the classpath %s.", // + resources.size(), // + "io/cucumber/junit/platform/engine/single.feature", // + resources.stream().map(Resource::getUri).collect(toList())); + } + + @Test + void supportsClasspathResourceSelectorWithFilePosition() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/rule.feature", // + FilePosition.from(5))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(2, event(scenario("scenario:5", "An example of this rule"))); + } + + @Test + void supportsMultipleClasspathResourceSelectors() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/single.feature"), + selectClasspathResource("io/cucumber/junit/platform/engine/scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(2, event(feature("single.feature", "A feature with a single scenario"))) + .haveExactly(2, event(feature("scenario-outline.feature", "A feature with scenario outlines"))); + } + + @Test + void supportsClasspathResourceSelectorWithSpaceInResourceName() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/with space.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event(scenario(), finishedSuccessfully())); + } + + @Test + void supportsClasspathRootSelector() { + Path classpathRoot = Paths.get("src/test/resources/"); + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathRoots(singleton(classpathRoot)).get(0)) + .execute() + .containerEvents() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature"), + feature("root.feature")); + } + + @Test + void supportsDirectorySelector() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectDirectory("src/test/resources/io/cucumber/junit/platform/engine")) + .execute() + .containerEvents() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature")); + } + + @Test + void supportsFileSelector() { EngineTestKit.engine(ENGINE_ID) - .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") .selectors(selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature")) .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + scenario("scenario:3", "A single scenario"), // + finishedSuccessfully())); + } + + @Test + void supportsFileSelectorWithFilePosition() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectFile("src/test/resources/io/cucumber/junit/platform/engine/rule.feature", // + FilePosition.from(5))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + scenario("scenario:5", "An example of this rule"), // + finishedSuccessfully())); + } + + @Test + void supportsPackageSelector() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectPackage("io.cucumber.junit.platform.engine")) + .execute() + .containerEvents() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature")); + } + + @Test + void supportsUriSelector() { + File file = new File("src/test/resources/io/cucumber/junit/platform/engine/single.feature"); + EngineTestKit.engine(ENGINE_ID) + .selectors(selectUri(file.toURI())) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + scenario("scenario:3", "A single scenario"), // + finishedSuccessfully())); + } + + @Test + void supportsUriSelectorWithFilePosition() { + File file = new File("src/test/resources/io/cucumber/junit/platform/engine/rule.feature"); + EngineTestKit.engine(ENGINE_ID) + .selectors(selectUri(file.toURI() + "?line=5")) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event(scenario("scenario:5", "An example of this rule"), finishedSuccessfully())); + } + + @ParameterizedTest + @MethodSource({ + "supportsUniqueIdSelectorFromClasspathUri", + "supportsUniqueIdSelectorFromFileUri", + "supportsUniqueIdSelectorFromJarFileUri" + }) + void supportsUniqueIdSelector(UniqueId selected) { + EngineTestKit.engine(ENGINE_ID) + .selectors(DiscoverySelectors.selectUniqueId(selected)) + .execute() .testEvents() .assertThatEvents() - .haveExactly(2, event(test())) - .haveExactly(1, event(finishedSuccessfully())); + .haveAtLeastOne(event(prefix(selected), finishedSuccessfully())); + } + + static Set supportsUniqueIdSelectorFromClasspathUri() { + return discoverUniqueIds(selectPackage("io.cucumber.junit.platform.engine")); + + } + + static Set supportsUniqueIdSelectorFromFileUri() { + return discoverUniqueIds(selectDirectory("src/test/resources/io/cucumber/junit/platform/engine")); + + } + + static Set supportsUniqueIdSelectorFromJarFileUri() { + URI uri = new File("src/test/resources/feature.jar").toURI(); + return discoverUniqueIds(selectUri(uri)); } @Test - void selectAndExecuteSingleScenarioThroughFeaturesProperty() { + void supportsUniqueIdSelectorWithMultipleSelectors() { + UniqueId a = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/scenario-outline.feature")) + .execute() + .allEvents() + .map(Event::getTestDescriptor) + .filter(PickleDescriptor.class::isInstance) + .map(TestDescriptor::getUniqueId) + .findAny() + .orElseThrow(); + + UniqueId b = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/single.feature")) + .execute() + .allEvents() + .map(Event::getTestDescriptor) + .filter(PickleDescriptor.class::isInstance) + .map(TestDescriptor::getUniqueId) + .findAny() + .orElseThrow(); + + EngineTestKit.engine(ENGINE_ID) + .selectors(selectUniqueId(a), selectUniqueId(b)) + .execute() + .testEvents() + .assertThatEvents() + .haveAtLeastOne(event(prefix(a), finishedSuccessfully())) + .haveAtLeastOne(event(prefix(b), finishedSuccessfully())); + } + + @Test + void supportsUniqueIdSelectorCachesParsedFeaturesAndPickles() { + DiscoverySelector featureSelector = selectClasspathResource( + "io/cucumber/junit/platform/engine/scenario-outline.feature"); + DiscoverySelector[] uniqueIdsFromFeature = discoverUniqueIds(featureSelector) + .stream() + .map(DiscoverySelectors::selectUniqueId) + .toArray(DiscoverySelector[]::new); + + EngineDiscoveryResults results = EngineTestKit.engine(ENGINE_ID) + .selectors(featureSelector) + .selectors(uniqueIdsFromFeature) + .discover(); + + Set pickleIdsFromFeature = results + .getEngineDescriptor().getChildren().stream() + .filter(FeatureDescriptor.class::isInstance) + .map(FeatureDescriptor.class::cast) + .map(FeatureDescriptor::getFeature) + .map(Feature::getPickles) + .flatMap(Collection::stream) + .map(Pickle::getId) + .collect(toSet()); + + Set pickleIdsFromPickles = results + .getEngineDescriptor().getDescendants().stream() + .filter(PickleDescriptor.class::isInstance) + .map(PickleDescriptor.class::cast) + .map(PickleDescriptor::getPickle) + .map(Pickle::getId) + .collect(toSet()); + + assertEquals(pickleIdsFromFeature, pickleIdsFromPickles); + } + + private static Set discoverUniqueIds(DiscoverySelector discoverySelector) { + return EngineTestKit.engine(ENGINE_ID) + .selectors(discoverySelector) + .execute() + .allEvents() + .map(Event::getTestDescriptor) + .filter(Predicate.not(TestDescriptor::isRoot)) + .map(TestDescriptor::getUniqueId) + .collect(toSet()); + } + + @Test + void supportsFilePositionFeature() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectFile("src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature", // + FilePosition.from(2))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(2, event(feature("scenario-outline.feature", "A feature with scenario outlines"))); + } + + @Test + void supportsFilePositionScenario() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectFile("src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature", // + FilePosition.from(5))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + scenario("scenario:5", "A scenario"), // + finishedSuccessfully())); + } + + @Test + void supportsFilePositionScenarioOutline() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectFile("src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature", // + FilePosition.from(11))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + scenario("scenario:11", "A scenario outline"), // + finishedSuccessfully())); + } + + @Test + void supportsFilePositionExamples() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectFile("src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature", // + FilePosition.from(17))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + examples("examples:17", "With some text"), // + finishedSuccessfully())); + } + + @Test + void supportsFilePositionExample() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectFile("src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature", // + FilePosition.from(19))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(1, event( // + example("example:19", "Example #1.1"), // + finishedSuccessfully())); + } + + @Test + void supportsFilePositionRule() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/rule.feature", // + FilePosition.from(3))) + .execute() + .allEvents() + .assertThatEvents() + .haveExactly(2, event(rule("rule:3", "A rule"))); + } + + @Test + void executesFeaturesInUriOrderByDefault() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectPackage("")) + .execute() + .containerEvents() + .started() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature"), + feature("root.feature")); + } + + @Test + void supportsFeaturesProperty() { EngineTestKit.engine(ENGINE_ID) - .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") .configurationParameter(FEATURES_PROPERTY_NAME, "src/test/resources/io/cucumber/junit/platform/engine/single.feature") .execute() @@ -91,9 +580,22 @@ void selectAndExecuteSingleScenarioThroughFeaturesProperty() { } @Test - void selectAndExecuteSingleScenarioWithoutFeaturesProperty() { + void supportsFeaturesPropertyWillIgnoreOtherSelectors() { + EngineDiscoveryResults discoveryResult = EngineTestKit.engine(ENGINE_ID) + .configurationParameter(FEATURES_PROPERTY_NAME, + "src/test/resources/io/cucumber/junit/platform/engine/single.feature") + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/rule.feature")) + .discover(); + + DiscoveryIssue discoveryIssue = discoveryResult.getDiscoveryIssues().get(0); + assertThat(discoveryIssue.message()) + .startsWith( + "Discovering tests using the cucumber.features property. Other discovery selectors are ignored!"); + } + + @Test + void onlySetsEngineSourceWhenFeaturesPropertyIsUsed() { EngineTestKit.engine(ENGINE_ID) - .configurationParameter(PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, "true") .selectors(selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature")) .execute() .allEvents() @@ -105,7 +607,6 @@ void selectAndExecuteSingleScenarioWithoutFeaturesProperty() { @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() @@ -119,28 +620,416 @@ 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() .testEvents() .assertThatEvents() - .haveExactly(1, event(test())) - .haveExactly(1, - event(skippedWithReason("'cucumber.filter.name=^Nothing$' did not match this scenario"))); + .haveExactly(1, event(test(), + event(skippedWithReason("'cucumber.filter.name=^Nothing$' did not match this scenario")))); } - private static Condition engine(Condition condition) { - return Assertions.allOf(EventConditions.engine(), condition); + @Test + void cucumberTagsAreConvertedToJunitTags() { + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource("io/cucumber/junit/platform/engine/scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), tags(emptySet()))) + .haveAtLeastOne( + event(scenario("scenario:5"), tags("FeatureTag", "ScenarioTag"))) + .haveAtLeastOne(event(scenario("scenario:11"), tags(emptySet()))) + .haveAtLeastOne(event(examples("examples:17"), tags(emptySet()))) + .haveAtLeastOne(event(example("example:19"), tags("FeatureTag", "ScenarioOutlineTag", "Example1Tag"))); + } + + @Test + void providesClasspathSourceWhenClasspathResourceIsSelected() { + String feature = "io/cucumber/junit/platform/engine/scenario-outline.feature"; + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathResource(feature)) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), source(ClasspathResourceSource.from(feature, from(2, 1))))) + .haveAtLeastOne( + event(scenario("scenario:5"), source(ClasspathResourceSource.from(feature, from(5, 3))))) + .haveAtLeastOne( + event(scenario("scenario:11"), source(ClasspathResourceSource.from(feature, from(11, 3))))) + .haveAtLeastOne( + event(examples("examples:17"), source(ClasspathResourceSource.from(feature, from(17, 5))))) + .haveAtLeastOne( + event(example("example:19"), source(ClasspathResourceSource.from(feature, from(19, 7))))); + } + + @Test + void providesFileSourceWhenFileIsSelected() { + File feature = new File("src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature"); + EngineTestKit.engine(ENGINE_ID) + .selectors(selectFile(feature)) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), source(FileSource.from(feature, from(2, 1))))) + .haveAtLeastOne(event(scenario("scenario:5"), source(FileSource.from(feature, from(5, 3))))) + .haveAtLeastOne(event(scenario("scenario:11"), source(FileSource.from(feature, from(11, 3))))) + .haveAtLeastOne(event(examples("examples:17"), source(FileSource.from(feature, from(17, 5))))) + .haveAtLeastOne(event(example("example:19"), source(FileSource.from(feature, from(19, 7))))); + } + + @Test + void supportsPackageFilterForClasspathResources() { + Path classpathRoot = Paths.get("src/test/resources/"); + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClasspathRoots(singleton(classpathRoot)).get(0)) + .filters(includePackageNames("io.cucumber.junit.platform")) + .execute() + .containerEvents() + .assertEventsMatchLooselyInOrder( + feature("disabled.feature"), + feature("empty-scenario.feature"), + feature("scenario-outline.feature"), + feature("rule.feature"), + feature("single.feature"), + feature("with%20space.feature")); } - private static Condition source(TestSource testSource) { - return new Condition<>(event -> event.getTestDescriptor().getSource().filter(testSource::equals).isPresent(), - "test engine with test source '%s'", testSource); + @Test + void defaultsToShortWithNumberAndPickleIfParameterizedNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), displayName("A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName("Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName("Example #1.1: A scenario full of Cucumbers"))); } - private static Condition emptySource() { - return new Condition<>(event -> !event.getTestDescriptor().getSource().isPresent(), "without a test source"); + @Test + void supportsLongWithNumberNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long") + .configurationParameter(JUNIT_PLATFORM_LONG_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "number") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), + displayName("A feature with a parameterized scenario outline - A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName( + "A feature with a parameterized scenario outline - A scenario full of s - Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName( + "A feature with a parameterized scenario outline - A scenario full of s - Of the Gherkin variety - Example #1.1"))); + } + + @Test + void supportsLongWithPickleNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long") + .configurationParameter(JUNIT_PLATFORM_LONG_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "pickle") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), + displayName("A feature with a parameterized scenario outline - A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName( + "A feature with a parameterized scenario outline - A scenario full of s - Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName( + "A feature with a parameterized scenario outline - A scenario full of s - Of the Gherkin variety - A scenario full of Cucumbers"))); } + @Test + void supportsLongWithNumberAndPickleIfParameterizedNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long") + .configurationParameter(JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, + "number-and-pickle-if-parameterized") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), + displayName("A feature with a parameterized scenario outline - A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName( + "A feature with a parameterized scenario outline - A scenario full of s - Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName( + "A feature with a parameterized scenario outline - A scenario full of s - Of the Gherkin variety - Example #1.1: A scenario full of Cucumbers"))); + } + + @Test + void supportsShortWithPickleNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "short") + .configurationParameter(JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "pickle") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), displayName("A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName("Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName("A scenario full of Cucumbers"))); + } + + @Test + void supportsShortWithNumberNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "short") + .configurationParameter(JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "number") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), displayName("A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName("Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName("Example #1.1"))); + } + + @Test + void supportsShortWithNumberAndPickleIfParameterizedNamingStrategy() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "short") + .configurationParameter(JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, + "number-and-pickle-if-parameterized") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature")) + .execute() + .allEvents() + .assertThatEvents() + .haveAtLeastOne(event(feature(), displayName("A feature with a parameterized scenario outline"))) + .haveAtLeastOne(event(scenario(), displayName("A scenario full of s"))) + .haveAtLeastOne(event(examples(), displayName("Of the Gherkin variety"))) + .haveAtLeastOne(event(example(), displayName("Example #1.1: A scenario full of Cucumbers"))); + } + + @Test + void defaultsToLexicalOrder() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/single.feature"), + selectClasspathResource("io/cucumber/junit/platform/engine/ordering.feature")) + .execute() + .allEvents() + .started() + .assertThatEvents() + .extracting(Event::getTestDescriptor) + .extracting(TestDescriptor::getDisplayName) + .containsExactly("Cucumber", + "1. A feature to order scenarios", + "1. A feature to order scenarios - 1.1", + "1. A feature to order scenarios - 1.2", + "1. A feature to order scenarios - 1.2 - 1.2.1", + "1. A feature to order scenarios - 1.2 - 1.2.1 - Example #1.1", + "1. A feature to order scenarios - 1.2 - 1.2.1 - Example #1.2", + "1. A feature to order scenarios - 1.2 - 1.2.2", + "1. A feature to order scenarios - 1.2 - 1.2.2 - Example #2.1", + "1. A feature to order scenarios - 1.2 - 1.2.2 - Example #2.2", + "1. A feature to order scenarios - 1.3 A rule", + "1. A feature to order scenarios - 1.3 A rule - 1.3.1", + "1. A feature to order scenarios - 1.3 A rule - 1.3.2", + "1. A feature to order scenarios - 1.4", + "1. A feature to order scenarios - 1.4 - 1.4.1", + "1. A feature to order scenarios - 1.4 - 1.4.2", + "A feature with a single scenario", + "A feature with a single scenario - A single scenario"); + } + + @Test + void supportsReverseOrder() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(EXECUTION_ORDER_PROPERTY_NAME, "reverse") + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/single.feature"), + selectClasspathResource("io/cucumber/junit/platform/engine/ordering.feature")) + .execute() + .allEvents() + .started() + .assertThatEvents() + .extracting(Event::getTestDescriptor) + .extracting(TestDescriptor::getDisplayName) + .containsExactly("Cucumber", + "A feature with a single scenario", + "A feature with a single scenario - A single scenario", + "1. A feature to order scenarios", + "1. A feature to order scenarios - 1.4", + "1. A feature to order scenarios - 1.4 - 1.4.2", + "1. A feature to order scenarios - 1.4 - 1.4.1", + "1. A feature to order scenarios - 1.3 A rule", + "1. A feature to order scenarios - 1.3 A rule - 1.3.2", + "1. A feature to order scenarios - 1.3 A rule - 1.3.1", + "1. A feature to order scenarios - 1.2", + "1. A feature to order scenarios - 1.2 - 1.2.2", + "1. A feature to order scenarios - 1.2 - 1.2.2 - Example #2.2", + "1. A feature to order scenarios - 1.2 - 1.2.2 - Example #2.1", + "1. A feature to order scenarios - 1.2 - 1.2.1", + "1. A feature to order scenarios - 1.2 - 1.2.1 - Example #1.2", + "1. A feature to order scenarios - 1.2 - 1.2.1 - Example #1.1", + "1. A feature to order scenarios - 1.1"); + } + + @Test + void supportsRandomOrder(LogRecordListener logRecordListener) { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(EXECUTION_ORDER_PROPERTY_NAME, "random") + .discover(); + + LogRecord message = logRecordListener.getLogRecords() + .stream() + .filter(logRecord -> logRecord.getLoggerName() + .equals(DefaultDescriptorOrderingStrategy.class.getCanonicalName())) + .findFirst() + .orElseThrow(); + + assertAll( + () -> assertThat(message.getLevel()).isEqualTo(Level.CONFIG), + () -> assertThat(message.getMessage()) + .matches( + "Using generated seed for configuration parameter 'cucumber\\.execution\\.order\\.random\\.seed' with value '\\d+'.")); + } + + @Test + void supportsRandomOrderWithSeed() { + EngineTestKit.engine(ENGINE_ID) + .configurationParameter(EXECUTION_ORDER_PROPERTY_NAME, "random") + .configurationParameter(EXECUTION_ORDER_RANDOM_SEED_PROPERTY_NAME, "1234") + .configurationParameter(JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/single.feature"), + selectClasspathResource("io/cucumber/junit/platform/engine/ordering.feature")) + .execute() + .allEvents() + .started() + .assertThatEvents() + .extracting(Event::getTestDescriptor) + .extracting(TestDescriptor::getDisplayName) + .containsExactly("Cucumber", + "1. A feature to order scenarios", + "1. A feature to order scenarios - 1.4", + "1. A feature to order scenarios - 1.4 - 1.4.1", + "1. A feature to order scenarios - 1.4 - 1.4.2", + "1. A feature to order scenarios - 1.1", + "1. A feature to order scenarios - 1.3 A rule", + "1. A feature to order scenarios - 1.3 A rule - 1.3.2", + "1. A feature to order scenarios - 1.3 A rule - 1.3.1", + "1. A feature to order scenarios - 1.2", + "1. A feature to order scenarios - 1.2 - 1.2.2", + "1. A feature to order scenarios - 1.2 - 1.2.2 - Example #2.1", + "1. A feature to order scenarios - 1.2 - 1.2.2 - Example #2.2", + "1. A feature to order scenarios - 1.2 - 1.2.1", + "1. A feature to order scenarios - 1.2 - 1.2.1 - Example #1.2", + "1. A feature to order scenarios - 1.2 - 1.2.1 - Example #1.1", + "A feature with a single scenario", + "A feature with a single scenario - A single scenario"); + } + + @Test + void reportsParsErrorsAsDiscoveryIssues() { + EngineDiscoveryResults results = EngineTestKit.engine(ENGINE_ID) + .selectors( + selectFile("src/test/bad-features/parse-error.feature")) + .discover(); + + DiscoveryIssue issue = results.getDiscoveryIssues().get(0); + + assertAll(() -> { + assertThat(issue.message()).startsWith("Failed to parse resource at: "); + assertThat(issue.source()) + .contains(FileSource.from(new File("src/test/bad-features/parse-error.feature"))); + }); + } + + @Test + void supportsExclusiveResources() { + PickleDescriptor pickleDescriptor = EngineTestKit.engine(ENGINE_ID) + .configurationParameter(EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + "ResourceA" + READ_WRITE_SUFFIX, + "resource-a") + .configurationParameter(EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + "ResourceAReadOnly" + READ_SUFFIX, + "resource-a") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/resource.feature")) + .discover() + .getEngineDescriptor() + .getDescendants() + .stream() + .filter(PickleDescriptor.class::isInstance) + .map(PickleDescriptor.class::cast) + .findAny() + .orElseThrow(); + + assertThat(pickleDescriptor.getExclusiveResources()) + .containsExactlyInAnyOrder( + new ExclusiveResource("resource-a", ExclusiveResource.LockMode.READ_WRITE), + new ExclusiveResource("resource-a", ExclusiveResource.LockMode.READ)); + + } + + @Test + void supportsConcurrentExecutionOfFeatureElements() { + Set> testDescriptors = EngineTestKit.engine(ENGINE_ID) + .configurationParameter(EXECUTION_MODE_FEATURE_PROPERTY_NAME, "concurrent") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/single.feature")) + .discover() + .getEngineDescriptor() + .getDescendants() + .stream() + .filter(Node.class::isInstance) + .map(testDescriptor -> (Node) testDescriptor) + .collect(toSet()); + + assertThat(testDescriptors) + .isNotEmpty() + .extracting(Node::getExecutionMode) + .containsOnly(CONCURRENT); + } + + @Test + void supportsSameThreadExecutionOfFeatureElements() { + Set testDescriptors = EngineTestKit.engine(ENGINE_ID) + .configurationParameter(EXECUTION_MODE_FEATURE_PROPERTY_NAME, "same_thread") + .selectors( + selectClasspathResource("io/cucumber/junit/platform/engine/single.feature")) + .discover() + .getEngineDescriptor() + .getDescendants(); + + Set featureDescriptors = testDescriptors + .stream() + .filter(FeatureDescriptor.class::isInstance) + .collect(toSet()); + + assertThat(featureDescriptors) + .isNotEmpty() + .map(Node.class::cast) + .extracting(Node::getExecutionMode) + .containsOnly(CONCURRENT); + + Set pickleDescriptors = testDescriptors + .stream() + .filter(testDescriptor -> !featureDescriptors.contains(testDescriptor)) + .collect(toSet()); + + assertThat(pickleDescriptors) + .isNotEmpty() + .map(Node.class::cast) + .extracting(Node::getExecutionMode) + .containsOnly(SAME_THREAD); + } } diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolverTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolverTest.java deleted file mode 100644 index 2a240907aa..0000000000 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolverTest.java +++ /dev/null @@ -1,507 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import io.cucumber.core.gherkin.Feature; -import io.cucumber.core.gherkin.Pickle; -import io.cucumber.core.logging.LogRecordListener; -import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor; -import io.cucumber.junit.platform.engine.nofeatures.NoFeatures; -import org.hamcrest.CustomTypeSafeMatcher; -import org.hamcrest.Matcher; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.DiscoveryFilter; -import org.junit.platform.engine.DiscoverySelector; -import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.discovery.DiscoverySelectors; -import org.junit.platform.engine.discovery.FilePosition; -import org.junit.platform.engine.discovery.UniqueIdSelector; - -import java.io.File; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; -import static java.util.Collections.singleton; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toSet; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathResource; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathRoots; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectDirectory; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectFile; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUri; - -@WithLogRecordListener -class DiscoverySelectorResolverTest { - - private final DiscoverySelectorResolver resolver = new DiscoverySelectorResolver(); - private CucumberEngineDescriptor testDescriptor; - - @BeforeEach - void before() { - UniqueId id = UniqueId.forEngine(new CucumberTestEngine().getId()); - testDescriptor = new CucumberEngineDescriptor(id); - assertEquals(0, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithClasspathResourceSelector() { - DiscoverySelector resource = selectClasspathResource("io/cucumber/junit/platform/engine/single.feature"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(1, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithClasspathResourceSelectorAndWithSpaceInFilename() { - DiscoverySelector resource = selectClasspathResource("io/cucumber/junit/platform/engine/with space.feature"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(1, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithClasspathResourceSelectorAndFilePosition() { - String feature = "io/cucumber/junit/platform/engine/rule.feature"; - FilePosition line = FilePosition.from(5); - DiscoverySelector resource = selectClasspathResource(feature, line); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(1L, testDescriptor.getDescendants() - .stream() - .filter(TestDescriptor::isTest) - .count()); - } - - @Test - void resolveRequestWithClasspathResourceSelectorAndFilePositionOfContainer() { - String feature = "io/cucumber/junit/platform/engine/rule.feature"; - FilePosition line = FilePosition.from(3); - DiscoverySelector resource = selectClasspathResource(feature, line); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(2L, testDescriptor.getDescendants() - .stream() - .filter(TestDescriptor::isTest) - .count()); - } - - @Test - void resolveRequestWithMultipleClasspathResourceSelector() { - DiscoverySelector resource1 = selectClasspathResource("io/cucumber/junit/platform/engine/single.feature"); - DiscoverySelector resource2 = selectClasspathResource( - "io/cucumber/junit/platform/engine/feature-with-outline.feature"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource1, resource2); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(2, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithClasspathRootSelector() { - Path classpathRoot = Paths.get("src/test/resources/"); - DiscoverySelector resource = selectClasspathRoots(singleton(classpathRoot)).get(0); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(7, testDescriptor.getChildren().size()); - } - - @Test - void resolveFeatureTestDescriptorsInUriOrder() { - Path classpathRoot = Paths.get("src/test/resources/"); - DiscoverySelector resource = selectClasspathRoots(singleton(classpathRoot)).get(0); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - Set features = testDescriptor.getChildren(); - List unsorted = new ArrayList<>(features); - List sorted = new ArrayList<>(features); - // Sorts by URI - sorted.sort(comparing(feature -> feature.getUniqueId().getSegments().get(1).getValue())); - assertEquals(unsorted, sorted); - } - - @Test - void resolveRequestWithUriSelectorWithScenarioOutlineLine() { - File file = new File("src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature"); - URI uri = URI.create(file.toURI() + "?line=11"); - DiscoverySelector resource = selectUri(uri); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - List tests = testDescriptor.getDescendants().stream() - .filter(TestDescriptor::isTest) - .collect(Collectors.toList()); - assertEquals(4, tests.size()); // 4 examples in the outline - } - - @Test - void resolveRequestWithUriSelectorWithExamplesSectionLine() { - File file = new File("src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature"); - URI uri = URI.create(file.toURI() + "?line=17"); - DiscoverySelector resource = selectUri(uri); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - List tests = testDescriptor.getDescendants().stream() - .filter(TestDescriptor::isTest) - .collect(Collectors.toList()); - assertEquals(2, tests.size()); // 2 examples in the examples section - } - - @Test - void resolveRequestWithUriSelectorWithExampleLine() { - File file = new File("src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature"); - URI uri1 = URI.create(file.toURI() + "?line=20"); - DiscoverySelector resource = selectUri(uri1); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - List tests = testDescriptor.getDescendants().stream() - .filter(TestDescriptor::isTest) - .collect(Collectors.toList()); - assertEquals(1, tests.size()); - } - - @Test - void resolveRequestWithClassPathUriSelectorWithLine() { - URI uri = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature?line=20"); - DiscoverySelector resource = selectUri(uri); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - List tests = testDescriptor.getDescendants().stream() - .filter(TestDescriptor::isTest) - .collect(Collectors.toList()); - assertEquals(1, tests.size()); - } - - @Test - void resolveRequestWithUriSelectorThroughProperty() { - URI uri = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature:19:20"); - ConfigurationParameters parameters = new MapConfigurationParameters(FEATURES_PROPERTY_NAME, uri.toString()); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(parameters); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - List tests = testDescriptor.getDescendants().stream() - .filter(TestDescriptor::isTest) - .collect(Collectors.toList()); - assertEquals(2, tests.size()); - } - - @Test - void resolveRequestWithUriSelectorThroughPropertyIgnoresOtherSelectors() { - URI uri1 = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature:19"); - ConfigurationParameters parameters = new MapConfigurationParameters(FEATURES_PROPERTY_NAME, uri1.toString()); - - URI uri2 = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature?line=20"); - DiscoverySelector resource = selectUri(uri2); - - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(parameters, resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - List tests = testDescriptor.getDescendants().stream() - .filter(TestDescriptor::isTest) - .collect(Collectors.toList()); - assertEquals(1, tests.size()); - } - - @Test - void resolveRequestWithFileSelector() { - DiscoverySelector resource = selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(1, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithFileSelectorAndPosition() { - String feature = "src/test/resources/io/cucumber/junit/platform/engine/rule.feature"; - FilePosition line = FilePosition.from(5); - DiscoverySelector resource = selectFile(feature, line); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(1L, testDescriptor.getDescendants() - .stream() - .filter(TestDescriptor::isTest) - .count()); - } - - @Test - void resolveRequestWithFileSelectorAndPositionOfContainer() { - String feature = "src/test/resources/io/cucumber/junit/platform/engine/rule.feature"; - FilePosition line = FilePosition.from(3); - DiscoverySelector resource = selectFile(feature, line); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(2L, testDescriptor.getDescendants() - .stream() - .filter(TestDescriptor::isTest) - .count()); - } - - @Test - void resolveRequestWithDirectorySelector() { - DiscoverySelector resource = selectDirectory("src/test/resources/io/cucumber/junit/platform/engine"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(6, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithPackageSelector() { - DiscoverySelector resource = selectPackage("io.cucumber.junit.platform.engine"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(6, testDescriptor.getChildren().size()); - } - - @Test - void ignoreRequestWithUniqueIdSelectorFromDifferentEngine() { - DiscoverySelector selector = selectUniqueId(UniqueId.forEngine("not-cucumber")); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(selector); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertTrue(testDescriptor.getDescendants().isEmpty()); - } - - @Test - void resolveRequestWithUniqueIdSelectorFromClasspath() { - DiscoverySelector resource = selectPackage("io.cucumber.junit.platform.engine"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - Set descendants = testDescriptor.getDescendants(); - - descendants.forEach(targetDescriptor -> { - resetTestDescriptor(); - resolveRequestWithUniqueIdSelector(targetDescriptor.getUniqueId()); - assertEquals(1, testDescriptor.getChildren().size()); - assertThat(testDescriptor, allDescriptorsPrefixedBy(targetDescriptor.getUniqueId())); - }); - } - - private void resetTestDescriptor() { - Set descendants = new HashSet<>(testDescriptor.getDescendants()); - descendants.forEach(o -> testDescriptor.removeChild(o)); - } - - private void resolveRequestWithUniqueIdSelector(UniqueId targetId) { - UniqueIdSelector uniqueIdSelector = selectUniqueId(targetId); - EngineDiscoveryRequest descendantRequest = new SelectorRequest(uniqueIdSelector); - resolver.resolveSelectors(descendantRequest, testDescriptor); - } - - private static Matcher allDescriptorsPrefixedBy(UniqueId targetId) { - return new CustomTypeSafeMatcher("All descendants are prefixed by " + targetId) { - @Override - protected boolean matchesSafely(TestDescriptor descriptor) { - return descriptor.getDescendants() - .stream() - .filter(TestDescriptor::isTest) - .map(TestDescriptor::getUniqueId) - .allMatch(selectedId -> selectedId.hasPrefix(targetId)); - } - }; - } - - @Test - void resolveRequestWithUniqueIdSelectorFromFileUri() { - DiscoverySelector resource = selectDirectory("src/test/resources/io/cucumber/junit/platform/engine"); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - Set descendants = testDescriptor.getDescendants(); - - descendants.forEach(targetDescriptor -> { - resetTestDescriptor(); - resolveRequestWithUniqueIdSelector(targetDescriptor.getUniqueId()); - assertEquals(1, testDescriptor.getChildren().size()); - assertThat(testDescriptor, allDescriptorsPrefixedBy(targetDescriptor.getUniqueId())); - }); - } - - @Test - void resolveRequestWithUniqueIdSelectorFromJarFileUri() { - URI uri = new File("src/test/resources/feature.jar").toURI(); - DiscoverySelector resource = selectUri(uri); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - assertEquals(1, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithUniqueIdSelectorFromJarUri() { - String root = Paths.get("").toAbsolutePath().toUri().getSchemeSpecificPart(); - URI uri = URI.create("jar:file:" + root + "/src/test/resources/feature.jar!/single.feature"); - - DiscoverySelector resource = selectUri(uri); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - assertEquals(1, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithMultipleUniqueIdSelector() { - Set selectors = new HashSet<>(); - - DiscoverySelector resource = selectDirectory( - "src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature"); - selectSomePickle(resource).ifPresent(selectors::add); - - DiscoverySelector resource2 = selectDirectory( - "src/test/resources/io/cucumber/junit/platform/engine/single.feature"); - selectSomePickle(resource2).ifPresent(selectors::add); - - EngineDiscoveryRequest discoveryRequest = new SelectorRequest( - selectors.stream() - .map(DiscoverySelectors::selectUniqueId) - .collect(Collectors.toList())); - - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - assertEquals( - selectors, - testDescriptor.getDescendants() - .stream() - .filter(PickleDescriptor.class::isInstance) - .map(TestDescriptor::getUniqueId) - .collect(toSet())); - } - - @Test - void resolveRequestWithMultipleUniqueIdSelectorFromTheSameFeature() { - Set selectors = new HashSet<>(); - - DiscoverySelector resource = selectDirectory( - "src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature"); - selectAllPickles(resource).forEach(selectors::add); - - EngineDiscoveryRequest discoveryRequest = new SelectorRequest( - selectors.stream() - .map(DiscoverySelectors::selectUniqueId) - .collect(Collectors.toList())); - - resolver.resolveSelectors(discoveryRequest, testDescriptor); - - Set pickleIdsFromFeature = testDescriptor.getDescendants() - .stream() - .filter(FeatureDescriptor.class::isInstance) - .map(FeatureDescriptor.class::cast) - .map(FeatureDescriptor::getFeature) - .map(Feature::getPickles) - .flatMap(Collection::stream) - .map(Pickle::getId) - .collect(toSet()); - - Set pickleIdsFromPickles = testDescriptor.getDescendants() - .stream() - .filter(PickleDescriptor.class::isInstance) - .map(PickleDescriptor.class::cast) - .map(PickleDescriptor::getPickle) - .map(Pickle::getId) - .collect(toSet()); - - assertEquals(pickleIdsFromFeature, pickleIdsFromPickles); - } - - private Optional selectSomePickle(DiscoverySelector resource) { - return selectAllPickles(resource).findFirst(); - } - - private Stream selectAllPickles(DiscoverySelector resource) { - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - Set descendants = testDescriptor.getDescendants(); - resetTestDescriptor(); - return descendants.stream() - .filter(PickleDescriptor.class::isInstance) - .map(TestDescriptor::getUniqueId); - } - - @Test - void resolveRequestWithClassSelector() { - DiscoverySelector resource = selectClass(RunCucumberTest.class); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(6, testDescriptor.getChildren().size()); - } - - @Test - void resolveRequestWithClassSelectorShouldLogWarnIfNoFeaturesFound(LogRecordListener logRecordListener) { - DiscoverySelector resource = selectClass(NoFeatures.class); - EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource); - resolver.resolveSelectors(discoveryRequest, testDescriptor); - assertEquals(0, testDescriptor.getChildren().size()); - assertEquals(1, logRecordListener.getLogRecords().size()); - LogRecord logRecord = logRecordListener.getLogRecords().get(0); - assertEquals(Level.WARNING, logRecord.getLevel()); - assertEquals("No features found in package 'io.cucumber.junit.platform.engine.nofeatures'", - logRecord.getMessage()); - } - - private static class SelectorRequest implements EngineDiscoveryRequest { - - private final Map, List> resources = new HashMap<>(); - private final ConfigurationParameters parameters; - - SelectorRequest(ConfigurationParameters parameters, DiscoverySelector... selectors) { - this(parameters, Arrays.asList(selectors)); - } - - SelectorRequest(DiscoverySelector... selectors) { - this(new EmptyConfigurationParameters(), Arrays.asList(selectors)); - } - - SelectorRequest(List selectors) { - this(new EmptyConfigurationParameters(), selectors); - } - - SelectorRequest(ConfigurationParameters parameters, List selectors) { - this.parameters = parameters; - for (DiscoverySelector discoverySelector : selectors) { - resources.putIfAbsent(discoverySelector.getClass(), new ArrayList<>()); - resources.get(discoverySelector.getClass()).add(discoverySelector); - } - } - - @SuppressWarnings("unchecked") - @Override - public List getSelectorsByType(Class selectorType) { - if (resources.containsKey(selectorType)) { - return (List) resources.get(selectorType); - } - - return Collections.emptyList(); - } - - @Override - public > List getFiltersByType(Class filterType) { - return Collections.emptyList(); - } - - @Override - public ConfigurationParameters getConfigurationParameters() { - return parameters; - } - - } - -} diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/EmptyEngineDiscoveryRequest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/EmptyEngineDiscoveryRequest.java deleted file mode 100644 index b9bee9d1b5..0000000000 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/EmptyEngineDiscoveryRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.DiscoveryFilter; -import org.junit.platform.engine.DiscoverySelector; -import org.junit.platform.engine.EngineDiscoveryRequest; - -import java.util.Collections; -import java.util.List; - -class EmptyEngineDiscoveryRequest implements EngineDiscoveryRequest { - - private final ConfigurationParameters config; - - EmptyEngineDiscoveryRequest(ConfigurationParameters config) { - this.config = config; - } - - @Override - public List getSelectorsByType(Class selectorType) { - return Collections.emptyList(); - } - - @Override - public > List getFiltersByType(Class filterType) { - return Collections.emptyList(); - } - - @Override - public ConfigurationParameters getConfigurationParameters() { - return config; - } - -} diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/EmptyEngineExecutionListener.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/EmptyEngineExecutionListener.java deleted file mode 100644 index fb998cb918..0000000000 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/EmptyEngineExecutionListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.reporting.ReportEntry; - -class EmptyEngineExecutionListener implements EngineExecutionListener { - - @Override - public void dynamicTestRegistered(TestDescriptor testDescriptor) { - - } - - @Override - public void executionSkipped(TestDescriptor testDescriptor, String reason) { - - } - - @Override - public void executionStarted(TestDescriptor testDescriptor) { - - } - - @Override - public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { - - } - - @Override - public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { - - } - -} diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/FeatureResolverTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/FeatureResolverTest.java deleted file mode 100644 index 14c018b2c2..0000000000 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/FeatureResolverTest.java +++ /dev/null @@ -1,293 +0,0 @@ -package io.cucumber.junit.platform.engine; - -import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor; -import org.junit.jupiter.api.Test; -import org.junit.platform.engine.ConfigurationParameters; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.hierarchical.ExclusiveResource; -import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode; -import org.junit.platform.engine.support.hierarchical.Node; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME_PREFIX; -import static io.cucumber.junit.platform.engine.Constants.EXECUTION_EXCLUSIVE_RESOURCES_PREFIX; -import static io.cucumber.junit.platform.engine.Constants.EXECUTION_MODE_FEATURE_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.JUNIT_PLATFORM_LONG_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.READ_SUFFIX; -import static io.cucumber.junit.platform.engine.Constants.READ_WRITE_SUFFIX; -import static java.util.Arrays.asList; -import static java.util.Collections.emptySet; -import static java.util.Optional.of; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.platform.engine.TestDescriptor.Type.CONTAINER; -import static org.junit.platform.engine.TestDescriptor.Type.TEST; -import static org.junit.platform.engine.TestTag.create; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathResource; -import static org.junit.platform.engine.support.descriptor.ClasspathResourceSource.from; -import static org.junit.platform.engine.support.descriptor.FilePosition.from; - -class FeatureResolverTest { - - private final String featurePath = "io/cucumber/junit/platform/engine/feature-with-outline.feature"; - private final String featureSegmentValue = CLASSPATH_SCHEME_PREFIX + featurePath; - private final UniqueId id = UniqueId.forEngine(new CucumberTestEngine().getId()); - private final CucumberEngineDescriptor engineDescriptor = new CucumberEngineDescriptor(id); - private ConfigurationParameters configurationParameters = new EmptyConfigurationParameters(); - - @Test - void feature() { - TestDescriptor feature = getFeature(); - assertEquals("A feature with scenario outlines", feature.getDisplayName()); - assertEquals(emptySet(), feature.getTags()); - assertEquals(of(from(featurePath)), feature.getSource()); - assertEquals(CONTAINER, feature.getType()); - assertEquals( - id.append("feature", featureSegmentValue), - feature.getUniqueId()); - } - - private TestDescriptor getFeature() { - FeatureResolver featureResolver = FeatureResolver.create(configurationParameters, engineDescriptor, - aPackage -> true); - featureResolver.resolveClasspathResource(selectClasspathResource(featurePath)); - Set features = engineDescriptor.getChildren(); - return features.iterator().next(); - } - - @Test - void scenario() { - TestDescriptor scenario = getScenario(); - assertEquals("A scenario", scenario.getDisplayName()); - assertEquals( - asSet(create("FeatureTag"), create("ScenarioTag"), create("ResourceA"), create("ResourceAReadOnly")), - scenario.getTags()); - assertEquals(of(from(featurePath, from(5, 3))), scenario.getSource()); - assertEquals(TEST, scenario.getType()); - assertEquals( - id.append("feature", featureSegmentValue) - .append("scenario", "5"), - scenario.getUniqueId()); - PickleDescriptor pickleDescriptor = (PickleDescriptor) scenario; - assertEquals(Optional.of("io.cucumber.junit.platform.engine"), pickleDescriptor.getPackage()); - } - - @Test - void exclusiveResources() { - configurationParameters = new MapConfigurationParameters( - new HashMap() { - { - put(EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + "ResourceA" + READ_WRITE_SUFFIX, "resource-a"); - put(EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + "ResourceAReadOnly" + READ_SUFFIX, "resource-a"); - } - }); - - PickleDescriptor pickleDescriptor = (PickleDescriptor) getScenario(); - assertEquals( - asSet( - new ExclusiveResource("resource-a", LockMode.READ_WRITE), - new ExclusiveResource("resource-a", LockMode.READ)), - pickleDescriptor.getExclusiveResources()); - } - - private TestDescriptor getScenario() { - return getFeature().getChildren().iterator().next(); - } - - @SafeVarargs - private static Set asSet(T... tags) { - return new HashSet<>(asList(tags)); - } - - @Test - void outline() { - TestDescriptor outline = getOutline(); - assertEquals("A scenario outline", outline.getDisplayName()); - assertEquals( - emptySet(), - outline.getTags()); - assertEquals(of(from(featurePath, from(11, 3))), outline.getSource()); - assertEquals(CONTAINER, outline.getType()); - assertEquals( - id.append("feature", featureSegmentValue) - .append("scenario", "11"), - outline.getUniqueId()); - } - - private TestDescriptor getOutline() { - Iterator iterator = getFeature().getChildren().iterator(); - iterator.next(); - return iterator.next(); - } - - private TestDescriptor getParameterizedOutline() { - Iterator iterator = getFeature().getChildren().iterator(); - iterator.next(); - iterator.next(); - iterator.next(); - return iterator.next(); - } - - @Test - void example() { - TestDescriptor example = getExample(); - assertEquals("Example #1.1", example.getDisplayName()); - assertEquals( - asSet(create("FeatureTag"), create("Example1Tag"), create("ScenarioOutlineTag")), - example.getTags()); - assertEquals(of(from(featurePath, from(19, 7))), example.getSource()); - assertEquals(TEST, example.getType()); - - assertEquals( - id.append("feature", featureSegmentValue) - .append("scenario", "11") - .append("examples", "17") - .append("example", "19"), - example.getUniqueId()); - - PickleDescriptor pickleDescriptor = (PickleDescriptor) example; - assertEquals(Optional.of("io.cucumber.junit.platform.engine"), pickleDescriptor.getPackage()); - } - - @Test - void longNames() { - configurationParameters = new MapConfigurationParameters(Map.of( - JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long", - JUNIT_PLATFORM_LONG_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "number")); - - TestDescriptor example = getExample(); - assertEquals("A feature with scenario outlines - A scenario outline - With some text - Example #1.1", - example.getDisplayName()); - } - - @Test - void longNamesWithPickleNames() { - configurationParameters = new MapConfigurationParameters(Map.of( - JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long", - JUNIT_PLATFORM_LONG_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "pickle")); - - TestDescriptor example = getExample(); - assertEquals("A feature with scenario outlines - A scenario outline - With some text - A scenario outline", - example.getDisplayName()); - } - - @Test - void longNamesWithPickleNamesIfParameterized() { - configurationParameters = new MapConfigurationParameters( - JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "long"); - - TestDescriptor example = getParametrizedExample(); - assertEquals( - "A feature with scenario outlines - A scenario with - Examples - Example #1.1: A scenario with A", - example.getDisplayName()); - } - - @Test - void shortNamesWithExampleNumbers() { - configurationParameters = new MapConfigurationParameters( - JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "number"); - - TestDescriptor example = getExample(); - assertEquals("Example #1.1", example.getDisplayName()); - } - - @Test - void shortNamesWithPickleNames() { - configurationParameters = new MapConfigurationParameters(Map.of( - JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "short", - JUNIT_PLATFORM_SHORT_NAMING_STRATEGY_EXAMPLE_NAME_PROPERTY_NAME, "pickle")); - - TestDescriptor example = getExample(); - assertEquals("A scenario outline", example.getDisplayName()); - } - - @Test - void shortNamesWithPickleNamesIfParameterized() { - configurationParameters = new MapConfigurationParameters( - JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "short"); - - TestDescriptor example = getParametrizedExample(); - assertEquals("Example #1.1: A scenario with A", example.getDisplayName()); - } - - @Test - void surefireNamesWithPickleNamesIfParameterized() { - configurationParameters = new MapConfigurationParameters( - JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, "surefire"); - - // The test node gets the long name, without the feature name. - TestDescriptor example = getParametrizedExample(); - assertEquals("A scenario with - Examples - Example #1.1: A scenario with A", - example.getDisplayName()); - - // The parent node gets the feature name. - TestDescriptor examples = example.getParent().get(); - assertEquals("A feature with scenario outlines", - examples.getDisplayName()); - - // Remaining nodes are named by their short names. - TestDescriptor scenarioOutline = examples.getParent().get(); - assertEquals("A scenario with ", - scenarioOutline.getDisplayName()); - - TestDescriptor feature = scenarioOutline.getParent().get(); - assertEquals("A feature with scenario outlines", - feature.getDisplayName()); - } - - private TestDescriptor getExample() { - return getOutline().getChildren().iterator().next().getChildren().iterator().next(); - } - - private TestDescriptor getParametrizedExample() { - return getParameterizedOutline().getChildren().iterator().next().getChildren().iterator().next(); - } - - @Test - void parallelExecutionForFeaturesEnabled() { - configurationParameters = new MapConfigurationParameters( - EXECUTION_MODE_FEATURE_PROPERTY_NAME, "concurrent"); - - assertTrue(getNodes().size() > 0); - assertTrue(getPickles().size() > 0); - getNodes().forEach(node -> assertEquals(Node.ExecutionMode.CONCURRENT, node.getExecutionMode())); - getPickles().forEach(pickle -> assertEquals(Node.ExecutionMode.CONCURRENT, pickle.getExecutionMode())); - } - - @Test - void parallelExecutionForFeaturesDisabled() { - configurationParameters = new MapConfigurationParameters( - EXECUTION_MODE_FEATURE_PROPERTY_NAME, "same_thread"); - - assertTrue(getNodes().size() > 0); - assertTrue(getPickles().size() > 0); - getNodes().forEach(node -> assertEquals(Node.ExecutionMode.SAME_THREAD, node.getExecutionMode())); - getPickles().forEach(pickle -> assertEquals(Node.ExecutionMode.SAME_THREAD, pickle.getExecutionMode())); - } - - private Set getNodes() { - return getFeature().getChildren().stream() - .filter(TestDescriptor::isContainer) - .map(node -> (NodeDescriptor) node) - .collect(Collectors.toSet()); - } - - private Set getPickles() { - return getFeature().getChildren().stream() - .filter(TestDescriptor::isContainer) - .flatMap(examplesNode -> examplesNode.getChildren().stream()) - .flatMap(exampleNode -> exampleNode.getChildren().stream()) - .map(example -> (PickleDescriptor) example) - .collect(Collectors.toSet()); - } -} diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java index 55fe8f90b2..57348a6f46 100644 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java @@ -45,7 +45,7 @@ public void loadGlue(Glue glue, List gluePaths) { glue.addStepDefinition(createStepDefinition("B is used")); glue.addStepDefinition(createStepDefinition("C is used")); glue.addStepDefinition(createStepDefinition("D is used")); - + glue.addStepDefinition(createStepDefinition("a parameterized scenario outline")); } private StepDefinition createStepDefinition(final String pattern) { diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/nofeatures/NoFeatures.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/nofeatures/NoFeatures.java deleted file mode 100644 index f42bb9b00e..0000000000 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/nofeatures/NoFeatures.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.cucumber.junit.platform.engine.nofeatures; - -import io.cucumber.junit.platform.engine.Cucumber; - -@Cucumber -public class NoFeatures { - -} diff --git a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/ordering.feature b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/ordering.feature new file mode 100644 index 0000000000..05fda788f2 --- /dev/null +++ b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/ordering.feature @@ -0,0 +1,37 @@ +Feature: 1. A feature to order scenarios + + Scenario: 1.1 + Given a single scenario + + Scenario Outline: 1.2 + Given a single scenario + + Examples: 1.2.1 + + | key | + | a | + | b | + + Examples: 1.2.2 + + | key | + | c | + | d | + + Rule: 1.3 A rule + + Example: 1.3.1 + Given a single scenario + + Example: 1.3.2 + Given a single scenario + + Rule: 1.4 + + Example: 1.4.1 + Given a single scenario + + Example: 1.4.2 + Given a single scenario + + diff --git a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature new file mode 100644 index 0000000000..6d23dd45d2 --- /dev/null +++ b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/parameterized-scenario-outline.feature @@ -0,0 +1,10 @@ +Feature: A feature with a parameterized scenario outline + + Scenario Outline: A scenario full of s + Given a scenario outline + + @Example1Tag + Examples: Of the Gherkin variety + | vegetable | + | Cucumber | + | Zucchini | diff --git a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/resource.feature b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/resource.feature new file mode 100644 index 0000000000..e0e2429a3a --- /dev/null +++ b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/resource.feature @@ -0,0 +1,7 @@ +Feature: A feature with a single scenario + + @ResourceA @ResourceAReadOnly + Scenario: A single scenario + Given a single scenario + When it is executed + Then is only runs once diff --git a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature similarity index 96% rename from cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature rename to cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature index 46da5441de..b9c779a886 100644 --- a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature +++ b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/scenario-outline.feature @@ -1,7 +1,7 @@ @FeatureTag Feature: A feature with scenario outlines - @ScenarioTag @ResourceA @ResourceAReadOnly + @ScenarioTag Scenario: A scenario Given a scenario When it is executed diff --git a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/single.feature b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/single.feature index 23641dfe27..20236c9130 100644 --- a/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/single.feature +++ b/cucumber-junit-platform-engine/src/test/resources/io/cucumber/junit/platform/engine/single.feature @@ -3,4 +3,4 @@ Feature: A feature with a single scenario Scenario: A single scenario Given a single scenario When it is executed - Then nothing else happens + Then is only runs once diff --git a/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Location.java b/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Location.java index d046ea10c2..b85a6e5db2 100644 --- a/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Location.java +++ b/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Location.java @@ -5,7 +5,7 @@ import java.util.Objects; @API(status = API.Status.EXPERIMENTAL) -public final class Location { +public final class Location implements Comparable { private final int line; private final int column; @@ -39,4 +39,13 @@ public boolean equals(Object o) { column == location.column; } + @Override + public int compareTo(Location o) { + Objects.requireNonNull(o); + int c = Integer.compare(line, o.line); + if (c != 0) { + return c; + } + return Integer.compare(column, o.column); + } } diff --git a/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Node.java b/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Node.java index 671f8ec809..79d3a44a06 100644 --- a/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Node.java +++ b/cucumber-plugin/src/main/java/io/cucumber/plugin/event/Node.java @@ -2,6 +2,7 @@ import org.apiguardian.api.API; +import java.net.URI; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -35,6 +36,10 @@ @API(status = API.Status.EXPERIMENTAL) public interface Node { + default URI getUri() { + throw new UnsupportedOperationException("Not yet implemented"); + }; + Location getLocation(); Optional getKeyword(); diff --git a/cucumber-plugin/src/test/java/io/cucumber/plugin/event/NodeTest.java b/cucumber-plugin/src/test/java/io/cucumber/plugin/event/NodeTest.java index 0c3053f315..b84298de06 100644 --- a/cucumber-plugin/src/test/java/io/cucumber/plugin/event/NodeTest.java +++ b/cucumber-plugin/src/test/java/io/cucumber/plugin/event/NodeTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import java.net.URI; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -13,6 +14,11 @@ class NodeTest { private final Node.Example example1 = new Node.Example() { + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -40,6 +46,11 @@ public Optional getParent() { }; private final Node.Example example2 = new Node.Example() { + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -66,6 +77,11 @@ public Optional getParent() { } }; private final Node.Example example3 = new Node.Example() { + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -93,6 +109,11 @@ public Optional getParent() { }; private final Node.Example example4 = new Node.Example() { + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -125,6 +146,11 @@ public Collection elements() { return asList(example1, example2); } + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -157,6 +183,11 @@ public Collection elements() { return asList(example3, example4); } + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -189,6 +220,11 @@ public Collection elements() { return Collections.emptyList(); } + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -221,6 +257,11 @@ public Collection elements() { return Collections.emptyList(); } + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -253,6 +294,11 @@ public Collection elements() { return asList(examplesA, examplesB); } + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; @@ -285,6 +331,11 @@ public Collection elements() { return asList(emptyExamplesA, emptyExamplesB); } + @Override + public URI getUri() { + return null; + } + @Override public Location getLocation() { return null; diff --git a/examples/calculator-java-junit5/pom.xml b/examples/calculator-java-junit5/pom.xml index ded19fa8a1..bec34265dc 100644 --- a/examples/calculator-java-junit5/pom.xml +++ b/examples/calculator-java-junit5/pom.xml @@ -28,7 +28,7 @@ org.junit junit-bom - 5.12.2 + 5.13.1 pom import diff --git a/examples/calculator-kotlin-junit5/pom.xml b/examples/calculator-kotlin-junit5/pom.xml index 8f971fc7fd..2270e218ca 100644 --- a/examples/calculator-kotlin-junit5/pom.xml +++ b/examples/calculator-kotlin-junit5/pom.xml @@ -31,7 +31,7 @@ org.junit junit-bom - 5.12.2 + 5.13.1 pom import diff --git a/examples/spring-java-junit5/pom.xml b/examples/spring-java-junit5/pom.xml index 57af102446..603f10fbd7 100644 --- a/examples/spring-java-junit5/pom.xml +++ b/examples/spring-java-junit5/pom.xml @@ -12,8 +12,8 @@ io.cucumber.examples.spring.application - 3.2.0 - 5.10.1 + 3.5.0 + 5.13.1 diff --git a/examples/wicket-java-junit4/wicket-main/pom.xml b/examples/wicket-java-junit4/wicket-main/pom.xml index d9a5ba27f4..184b957acd 100644 --- a/examples/wicket-java-junit4/wicket-main/pom.xml +++ b/examples/wicket-java-junit4/wicket-main/pom.xml @@ -14,7 +14,7 @@ 9.4.0 9.4.43.v20210629 2.0.5 - 5.10.1 + 5.13.1 1.4.7 diff --git a/examples/wicket-java-junit4/wicket-test/pom.xml b/examples/wicket-java-junit4/wicket-test/pom.xml index 3fee71e19e..185251a5f5 100644 --- a/examples/wicket-java-junit4/wicket-test/pom.xml +++ b/examples/wicket-java-junit4/wicket-test/pom.xml @@ -10,7 +10,7 @@ io.cucumber.examples.wicket.test - 5.10.1 + 5.13.1 4.13.0