diff --git a/CHANGELOG.md b/CHANGELOG.md
index b194f290f7..a2f9e8decf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,9 @@ 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]
-### Changed
+### Added
- [Core] The TeamCityPlugin for IntelliJ IDEA now uses the hook's method name for the name of the hook itself. ([#2798](https://github.com/cucumber/cucumber-jvm/issues/2798) V.V. Belov)
+- [Core] Allow feature with line syntax to target rules and examples. ([#2884](https://github.com/cucumber/cucumber-jvm/issues/2884) M.P. Korstanje)
## [7.17.0] - 2024-04-18
### Added
diff --git a/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureWithLines.java b/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureWithLines.java
index d542f94e64..5be63a1a63 100644
--- a/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureWithLines.java
+++ b/cucumber-core/src/main/java/io/cucumber/core/feature/FeatureWithLines.java
@@ -14,8 +14,8 @@
import java.util.stream.Collectors;
/**
- * Identifies either a directory containing feature files, a specific feature or
- * specific scenarios and examples (pickles) in a feature.
+ * Identifies either a directory containing feature files, a specific feature
+ * file or a feature, rules, scenarios, and/or examples in a feature file.
*
* The syntax of a feature with lines defined as either a {@link FeaturePath} or
* a {@link FeatureIdentifier} followed by a sequence of line numbers each
diff --git a/cucumber-core/src/main/java/io/cucumber/core/filter/LinePredicate.java b/cucumber-core/src/main/java/io/cucumber/core/filter/LinePredicate.java
index fac901c95d..6446ce45e5 100644
--- a/cucumber-core/src/main/java/io/cucumber/core/filter/LinePredicate.java
+++ b/cucumber-core/src/main/java/io/cucumber/core/filter/LinePredicate.java
@@ -1,6 +1,7 @@
package io.cucumber.core.filter;
import io.cucumber.core.gherkin.Pickle;
+import io.cucumber.plugin.event.Location;
import java.net.URI;
import java.util.Collection;
@@ -24,7 +25,10 @@ public boolean test(Pickle pickle) {
}
for (Integer line : lineFilters.get(picklePath)) {
if (Objects.equals(line, pickle.getLocation().getLine())
- || Objects.equals(line, pickle.getScenarioLocation().getLine())) {
+ || Objects.equals(line, pickle.getScenarioLocation().getLine())
+ || pickle.getExamplesLocation().map(Location::getLine).map(line::equals).orElse(false)
+ || pickle.getRuleLocation().map(Location::getLine).map(line::equals).orElse(false)
+ || pickle.getFeatureLocation().map(Location::getLine).map(line::equals).orElse(false)) {
return true;
}
}
diff --git a/cucumber-core/src/test/java/io/cucumber/core/filter/LinePredicateTest.java b/cucumber-core/src/test/java/io/cucumber/core/filter/LinePredicateTest.java
index 20bf7d8c9b..ae05175a7e 100644
--- a/cucumber-core/src/test/java/io/cucumber/core/filter/LinePredicateTest.java
+++ b/cucumber-core/src/test/java/io/cucumber/core/filter/LinePredicateTest.java
@@ -21,13 +21,22 @@ class LinePredicateTest {
featurePath,
"" +
"Feature: Test feature\n" +
- " Scenario Outline: Test scenario\n" +
- " Given I have 4 in my belly\n" +
- " Examples:\n" +
- " | thing | \n" +
- " | cucumber | \n" +
- " | gherkin | \n");
- private final Pickle pickle = feature.getPickles().get(0);
+ " Rule: Test rule\n" +
+ " Scenario Outline: Test scenario\n" +
+ " Given I have 4 in my belly\n" +
+ " Examples: First\n" +
+ " | thing | \n" +
+ " | cucumber | \n" +
+ " | gherkin | \n" +
+ "\n" +
+ " Examples: Second\n" +
+ " | thing | \n" +
+ " | zukini | \n" +
+ " | pickle | \n");
+ private final Pickle firstPickle = feature.getPickles().get(0);
+ private final Pickle secondPickle = feature.getPickles().get(1);
+ private final Pickle thirdPickle = feature.getPickles().get(2);
+ private final Pickle fourthPickle = feature.getPickles().get(3);
@Test
void matches_pickles_from_files_not_in_the_predicate_map() {
@@ -37,47 +46,172 @@ void matches_pickles_from_files_not_in_the_predicate_map() {
LinePredicate predicate = new LinePredicate(singletonMap(
URI.create("classpath:another_path/file.feature"),
singletonList(8)));
- assertTrue(predicate.test(pickle));
+ assertTrue(predicate.test(firstPickle));
}
@Test
- void does_not_matches_pickles_for_no_lines_in_predicate() {
+ void empty() {
LinePredicate predicate = new LinePredicate(singletonMap(
featurePath,
emptyList()));
- assertFalse(predicate.test(pickle));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
}
@Test
- void matches_pickles_for_any_line_in_predicate() {
+ void matches_at_least_one_line() {
LinePredicate predicate = new LinePredicate(singletonMap(
featurePath,
- asList(2, 4)));
- assertTrue(predicate.test(pickle));
+ asList(3, 4)));
+ assertTrue(predicate.test(firstPickle));
+ assertTrue(predicate.test(secondPickle));
+ assertTrue(predicate.test(thirdPickle));
+ assertTrue(predicate.test(fourthPickle));
}
@Test
- void matches_pickles_on_scenario_location_of_the_pickle() {
+ void matches_feature() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(1)));
+ assertTrue(predicate.test(firstPickle));
+ assertTrue(predicate.test(secondPickle));
+ assertTrue(predicate.test(thirdPickle));
+ assertTrue(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void matches_rule() {
LinePredicate predicate = new LinePredicate(singletonMap(
featurePath,
singletonList(2)));
- assertTrue(predicate.test(pickle));
+ assertTrue(predicate.test(firstPickle));
+ assertTrue(predicate.test(secondPickle));
+ assertTrue(predicate.test(thirdPickle));
+ assertTrue(predicate.test(fourthPickle));
}
@Test
- void matches_pickles_on_example_location_of_the_pickle() {
+ void matches_scenario() {
LinePredicate predicate = new LinePredicate(singletonMap(
featurePath,
- singletonList(6)));
- assertTrue(predicate.test(pickle));
+ singletonList(3)));
+ assertTrue(predicate.test(firstPickle));
+ assertTrue(predicate.test(secondPickle));
+ assertTrue(predicate.test(thirdPickle));
+ assertTrue(predicate.test(fourthPickle));
}
@Test
- void does_not_matches_pickles_not_on_any_line_of_the_predicate() {
+ void does_not_match_step() {
LinePredicate predicate = new LinePredicate(singletonMap(
featurePath,
singletonList(4)));
- assertFalse(predicate.test(pickle));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void matches_first_examples() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(5)));
+ assertTrue(predicate.test(firstPickle));
+ assertTrue(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void does_not_match_example_header() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(6)));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void matches_first_example() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(7)));
+ assertTrue(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void Matches_second_example() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(8)));
+ assertFalse(predicate.test(firstPickle));
+ assertTrue(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void does_not_match_empty_line() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(9)));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void matches_second_examples() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(10)));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertTrue(predicate.test(thirdPickle));
+ assertTrue(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void does_not_match_second_examples_header() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(11)));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void matches_third_example() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(12)));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertTrue(predicate.test(thirdPickle));
+ assertFalse(predicate.test(fourthPickle));
+ }
+
+ @Test
+ void matches_fourth_example() {
+ LinePredicate predicate = new LinePredicate(singletonMap(
+ featurePath,
+ singletonList(13)));
+ assertFalse(predicate.test(firstPickle));
+ assertFalse(predicate.test(secondPickle));
+ assertFalse(predicate.test(thirdPickle));
+ assertTrue(predicate.test(fourthPickle));
}
}
diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/CucumberQuery.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/CucumberQuery.java
index fcea9c4952..273c4ade92 100644
--- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/CucumberQuery.java
+++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/CucumberQuery.java
@@ -4,6 +4,9 @@
import io.cucumber.messages.types.Examples;
import io.cucumber.messages.types.Feature;
import io.cucumber.messages.types.Location;
+import io.cucumber.messages.types.Pickle;
+import io.cucumber.messages.types.PickleStep;
+import io.cucumber.messages.types.Rule;
import io.cucumber.messages.types.Scenario;
import io.cucumber.messages.types.Step;
import io.cucumber.messages.types.TableRow;
@@ -11,11 +14,15 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import static java.util.Objects.requireNonNull;
final class CucumberQuery {
+ private final Map ruleByScenarioId = new HashMap<>();
+ private final Map examplesByExampleId = new HashMap<>();
+ private final Map featureByScenarioId = new HashMap<>();
private final Map gherkinStepById = new HashMap<>();
private final Map gherkinScenarioById = new HashMap<>();
private final Map locationBySourceId = new HashMap<>();
@@ -23,11 +30,13 @@ final class CucumberQuery {
void update(Feature feature) {
feature.getChildren().forEach(featureChild -> {
featureChild.getBackground().ifPresent(this::updateBackground);
- featureChild.getScenario().ifPresent(this::updateScenario);
- featureChild.getRule().ifPresent(rule -> rule.getChildren().forEach(ruleChild -> {
- ruleChild.getBackground().ifPresent(this::updateBackground);
- ruleChild.getScenario().ifPresent(this::updateScenario);
- }));
+ featureChild.getScenario().ifPresent(scenario -> updateScenario(feature, null, scenario));
+ featureChild.getRule().ifPresent(rule -> {
+ rule.getChildren().forEach(ruleChild -> {
+ ruleChild.getBackground().ifPresent(this::updateBackground);
+ ruleChild.getScenario().ifPresent(scenario -> updateScenario(feature, rule, scenario));
+ });
+ });
});
}
@@ -35,16 +44,23 @@ private void updateBackground(Background background) {
updateStep(background.getSteps());
}
- private void updateScenario(Scenario scenario) {
+ private void updateScenario(Feature feature, Rule rule, Scenario scenario) {
gherkinScenarioById.put(requireNonNull(scenario.getId()), scenario);
locationBySourceId.put(requireNonNull(scenario.getId()), scenario.getLocation());
updateStep(scenario.getSteps());
for (Examples examples : scenario.getExamples()) {
for (TableRow tableRow : examples.getTableBody()) {
- this.locationBySourceId.put(requireNonNull(tableRow.getId()), tableRow.getLocation());
+ this.examplesByExampleId.put(tableRow.getId(), examples);
+ this.locationBySourceId.put(tableRow.getId(), tableRow.getLocation());
}
}
+
+ if (rule != null) {
+ ruleByScenarioId.put(scenario.getId(), rule);
+ }
+
+ featureByScenarioId.put(scenario.getId(), feature);
}
private void updateStep(List stepsList) {
@@ -54,17 +70,41 @@ private void updateStep(List stepsList) {
}
}
- Step getGherkinStep(String sourceId) {
- return requireNonNull(gherkinStepById.get(requireNonNull(sourceId)));
+ Step getStepBy(PickleStep pickleStep) {
+ requireNonNull(pickleStep);
+ String gherkinStepId = pickleStep.getAstNodeIds().get(0);
+ return requireNonNull(gherkinStepById.get(gherkinStepId));
+ }
+
+ Scenario getScenarioBy(Pickle pickle) {
+ requireNonNull(pickle);
+ return requireNonNull(gherkinScenarioById.get(pickle.getAstNodeIds().get(0)));
}
- Scenario getGherkinScenario(String sourceId) {
- return requireNonNull(gherkinScenarioById.get(requireNonNull(sourceId)));
+ Optional findRuleBy(Pickle pickle) {
+ requireNonNull(pickle);
+ Scenario scenario = getScenarioBy(pickle);
+ return Optional.ofNullable(ruleByScenarioId.get(scenario.getId()));
}
- Location getLocation(String sourceId) {
- Location location = locationBySourceId.get(requireNonNull(sourceId));
+ Location getLocationBy(Pickle pickle) {
+ requireNonNull(pickle);
+ List sourceIds = pickle.getAstNodeIds();
+ String sourceId = sourceIds.get(sourceIds.size() - 1);
+ Location location = locationBySourceId.get(sourceId);
return requireNonNull(location);
}
+ Optional findFeatureBy(Pickle pickle) {
+ requireNonNull(pickle);
+ Scenario scenario = getScenarioBy(pickle);
+ return Optional.ofNullable(featureByScenarioId.get(scenario.getId()));
+ }
+
+ Optional findExamplesBy(Pickle pickle) {
+ requireNonNull(pickle);
+ List sourceIds = pickle.getAstNodeIds();
+ String sourceId = sourceIds.get(sourceIds.size() - 1);
+ return Optional.ofNullable(examplesByExampleId.get(sourceId));
+ }
}
diff --git a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesPickle.java b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesPickle.java
index 0152ca744a..54303f3630 100644
--- a/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesPickle.java
+++ b/cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/GherkinMessagesPickle.java
@@ -4,13 +4,17 @@
import io.cucumber.core.gherkin.Step;
import io.cucumber.core.gherkin.StepType;
import io.cucumber.gherkin.GherkinDialect;
+import io.cucumber.messages.types.Examples;
+import io.cucumber.messages.types.Feature;
import io.cucumber.messages.types.PickleTag;
+import io.cucumber.messages.types.Rule;
import io.cucumber.messages.types.Scenario;
import io.cucumber.plugin.event.Location;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
/**
@@ -46,8 +50,7 @@ private static List createCucumberSteps(
.orElseThrow(() -> new IllegalStateException("No Given keyword for dialect: " + dialect.getName()));
for (io.cucumber.messages.types.PickleStep pickleStep : pickle.getSteps()) {
- String gherkinStepId = pickleStep.getAstNodeIds().get(0);
- io.cucumber.messages.types.Step gherkinStep = cucumberQuery.getGherkinStep(gherkinStepId);
+ io.cucumber.messages.types.Step gherkinStep = cucumberQuery.getStepBy(pickleStep);
Location location = GherkinMessagesLocation.from(gherkinStep.getLocation());
String keyword = gherkinStep.getKeyword();
@@ -62,7 +65,7 @@ private static List createCucumberSteps(
@Override
public String getKeyword() {
- return cucumberQuery.getGherkinScenario(pickle.getAstNodeIds().get(0)).getKeyword();
+ return cucumberQuery.getScenarioBy(pickle).getKeyword();
}
@Override
@@ -77,18 +80,34 @@ public String getName() {
@Override
public Location getLocation() {
- List sourceIds = pickle.getAstNodeIds();
- String sourceId = sourceIds.get(sourceIds.size() - 1);
- io.cucumber.messages.types.Location location = cucumberQuery.getLocation(sourceId);
- return GherkinMessagesLocation.from(location);
+ return GherkinMessagesLocation.from(cucumberQuery.getLocationBy(pickle));
}
@Override
public Location getScenarioLocation() {
- String sourceId = pickle.getAstNodeIds().get(0);
- Scenario scenario = cucumberQuery.getGherkinScenario(sourceId);
- io.cucumber.messages.types.Location location = scenario.getLocation();
- return GherkinMessagesLocation.from(location);
+ Scenario scenario = cucumberQuery.getScenarioBy(pickle);
+ return GherkinMessagesLocation.from(scenario.getLocation());
+ }
+
+ @Override
+ public Optional getRuleLocation() {
+ return cucumberQuery.findRuleBy(pickle)
+ .map(Rule::getLocation)
+ .map(GherkinMessagesLocation::from);
+ }
+
+ @Override
+ public Optional getFeatureLocation() {
+ return cucumberQuery.findFeatureBy(pickle)
+ .map(Feature::getLocation)
+ .map(GherkinMessagesLocation::from);
+ }
+
+ @Override
+ public Optional getExamplesLocation() {
+ return cucumberQuery.findExamplesBy(pickle)
+ .map(Examples::getLocation)
+ .map(GherkinMessagesLocation::from);
}
@Override
diff --git a/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Pickle.java b/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Pickle.java
index 1f1728f54e..4b4aaed894 100644
--- a/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Pickle.java
+++ b/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Pickle.java
@@ -4,6 +4,7 @@
import java.net.URI;
import java.util.List;
+import java.util.Optional;
public interface Pickle {
@@ -14,7 +15,7 @@ public interface Pickle {
String getName();
/**
- * Returns the location in feature file of the Scenario this pickle was
+ * Returns the location in the feature file of the Scenario this pickle was
* created from. If this pickle was created from a Scenario Outline this
* location is the location in the Example section used to fill in the place
* holders.
@@ -24,7 +25,7 @@ public interface Pickle {
Location getLocation();
/**
- * Returns the location in feature file of the Scenario this pickle was
+ * Returns the location in the feature file of the Scenario this pickle was
* created from. If this pickle was created from a Scenario Outline this
* location is that of the Scenario
*
@@ -32,6 +33,36 @@ public interface Pickle {
*/
Location getScenarioLocation();
+ /**
+ * Returns the location in the feature file of the Rule this pickle was
+ * created from.
+ *
+ * @return location in the feature file
+ */
+ default Optional getRuleLocation() {
+ return Optional.empty();
+ }
+
+ /**
+ * Returns the location in the feature file of the Feature this pickle was
+ * created from.
+ *
+ * @return location in the feature file
+ */
+ default Optional getFeatureLocation() {
+ return Optional.empty();
+ }
+
+ /**
+ * Returns the location in the feature file of the examples this pickle was
+ * created from.
+ *
+ * @return location in the feature file
+ */
+ default Optional getExamplesLocation() {
+ return Optional.empty();
+ }
+
List getSteps();
List getTags();