Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JUnit Platform] Enable parallel execution of features #2604

Merged
merged 11 commits into from
Sep 8, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions cucumber-junit-platform-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##

Expand Down Expand Up @@ -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
mpkorstanje marked this conversation as resolved.
Show resolved Hide resolved
# available thread

cucumber.execution.parallel.enabled= # true or false.
# default: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
* <p>
* Valid values are {@code same_thread} or {@code concurrent}. Default value
* is {@code concurrent}.
* <p>
* 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}
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -103,15 +108,17 @@ 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));
parent.addChild(descriptor);
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CucumberEngineExecutionContext> {

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<TestTag> tags;
private final Set<ExclusiveResource> 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<TestTag> 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<SkipResult> 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<SkipResult> 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<ExclusiveResource> getExclusiveResources() {
return exclusiveResources;
}

/**
* Returns the set of {@linkplain TestTag tags} for a pickle.
* <p>
* 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<TestTag> getTags() {
return tags;
}

Optional<String> 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<String> exclusiveReadWriteResource() {
return parameters.get(READ_WRITE_SUFFIX, s -> Arrays.stream(s.split(","))
.map(String::trim))
.orElse(Stream.empty());
}

public Stream<String> exclusiveReadResource() {
return parameters.get(READ_SUFFIX, s -> Arrays.stream(s.split(","))
.map(String::trim))
.orElse(Stream.empty());
}

}

}

}
Loading