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()); + } }