diff --git a/CHANGELOG.md b/CHANGELOG.md
index 310b9cabda..7cd71f04cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ 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] Enable parallel execution of features ([#2604](https://github.com/cucumber/cucumber-jvm/pull/2604) Sambathkumar Sekar)
## [7.6.0] - 2022-08-08
### Changed
diff --git a/cucumber-junit-platform-engine/README.md b/cucumber-junit-platform-engine/README.md
index 4feeec8804..70656d29e2 100644
--- a/cucumber-junit-platform-engine/README.md
+++ b/cucumber-junit-platform-engine/README.md
@@ -269,6 +269,12 @@ with this configuration:
```properties
cucumber.execution.exclusive-resources.isolated.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY
```
+### Executing features in parallel
+
+By default, when parallel execution in enabled, scenarios and examples are
+executed in parallel. Due to limitations JUnit 4 could only execute features in
+parallel. This behaviour can be restored by setting the configuration parameter
+`cucumber.execution.execution-mode.feature` to `same_thread`.
## Configuration Options ##
@@ -330,6 +336,13 @@ cucumber.snippet-type= # underscore or ca
cucumber.execution.dry-run= # true or false.
# default: false
+cucumber.execution.execution-mode.feature= # same_thread or concurrent
+ # default: concurrent
+ # same_thread - executes scenarios sequentially in the
+ # same thread as the parent feature
+ # conncurrent - executes scenarios concurrently on any
+ # available thread
+
cucumber.execution.parallel.enabled= # true or false.
# default: false
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 5b3dd1db84..233d6909a5 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
@@ -182,6 +182,21 @@ public final class Constants {
*/
public static final String SNIPPET_TYPE_PROPERTY_NAME = io.cucumber.core.options.Constants.SNIPPET_TYPE_PROPERTY_NAME;
+ /**
+ * Property name used to set the executing thread for all scenarios and
+ * examples in a feature: {@value}
+ *
+ * Valid values are {@code same_thread} or {@code concurrent}. Default value
+ * is {@code concurrent}.
+ *
+ * When parallel execution is enabled, scenarios are executed in parallel on
+ * any available thread. setting this property to {@code same_thread}
+ * executes scenarios sequentially in the same thread as the parent feature.
+ *
+ * @see #PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME
+ */
+ public static final String EXECUTION_MODE_FEATURE_PROPERTY_NAME = "cucumber.execution.execution-mode.feature";
+
/**
* Property name used to enable parallel test execution: {@value}
*
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 d55b62db7a..9ecc186237 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
@@ -3,6 +3,7 @@
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 org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.Filter;
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 eccc83f581..991616c3ea 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
@@ -9,6 +9,10 @@
import io.cucumber.core.logging.LoggerFactory;
import io.cucumber.core.resource.ClassLoaders;
import io.cucumber.core.resource.ResourceScanner;
+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.plugin.event.Node;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.TestDescriptor;
@@ -85,7 +89,8 @@ private FeatureDescriptor createFeatureDescriptor(Feature feature) {
source.featureSource(),
feature),
(Node.Rule node, TestDescriptor parent) -> {
- TestDescriptor descriptor = new NodeDescriptor(
+ TestDescriptor descriptor = new RuleDescriptor(
+ parameters,
source.ruleSegment(parent.getUniqueId(), node),
namingStrategy.name(node),
source.nodeSource(node));
@@ -103,7 +108,8 @@ private FeatureDescriptor createFeatureDescriptor(Feature feature) {
return descriptor;
},
(Node.ScenarioOutline node, TestDescriptor parent) -> {
- TestDescriptor descriptor = new NodeDescriptor(
+ TestDescriptor descriptor = new ScenarioOutlineDescriptor(
+ parameters,
source.scenarioSegment(parent.getUniqueId(), node),
namingStrategy.name(node),
source.nodeSource(node));
@@ -111,7 +117,8 @@ private FeatureDescriptor createFeatureDescriptor(Feature feature) {
return descriptor;
},
(Node.Examples node, TestDescriptor parent) -> {
- NodeDescriptor descriptor = new NodeDescriptor(
+ NodeDescriptor descriptor = new ExamplesDescriptor(
+ parameters,
source.examplesSegment(parent.getUniqueId(), node),
namingStrategy.name(node),
source.nodeSource(node));
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
index 9746859f7f..9d18e26ca0 100644
--- 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
@@ -1,18 +1,221 @@
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;
-class NodeDescriptor extends AbstractTestDescriptor {
+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;
- NodeDescriptor(UniqueId uniqueId, String name, TestSource source) {
+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 Type getType() {
- return Type.CONTAINER;
+ 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 pickleEvent;
+ private final Set tags;
+ private final Set exclusiveResources = new LinkedHashSet<>(0);
+
+ PickleDescriptor(
+ ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source,
+ Pickle pickleEvent
+ ) {
+ super(parameters, uniqueId, name, source);
+ this.pickleEvent = pickleEvent;
+ this.tags = getTags(pickleEvent);
+ 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);
+ });
+ }
+
+ 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(pickleEvent.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(pickleEvent.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(pickleEvent);
+ 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/PickleDescriptor.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/PickleDescriptor.java
deleted file mode 100644
index 16c9f2aaca..0000000000
--- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/PickleDescriptor.java
+++ /dev/null
@@ -1,155 +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.Node;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-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.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;
-import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
-
-class PickleDescriptor extends AbstractTestDescriptor implements Node {
-
- private final Pickle pickleEvent;
- private final Set tags;
- private final Set exclusiveResources = new LinkedHashSet<>(0);
-
- PickleDescriptor(
- ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source, Pickle pickleEvent
- ) {
- super(uniqueId, name, source);
- this.pickleEvent = pickleEvent;
- this.tags = getTags(pickleEvent);
- 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);
- });
- }
-
- 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(pickleEvent.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(pickleEvent.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(pickleEvent);
- 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/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/CucumberEngineOptionsTest.java
index e1670eae86..3f1bdaf915 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/CucumberEngineOptionsTest.java
@@ -4,6 +4,7 @@
import io.cucumber.core.snippets.SnippetType;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.ConfigurationParameters;
+import org.junit.platform.engine.support.hierarchical.Node;
import java.net.URI;
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
index a6b4d15691..07f91e0825 100644
--- 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
@@ -1,6 +1,7 @@
package io.cucumber.junit.platform.engine;
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;
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
index 6452d31130..a584bf65cb 100644
--- 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
@@ -1,20 +1,24 @@
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.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_NAMING_STRATEGY_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.READ_SUFFIX;
import static io.cucumber.junit.platform.engine.Constants.READ_WRITE_SUFFIX;
@@ -22,6 +26,7 @@
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;
@@ -157,4 +162,41 @@ private TestDescriptor getExample() {
return getOutline().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());
+ }
}