Skip to content

Commit 627ffa1

Browse files
[JUnit Platform] Enable parallel execution of features (#2604)
Enables the JUnit 4 behaviour where all scenarios in a feature are executed on the same thread Co-authored-by: M.P. Korstanje <[email protected]>
1 parent c22dce5 commit 627ffa1

File tree

10 files changed

+292
-162
lines changed

10 files changed

+292
-162
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1010
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111

1212
## [Unreleased]
13+
### Added
14+
- [JUnit Platform] Enable parallel execution of features ([#2604](https://github.com/cucumber/cucumber-jvm/pull/2604) Sambathkumar Sekar)
1315

1416
## [7.6.0] - 2022-08-08
1517
### Changed

cucumber-junit-platform-engine/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ with this configuration:
269269
```properties
270270
cucumber.execution.exclusive-resources.isolated.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY
271271
```
272+
### Executing features in parallel
273+
274+
By default, when parallel execution in enabled, scenarios and examples are
275+
executed in parallel. Due to limitations JUnit 4 could only execute features in
276+
parallel. This behaviour can be restored by setting the configuration parameter
277+
`cucumber.execution.execution-mode.feature` to `same_thread`.
272278

273279
## Configuration Options ##
274280

@@ -330,6 +336,13 @@ cucumber.snippet-type= # underscore or ca
330336
cucumber.execution.dry-run= # true or false.
331337
# default: false
332338
339+
cucumber.execution.execution-mode.feature= # same_thread or concurrent
340+
# default: concurrent
341+
# same_thread - executes scenarios sequentially in the
342+
# same thread as the parent feature
343+
# conncurrent - executes scenarios concurrently on any
344+
# available thread
345+
333346
cucumber.execution.parallel.enabled= # true or false.
334347
# default: false
335348

cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,21 @@ public final class Constants {
182182
*/
183183
public static final String SNIPPET_TYPE_PROPERTY_NAME = io.cucumber.core.options.Constants.SNIPPET_TYPE_PROPERTY_NAME;
184184

185+
/**
186+
* Property name used to set the executing thread for all scenarios and
187+
* examples in a feature: {@value}
188+
* <p>
189+
* Valid values are {@code same_thread} or {@code concurrent}. Default value
190+
* is {@code concurrent}.
191+
* <p>
192+
* When parallel execution is enabled, scenarios are executed in parallel on
193+
* any available thread. setting this property to {@code same_thread}
194+
* executes scenarios sequentially in the same thread as the parent feature.
195+
*
196+
* @see #PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME
197+
*/
198+
public static final String EXECUTION_MODE_FEATURE_PROPERTY_NAME = "cucumber.execution.execution-mode.feature";
199+
185200
/**
186201
* Property name used to enable parallel test execution: {@value}
187202
* <p>

cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolver.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.cucumber.core.feature.FeatureWithLines;
44
import io.cucumber.core.logging.Logger;
55
import io.cucumber.core.logging.LoggerFactory;
6+
import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor;
67
import org.junit.platform.engine.ConfigurationParameters;
78
import org.junit.platform.engine.EngineDiscoveryRequest;
89
import org.junit.platform.engine.Filter;

cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/FeatureResolver.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import io.cucumber.core.logging.LoggerFactory;
1010
import io.cucumber.core.resource.ClassLoaders;
1111
import io.cucumber.core.resource.ResourceScanner;
12+
import io.cucumber.junit.platform.engine.NodeDescriptor.ExamplesDescriptor;
13+
import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor;
14+
import io.cucumber.junit.platform.engine.NodeDescriptor.RuleDescriptor;
15+
import io.cucumber.junit.platform.engine.NodeDescriptor.ScenarioOutlineDescriptor;
1216
import io.cucumber.plugin.event.Node;
1317
import org.junit.platform.engine.ConfigurationParameters;
1418
import org.junit.platform.engine.TestDescriptor;
@@ -85,7 +89,8 @@ private FeatureDescriptor createFeatureDescriptor(Feature feature) {
8589
source.featureSource(),
8690
feature),
8791
(Node.Rule node, TestDescriptor parent) -> {
88-
TestDescriptor descriptor = new NodeDescriptor(
92+
TestDescriptor descriptor = new RuleDescriptor(
93+
parameters,
8994
source.ruleSegment(parent.getUniqueId(), node),
9095
namingStrategy.name(node),
9196
source.nodeSource(node));
@@ -103,15 +108,17 @@ private FeatureDescriptor createFeatureDescriptor(Feature feature) {
103108
return descriptor;
104109
},
105110
(Node.ScenarioOutline node, TestDescriptor parent) -> {
106-
TestDescriptor descriptor = new NodeDescriptor(
111+
TestDescriptor descriptor = new ScenarioOutlineDescriptor(
112+
parameters,
107113
source.scenarioSegment(parent.getUniqueId(), node),
108114
namingStrategy.name(node),
109115
source.nodeSource(node));
110116
parent.addChild(descriptor);
111117
return descriptor;
112118
},
113119
(Node.Examples node, TestDescriptor parent) -> {
114-
NodeDescriptor descriptor = new NodeDescriptor(
120+
NodeDescriptor descriptor = new ExamplesDescriptor(
121+
parameters,
115122
source.examplesSegment(parent.getUniqueId(), node),
116123
namingStrategy.name(node),
117124
source.nodeSource(node));
Lines changed: 207 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,221 @@
11
package io.cucumber.junit.platform.engine;
22

3+
import io.cucumber.core.gherkin.Pickle;
4+
import io.cucumber.core.resource.ClasspathSupport;
5+
import org.junit.platform.engine.ConfigurationParameters;
36
import org.junit.platform.engine.TestSource;
7+
import org.junit.platform.engine.TestTag;
48
import org.junit.platform.engine.UniqueId;
9+
import org.junit.platform.engine.support.config.PrefixedConfigurationParameters;
510
import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
11+
import org.junit.platform.engine.support.descriptor.ClasspathResourceSource;
12+
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
13+
import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
14+
import org.junit.platform.engine.support.hierarchical.Node;
615

7-
class NodeDescriptor extends AbstractTestDescriptor {
16+
import java.util.Arrays;
17+
import java.util.Collections;
18+
import java.util.LinkedHashSet;
19+
import java.util.Locale;
20+
import java.util.Optional;
21+
import java.util.Set;
22+
import java.util.stream.Stream;
823

9-
NodeDescriptor(UniqueId uniqueId, String name, TestSource source) {
24+
import static io.cucumber.junit.platform.engine.Constants.EXECUTION_EXCLUSIVE_RESOURCES_PREFIX;
25+
import static io.cucumber.junit.platform.engine.Constants.EXECUTION_MODE_FEATURE_PROPERTY_NAME;
26+
import static io.cucumber.junit.platform.engine.Constants.READ_SUFFIX;
27+
import static io.cucumber.junit.platform.engine.Constants.READ_WRITE_SUFFIX;
28+
import static java.util.stream.Collectors.collectingAndThen;
29+
import static java.util.stream.Collectors.toCollection;
30+
31+
abstract class NodeDescriptor extends AbstractTestDescriptor implements Node<CucumberEngineExecutionContext> {
32+
33+
private final ExecutionMode executionMode;
34+
35+
NodeDescriptor(ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source) {
1036
super(uniqueId, name, source);
37+
this.executionMode = parameters
38+
.get(EXECUTION_MODE_FEATURE_PROPERTY_NAME,
39+
value -> ExecutionMode.valueOf(value.toUpperCase(Locale.US)))
40+
.orElse(ExecutionMode.CONCURRENT);
1141
}
1242

1343
@Override
14-
public Type getType() {
15-
return Type.CONTAINER;
44+
public ExecutionMode getExecutionMode() {
45+
return executionMode;
46+
}
47+
48+
static final class ExamplesDescriptor extends NodeDescriptor {
49+
50+
ExamplesDescriptor(ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source) {
51+
super(parameters, uniqueId, name, source);
52+
}
53+
54+
@Override
55+
public Type getType() {
56+
return Type.CONTAINER;
57+
}
58+
59+
}
60+
61+
static final class RuleDescriptor extends NodeDescriptor {
62+
63+
RuleDescriptor(ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source) {
64+
super(parameters, uniqueId, name, source);
65+
}
66+
67+
@Override
68+
public Type getType() {
69+
return Type.CONTAINER;
70+
}
71+
72+
}
73+
74+
static final class ScenarioOutlineDescriptor extends NodeDescriptor {
75+
76+
ScenarioOutlineDescriptor(
77+
ConfigurationParameters parameters, UniqueId uniqueId, String name,
78+
TestSource source
79+
) {
80+
super(parameters, uniqueId, name, source);
81+
}
82+
83+
@Override
84+
public Type getType() {
85+
return Type.CONTAINER;
86+
}
87+
88+
}
89+
90+
static final class PickleDescriptor extends NodeDescriptor {
91+
92+
private final Pickle pickleEvent;
93+
private final Set<TestTag> tags;
94+
private final Set<ExclusiveResource> exclusiveResources = new LinkedHashSet<>(0);
95+
96+
PickleDescriptor(
97+
ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source,
98+
Pickle pickleEvent
99+
) {
100+
super(parameters, uniqueId, name, source);
101+
this.pickleEvent = pickleEvent;
102+
this.tags = getTags(pickleEvent);
103+
this.tags.forEach(tag -> {
104+
ExclusiveResourceOptions exclusiveResourceOptions = new ExclusiveResourceOptions(parameters, tag);
105+
exclusiveResourceOptions.exclusiveReadWriteResource()
106+
.map(resource -> new ExclusiveResource(resource, LockMode.READ_WRITE))
107+
.forEach(exclusiveResources::add);
108+
exclusiveResourceOptions.exclusiveReadResource()
109+
.map(resource -> new ExclusiveResource(resource, LockMode.READ))
110+
.forEach(exclusiveResources::add);
111+
});
112+
}
113+
114+
private Set<TestTag> getTags(Pickle pickleEvent) {
115+
return pickleEvent.getTags().stream()
116+
.map(tag -> tag.substring(1))
117+
.filter(TestTag::isValid)
118+
.map(TestTag::create)
119+
// Retain input order
120+
.collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet));
121+
}
122+
123+
@Override
124+
public Type getType() {
125+
return Type.TEST;
126+
}
127+
128+
@Override
129+
public SkipResult shouldBeSkipped(CucumberEngineExecutionContext context) {
130+
return Stream.of(shouldBeSkippedByTagFilter(context), shouldBeSkippedByNameFilter(context))
131+
.flatMap(skipResult -> skipResult.map(Stream::of).orElseGet(Stream::empty))
132+
.filter(SkipResult::isSkipped)
133+
.findFirst()
134+
.orElseGet(SkipResult::doNotSkip);
135+
}
136+
137+
private Optional<SkipResult> shouldBeSkippedByTagFilter(CucumberEngineExecutionContext context) {
138+
return context.getOptions().tagFilter().map(expression -> {
139+
if (expression.evaluate(pickleEvent.getTags())) {
140+
return SkipResult.doNotSkip();
141+
}
142+
return SkipResult
143+
.skip(
144+
"'" + Constants.FILTER_TAGS_PROPERTY_NAME + "=" + expression
145+
+ "' did not match this scenario");
146+
});
147+
}
148+
149+
private Optional<SkipResult> shouldBeSkippedByNameFilter(CucumberEngineExecutionContext context) {
150+
return context.getOptions().nameFilter().map(pattern -> {
151+
if (pattern.matcher(pickleEvent.getName()).matches()) {
152+
return SkipResult.doNotSkip();
153+
}
154+
return SkipResult
155+
.skip("'" + Constants.FILTER_NAME_PROPERTY_NAME + "=" + pattern
156+
+ "' did not match this scenario");
157+
});
158+
}
159+
160+
@Override
161+
public CucumberEngineExecutionContext execute(
162+
CucumberEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor
163+
) {
164+
context.runTestCase(pickleEvent);
165+
return context;
166+
}
167+
168+
@Override
169+
public Set<ExclusiveResource> getExclusiveResources() {
170+
return exclusiveResources;
171+
}
172+
173+
/**
174+
* Returns the set of {@linkplain TestTag tags} for a pickle.
175+
* <p>
176+
* Note that Cucumber will remove the {code @} symbol from all Gherkin
177+
* tags. So a scenario tagged with {@code @Smoke} becomes a test tagged
178+
* with {@code Smoke}.
179+
*
180+
* @return the set of tags
181+
*/
182+
@Override
183+
public Set<TestTag> getTags() {
184+
return tags;
185+
}
186+
187+
Optional<String> getPackage() {
188+
return getSource()
189+
.filter(ClasspathResourceSource.class::isInstance)
190+
.map(ClasspathResourceSource.class::cast)
191+
.map(ClasspathResourceSource::getClasspathResourceName)
192+
.map(ClasspathSupport::packageNameOfResource);
193+
}
194+
195+
private static final class ExclusiveResourceOptions {
196+
197+
private final ConfigurationParameters parameters;
198+
199+
ExclusiveResourceOptions(ConfigurationParameters parameters, TestTag tag) {
200+
this.parameters = new PrefixedConfigurationParameters(
201+
parameters,
202+
EXECUTION_EXCLUSIVE_RESOURCES_PREFIX + tag.getName());
203+
}
204+
205+
public Stream<String> exclusiveReadWriteResource() {
206+
return parameters.get(READ_WRITE_SUFFIX, s -> Arrays.stream(s.split(","))
207+
.map(String::trim))
208+
.orElse(Stream.empty());
209+
}
210+
211+
public Stream<String> exclusiveReadResource() {
212+
return parameters.get(READ_SUFFIX, s -> Arrays.stream(s.split(","))
213+
.map(String::trim))
214+
.orElse(Stream.empty());
215+
}
216+
217+
}
218+
16219
}
17220

18221
}

0 commit comments

Comments
 (0)