Skip to content

Commit

Permalink
[Core] Allow feature with line syntax to target rules and examples (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
mpkorstanje authored May 16, 2024
1 parent 1464e96 commit dc62378
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 50 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ class LinePredicateTest {
featurePath,
"" +
"Feature: Test feature\n" +
" Scenario Outline: Test scenario\n" +
" Given I have 4 <thing> 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 <thing> 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() {
Expand All @@ -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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,63 @@
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;

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<String, Rule> ruleByScenarioId = new HashMap<>();
private final Map<String, Examples> examplesByExampleId = new HashMap<>();
private final Map<String, Feature> featureByScenarioId = new HashMap<>();
private final Map<String, Step> gherkinStepById = new HashMap<>();
private final Map<String, Scenario> gherkinScenarioById = new HashMap<>();
private final Map<String, Location> locationBySourceId = new HashMap<>();

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));
});
});
});
}

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<Step> stepsList) {
Expand All @@ -54,17 +70,41 @@ private void updateStep(List<Step> 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<Rule> 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<String> sourceIds = pickle.getAstNodeIds();
String sourceId = sourceIds.get(sourceIds.size() - 1);
Location location = locationBySourceId.get(sourceId);
return requireNonNull(location);
}

Optional<Feature> findFeatureBy(Pickle pickle) {
requireNonNull(pickle);
Scenario scenario = getScenarioBy(pickle);
return Optional.ofNullable(featureByScenarioId.get(scenario.getId()));
}

Optional<Examples> findExamplesBy(Pickle pickle) {
requireNonNull(pickle);
List<String> sourceIds = pickle.getAstNodeIds();
String sourceId = sourceIds.get(sourceIds.size() - 1);
return Optional.ofNullable(examplesByExampleId.get(sourceId));
}
}
Loading

0 comments on commit dc62378

Please sign in to comment.