Skip to content

Commit 52d9594

Browse files
authored
[JUnit Platform] Support cucumber.features property (#2498)
JUnit 5 support is still anything but first class (junit-team/junit-framework#2849). As such users do need a quick and easy way to select feature files from the commandline i.e: ``` mvn test -Dcucumber.features=path/to/example.feature ``` When enabled, other discovery selectors will be ignored. Of all possible options is the least-worst solution. Additionally a warning is logged prompting users to request better support. The relevant issues to request/support/sponsor are: - IDEA - https://youtrack.jetbrains.com/issue/IDEA-227508 - https://youtrack.jetbrains.com/issue/IDEA-276463 - https://youtrack.jetbrains.com/issue/IDEA-276477 - Eclipse - No issue exists (yet). - Maven Surefire - https://issues.apache.org/jira/browse/SUREFIRE-1724 - Gradle - gradle/gradle#4773
1 parent 517c5fb commit 52d9594

File tree

11 files changed

+143
-10
lines changed

11 files changed

+143
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88
## [Unreleased] (In Git)
99

1010
### Added
11+
* [JUnit Platform] Support `cucumber.features` property ([#2498](https://github.com/cucumber/cucumber-jvm/pull/2498) M.P. Korstanje)
1112

1213
### Changed
1314
* Update dependency io.cucumber:ci-environment to v9 ([#2475](https://github.com/cucumber/cucumber-jvm/pull/2475) M.P. Korstanje)

core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ cucumber.execution.wip= # true or false. default: false.
3232
# Fails if there any passing scenarios
3333
# CLI only.
3434
35-
cucumber.features= # command separated paths to feature files.
35+
cucumber.features= # comma separated paths to feature files.
3636
# example: path/to/example.feature, path/to/other.feature
3737
3838
cucumber.filter.name= # a regular expression

core/src/main/resources/io/cucumber/core/options/USAGE.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ cucumber.execution.wip= # true or false. default: false.
121121
# Fails if there any passing scenarios
122122
# CLI only.
123123

124-
cucumber.features= # command separated paths to feature files.
124+
cucumber.features= # comma separated paths to feature files.
125125
# example: path/to/example.feature, path/to/other.feature
126126

127127
cucumber.filter.name= # a regular expression

junit-platform-engine/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ cucumber.filter.name= # a regular expres
288288
# JUnit 5 prefer using JUnit 5s discovery request filters
289289
# or JUnit 5 tag expressions instead.
290290
291+
cucumber.features= # comma separated paths to feature files.
292+
# example: path/to/example.feature, path/to/other.feature
293+
# note: When used any discovery selectors from the JUnit
294+
# Platform will be ignored. Use with caution and care.
295+
291296
cucumber.filter.tags= # a cucumber tag expression.
292297
# only scenarios with matching tags are executed.
293298
# example: @Cucumber and not (@Gherkin or @Zucchini)

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,28 @@ public final class Constants {
4141
*/
4242
public static final String EXECUTION_EXCLUSIVE_RESOURCES_TAG_TEMPLATE_VARIABLE = "<tag-name>";
4343

44+
/**
45+
* Property name used to set feature location: {@value}
46+
* <p>
47+
* A comma separated list of:
48+
* <ul>
49+
* <li>{@code path/to/dir} - Load the files with the extension ".feature"
50+
* for the directory {@code path} and its sub directories.
51+
* <li>{@code path/name.feature} - Load the feature file
52+
* {@code path/name.feature} from the file system.</li>
53+
* <li>{@code classpath:path/name.feature} - Load the feature file
54+
* {@code path/name.feature} from the classpath.</li>
55+
* <li>{@code path/name.feature:3:9} - Load the scenarios on line 3 and line
56+
* 9 in the file {@code path/name.feature}.</li>
57+
* </ul>
58+
* <p>
59+
* NOTE: When used any discovery selectors from the JUnit Platform will be
60+
* ignored. Use with caution and care.
61+
*
62+
* @see io.cucumber.core.feature.FeatureWithLines
63+
*/
64+
public static final String FEATURES_PROPERTY_NAME = io.cucumber.core.options.Constants.FEATURES_PROPERTY_NAME;
65+
4466
/**
4567
* Property name used to set name filter: {@value}
4668
* <p>

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.cucumber.junit.platform.engine;
22

33
import io.cucumber.core.backend.ObjectFactory;
4+
import io.cucumber.core.feature.FeatureWithLines;
45
import io.cucumber.core.feature.GluePath;
56
import io.cucumber.core.options.ObjectFactoryParser;
67
import io.cucumber.core.options.PluginOption;
@@ -16,6 +17,7 @@
1617
import java.util.ArrayList;
1718
import java.util.Arrays;
1819
import java.util.Collections;
20+
import java.util.Comparator;
1921
import java.util.List;
2022
import java.util.Optional;
2123
import java.util.regex.Pattern;
@@ -25,6 +27,7 @@
2527
import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME_PREFIX;
2628
import static io.cucumber.junit.platform.engine.Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME;
2729
import static io.cucumber.junit.platform.engine.Constants.EXECUTION_DRY_RUN_PROPERTY_NAME;
30+
import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
2831
import static io.cucumber.junit.platform.engine.Constants.FILTER_NAME_PROPERTY_NAME;
2932
import static io.cucumber.junit.platform.engine.Constants.FILTER_TAGS_PROPERTY_NAME;
3033
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
@@ -165,4 +168,14 @@ NamingStrategy namingStrategy() {
165168
.orElse(DefaultNamingStrategy.SHORT);
166169
}
167170

171+
List<FeatureWithLines> featuresWithLines() {
172+
return configurationParameters.get(FEATURES_PROPERTY_NAME,
173+
s -> Arrays.stream(s.split(","))
174+
.map(String::trim)
175+
.map(FeatureWithLines::parse)
176+
.sorted(Comparator.comparing(FeatureWithLines::uri))
177+
.distinct()
178+
.collect(Collectors.toList()))
179+
.orElse(Collections.emptyList());
180+
}
168181
}

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package io.cucumber.junit.platform.engine;
22

3+
import io.cucumber.core.feature.FeatureWithLines;
4+
import io.cucumber.core.logging.Logger;
5+
import io.cucumber.core.logging.LoggerFactory;
6+
import org.junit.platform.engine.ConfigurationParameters;
37
import org.junit.platform.engine.EngineDiscoveryRequest;
48
import org.junit.platform.engine.Filter;
59
import org.junit.platform.engine.TestDescriptor;
@@ -13,13 +17,31 @@
1317
import org.junit.platform.engine.discovery.UniqueIdSelector;
1418
import org.junit.platform.engine.discovery.UriSelector;
1519

20+
import java.util.List;
1621
import java.util.function.Predicate;
1722

18-
import static io.cucumber.junit.platform.engine.FeatureResolver.createFeatureResolver;
23+
import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
24+
import static io.cucumber.junit.platform.engine.FeatureResolver.create;
1925
import static org.junit.platform.engine.Filter.composeFilters;
2026

2127
class DiscoverySelectorResolver {
2228

29+
private static final Logger log = LoggerFactory.getLogger(FeatureResolver.class);
30+
31+
private static boolean warnedWhenCucumberFeaturesPropertyIsUsed = false;
32+
33+
private static void warnWhenCucumberFeaturesPropertyIsUsed() {
34+
if (warnedWhenCucumberFeaturesPropertyIsUsed) {
35+
return;
36+
}
37+
warnedWhenCucumberFeaturesPropertyIsUsed = true;
38+
log.warn(
39+
() -> "Discovering tests using the " + FEATURES_PROPERTY_NAME
40+
+ " property. Other discovery selectors are ignored!\n" +
41+
"Please request/upvote/sponsor/ect better support for JUnit 5 discovery selectors.\n" +
42+
"See: https://github.com/cucumber/cucumber-jvm/pull/2498");
43+
}
44+
2345
void resolveSelectors(EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor) {
2446
Predicate<String> packageFilter = buildPackageFilter(request);
2547
resolve(request, engineDescriptor, packageFilter);
@@ -35,11 +57,20 @@ private Predicate<String> buildPackageFilter(EngineDiscoveryRequest request) {
3557
private void resolve(
3658
EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor, Predicate<String> packageFilter
3759
) {
38-
FeatureResolver featureResolver = createFeatureResolver(
39-
request.getConfigurationParameters(),
60+
ConfigurationParameters configuration = request.getConfigurationParameters();
61+
FeatureResolver featureResolver = create(
62+
configuration,
4063
engineDescriptor,
4164
packageFilter);
4265

66+
CucumberEngineOptions options = new CucumberEngineOptions(configuration);
67+
List<FeatureWithLines> featureWithLines = options.featuresWithLines();
68+
if (!featureWithLines.isEmpty()) {
69+
warnWhenCucumberFeaturesPropertyIsUsed();
70+
featureWithLines.forEach(featureResolver::resolveFeatureWithLines);
71+
return;
72+
}
73+
4374
request.getSelectorsByType(ClasspathRootSelector.class).forEach(featureResolver::resolveClasspathRoot);
4475
request.getSelectorsByType(ClasspathResourceSelector.class).forEach(featureResolver::resolveClasspathResource);
4576
request.getSelectorsByType(ClassSelector.class).forEach(featureResolver::resolveClass);

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.cucumber.core.feature.FeatureIdentifier;
44
import io.cucumber.core.feature.FeatureParser;
5+
import io.cucumber.core.feature.FeatureWithLines;
56
import io.cucumber.core.gherkin.Feature;
67
import io.cucumber.core.gherkin.Pickle;
78
import io.cucumber.core.logging.Logger;
@@ -54,7 +55,7 @@ private FeatureResolver(
5455
this.namingStrategy = new CucumberEngineOptions(parameters).namingStrategy();
5556
}
5657

57-
static FeatureResolver createFeatureResolver(
58+
static FeatureResolver create(
5859
ConfigurationParameters parameters, CucumberEngineDescriptor engineDescriptor,
5960
Predicate<String> packageFilter
6061
) {
@@ -234,6 +235,14 @@ void resolveUri(UriSelector selector) {
234235
});
235236
}
236237

238+
void resolveFeatureWithLines(FeatureWithLines selector) {
239+
resolveUri(selector.uri())
240+
.forEach(featureDescriptor -> {
241+
featureDescriptor.prune(TestDescriptorOnLine.from(selector));
242+
engineDescriptor.mergeFeature(featureDescriptor);
243+
});
244+
}
245+
237246
private static URI stripQuery(URI uri) {
238247
if (uri.getQuery() == null) {
239248
return uri;

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.cucumber.junit.platform.engine;
22

3+
import io.cucumber.core.feature.FeatureWithLines;
34
import org.junit.platform.engine.TestDescriptor;
45
import org.junit.platform.engine.discovery.ClasspathResourceSelector;
56
import org.junit.platform.engine.discovery.FileSelector;
@@ -37,6 +38,19 @@ private static boolean anyTestDescriptor(TestDescriptor testDescriptor) {
3738
return true;
3839
}
3940

41+
private static Predicate<TestDescriptor> eitherTestDescriptor(
42+
Predicate<TestDescriptor> a, Predicate<TestDescriptor> b
43+
) {
44+
return a.or(b);
45+
}
46+
47+
static Predicate<TestDescriptor> from(FeatureWithLines selector) {
48+
return selector.lines().stream()
49+
.map(TestDescriptorOnLine::testDescriptorOnLine)
50+
.reduce(TestDescriptorOnLine::eitherTestDescriptor)
51+
.orElse(TestDescriptorOnLine::anyTestDescriptor);
52+
}
53+
4054
static Predicate<TestDescriptor> from(UriSelector selector) {
4155
String query = selector.getUri().getQuery();
4256
return fromQuery(query)

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.logging.LogRecord;
3636
import java.util.stream.Collectors;
3737

38+
import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
3839
import static java.util.Collections.singleton;
3940
import static java.util.Comparator.comparing;
4041
import static java.util.stream.Collectors.toSet;
@@ -188,6 +189,34 @@ void resolveRequestWithClassPathUriSelectorWithLine() {
188189
assertEquals(1, tests.size());
189190
}
190191

192+
@Test
193+
void resolveRequestWithUriSelectorThroughProperty() {
194+
URI uri = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature:19:20");
195+
ConfigurationParameters parameters = new MapConfigurationParameters(FEATURES_PROPERTY_NAME, uri.toString());
196+
EngineDiscoveryRequest discoveryRequest = new SelectorRequest(parameters);
197+
resolver.resolveSelectors(discoveryRequest, testDescriptor);
198+
List<? extends TestDescriptor> tests = testDescriptor.getDescendants().stream()
199+
.filter(TestDescriptor::isTest)
200+
.collect(Collectors.toList());
201+
assertEquals(2, tests.size());
202+
}
203+
204+
@Test
205+
void resolveRequestWithUriSelectorThroughPropertyIgnoresOtherSelectors() {
206+
URI uri1 = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature:19");
207+
ConfigurationParameters parameters = new MapConfigurationParameters(FEATURES_PROPERTY_NAME, uri1.toString());
208+
209+
URI uri2 = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature?line=20");
210+
DiscoverySelector resource = selectUri(uri2);
211+
212+
EngineDiscoveryRequest discoveryRequest = new SelectorRequest(parameters, resource);
213+
resolver.resolveSelectors(discoveryRequest, testDescriptor);
214+
List<? extends TestDescriptor> tests = testDescriptor.getDescendants().stream()
215+
.filter(TestDescriptor::isTest)
216+
.collect(Collectors.toList());
217+
assertEquals(1, tests.size());
218+
}
219+
191220
@Test
192221
void resolveRequestWithFileSelector() {
193222
DiscoverySelector resource = selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature");
@@ -387,12 +416,22 @@ void resolveRequestWithClassSelectorShouldLogWarnIfNoFeaturesFound() {
387416
private static class SelectorRequest implements EngineDiscoveryRequest {
388417

389418
private final Map<Class<?>, List<DiscoverySelector>> resources = new HashMap<>();
419+
private final ConfigurationParameters parameters;
420+
421+
SelectorRequest(ConfigurationParameters parameters, DiscoverySelector... selectors) {
422+
this(parameters, Arrays.asList(selectors));
423+
}
390424

391425
SelectorRequest(DiscoverySelector... selectors) {
392-
this(Arrays.asList(selectors));
426+
this(new EmptyConfigurationParameters(), Arrays.asList(selectors));
393427
}
394428

395429
SelectorRequest(List<DiscoverySelector> selectors) {
430+
this(new EmptyConfigurationParameters(), selectors);
431+
}
432+
433+
SelectorRequest(ConfigurationParameters parameters, List<DiscoverySelector> selectors) {
434+
this.parameters = parameters;
396435
for (DiscoverySelector discoverySelector : selectors) {
397436
resources.putIfAbsent(discoverySelector.getClass(), new ArrayList<>());
398437
resources.get(discoverySelector.getClass()).add(discoverySelector);
@@ -416,7 +455,7 @@ public <T extends DiscoveryFilter<?>> List<T> getFiltersByType(Class<T> filterTy
416455

417456
@Override
418457
public ConfigurationParameters getConfigurationParameters() {
419-
return new EmptyConfigurationParameters();
458+
return parameters;
420459
}
421460

422461
}

0 commit comments

Comments
 (0)