Skip to content

Commit dc62378

Browse files
authored
[Core] Allow feature with line syntax to target rules and examples (#2884)
Given a feature file, it should be possible to provide the line of a Feature, Rule, Scenario, Example and Example. Cucumber should then run all pickles contained in these elements. For example `example.feature:5:13` should run the cucumber, gherkin and pickle pickles. While `example.feature:10` runs the zukini and pickle pickles. And using either lines 1,2 or 3 would run all pickles. ```feature Feature: Example feature # 1 Rule: Example rule # 2 Scenario Outline: Example scenario # 3 Given I have 4 <thing> in my belly # 4 Examples: First # 5 | thing | # 6 | cucumber | # 7 | gherkin | # 8 # 9 Examples: Second # 10 | thing | # 11 | zukini | # 12 | pickle | # 13 ``` This should make it possible to target (groups of) pickles with a bit more flexibility. And also allow IDEA to select rules. Note: Using the lines of backgrounds and steps will still not select any pickles.
1 parent 1464e96 commit dc62378

File tree

7 files changed

+279
-50
lines changed

7 files changed

+279
-50
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ 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-
### Changed
13+
### Added
1414
- [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)
15+
- [Core] Allow feature with line syntax to target rules and examples. ([#2884](https://github.com/cucumber/cucumber-jvm/issues/2884) M.P. Korstanje)
1516

1617
## [7.17.0] - 2024-04-18
1718
### Added

cucumber-core/src/main/java/io/cucumber/core/feature/FeatureWithLines.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
import java.util.stream.Collectors;
1515

1616
/**
17-
* Identifies either a directory containing feature files, a specific feature or
18-
* specific scenarios and examples (pickles) in a feature.
17+
* Identifies either a directory containing feature files, a specific feature
18+
* file or a feature, rules, scenarios, and/or examples in a feature file.
1919
* <p>
2020
* The syntax of a feature with lines defined as either a {@link FeaturePath} or
2121
* a {@link FeatureIdentifier} followed by a sequence of line numbers each

cucumber-core/src/main/java/io/cucumber/core/filter/LinePredicate.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.cucumber.core.filter;
22

33
import io.cucumber.core.gherkin.Pickle;
4+
import io.cucumber.plugin.event.Location;
45

56
import java.net.URI;
67
import java.util.Collection;
@@ -24,7 +25,10 @@ public boolean test(Pickle pickle) {
2425
}
2526
for (Integer line : lineFilters.get(picklePath)) {
2627
if (Objects.equals(line, pickle.getLocation().getLine())
27-
|| Objects.equals(line, pickle.getScenarioLocation().getLine())) {
28+
|| Objects.equals(line, pickle.getScenarioLocation().getLine())
29+
|| pickle.getExamplesLocation().map(Location::getLine).map(line::equals).orElse(false)
30+
|| pickle.getRuleLocation().map(Location::getLine).map(line::equals).orElse(false)
31+
|| pickle.getFeatureLocation().map(Location::getLine).map(line::equals).orElse(false)) {
2832
return true;
2933
}
3034
}

cucumber-core/src/test/java/io/cucumber/core/filter/LinePredicateTest.java

Lines changed: 154 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,22 @@ class LinePredicateTest {
2121
featurePath,
2222
"" +
2323
"Feature: Test feature\n" +
24-
" Scenario Outline: Test scenario\n" +
25-
" Given I have 4 <thing> in my belly\n" +
26-
" Examples:\n" +
27-
" | thing | \n" +
28-
" | cucumber | \n" +
29-
" | gherkin | \n");
30-
private final Pickle pickle = feature.getPickles().get(0);
24+
" Rule: Test rule\n" +
25+
" Scenario Outline: Test scenario\n" +
26+
" Given I have 4 <thing> in my belly\n" +
27+
" Examples: First\n" +
28+
" | thing | \n" +
29+
" | cucumber | \n" +
30+
" | gherkin | \n" +
31+
"\n" +
32+
" Examples: Second\n" +
33+
" | thing | \n" +
34+
" | zukini | \n" +
35+
" | pickle | \n");
36+
private final Pickle firstPickle = feature.getPickles().get(0);
37+
private final Pickle secondPickle = feature.getPickles().get(1);
38+
private final Pickle thirdPickle = feature.getPickles().get(2);
39+
private final Pickle fourthPickle = feature.getPickles().get(3);
3140

3241
@Test
3342
void matches_pickles_from_files_not_in_the_predicate_map() {
@@ -37,47 +46,172 @@ void matches_pickles_from_files_not_in_the_predicate_map() {
3746
LinePredicate predicate = new LinePredicate(singletonMap(
3847
URI.create("classpath:another_path/file.feature"),
3948
singletonList(8)));
40-
assertTrue(predicate.test(pickle));
49+
assertTrue(predicate.test(firstPickle));
4150
}
4251

4352
@Test
44-
void does_not_matches_pickles_for_no_lines_in_predicate() {
53+
void empty() {
4554
LinePredicate predicate = new LinePredicate(singletonMap(
4655
featurePath,
4756
emptyList()));
48-
assertFalse(predicate.test(pickle));
57+
assertFalse(predicate.test(firstPickle));
58+
assertFalse(predicate.test(secondPickle));
59+
assertFalse(predicate.test(thirdPickle));
60+
assertFalse(predicate.test(fourthPickle));
4961
}
5062

5163
@Test
52-
void matches_pickles_for_any_line_in_predicate() {
64+
void matches_at_least_one_line() {
5365
LinePredicate predicate = new LinePredicate(singletonMap(
5466
featurePath,
55-
asList(2, 4)));
56-
assertTrue(predicate.test(pickle));
67+
asList(3, 4)));
68+
assertTrue(predicate.test(firstPickle));
69+
assertTrue(predicate.test(secondPickle));
70+
assertTrue(predicate.test(thirdPickle));
71+
assertTrue(predicate.test(fourthPickle));
5772
}
5873

5974
@Test
60-
void matches_pickles_on_scenario_location_of_the_pickle() {
75+
void matches_feature() {
76+
LinePredicate predicate = new LinePredicate(singletonMap(
77+
featurePath,
78+
singletonList(1)));
79+
assertTrue(predicate.test(firstPickle));
80+
assertTrue(predicate.test(secondPickle));
81+
assertTrue(predicate.test(thirdPickle));
82+
assertTrue(predicate.test(fourthPickle));
83+
}
84+
85+
@Test
86+
void matches_rule() {
6187
LinePredicate predicate = new LinePredicate(singletonMap(
6288
featurePath,
6389
singletonList(2)));
64-
assertTrue(predicate.test(pickle));
90+
assertTrue(predicate.test(firstPickle));
91+
assertTrue(predicate.test(secondPickle));
92+
assertTrue(predicate.test(thirdPickle));
93+
assertTrue(predicate.test(fourthPickle));
6594
}
6695

6796
@Test
68-
void matches_pickles_on_example_location_of_the_pickle() {
97+
void matches_scenario() {
6998
LinePredicate predicate = new LinePredicate(singletonMap(
7099
featurePath,
71-
singletonList(6)));
72-
assertTrue(predicate.test(pickle));
100+
singletonList(3)));
101+
assertTrue(predicate.test(firstPickle));
102+
assertTrue(predicate.test(secondPickle));
103+
assertTrue(predicate.test(thirdPickle));
104+
assertTrue(predicate.test(fourthPickle));
73105
}
74106

75107
@Test
76-
void does_not_matches_pickles_not_on_any_line_of_the_predicate() {
108+
void does_not_match_step() {
77109
LinePredicate predicate = new LinePredicate(singletonMap(
78110
featurePath,
79111
singletonList(4)));
80-
assertFalse(predicate.test(pickle));
112+
assertFalse(predicate.test(firstPickle));
113+
assertFalse(predicate.test(secondPickle));
114+
assertFalse(predicate.test(thirdPickle));
115+
assertFalse(predicate.test(fourthPickle));
116+
}
117+
118+
@Test
119+
void matches_first_examples() {
120+
LinePredicate predicate = new LinePredicate(singletonMap(
121+
featurePath,
122+
singletonList(5)));
123+
assertTrue(predicate.test(firstPickle));
124+
assertTrue(predicate.test(secondPickle));
125+
assertFalse(predicate.test(thirdPickle));
126+
assertFalse(predicate.test(fourthPickle));
127+
}
128+
129+
@Test
130+
void does_not_match_example_header() {
131+
LinePredicate predicate = new LinePredicate(singletonMap(
132+
featurePath,
133+
singletonList(6)));
134+
assertFalse(predicate.test(firstPickle));
135+
assertFalse(predicate.test(secondPickle));
136+
assertFalse(predicate.test(thirdPickle));
137+
assertFalse(predicate.test(fourthPickle));
138+
}
139+
140+
@Test
141+
void matches_first_example() {
142+
LinePredicate predicate = new LinePredicate(singletonMap(
143+
featurePath,
144+
singletonList(7)));
145+
assertTrue(predicate.test(firstPickle));
146+
assertFalse(predicate.test(secondPickle));
147+
assertFalse(predicate.test(thirdPickle));
148+
assertFalse(predicate.test(fourthPickle));
149+
}
150+
151+
@Test
152+
void Matches_second_example() {
153+
LinePredicate predicate = new LinePredicate(singletonMap(
154+
featurePath,
155+
singletonList(8)));
156+
assertFalse(predicate.test(firstPickle));
157+
assertTrue(predicate.test(secondPickle));
158+
assertFalse(predicate.test(thirdPickle));
159+
assertFalse(predicate.test(fourthPickle));
160+
}
161+
162+
@Test
163+
void does_not_match_empty_line() {
164+
LinePredicate predicate = new LinePredicate(singletonMap(
165+
featurePath,
166+
singletonList(9)));
167+
assertFalse(predicate.test(firstPickle));
168+
assertFalse(predicate.test(secondPickle));
169+
assertFalse(predicate.test(thirdPickle));
170+
assertFalse(predicate.test(fourthPickle));
171+
}
172+
173+
@Test
174+
void matches_second_examples() {
175+
LinePredicate predicate = new LinePredicate(singletonMap(
176+
featurePath,
177+
singletonList(10)));
178+
assertFalse(predicate.test(firstPickle));
179+
assertFalse(predicate.test(secondPickle));
180+
assertTrue(predicate.test(thirdPickle));
181+
assertTrue(predicate.test(fourthPickle));
182+
}
183+
184+
@Test
185+
void does_not_match_second_examples_header() {
186+
LinePredicate predicate = new LinePredicate(singletonMap(
187+
featurePath,
188+
singletonList(11)));
189+
assertFalse(predicate.test(firstPickle));
190+
assertFalse(predicate.test(secondPickle));
191+
assertFalse(predicate.test(thirdPickle));
192+
assertFalse(predicate.test(fourthPickle));
193+
}
194+
195+
@Test
196+
void matches_third_example() {
197+
LinePredicate predicate = new LinePredicate(singletonMap(
198+
featurePath,
199+
singletonList(12)));
200+
assertFalse(predicate.test(firstPickle));
201+
assertFalse(predicate.test(secondPickle));
202+
assertTrue(predicate.test(thirdPickle));
203+
assertFalse(predicate.test(fourthPickle));
204+
}
205+
206+
@Test
207+
void matches_fourth_example() {
208+
LinePredicate predicate = new LinePredicate(singletonMap(
209+
featurePath,
210+
singletonList(13)));
211+
assertFalse(predicate.test(firstPickle));
212+
assertFalse(predicate.test(secondPickle));
213+
assertFalse(predicate.test(thirdPickle));
214+
assertTrue(predicate.test(fourthPickle));
81215
}
82216

83217
}

cucumber-gherkin-messages/src/main/java/io/cucumber/core/gherkin/messages/CucumberQuery.java

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,63 @@
44
import io.cucumber.messages.types.Examples;
55
import io.cucumber.messages.types.Feature;
66
import io.cucumber.messages.types.Location;
7+
import io.cucumber.messages.types.Pickle;
8+
import io.cucumber.messages.types.PickleStep;
9+
import io.cucumber.messages.types.Rule;
710
import io.cucumber.messages.types.Scenario;
811
import io.cucumber.messages.types.Step;
912
import io.cucumber.messages.types.TableRow;
1013

1114
import java.util.HashMap;
1215
import java.util.List;
1316
import java.util.Map;
17+
import java.util.Optional;
1418

1519
import static java.util.Objects.requireNonNull;
1620

1721
final class CucumberQuery {
1822

23+
private final Map<String, Rule> ruleByScenarioId = new HashMap<>();
24+
private final Map<String, Examples> examplesByExampleId = new HashMap<>();
25+
private final Map<String, Feature> featureByScenarioId = new HashMap<>();
1926
private final Map<String, Step> gherkinStepById = new HashMap<>();
2027
private final Map<String, Scenario> gherkinScenarioById = new HashMap<>();
2128
private final Map<String, Location> locationBySourceId = new HashMap<>();
2229

2330
void update(Feature feature) {
2431
feature.getChildren().forEach(featureChild -> {
2532
featureChild.getBackground().ifPresent(this::updateBackground);
26-
featureChild.getScenario().ifPresent(this::updateScenario);
27-
featureChild.getRule().ifPresent(rule -> rule.getChildren().forEach(ruleChild -> {
28-
ruleChild.getBackground().ifPresent(this::updateBackground);
29-
ruleChild.getScenario().ifPresent(this::updateScenario);
30-
}));
33+
featureChild.getScenario().ifPresent(scenario -> updateScenario(feature, null, scenario));
34+
featureChild.getRule().ifPresent(rule -> {
35+
rule.getChildren().forEach(ruleChild -> {
36+
ruleChild.getBackground().ifPresent(this::updateBackground);
37+
ruleChild.getScenario().ifPresent(scenario -> updateScenario(feature, rule, scenario));
38+
});
39+
});
3140
});
3241
}
3342

3443
private void updateBackground(Background background) {
3544
updateStep(background.getSteps());
3645
}
3746

38-
private void updateScenario(Scenario scenario) {
47+
private void updateScenario(Feature feature, Rule rule, Scenario scenario) {
3948
gherkinScenarioById.put(requireNonNull(scenario.getId()), scenario);
4049
locationBySourceId.put(requireNonNull(scenario.getId()), scenario.getLocation());
4150
updateStep(scenario.getSteps());
4251

4352
for (Examples examples : scenario.getExamples()) {
4453
for (TableRow tableRow : examples.getTableBody()) {
45-
this.locationBySourceId.put(requireNonNull(tableRow.getId()), tableRow.getLocation());
54+
this.examplesByExampleId.put(tableRow.getId(), examples);
55+
this.locationBySourceId.put(tableRow.getId(), tableRow.getLocation());
4656
}
4757
}
58+
59+
if (rule != null) {
60+
ruleByScenarioId.put(scenario.getId(), rule);
61+
}
62+
63+
featureByScenarioId.put(scenario.getId(), feature);
4864
}
4965

5066
private void updateStep(List<Step> stepsList) {
@@ -54,17 +70,41 @@ private void updateStep(List<Step> stepsList) {
5470
}
5571
}
5672

57-
Step getGherkinStep(String sourceId) {
58-
return requireNonNull(gherkinStepById.get(requireNonNull(sourceId)));
73+
Step getStepBy(PickleStep pickleStep) {
74+
requireNonNull(pickleStep);
75+
String gherkinStepId = pickleStep.getAstNodeIds().get(0);
76+
return requireNonNull(gherkinStepById.get(gherkinStepId));
77+
}
78+
79+
Scenario getScenarioBy(Pickle pickle) {
80+
requireNonNull(pickle);
81+
return requireNonNull(gherkinScenarioById.get(pickle.getAstNodeIds().get(0)));
5982
}
6083

61-
Scenario getGherkinScenario(String sourceId) {
62-
return requireNonNull(gherkinScenarioById.get(requireNonNull(sourceId)));
84+
Optional<Rule> findRuleBy(Pickle pickle) {
85+
requireNonNull(pickle);
86+
Scenario scenario = getScenarioBy(pickle);
87+
return Optional.ofNullable(ruleByScenarioId.get(scenario.getId()));
6388
}
6489

65-
Location getLocation(String sourceId) {
66-
Location location = locationBySourceId.get(requireNonNull(sourceId));
90+
Location getLocationBy(Pickle pickle) {
91+
requireNonNull(pickle);
92+
List<String> sourceIds = pickle.getAstNodeIds();
93+
String sourceId = sourceIds.get(sourceIds.size() - 1);
94+
Location location = locationBySourceId.get(sourceId);
6795
return requireNonNull(location);
6896
}
6997

98+
Optional<Feature> findFeatureBy(Pickle pickle) {
99+
requireNonNull(pickle);
100+
Scenario scenario = getScenarioBy(pickle);
101+
return Optional.ofNullable(featureByScenarioId.get(scenario.getId()));
102+
}
103+
104+
Optional<Examples> findExamplesBy(Pickle pickle) {
105+
requireNonNull(pickle);
106+
List<String> sourceIds = pickle.getAstNodeIds();
107+
String sourceId = sourceIds.get(sourceIds.size() - 1);
108+
return Optional.ofNullable(examplesByExampleId.get(sourceId));
109+
}
70110
}

0 commit comments

Comments
 (0)