diff --git a/CHANGES.rst b/CHANGES.rst index 9240bfd95..68381b03d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,15 +6,16 @@ Unreleased This release introduces breaking changes in order to be more in line with the official gherkin specification. -- Cleanup of the documentation and tests related to parametrization (elchupanebrej) -- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) -- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) -- Step arguments are no longer fixtures (olegpidsadnyi) -- Drop support of python 3.6, pytest 4 (elchupanebrej) -- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux) -- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) -- Add type decorations -- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods. +- Cleanup of the documentation and tests related to parametrization (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/469 +- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/490 +- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/492 +- Step arguments are no longer fixtures (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/493 +- Drop support of python 3.6, pytest 4 (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/495 https://github.com/pytest-dev/pytest-bdd/pull/504 +- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux) https://github.com/pytest-dev/pytest-bdd/pull/503 +- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/499 +- Add type annotations (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505 +- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods. (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505 +- Angular brackets in step definitions are only parsed in "Scenario Outline" (previously they were parsed also in normal "Scenario"s) (youtux) https://github.com/pytest-dev/pytest-bdd/pull/524. diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 0929208c1..051346580 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -153,11 +153,17 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu # Remove Feature, Given, When, Then, And keyword, parsed_line = parse_line(clean_line) + if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: tags = get_tags(prev_line) - feature.scenarios[parsed_line] = scenario = ScenarioTemplate( - feature=feature, name=parsed_line, line_number=line_number, tags=tags + scenario = ScenarioTemplate( + feature=feature, + name=parsed_line, + line_number=line_number, + tags=tags, + templated=mode == types.SCENARIO_OUTLINE, ) + feature.scenarios[parsed_line] = scenario elif mode == types.BACKGROUND: feature.background = Background(feature=feature, line_number=line_number) elif mode == types.EXAMPLES: @@ -210,7 +216,7 @@ class ScenarioTemplate: Created when parsing the feature file, it will then be combined with the examples to create a Scenario.""" - def __init__(self, feature: Feature, name: str, line_number: int, tags=None) -> None: + def __init__(self, feature: Feature, name: str, line_number: int, templated: bool, tags=None) -> None: """ :param str name: Scenario name. @@ -223,6 +229,7 @@ def __init__(self, feature: Feature, name: str, line_number: int, tags=None) -> self.examples = Examples() self.line_number = line_number self.tags = tags or set() + self.templated = templated def add_step(self, step: Step) -> None: """Add step to the scenario. @@ -238,16 +245,21 @@ def steps(self) -> list[Step]: return (background.steps if background else []) + self._steps def render(self, context: Mapping[str, Any]) -> Scenario: - steps = [ - Step( - name=templated_step.render(context), - type=templated_step.type, - indent=templated_step.indent, - line_number=templated_step.line_number, - keyword=templated_step.keyword, - ) - for templated_step in self.steps - ] + background_steps = self.feature.background.steps if self.feature.background else [] + if not self.templated: + scenario_steps = self._steps + else: + scenario_steps = [ + Step( + name=step.render(context), + type=step.type, + indent=step.indent, + line_number=step.line_number, + keyword=step.keyword, + ) + for step in self._steps + ] + steps = background_steps + scenario_steps return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) @@ -255,7 +267,7 @@ class Scenario: """Scenario.""" - def __init__(self, feature: Feature, name: str, line_number: int, steps: list[Step], tags=None) -> None: + def __init__(self, feature: Feature, name: str, line_number: int, steps: list[Step] = None, tags=None) -> None: """Scenario constructor. :param pytest_bdd.parser.Feature feature: Feature. @@ -263,6 +275,8 @@ def __init__(self, feature: Feature, name: str, line_number: int, steps: list[St :param int line_number: Scenario line number. :param set tags: Set of tags. """ + if steps is None: + steps = [] self.feature = feature self.name = name self.steps = steps diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index cc43d5f23..7adb206c0 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -74,18 +74,18 @@ def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) try: # Simple case where no parser is used for the step return request.getfixturevalue(get_step_fixture_name(name, step.type)) - except FixtureLookupError: + except FixtureLookupError as e: try: # Could not find a fixture with the same name, let's see if there is a parser involved argumented_name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request) if argumented_name: return request.getfixturevalue(argumented_name) - raise - except FixtureLookupError: + raise e + except FixtureLookupError as e2: raise exceptions.StepDefinitionNotFoundError( f"Step definition is not found: {step}. " f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"' - ) + ) from e2 def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable) -> None: diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 580874dcc..bd6273912 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -62,7 +62,7 @@ def test_step_trace(testdir): And a failing step @scenario-outline-passing-tag - Scenario: Passing outline + Scenario Outline: Passing outline Given type and value Examples: example1 diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 9acb3ac00..a8794f5c3 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -267,7 +267,7 @@ def test_complex_types(testdir, pytestconfig): """ Feature: Report serialization containing parameters of complex types - Scenario: Complex + Scenario Outline: Complex Given there is a coordinate Examples: diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 2caeee7ea..e6cc179e4 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -148,3 +148,49 @@ def bar(): ) result = testdir.runpytest_subprocess(*pytest_params) result.assert_outcomes(passed=1) + + +def test_angular_brakets_are_not_parsed(testdir): + """Test that angular brackets are not parsed for "Scenario"s. + + (They should be parsed only when used in "Scenario Outline") + + """ + testdir.makefile( + ".feature", + simple=""" + Feature: Simple feature + Scenario: Simple scenario + Given I have a + Then pass + + Scenario Outline: Outlined scenario + Given I have a templated + Then pass + + Examples: + | foo | + | bar | + """, + ) + testdir.makepyfile( + """ + from pytest_bdd import scenarios, given, then, parsers + + scenarios("simple.feature") + + @given("I have a ") + def bar(): + return "tag" + + @given(parsers.parse("I have a templated {foo}")) + def bar(foo): + return "foo" + + @then("pass") + def bar(): + pass + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=2)