Skip to content

Commit ee2a0bd

Browse files
authored
[JUnit Platform Engine] Cache parsed features (#2711)
The JUnit Platform allows Scenarios and Examples to be selected by their unique id. This id is stable between test executions and can be used to rerun failing Scenarios and Examples. For practical reasons each unique id is processed individually[+] and results in a feature file being parsed. The parsed feature is then compiled into pickles. Both are mapped to a hierarchy of JUnit 5 test descriptors. These are then merged. The merge process will discard any duplicate nodes. Each time a feature file is parsed the parser will assign unique identifiers to all ast nodes and pickles. As a result the merged hierarchy of junit test descriptors contained pickles that belonged to a different feature file. This poses a problem for tools such as the junit-xml-formatter that depend on the identifiers in pickles and features to stitch everything back together. By caching the parsed feature files we ensure that within a single execution the internal identifiers do not change. + : It would actually matter little if we did process all unique ids at once, the problem would persist when uri selectors are used, either alone or in combination with unique id selectors. Fixes: #2709
1 parent 12c5029 commit ee2a0bd

File tree

7 files changed

+88
-11
lines changed

7 files changed

+88
-11
lines changed

CHANGELOG.md

+2
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+
### Fixed
14+
- [JUnit Platform Engine] Corrupted junit-xml report when using `surefire.rerunFailingTestsCount` parameter ([#2709](https://github.com/cucumber/cucumber-jvm/pull/2709) M.P. Korstanje)
1315

1416
## [7.11.1] - 2023-01-27
1517
### Added
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.cucumber.junit.platform.engine;
2+
3+
import io.cucumber.core.feature.FeatureParser;
4+
import io.cucumber.core.gherkin.Feature;
5+
import io.cucumber.core.resource.Resource;
6+
7+
import java.net.URI;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
12+
class CachingFeatureParser {
13+
14+
private final Map<URI, Optional<Feature>> cache = new HashMap<>();
15+
private final FeatureParser delegate;
16+
17+
CachingFeatureParser(FeatureParser delegate) {
18+
this.delegate = delegate;
19+
}
20+
21+
Optional<Feature> parseResource(Resource resource) {
22+
return cache.computeIfAbsent(resource.getUri(), uri -> delegate.parseResource(resource));
23+
}
24+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
class DiscoverySelectorResolver {
2929

30-
private static final Logger log = LoggerFactory.getLogger(FeatureResolver.class);
30+
private static final Logger log = LoggerFactory.getLogger(DiscoverySelectorResolver.class);
3131

3232
private static boolean warnedWhenCucumberFeaturesPropertyIsUsed = false;
3333

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

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class FeatureDescriptor extends AbstractTestDescriptor implements Node<CucumberE
2020
this.feature = feature;
2121
}
2222

23+
Feature getFeature() {
24+
return feature;
25+
}
26+
2327
private static void pruneRecursively(TestDescriptor descriptor, Predicate<TestDescriptor> toKeep) {
2428
if (!toKeep.test(descriptor)) {
2529
if (descriptor.isTest()) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ final class FeatureResolver {
3838

3939
private static final Logger log = LoggerFactory.getLogger(FeatureResolver.class);
4040

41-
private final FeatureParser featureParser = new FeatureParser(UUID::randomUUID);
41+
private final CachingFeatureParser featureParser = new CachingFeatureParser(new FeatureParser(UUID::randomUUID));
4242
private final ResourceScanner<Feature> featureScanner = new ResourceScanner<>(
4343
ClassLoaders::getDefaultClassLoader,
4444
FeatureIdentifier::isFeature,

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

+11-7
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,17 @@ public Type getType() {
8989

9090
static final class PickleDescriptor extends NodeDescriptor {
9191

92-
private final Pickle pickleEvent;
92+
private final Pickle pickle;
9393
private final Set<TestTag> tags;
9494
private final Set<ExclusiveResource> exclusiveResources = new LinkedHashSet<>(0);
9595

9696
PickleDescriptor(
9797
ConfigurationParameters parameters, UniqueId uniqueId, String name, TestSource source,
98-
Pickle pickleEvent
98+
Pickle pickle
9999
) {
100100
super(parameters, uniqueId, name, source);
101-
this.pickleEvent = pickleEvent;
102-
this.tags = getTags(pickleEvent);
101+
this.pickle = pickle;
102+
this.tags = getTags(pickle);
103103
this.tags.forEach(tag -> {
104104
ExclusiveResourceOptions exclusiveResourceOptions = new ExclusiveResourceOptions(parameters, tag);
105105
exclusiveResourceOptions.exclusiveReadWriteResource()
@@ -111,6 +111,10 @@ static final class PickleDescriptor extends NodeDescriptor {
111111
});
112112
}
113113

114+
Pickle getPickle() {
115+
return pickle;
116+
}
117+
114118
private Set<TestTag> getTags(Pickle pickleEvent) {
115119
return pickleEvent.getTags().stream()
116120
.map(tag -> tag.substring(1))
@@ -136,7 +140,7 @@ public SkipResult shouldBeSkipped(CucumberEngineExecutionContext context) {
136140

137141
private Optional<SkipResult> shouldBeSkippedByTagFilter(CucumberEngineExecutionContext context) {
138142
return context.getOptions().tagFilter().map(expression -> {
139-
if (expression.evaluate(pickleEvent.getTags())) {
143+
if (expression.evaluate(pickle.getTags())) {
140144
return SkipResult.doNotSkip();
141145
}
142146
return SkipResult
@@ -148,7 +152,7 @@ private Optional<SkipResult> shouldBeSkippedByTagFilter(CucumberEngineExecutionC
148152

149153
private Optional<SkipResult> shouldBeSkippedByNameFilter(CucumberEngineExecutionContext context) {
150154
return context.getOptions().nameFilter().map(pattern -> {
151-
if (pattern.matcher(pickleEvent.getName()).matches()) {
155+
if (pattern.matcher(pickle.getName()).matches()) {
152156
return SkipResult.doNotSkip();
153157
}
154158
return SkipResult
@@ -161,7 +165,7 @@ private Optional<SkipResult> shouldBeSkippedByNameFilter(CucumberEngineExecution
161165
public CucumberEngineExecutionContext execute(
162166
CucumberEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor
163167
) {
164-
context.runTestCase(pickleEvent);
168+
context.runTestCase(pickle);
165169
return context;
166170
}
167171

cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/DiscoverySelectorResolverTest.java

+45-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.cucumber.junit.platform.engine;
22

3+
import io.cucumber.core.gherkin.Feature;
4+
import io.cucumber.core.gherkin.Pickle;
35
import io.cucumber.core.logging.LogRecordListener;
46
import io.cucumber.junit.platform.engine.NodeDescriptor.PickleDescriptor;
57
import io.cucumber.junit.platform.engine.nofeatures.NoFeatures;
@@ -23,6 +25,7 @@
2325
import java.nio.file.Paths;
2426
import java.util.ArrayList;
2527
import java.util.Arrays;
28+
import java.util.Collection;
2629
import java.util.Collections;
2730
import java.util.HashMap;
2831
import java.util.HashSet;
@@ -33,6 +36,7 @@
3336
import java.util.logging.Level;
3437
import java.util.logging.LogRecord;
3538
import java.util.stream.Collectors;
39+
import java.util.stream.Stream;
3640

3741
import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
3842
import static java.util.Collections.singleton;
@@ -382,15 +386,54 @@ void resolveRequestWithMultipleUniqueIdSelector() {
382386
.collect(toSet()));
383387
}
384388

389+
@Test
390+
void resolveRequestWithMultipleUniqueIdSelectorFromTheSameFeature() {
391+
Set<UniqueId> selectors = new HashSet<>();
392+
393+
DiscoverySelector resource = selectDirectory(
394+
"src/test/resources/io/cucumber/junit/platform/engine/feature-with-outline.feature");
395+
selectAllPickles(resource).forEach(selectors::add);
396+
397+
EngineDiscoveryRequest discoveryRequest = new SelectorRequest(
398+
selectors.stream()
399+
.map(DiscoverySelectors::selectUniqueId)
400+
.collect(Collectors.toList()));
401+
402+
resolver.resolveSelectors(discoveryRequest, testDescriptor);
403+
404+
Set<String> pickleIdsFromFeature = testDescriptor.getDescendants()
405+
.stream()
406+
.filter(FeatureDescriptor.class::isInstance)
407+
.map(FeatureDescriptor.class::cast)
408+
.map(FeatureDescriptor::getFeature)
409+
.map(Feature::getPickles)
410+
.flatMap(Collection::stream)
411+
.map(Pickle::getId)
412+
.collect(toSet());
413+
414+
Set<String> pickleIdsFromPickles = testDescriptor.getDescendants()
415+
.stream()
416+
.filter(PickleDescriptor.class::isInstance)
417+
.map(PickleDescriptor.class::cast)
418+
.map(PickleDescriptor::getPickle)
419+
.map(Pickle::getId)
420+
.collect(toSet());
421+
422+
assertEquals(pickleIdsFromFeature, pickleIdsFromPickles);
423+
}
424+
385425
private Optional<UniqueId> selectSomePickle(DiscoverySelector resource) {
426+
return selectAllPickles(resource).findFirst();
427+
}
428+
429+
private Stream<UniqueId> selectAllPickles(DiscoverySelector resource) {
386430
EngineDiscoveryRequest discoveryRequest = new SelectorRequest(resource);
387431
resolver.resolveSelectors(discoveryRequest, testDescriptor);
388432
Set<? extends TestDescriptor> descendants = testDescriptor.getDescendants();
389433
resetTestDescriptor();
390434
return descendants.stream()
391435
.filter(PickleDescriptor.class::isInstance)
392-
.map(TestDescriptor::getUniqueId)
393-
.findFirst();
436+
.map(TestDescriptor::getUniqueId);
394437
}
395438

396439
@Test

0 commit comments

Comments
 (0)