Skip to content

Commit

Permalink
[JUnit Platform] Support cucumber.features property
Browse files Browse the repository at this point in the history
JUnit 5 support is still anything but first class
(junit-team/junit5#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
  • Loading branch information
mpkorstanje committed Mar 9, 2022
1 parent 517c5fb commit 0b72aa2
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] (In Git)

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

### Changed
* Update dependency io.cucumber:ci-environment to v9 ([#2475](https://github.com/cucumber/cucumber-jvm/pull/2475) M.P. Korstanje)
Expand Down
2 changes: 1 addition & 1 deletion core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ cucumber.execution.wip= # true or false. default: false.
# Fails if there any passing scenarios
# CLI only.
cucumber.features= # command separated paths to feature files.
cucumber.features= # comma separated paths to feature files.
# example: path/to/example.feature, path/to/other.feature
cucumber.filter.name= # a regular expression
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/io/cucumber/core/options/USAGE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ cucumber.execution.wip= # true or false. default: false.
# Fails if there any passing scenarios
# CLI only.

cucumber.features= # command separated paths to feature files.
cucumber.features= # comma separated paths to feature files.
# example: path/to/example.feature, path/to/other.feature

cucumber.filter.name= # a regular expression
Expand Down
5 changes: 5 additions & 0 deletions junit-platform-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ cucumber.filter.name= # a regular expres
# JUnit 5 prefer using JUnit 5s discovery request filters
# or JUnit 5 tag expressions instead.
cucumber.features= # comma separated paths to feature files.
# example: path/to/example.feature, path/to/other.feature
# note: When used any discovery selectors from the JUnit
# Platform will be ignored. Use with caution and care.
cucumber.filter.tags= # a cucumber tag expression.
# only scenarios with matching tags are executed.
# example: @Cucumber and not (@Gherkin or @Zucchini)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ public final class Constants {
*/
public static final String EXECUTION_EXCLUSIVE_RESOURCES_TAG_TEMPLATE_VARIABLE = "<tag-name>";

/**
* Property name used to set feature location: {@value}
* <p>
* A comma separated list of:
* <ul>
* <li>{@code path/to/dir} - Load the files with the extension ".feature"
* for the directory {@code path} and its sub directories.
* <li>{@code path/name.feature} - Load the feature file
* {@code path/name.feature} from the file system.</li>
* <li>{@code classpath:path/name.feature} - Load the feature file
* {@code path/name.feature} from the classpath.</li>
* <li>{@code path/name.feature:3:9} - Load the scenarios on line 3 and line
* 9 in the file {@code path/name.feature}.</li>
* </ul>
* <p>
* NOTE: When used any discovery selectors from the JUnit Platform will be
* ignored. Use with caution and care.
*
* @see io.cucumber.core.feature.FeatureWithLines
*/
public static final String FEATURES_PROPERTY_NAME = io.cucumber.core.options.Constants.FEATURES_PROPERTY_NAME;

/**
* Property name used to set name filter: {@value}
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.cucumber.junit.platform.engine;

import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.core.feature.FeatureWithLines;
import io.cucumber.core.feature.GluePath;
import io.cucumber.core.options.ObjectFactoryParser;
import io.cucumber.core.options.PluginOption;
Expand All @@ -16,6 +17,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
Expand All @@ -25,6 +27,7 @@
import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME_PREFIX;
import static io.cucumber.junit.platform.engine.Constants.ANSI_COLORS_DISABLED_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.EXECUTION_DRY_RUN_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.FILTER_NAME_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.FILTER_TAGS_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
Expand Down Expand Up @@ -165,4 +168,14 @@ NamingStrategy namingStrategy() {
.orElse(DefaultNamingStrategy.SHORT);
}

List<FeatureWithLines> featuresWithLines() {
return configurationParameters.get(FEATURES_PROPERTY_NAME,
s -> Arrays.stream(s.split(","))
.map(String::trim)
.map(FeatureWithLines::parse)
.sorted(Comparator.comparing(FeatureWithLines::uri))
.distinct()
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.cucumber.junit.platform.engine;

import io.cucumber.core.feature.FeatureWithLines;
import io.cucumber.core.logging.Logger;
import io.cucumber.core.logging.LoggerFactory;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.Filter;
import org.junit.platform.engine.TestDescriptor;
Expand All @@ -13,13 +17,25 @@
import org.junit.platform.engine.discovery.UniqueIdSelector;
import org.junit.platform.engine.discovery.UriSelector;

import java.util.List;
import java.util.function.Predicate;

import static io.cucumber.junit.platform.engine.FeatureResolver.createFeatureResolver;
import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.FeatureResolver.create;
import static org.junit.platform.engine.Filter.composeFilters;

class DiscoverySelectorResolver {

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

private static void warnWhenCucumberFeaturesPropertyIsUsed() {
log.warn(
() -> "Discovering tests using the " + FEATURES_PROPERTY_NAME
+ " property. Other discovery selectors are ignored!\n" +
"Please request/upvote/sponsor/ect better support for JUnit 5 discovery selectors.\n" +
"See: https://github.com/cucumber/cucumber-jvm/pull/2498");
}

void resolveSelectors(EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor) {
Predicate<String> packageFilter = buildPackageFilter(request);
resolve(request, engineDescriptor, packageFilter);
Expand All @@ -35,11 +51,20 @@ private Predicate<String> buildPackageFilter(EngineDiscoveryRequest request) {
private void resolve(
EngineDiscoveryRequest request, CucumberEngineDescriptor engineDescriptor, Predicate<String> packageFilter
) {
FeatureResolver featureResolver = createFeatureResolver(
request.getConfigurationParameters(),
ConfigurationParameters configuration = request.getConfigurationParameters();
FeatureResolver featureResolver = create(
configuration,
engineDescriptor,
packageFilter);

CucumberEngineOptions options = new CucumberEngineOptions(configuration);
List<FeatureWithLines> featureWithLines = options.featuresWithLines();
if (!featureWithLines.isEmpty()) {
warnWhenCucumberFeaturesPropertyIsUsed();
featureWithLines.forEach(featureResolver::resolveFeatureWithLines);
return;
}

request.getSelectorsByType(ClasspathRootSelector.class).forEach(featureResolver::resolveClasspathRoot);
request.getSelectorsByType(ClasspathResourceSelector.class).forEach(featureResolver::resolveClasspathResource);
request.getSelectorsByType(ClassSelector.class).forEach(featureResolver::resolveClass);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.cucumber.core.feature.FeatureIdentifier;
import io.cucumber.core.feature.FeatureParser;
import io.cucumber.core.feature.FeatureWithLines;
import io.cucumber.core.gherkin.Feature;
import io.cucumber.core.gherkin.Pickle;
import io.cucumber.core.logging.Logger;
Expand Down Expand Up @@ -54,7 +55,7 @@ private FeatureResolver(
this.namingStrategy = new CucumberEngineOptions(parameters).namingStrategy();
}

static FeatureResolver createFeatureResolver(
static FeatureResolver create(
ConfigurationParameters parameters, CucumberEngineDescriptor engineDescriptor,
Predicate<String> packageFilter
) {
Expand Down Expand Up @@ -234,6 +235,14 @@ void resolveUri(UriSelector selector) {
});
}

void resolveFeatureWithLines(FeatureWithLines selector) {
resolveUri(selector.uri())
.forEach(featureDescriptor -> {
featureDescriptor.prune(TestDescriptorOnLine.from(selector));
engineDescriptor.mergeFeature(featureDescriptor);
});
}

private static URI stripQuery(URI uri) {
if (uri.getQuery() == null) {
return uri;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.cucumber.junit.platform.engine;

import io.cucumber.core.feature.FeatureWithLines;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.discovery.ClasspathResourceSelector;
import org.junit.platform.engine.discovery.FileSelector;
Expand Down Expand Up @@ -37,6 +38,19 @@ private static boolean anyTestDescriptor(TestDescriptor testDescriptor) {
return true;
}

private static Predicate<TestDescriptor> eitherTestDescriptor(
Predicate<TestDescriptor> a, Predicate<TestDescriptor> b
) {
return a.or(b);
}

static Predicate<TestDescriptor> from(FeatureWithLines selector) {
return selector.lines().stream()
.map(TestDescriptorOnLine::testDescriptorOnLine)
.reduce(TestDescriptorOnLine::eitherTestDescriptor)
.orElse(TestDescriptorOnLine::anyTestDescriptor);
}

static Predicate<TestDescriptor> from(UriSelector selector) {
String query = selector.getUri().getQuery();
return fromQuery(query)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.logging.LogRecord;
import java.util.stream.Collectors;

import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME;
import static java.util.Collections.singleton;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toSet;
Expand Down Expand Up @@ -188,6 +189,34 @@ void resolveRequestWithClassPathUriSelectorWithLine() {
assertEquals(1, tests.size());
}

@Test
void resolveRequestWithUriSelectorThroughProperty() {
URI uri = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature:19:20");
ConfigurationParameters parameters = new MapConfigurationParameters(FEATURES_PROPERTY_NAME, uri.toString());
EngineDiscoveryRequest discoveryRequest = new SelectorRequest(parameters);
resolver.resolveSelectors(discoveryRequest, testDescriptor);
List<? extends TestDescriptor> tests = testDescriptor.getDescendants().stream()
.filter(TestDescriptor::isTest)
.collect(Collectors.toList());
assertEquals(2, tests.size());
}

@Test
void resolveRequestWithUriSelectorThroughPropertyIgnoresOtherSelectors() {
URI uri1 = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature:19");
ConfigurationParameters parameters = new MapConfigurationParameters(FEATURES_PROPERTY_NAME, uri1.toString());

URI uri2 = URI.create("classpath:/io/cucumber/junit/platform/engine/feature-with-outline.feature?line=20");
DiscoverySelector resource = selectUri(uri2);

EngineDiscoveryRequest discoveryRequest = new SelectorRequest(parameters, resource);
resolver.resolveSelectors(discoveryRequest, testDescriptor);
List<? extends TestDescriptor> tests = testDescriptor.getDescendants().stream()
.filter(TestDescriptor::isTest)
.collect(Collectors.toList());
assertEquals(1, tests.size());
}

@Test
void resolveRequestWithFileSelector() {
DiscoverySelector resource = selectFile("src/test/resources/io/cucumber/junit/platform/engine/single.feature");
Expand Down Expand Up @@ -387,12 +416,22 @@ void resolveRequestWithClassSelectorShouldLogWarnIfNoFeaturesFound() {
private static class SelectorRequest implements EngineDiscoveryRequest {

private final Map<Class<?>, List<DiscoverySelector>> resources = new HashMap<>();
private final ConfigurationParameters parameters;

SelectorRequest(ConfigurationParameters parameters, DiscoverySelector... selectors) {
this(parameters, Arrays.asList(selectors));
}

SelectorRequest(DiscoverySelector... selectors) {
this(Arrays.asList(selectors));
this(new EmptyConfigurationParameters(), Arrays.asList(selectors));
}

SelectorRequest(List<DiscoverySelector> selectors) {
this(new EmptyConfigurationParameters(), selectors);
}

SelectorRequest(ConfigurationParameters parameters, List<DiscoverySelector> selectors) {
this.parameters = parameters;
for (DiscoverySelector discoverySelector : selectors) {
resources.putIfAbsent(discoverySelector.getClass(), new ArrayList<>());
resources.get(discoverySelector.getClass()).add(discoverySelector);
Expand All @@ -416,7 +455,7 @@ public <T extends DiscoveryFilter<?>> List<T> getFiltersByType(Class<T> filterTy

@Override
public ConfigurationParameters getConfigurationParameters() {
return new EmptyConfigurationParameters();
return parameters;
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
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;
import static io.cucumber.junit.platform.engine.FeatureResolver.createFeatureResolver;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Optional.of;
Expand Down Expand Up @@ -51,7 +50,7 @@ void feature() {
}

private TestDescriptor getFeature() {
FeatureResolver featureResolver = createFeatureResolver(configurationParameters, engineDescriptor,
FeatureResolver featureResolver = FeatureResolver.create(configurationParameters, engineDescriptor,
aPackage -> true);
featureResolver.resolveClasspathResource(selectClasspathResource(featurePath));
Set<? extends TestDescriptor> features = engineDescriptor.getChildren();
Expand Down

0 comments on commit 0b72aa2

Please sign in to comment.