From c0a790db150b2e39b8058f693ddb4b35912d7c98 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 16 May 2024 20:29:07 +0200 Subject: [PATCH 1/3] [Core] Allow feature with line syntax to target rules and examples 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 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. --- .../core/feature/FeatureWithLines.java | 4 +- .../cucumber/core/filter/LinePredicate.java | 6 +- .../core/filter/LinePredicateTest.java | 171 ++++++++++++++++-- .../core/gherkin/messages/CucumberQuery.java | 66 +++++-- .../messages/GherkinMessagesPickle.java | 41 +++-- .../java/io/cucumber/core/gherkin/Pickle.java | 35 +++- 6 files changed, 274 insertions(+), 49 deletions(-) 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..cecbb4771a 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,23 @@ 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 +47,168 @@ 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(); From ff3e2873e1de83dacbd1dc5c2381e21d66315822 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 16 May 2024 20:33:37 +0200 Subject: [PATCH 2/3] Formatting --- .../io/cucumber/core/filter/LinePredicateTest.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 cecbb4771a..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 @@ -32,8 +32,7 @@ class LinePredicateTest { " Examples: Second\n" + " | thing | \n" + " | zukini | \n" + - " | pickle | \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); @@ -130,8 +129,8 @@ void matches_first_examples() { @Test void does_not_match_example_header() { LinePredicate predicate = new LinePredicate(singletonMap( - featurePath, - singletonList(6))); + featurePath, + singletonList(6))); assertFalse(predicate.test(firstPickle)); assertFalse(predicate.test(secondPickle)); assertFalse(predicate.test(thirdPickle)); @@ -148,6 +147,7 @@ void matches_first_example() { assertFalse(predicate.test(thirdPickle)); assertFalse(predicate.test(fourthPickle)); } + @Test void Matches_second_example() { LinePredicate predicate = new LinePredicate(singletonMap( @@ -180,6 +180,7 @@ void matches_second_examples() { assertTrue(predicate.test(thirdPickle)); assertTrue(predicate.test(fourthPickle)); } + @Test void does_not_match_second_examples_header() { LinePredicate predicate = new LinePredicate(singletonMap( @@ -190,6 +191,7 @@ void does_not_match_second_examples_header() { assertFalse(predicate.test(thirdPickle)); assertFalse(predicate.test(fourthPickle)); } + @Test void matches_third_example() { LinePredicate predicate = new LinePredicate(singletonMap( @@ -200,6 +202,7 @@ void matches_third_example() { assertTrue(predicate.test(thirdPickle)); assertFalse(predicate.test(fourthPickle)); } + @Test void matches_fourth_example() { LinePredicate predicate = new LinePredicate(singletonMap( From 1dec0c1b802d1775edaa5f82229d7920eedeed5b Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 16 May 2024 21:06:19 +0200 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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