From 6805f9c90cd3cf78126eb822bc7c4656202c690d Mon Sep 17 00:00:00 2001 From: Jason Allen Date: Sun, 20 Oct 2024 10:43:23 +0100 Subject: [PATCH 1/5] Don't hardcode keywords --- src/pytest_bdd/cucumber_json.py | 4 ++-- src/pytest_bdd/gherkin_terminal_reporter.py | 8 ++++---- src/pytest_bdd/parser.py | 8 ++++++++ src/pytest_bdd/reporting.py | 2 ++ tests/feature/test_cucumber_json.py | 6 +++--- tests/feature/test_gherkin_terminal_reporter.py | 2 +- tests/feature/test_report.py | 8 ++++++++ 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/pytest_bdd/cucumber_json.py b/src/pytest_bdd/cucumber_json.py index d51866a6..1a072573 100644 --- a/src/pytest_bdd/cucumber_json.py +++ b/src/pytest_bdd/cucumber_json.py @@ -114,7 +114,7 @@ def stepmap(step: dict[str, Any]) -> dict[str, Any]: if scenario["feature"]["filename"] not in self.features: self.features[scenario["feature"]["filename"]] = { - "keyword": "Feature", + "keyword": scenario["feature"]["keyword"], "uri": scenario["feature"]["rel_filename"], "name": scenario["feature"]["name"] or scenario["feature"]["rel_filename"], "id": scenario["feature"]["rel_filename"].lower().replace(" ", "-"), @@ -126,7 +126,7 @@ def stepmap(step: dict[str, Any]) -> dict[str, Any]: self.features[scenario["feature"]["filename"]]["elements"].append( { - "keyword": "Scenario", + "keyword": scenario["keyword"], "id": report.item["name"], "name": scenario["name"], "line": scenario["line_number"], diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index fb73ebb6..d023928c 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -72,20 +72,20 @@ def pytest_runtest_logreport(self, report: TestReport) -> Any: if self.verbosity == 1: self.ensure_newline() - self._tw.write("Feature: ", **feature_markup) + self._tw.write(f"{report.scenario['feature']['keyword']}: ", **feature_markup) self._tw.write(report.scenario["feature"]["name"], **feature_markup) self._tw.write("\n") - self._tw.write(" Scenario: ", **scenario_markup) + self._tw.write(f" {report.scenario['keyword']}: ", **scenario_markup) self._tw.write(report.scenario["name"], **scenario_markup) self._tw.write(" ") self._tw.write(word, **word_markup) self._tw.write("\n") elif self.verbosity > 1: self.ensure_newline() - self._tw.write("Feature: ", **feature_markup) + self._tw.write(f"{report.scenario['feature']['keyword']}: ", **feature_markup) self._tw.write(report.scenario["feature"]["name"], **feature_markup) self._tw.write("\n") - self._tw.write(" Scenario: ", **scenario_markup) + self._tw.write(f" {report.scenario['keyword']}: ", **scenario_markup) self._tw.write(report.scenario["name"], **scenario_markup) self._tw.write("\n") for step in report.scenario["steps"]: diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index 93b3bfc9..9a1e2e5a 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -40,6 +40,7 @@ class Feature: scenarios: OrderedDict[str, ScenarioTemplate] filename: str rel_filename: str + keyword: str name: str | None tags: set[str] background: Background | None @@ -104,6 +105,7 @@ class ScenarioTemplate: Attributes: feature (Feature): The feature to which this scenario belongs. + keyword (str): The keyword used to define the scenario. name (str): The name of the scenario. line_number (int): The line number where the scenario starts in the file. templated (bool): Whether the scenario is templated. @@ -114,6 +116,7 @@ class ScenarioTemplate: """ feature: Feature + keyword: str name: str line_number: int templated: bool @@ -165,6 +168,7 @@ def render(self, context: Mapping[str, Any]) -> Scenario: steps = background_steps + scenario_steps return Scenario( feature=self.feature, + keyword=self.keyword, name=self.name, line_number=self.line_number, steps=steps, @@ -179,6 +183,7 @@ class Scenario: Attributes: feature (Feature): The feature to which this scenario belongs. + keyword (str): The keyword used to define the scenario. name (str): The name of the scenario. line_number (int): The line number where the scenario starts in the file. steps (List[Step]): The list of steps in the scenario. @@ -187,6 +192,7 @@ class Scenario: """ feature: Feature + keyword: str name: str line_number: int steps: list[Step] @@ -386,6 +392,7 @@ def parse_scenario(self, scenario_data: GherkinScenario, feature: Feature) -> Sc templated = bool(scenario_data.examples) scenario = ScenarioTemplate( feature=feature, + keyword=scenario_data.keyword, name=scenario_data.name, line_number=scenario_data.location.line, templated=templated, @@ -434,6 +441,7 @@ def parse(self) -> Feature: feature_data: GherkinFeature = gherkin_doc.feature feature = Feature( scenarios=OrderedDict(), + keyword=feature_data.keyword, filename=self.abs_filename, rel_filename=self.rel_filename, name=feature_data.name, diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index 39ae691e..2c4a4a2e 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -110,10 +110,12 @@ def serialize(self) -> dict[str, Any]: return { "steps": [step_report.serialize() for step_report in self.step_reports], + "keyword": scenario.keyword, "name": scenario.name, "line_number": scenario.line_number, "tags": sorted(scenario.tags), "feature": { + "keyword": feature.keyword, "name": feature.name, "filename": feature.filename, "rel_filename": feature.rel_filename, diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 34e4b1a6..d3897b77 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -170,7 +170,7 @@ def test_passing_outline(): }, { "description": "", - "keyword": "Scenario", + "keyword": "Scenario Outline", "tags": [{"line": 14, "name": "scenario-outline-passing-tag"}], "steps": [ { @@ -188,7 +188,7 @@ def test_passing_outline(): }, { "description": "", - "keyword": "Scenario", + "keyword": "Scenario Outline", "tags": [{"line": 14, "name": "scenario-outline-passing-tag"}], "steps": [ { @@ -206,7 +206,7 @@ def test_passing_outline(): }, { "description": "", - "keyword": "Scenario", + "keyword": "Scenario Outline", "tags": [{"line": 14, "name": "scenario-outline-passing-tag"}], "steps": [ { diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index ea9e4033..7db76571 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -231,7 +231,7 @@ def test_scenario_2(): result = pytester.runpytest("--gherkin-terminal-reporter", "-vv") result.assert_outcomes(passed=1, failed=0) - result.stdout.fnmatch_lines("*Scenario: Scenario example 2") + result.stdout.fnmatch_lines("*Scenario Outline: Scenario example 2") result.stdout.fnmatch_lines("*Given there are {start} cucumbers".format(**example)) result.stdout.fnmatch_lines("*When I eat {eat} cucumbers".format(**example)) result.stdout.fnmatch_lines("*Then I should have {left} cucumbers".format(**example)) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 4b943a30..533b24c8 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -106,12 +106,14 @@ def _(cucumbers, left): expected = { "feature": { "description": "", + "keyword": "Feature", "filename": str(feature), "line_number": 2, "name": "One passing scenario, one failing scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, + "keyword": "Scenario", "line_number": 5, "name": "Passing", "steps": [ @@ -141,12 +143,14 @@ def _(cucumbers, left): expected = { "feature": { "description": "", + "keyword": "Feature", "filename": str(feature), "line_number": 2, "name": "One passing scenario, one failing scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, + "keyword": "Scenario", "line_number": 10, "name": "Failing", "steps": [ @@ -175,12 +179,14 @@ def _(cucumbers, left): expected = { "feature": { "description": "", + "keyword": "Feature", "filename": str(feature), "line_number": 2, "name": "One passing scenario, one failing scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, + "keyword": "Scenario Outline", "line_number": 14, "name": "Outlined", "steps": [ @@ -217,12 +223,14 @@ def _(cucumbers, left): expected = { "feature": { "description": "", + "keyword": "Feature", "filename": str(feature), "line_number": 2, "name": "One passing scenario, one failing scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, + "keyword": "Scenario Outline", "line_number": 14, "name": "Outlined", "steps": [ From 38e112ee337dcf0f8891b5a21c3b03ec169093de Mon Sep 17 00:00:00 2001 From: Jason Allen Date: Sun, 20 Oct 2024 11:54:20 +0100 Subject: [PATCH 2/5] Add test for keyword aliases --- .../feature/test_gherkin_terminal_reporter.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index 7db76571..d034373c 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -236,3 +236,64 @@ def test_scenario_2(): result.stdout.fnmatch_lines("*When I eat {eat} cucumbers".format(**example)) result.stdout.fnmatch_lines("*Then I should have {left} cucumbers".format(**example)) result.stdout.fnmatch_lines("*PASSED") + + +def test_scenario_alias_keywords_are_accepted(pytester): + """ + Test that aliases for various keywords are accepted and reported correctly. + see https://cucumber.io/docs/gherkin/reference/ + """ + pytester.makefile( + ".feature", + simple=""" + Feature: Simple feature + Scenario: Simple scenario + Given I have a + Then pass + + Example: Simple example + Given I have a + Then pass + + Scenario Outline: Outlined scenario + Given I have a templated + Then pass + + Examples: + | foo | + | bar | + + Scenario Template: Templated scenario + Given I have a templated + Then pass + + Scenarios: + | foo | + | bar | + """, + ) + pytester.makepyfile( + """ + from pytest_bdd import scenarios, given, then, parsers + + scenarios("simple.feature") + + @given("I have a ") + def _(): + return "tag" + + @given(parsers.parse("I have a templated {foo}")) + def _(foo): + return "foo" + + @then("pass") + def _(): + pass + """ + ) + result = pytester.runpytest("--gherkin-terminal-reporter", "-vv") + result.assert_outcomes(passed=4, failed=0) + result.stdout.fnmatch_lines("*Feature: Simple feature*") + result.stdout.fnmatch_lines("*Example: Simple example*") + result.stdout.fnmatch_lines("*Scenario: Simple scenario*") + result.stdout.fnmatch_lines("*Scenario Outline: Outlined scenario*") From 9061eacc0076cfcee1dd5d6d5b1f5ccbae7dc817 Mon Sep 17 00:00:00 2001 From: jsa34 <31512041+jsa34@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:51:19 +0100 Subject: [PATCH 3/5] Update CHANGES.rst Add changes to use gherkin keywords from parser --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index f61e82e0..bb1490a2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ Unreleased ---------- - Dropped support for python 3.8. Supported python versions: 3.9, 3.10, 3.11, 3.12, 3.13. - Text after the `#` character is no longer stripped from the Scenario and Feature name. +- All gherkin-compliant keywords permitted in feature-files and correctly reported in json and terminal output. 8.0.0b2 ---------- From 3faab809fd8f5da157a2ac76845f605ef2772bb5 Mon Sep 17 00:00:00 2001 From: jsa34 <31512041+jsa34@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:57:09 +0100 Subject: [PATCH 4/5] Update CHANGES.rst Clearer maybe? --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bb1490a2..2b9421ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Unreleased ---------- - Dropped support for python 3.8. Supported python versions: 3.9, 3.10, 3.11, 3.12, 3.13. - Text after the `#` character is no longer stripped from the Scenario and Feature name. -- All gherkin-compliant keywords permitted in feature-files and correctly reported in json and terminal output. +- Gherkin keyword aliases can now be used and correctly reported in json and terminal output (see `Keywords ` for permitted list. 8.0.0b2 ---------- From 961f98a5f9912ef2e21ac65ab6387627f807009c Mon Sep 17 00:00:00 2001 From: jsa34 <31512041+jsa34@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:58:06 +0100 Subject: [PATCH 5/5] Update CHANGES.rst Missing bracket --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2b9421ff..7558181f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Unreleased ---------- - Dropped support for python 3.8. Supported python versions: 3.9, 3.10, 3.11, 3.12, 3.13. - Text after the `#` character is no longer stripped from the Scenario and Feature name. -- Gherkin keyword aliases can now be used and correctly reported in json and terminal output (see `Keywords ` for permitted list. +- Gherkin keyword aliases can now be used and correctly reported in json and terminal output (see `Keywords ` for permitted list). 8.0.0b2 ----------