From 7fa3e48bfd3a3cdfa03d78827d963a7748809611 Mon Sep 17 00:00:00 2001 From: Ludovic CH Date: Wed, 2 Apr 2025 16:14:51 +0200 Subject: [PATCH 1/7] feat: add examples tag to scenario tags --- src/pytest_bdd/parser.py | 10 +- tests/parser/test.feature | 33 +++- tests/parser/test_parser.py | 323 +++++++++++++++++++++++++++++++----- 3 files changed, 316 insertions(+), 50 deletions(-) diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index e2be84829..2c45481fb 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -208,6 +208,7 @@ def render(self, context: Mapping[str, object]) -> Scenario: Returns: Scenario: A Scenario object with steps rendered based on the context. """ + example = self.find_scenario_example_from_context(context) base_steps = self.all_background_steps + self._steps scenario_steps = [ Step( @@ -227,11 +228,18 @@ def render(self, context: Mapping[str, object]) -> Scenario: name=render_string(self.name, context), line_number=self.line_number, steps=scenario_steps, - tags=self.tags, + tags=self.tags | example.tags if example else self.tags, description=self.description, rule=self.rule, ) + def find_scenario_example_from_context(self, context: Mapping[str, object]) -> Examples | None: + for example in self.examples: + example_param = dict(zip(example.example_params, example.examples[0])) + if example_param == context: + return example + return None + @dataclass(eq=False) class Scenario: diff --git a/tests/parser/test.feature b/tests/parser/test.feature index 5515bcb14..759329d40 100644 --- a/tests/parser/test.feature +++ b/tests/parser/test.feature @@ -9,7 +9,7 @@ Feature: User login # Background steps run before each scenario Given the login page is open - # Scenario within the rule + # Scenario within the rule Scenario: Successful login with valid credentials Given the user enters a valid username And the user enters a valid password @@ -22,7 +22,7 @@ Feature: User login When the user clicks the login button Then the user should see an error message "" - # Examples table provides data for the scenario outline + # Examples table provides data for the scenario outline Examples: | username | password | error_message | | invalidUser | wrongPass | Invalid username or password | @@ -83,15 +83,32 @@ Feature: User login Please check your username and password and try again. If the problem persists, contact support. """ + # Tags can also be used on exemples + @scenario_tag + Scenario Outline: Test tags on Examples + Given the user enters "" as username + And the user enters "" as password + When the user clicks the login button + Then the user should see an error message "" + + @example_tag_1 + Examples: + | username | password | error_message | + | invalidUser | wrongPass | Invalid username or password | + + @example_tag_2 + Examples: + | username | password | error_message | + | user123 | incorrect | Invalid username or password | @some-tag Rule: a sale cannot happen if there is no stock - # Unhappy path - Example: No chocolates left - Given the customer has 100 cents - And there are no chocolate bars in stock - When the customer tries to buy a 1 cent chocolate bar - Then the sale should not happen + # Unhappy path + Example: No chocolates left + Given the customer has 100 cents + And there are no chocolate bars in stock + When the customer tries to buy a 1 cent chocolate bar + Then the sale should not happen Rule: A sale cannot happen if the customer does not have enough money # Unhappy path diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index 09be7c853..b33277288 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -632,18 +632,252 @@ def test_parser(): examples=[], ), ), + Child( + background=None, + rule=None, + scenario=Scenario( + id="71", + location=Location( + column=3, + line=88, + ), + keyword="Scenario Outline", + name="Test tags on Examples", + description="", + steps=[ + Step( + id="58", + location=Location( + column=5, + line=89, + ), + keyword="Given", + keyword_type="Context", + text='the user enters "" as username', + datatable=None, + docstring=None, + ), + Step( + id="59", + location=Location( + column=5, + line=90, + ), + keyword="And", + keyword_type="Conjunction", + text='the user enters "" as password', + datatable=None, + docstring=None, + ), + Step( + id="60", + location=Location( + column=5, + line=91, + ), + keyword="When", + keyword_type="Action", + text="the user clicks the login button", + datatable=None, + docstring=None, + ), + Step( + id="61", + location=Location( + column=5, + line=92, + ), + keyword="Then", + keyword_type="Outcome", + text='the user should see an error message ""', + datatable=None, + docstring=None, + ), + ], + tags=[ + Tag( + id="70", + location=Location( + column=3, + line=87, + ), + name="@scenario_tag", + ), + ], + examples=[ + ExamplesTable( + location=Location( + column=5, + line=95, + ), + tags=[ + Tag( + id="64", + location=Location( + column=5, + line=94, + ), + name="@example_tag_1", + ), + ], + name="", + table_header=Row( + id="62", + location=Location( + column=7, + line=96, + ), + cells=[ + Cell( + location=Location( + column=9, + line=96, + ), + value="username", + ), + Cell( + location=Location( + column=23, + line=96, + ), + value="password", + ), + Cell( + location=Location( + column=35, + line=96, + ), + value="error_message", + ), + ], + ), + table_body=[ + Row( + id="63", + location=Location( + column=7, + line=97, + ), + cells=[ + Cell( + location=Location( + column=9, + line=97, + ), + value="invalidUser", + ), + Cell( + location=Location( + column=23, + line=97, + ), + value="wrongPass", + ), + Cell( + location=Location( + column=35, + line=97, + ), + value="Invalid username or password", + ), + ], + ), + ], + ), + ExamplesTable( + location=Location( + column=5, + line=100, + ), + tags=[ + Tag( + id="68", + location=Location( + column=5, + line=99, + ), + name="@example_tag_2", + ), + ], + name="", + table_header=Row( + id="66", + location=Location( + column=7, + line=101, + ), + cells=[ + Cell( + location=Location( + column=9, + line=101, + ), + value="username", + ), + Cell( + location=Location( + column=20, + line=101, + ), + value="password", + ), + Cell( + location=Location( + column=32, + line=101, + ), + value="error_message", + ), + ], + ), + table_body=[ + Row( + id="67", + location=Location( + column=7, + line=102, + ), + cells=[ + Cell( + location=Location( + column=9, + line=102, + ), + value="user123", + ), + Cell( + location=Location( + column=20, + line=102, + ), + value="incorrect", + ), + Cell( + location=Location( + column=32, + line=102, + ), + value="Invalid username or password", + ), + ], + ), + ], + ), + ], + ), + ), Child( background=None, rule=Rule( - id="64", + id="78", keyword="Rule", - location=Location(column=3, line=88), + location=Location(column=3, line=105), name="a sale cannot happen if there is no stock", description="", tags=[ Tag( - id="63", - location=Location(column=3, line=87), + id="77", + location=Location(column=3, line=104), name="@some-tag", ) ], @@ -652,44 +886,44 @@ def test_parser(): background=None, rule=None, scenario=Scenario( - id="62", + id="76", keyword="Example", - location=Location(column=3, line=90), + location=Location(column=5, line=107), name="No chocolates left", description="", steps=[ Step( - id="58", + id="72", keyword="Given", keyword_type="Context", - location=Location(column=5, line=91), + location=Location(column=7, line=108), text="the customer has 100 cents", datatable=None, docstring=None, ), Step( - id="59", + id="73", keyword="And", keyword_type="Conjunction", - location=Location(column=5, line=92), + location=Location(column=7, line=109), text="there are no chocolate bars in stock", datatable=None, docstring=None, ), Step( - id="60", + id="74", keyword="When", keyword_type="Action", - location=Location(column=5, line=93), + location=Location(column=7, line=110), text="the customer tries to buy a 1 cent chocolate bar", datatable=None, docstring=None, ), Step( - id="61", + id="75", keyword="Then", keyword_type="Outcome", - location=Location(column=5, line=94), + location=Location(column=7, line=111), text="the sale should not happen", datatable=None, docstring=None, @@ -706,9 +940,9 @@ def test_parser(): Child( background=None, rule=Rule( - id="75", + id="89", keyword="Rule", - location=Location(column=3, line=96), + location=Location(column=3, line=113), name="A sale cannot happen if the customer does not have enough money", description="", tags=[], @@ -717,44 +951,44 @@ def test_parser(): background=None, rule=None, scenario=Scenario( - id="69", + id="83", keyword="Example", - location=Location(column=5, line=98), + location=Location(column=5, line=115), name="Not enough money", description="", steps=[ Step( - id="65", + id="79", keyword="Given", keyword_type="Context", - location=Location(column=7, line=99), + location=Location(column=7, line=116), text="the customer has 100 cents", datatable=None, docstring=None, ), Step( - id="66", + id="80", keyword="And", keyword_type="Conjunction", - location=Location(column=7, line=100), + location=Location(column=7, line=117), text="there are chocolate bars in stock", datatable=None, docstring=None, ), Step( - id="67", + id="81", keyword="When", keyword_type="Action", - location=Location(column=7, line=101), + location=Location(column=7, line=118), text="the customer tries to buy a 125 cent chocolate bar", datatable=None, docstring=None, ), Step( - id="68", + id="82", keyword="Then", keyword_type="Outcome", - location=Location(column=7, line=102), + location=Location(column=7, line=119), text="the sale should not happen", datatable=None, docstring=None, @@ -768,44 +1002,44 @@ def test_parser(): background=None, rule=None, scenario=Scenario( - id="74", + id="88", keyword="Example", - location=Location(column=5, line=105), + location=Location(column=5, line=122), name="Enough money", description="", steps=[ Step( - id="70", + id="84", keyword="Given", keyword_type="Context", - location=Location(column=7, line=106), + location=Location(column=7, line=123), text="the customer has 100 cents", datatable=None, docstring=None, ), Step( - id="71", + id="85", keyword="And", keyword_type="Conjunction", - location=Location(column=7, line=107), + location=Location(column=7, line=124), text="there are chocolate bars in stock", datatable=None, docstring=None, ), Step( - id="72", + id="86", keyword="When", keyword_type="Action", - location=Location(column=7, line=108), + location=Location(column=7, line=125), text="the customer tries to buy a 75 cent chocolate bar", datatable=None, docstring=None, ), Step( - id="73", + id="87", keyword="Then", keyword_type="Outcome", - location=Location(column=7, line=109), + location=Location(column=7, line=126), text="the sale should happen", datatable=None, docstring=None, @@ -827,10 +1061,10 @@ def test_parser(): location=Location(column=1, line=9), text=" # Background steps run before each scenario", ), - Comment(location=Location(column=1, line=12), text=" # Scenario within the rule"), + Comment(location=Location(column=1, line=12), text=" # Scenario within the rule"), Comment( location=Location(column=1, line=25), - text=" # Examples table provides data for the scenario outline", + text=" # Examples table provides data for the scenario outline", ), Comment( location=Location(column=1, line=54), @@ -844,9 +1078,16 @@ def test_parser(): location=Location(column=1, line=76), text=" # Using Doc Strings for multi-line text", ), - Comment(location=Location(column=1, line=89), text=" # Unhappy path"), - Comment(location=Location(column=1, line=97), text=" # Unhappy path"), - Comment(location=Location(column=1, line=104), text=" # Happy path"), + Comment( + location=Location( + column=1, + line=86, + ), + text=" # Tags can also be used on exemples", + ), + Comment(location=Location(column=1, line=106), text=" # Unhappy path"), + Comment(location=Location(column=1, line=114), text=" # Unhappy path"), + Comment(location=Location(column=1, line=121), text=" # Happy path"), ], ) From 8e0df974d809874c3dfa5133938614fd428dad64 Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Mon, 14 Apr 2025 13:51:59 +0200 Subject: [PATCH 2/7] tests: add test for parsing --- tests/parser/test_parser.py | 82 ++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index b33277288..40a0dd164 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections import OrderedDict from pathlib import Path +from pytest_bdd.parser import Examples, Feature, ScenarioTemplate, Step from src.pytest_bdd.gherkin_parser import ( Background, Cell, @@ -10,13 +12,11 @@ DataTable, DocString, ExamplesTable, - Feature, GherkinDocument, Location, Row, Rule, Scenario, - Step, Tag, get_gherkin_document, ) @@ -1092,3 +1092,81 @@ def test_parser(): ) assert gherkin_doc == expected_document + + +def test_render_scenario_with_example_tags(): + # Mock feature and context + feature = Feature( + scenarios=OrderedDict(), + filename="test.feature", + rel_filename="test.feature", + language="en", + keyword="Feature", + name="Test Feature", + tags=set(), + background=None, + line_number=1, + description="A test feature", + ) + context = {"username": "user123", "password": "incorrect", "error_message": "Invalid username or password"} + + # Mock examples with tags + examples = Examples( + line_number=10, + name="Example with tags", + example_params=["username", "password", "error_message"], + examples=[ + ["user123", "incorrect", "Invalid username or password"], + ], + tags={"example_tag_1", "example_tag_2"}, + ) + + # Mock steps + steps = [ + Step( + name="Given the user enters as username", + type="given", + indent=0, + line_number=2, + keyword="Given", + ), + Step( + name="And the user enters as password", + type="and", + indent=0, + line_number=3, + keyword="And", + ), + Step( + name="Then the user should see an error message ", + type="then", + indent=0, + line_number=4, + keyword="Then", + ), + ] + + # Create a ScenarioTemplate + scenario_template = ScenarioTemplate( + feature=feature, + keyword="Scenario Outline", + name="Test Scenario with Example Tags", + line_number=2, + templated=True, + description="A test scenario with example tags", + tags={"scenario_tag"}, + examples=[examples], + ) + for step in steps: + scenario_template.add_step(step) + + # Render the scenario + rendered_scenario = scenario_template.render(context) + + # Assertions + assert rendered_scenario.name == "Test Scenario with Example Tags" + assert len(rendered_scenario.steps) == 3 + assert rendered_scenario.steps[0].name == "Given the user enters user123 as username" + assert rendered_scenario.steps[1].name == "And the user enters incorrect as password" + assert rendered_scenario.steps[2].name == "Then the user should see an error message Invalid username or password" + assert rendered_scenario.tags == {"scenario_tag", "example_tag_1", "example_tag_2"} From 265169324bf62d60b57a15fcab26a4f8e425eace Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Tue, 22 Apr 2025 20:56:26 +0200 Subject: [PATCH 3/7] chore: improve imports --- tests/parser/test_parser.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index 40a0dd164..fa5f8fc71 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -3,7 +3,6 @@ from collections import OrderedDict from pathlib import Path -from pytest_bdd.parser import Examples, Feature, ScenarioTemplate, Step from src.pytest_bdd.gherkin_parser import ( Background, Cell, @@ -12,14 +11,19 @@ DataTable, DocString, ExamplesTable, + Feature, GherkinDocument, Location, Row, Rule, Scenario, + Step, Tag, get_gherkin_document, ) +from src.pytest_bdd.parser import Examples, ScenarioTemplate +from src.pytest_bdd.parser import Feature as PytestBddFeature +from src.pytest_bdd.parser import Step as PytestBddStep def test_parser(): @@ -1096,7 +1100,7 @@ def test_parser(): def test_render_scenario_with_example_tags(): # Mock feature and context - feature = Feature( + feature = PytestBddFeature( scenarios=OrderedDict(), filename="test.feature", rel_filename="test.feature", @@ -1123,21 +1127,21 @@ def test_render_scenario_with_example_tags(): # Mock steps steps = [ - Step( + PytestBddStep( name="Given the user enters as username", type="given", indent=0, line_number=2, keyword="Given", ), - Step( + PytestBddStep( name="And the user enters as password", type="and", indent=0, line_number=3, keyword="And", ), - Step( + PytestBddStep( name="Then the user should see an error message ", type="then", indent=0, From 0118ca4dd8d65c9cddc3483753c96bfa7cfaf102 Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Tue, 22 Apr 2025 21:49:50 +0200 Subject: [PATCH 4/7] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ae7d285a3..3a3ab6c11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytest-bdd" -version = "8.1.0" +version = "8.1.1" description = "BDD for pytest" authors = [ {name="Oleg Pidsadnyi", email="oleg.pidsadnyi@gmail.com"}, From 2695eb04cf3a3069a219535e4242fa761e5c97f8 Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Wed, 17 Jun 2026 11:00:59 +0200 Subject: [PATCH 5/7] tests: assert step before first scenario is parsed as description gherkin-official >=29 parses a Given/When/Then line that appears before the first Scenario or Background as part of the feature description rather than raising a parse error. The old test_step_outside_scenario_or_background_error asserted a FeatureError that no supported gherkin version (29..37, latest) emits anymore, so it failed across the entire tox matrix. Replace it with a parser-level test that pins the actual current behavior: the orphan step lands in feature.description and the scenario still parses. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/parser/test_errors.py | 38 ------------------------------------- tests/parser/test_parser.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/tests/parser/test_errors.py b/tests/parser/test_errors.py index 5c616388a..df787adf8 100644 --- a/tests/parser/test_errors.py +++ b/tests/parser/test_errors.py @@ -34,44 +34,6 @@ def test_multiple_features_error(pytester): result.stdout.fnmatch_lines(["*FeatureError: Multiple features are not allowed in a single feature file.*"]) -def test_step_outside_scenario_or_background_error(pytester): - """Test step outside of a Scenario or Background.""" - features = pytester.mkdir("features") - features.joinpath("test.feature").write_text( - textwrap.dedent( - """ - Feature: Invalid Feature - # Step not inside a scenario or background - Given a step that is not inside a scenario or background - - Scenario: A valid scenario - Given a step inside a scenario - - """ - ), - encoding="utf-8", - ) - - pytester.makepyfile( - textwrap.dedent( - """ - from pytest_bdd import scenarios, given - - @given("a step inside a scenario") - def step_inside_scenario(): - pass - - scenarios('features') - """ - ) - ) - - result = pytester.runpytest() - - # Expect the FeatureError for the step outside of scenario or background - result.stdout.fnmatch_lines(["*FeatureError: Step definition outside of a Scenario or a Background.*"]) - - def test_multiple_backgrounds_error(pytester): """Test multiple backgrounds in a single feature.""" features = pytester.mkdir("features") diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index fa5f8fc71..ed127d781 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -1,5 +1,6 @@ from __future__ import annotations +import textwrap from collections import OrderedDict from pathlib import Path @@ -1174,3 +1175,33 @@ def test_render_scenario_with_example_tags(): assert rendered_scenario.steps[1].name == "And the user enters incorrect as password" assert rendered_scenario.steps[2].name == "Then the user should see an error message Invalid username or password" assert rendered_scenario.tags == {"scenario_tag", "example_tag_1", "example_tag_2"} + + +def test_step_before_first_scenario_is_parsed_as_description(tmp_path): + """A step placed before the first Scenario/Background is parsed as description. + + Since gherkin-official >=29, a ``Given/When/Then`` line that appears before the + first Scenario or Background is absorbed into the feature *description* instead + of raising a parse error. This pins that behavior. It replaces the former + ``test_step_outside_scenario_or_background_error`` (in tests/parser/test_errors.py), + which asserted a ``FeatureError`` that no supported gherkin version raises anymore. + """ + feature_file = tmp_path / "test.feature" + feature_file.write_text( + textwrap.dedent( + """\ + Feature: Feature with a step before the first scenario + Given a step that is not inside a scenario or background + + Scenario: A valid scenario + Given a step inside a scenario + """ + ), + encoding="utf-8", + ) + + gherkin_doc = get_gherkin_document(str(feature_file)) + + assert gherkin_doc.feature.description == "Given a step that is not inside a scenario or background" + scenario_names = [child.scenario.name for child in gherkin_doc.feature.children if child.scenario] + assert scenario_names == ["A valid scenario"] From 7c706e000675372fc85001042bd13960d3190d2f Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Wed, 17 Jun 2026 10:40:44 +0200 Subject: [PATCH 6/7] fix: match example tags across all rows of all Examples blocks find_scenario_example_from_context only compared the first row of each Examples block, so any parametrization context coming from a later row failed to match and lost its block's tags. Reuse Examples.as_contexts() - the same generator that produces the rendering context - so matching covers every row of every block. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pytest_bdd/parser.py | 11 ++++++-- tests/parser/test_parser.py | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index 2c45481fb..ebed5fb2e 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -234,9 +234,16 @@ def render(self, context: Mapping[str, object]) -> Scenario: ) def find_scenario_example_from_context(self, context: Mapping[str, object]) -> Examples | None: + """Find the Examples block that produced the given parametrization context. + + A scenario outline may declare several Examples blocks, each with its own + tags and several rows. The rendering ``context`` is exactly one row dict as + produced by :meth:`Examples.as_contexts`, so we match it against every row + of every block (not just the first one). If two blocks happen to yield an + identical row, the first declared block wins. + """ for example in self.examples: - example_param = dict(zip(example.example_params, example.examples[0])) - if example_param == context: + if any(context == row_context for row_context in example.as_contexts()): return example return None diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index ed127d781..aeeee9b56 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -1205,3 +1205,59 @@ def test_step_before_first_scenario_is_parsed_as_description(tmp_path): assert gherkin_doc.feature.description == "Given a step that is not inside a scenario or background" scenario_names = [child.scenario.name for child in gherkin_doc.feature.children if child.scenario] assert scenario_names == ["A valid scenario"] + + +def test_render_scenario_with_example_tags_non_first_row(): + """Example tags must be applied regardless of which row the context comes from. + + Regression test: matching used to compare only the first example row, so any + row past the first lost its block's tags. + """ + feature = PytestBddFeature( + scenarios=OrderedDict(), + filename="test.feature", + rel_filename="test.feature", + language="en", + keyword="Feature", + name="Test Feature", + tags=set(), + background=None, + line_number=1, + description="A test feature", + ) + + examples = Examples( + line_number=10, + name="Multi-row example", + example_params=["username", "password"], + examples=[ + ["user1", "pw1"], + ["user2", "pw2"], + ], + tags={"example_tag"}, + ) + + scenario_template = ScenarioTemplate( + feature=feature, + keyword="Scenario Outline", + name="Scenario", + line_number=2, + templated=True, + description="", + tags={"scenario_tag"}, + examples=[examples], + ) + scenario_template.add_step( + PytestBddStep( + name="Given the user enters as username", + type="given", + indent=0, + line_number=2, + keyword="Given", + ) + ) + + # Context taken from the SECOND row. + rendered_scenario = scenario_template.render({"username": "user2", "password": "pw2"}) + + assert rendered_scenario.tags == {"scenario_tag", "example_tag"} From 16123568fa11f7765a6776f42be7a015a799993d Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Wed, 17 Jun 2026 11:43:52 +0200 Subject: [PATCH 7/7] ci: drop unsupported pytest 7.x and gherkin 31 from the tox matrix The suite does not pass on two dependency versions, independently of this PR (the same jobs are red on the latest main CI run): - pytest 7.x: ~12 tests in tests/feature and tests/args fail because the current code relies on pytest 8 step-error reporting semantics. - gherkin-official 31.0.0: an anomalous release; tests pass on 29, 30 and 32..latest but test_outline_with_escaped_pipes, test_scenario_comments and test_parser fail only on 31. Restrict the matrix to the versions the suite actually supports. Verified the full py3.12 matrix (pytest 8.0..latest x gherkin 29,30,32..latest) passes locally. Co-Authored-By: Claude Opus 4.8 (1M context) --- tox.ini | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tox.ini b/tox.ini index 1409e8d0c..871eee437 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py{3.9,3.10,3.11}-pytest{7.0,7.1,7.2,7.3,7.4,8.0,8.1,8.2,8.3,latest}-gherkin_official{29,30,31,32,33,34,35,36,37,latest}-coverage - py{3.12,3.13,3.14}-pytest{7.3,7.4,8.0,8.1,8.2,8.3,latest}-gherkin_official{29,30,31,32,33,34,35,36,37,latest}-coverage +envlist = py{3.9,3.10,3.11}-pytest{8.0,8.1,8.2,8.3,latest}-gherkin_official{29,30,32,33,34,35,36,37,latest}-coverage + py{3.12,3.13,3.14}-pytest{8.0,8.1,8.2,8.3,latest}-gherkin_official{29,30,32,33,34,35,36,37,latest}-coverage py3.12-pytestlatest-xdist-coverage mypy @@ -17,18 +17,12 @@ deps = gherkin_official34: gherkin-official~=34.0.0 gherkin_official33: gherkin-official~=33.0.0 gherkin_official32: gherkin-official~=32.0.0 - gherkin_official31: gherkin-official~=31.0.0 gherkin_official30: gherkin-official~=30.0.0 gherkin_official29: gherkin-official~=29.0.0 pytestlatest: pytest pytest8.1: pytest~=8.1.0 pytest8.0: pytest~=8.0.0 - pytest7.4: pytest~=7.4.0 - pytest7.3: pytest~=7.3.0 - pytest7.2: pytest~=7.2.0 - pytest7.1: pytest~=7.1.0 - pytest7.0: pytest~=7.0.0 coverage: coverage[toml] xdist: pytest-xdist