From 8cfd432f6a0ff6bb114d07e067e5218ca4c19b3a Mon Sep 17 00:00:00 2001 From: Kostiantyn Goloveshko Date: Sun, 8 Aug 2021 12:48:43 +0300 Subject: [PATCH] Possible redefine steps during step variable substitution --- pytest_bdd/parser.py | 2 +- pytest_bdd/scenario.py | 77 ++++++++++++++--------------------- tests/feature/test_outline.py | 15 ++++--- 3 files changed, 40 insertions(+), 54 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 6669e9b2..acf2e28e 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -457,4 +457,4 @@ def get_tags(line): STEP_PARAM_TEMPLATE = "<{param}>" -STEP_PARAM_RE = re.compile(STEP_PARAM_TEMPLATE.format(param="([^<>]+?)")) +STEP_PARAM_RE = re.compile(STEP_PARAM_TEMPLATE.format(param="((?<=<)[^<>]+(?=>))")) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 9b8ec3cd..6f818f36 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -10,9 +10,9 @@ scenario_name="Publishing the article", ) """ -import collections import os import re +from itertools import chain, product import pytest from _pytest.fixtures import FixtureLookupError @@ -27,68 +27,54 @@ ALPHA_REGEX = re.compile(r"^\d+_*") -def substitute_step_parameters(name: str, request): +def generate_partial_substituted_step_parameters(name: str, request): """Returns step name with substituted parameters from fixtures, example tables, param marks""" - while True: - match = re.search(STEP_PARAM_RE, name) - if not match: - break + yield name + matches = re.finditer(STEP_PARAM_RE, name) + for match in matches: param_name = match.group(1) - name = re.sub( + sub_name = re.sub( STEP_PARAM_TEMPLATE.format(param=re.escape(param_name)), str(request.getfixturevalue(param_name)), name, - count=1 + count=1, ) - return name + yield from generate_partial_substituted_step_parameters(sub_name, request) def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None): """Find argumented step fixture name.""" # happens to be that _arg2fixturedefs is changed during the iteration so we use a copy - step_name_parameters_substituted = substitute_step_parameters(name, request) if request else None - - for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()): - for fixturedef in fixturedefs: - parser = getattr(fixturedef.func, "parser", None) - if parser is None: - continue - - is_step_name_directly_processable = parser.is_matching(name) - is_step_name_param_substitution_needed = not is_step_name_directly_processable - is_step_name_param_substituted_processable = step_name_parameters_substituted and parser.is_matching( - step_name_parameters_substituted - ) - is_step_name_processable = is_step_name_directly_processable or is_step_name_param_substituted_processable - - if not is_step_name_processable: - continue + fixturedefs = chain.from_iterable(fixturedefs for _, fixturedefs in list(fixturemanager._arg2fixturedefs.items())) + fixturedef_funcs = (fixturedef.func for fixturedef in fixturedefs) + parsers_fixturedef_function_mappings = ( + (fixturedef_func.parser, fixturedef_func) + for fixturedef_func in fixturedef_funcs + if hasattr(fixturedef_func, "parser") + ) - if is_step_name_param_substitution_needed: - step_name = step_name_parameters_substituted - else: - step_name = name + matched_steps_with_parsers = ( + (step_name, parser, getattr(fixturedef_function, "converters", {})) + for step_name, (parser, fixturedef_function) in product( + generate_partial_substituted_step_parameters(name, request), parsers_fixturedef_function_mappings + ) + if parser.is_matching(step_name) + ) - converters = getattr(fixturedef.func, "converters", {}) + for step_name, parser, converters in matched_steps_with_parsers: + if request: for arg, value in parser.parse_arguments(step_name).items(): if arg in converters: value = converters[arg](value) - if request: - try: - overridable_fixture_value = request.getfixturevalue(arg) - except FixtureLookupError: - inject_fixture(request, arg, value) - else: - if overridable_fixture_value != value: - inject_fixture(request, arg, value) - parser_name = get_step_fixture_name(parser.name, type_) - if request: try: - request.getfixturevalue(parser_name) + overridable_fixture_value = request.getfixturevalue(arg) except FixtureLookupError: - continue - return parser_name + inject_fixture(request, arg, value) + else: + if overridable_fixture_value != value: + inject_fixture(request, arg, value) + return get_step_fixture_name(parser.name, type_) def _find_step_function(request, step, scenario): @@ -176,9 +162,6 @@ def _execute_scenario(feature, scenario, request): request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario) -FakeRequest = collections.namedtuple("FakeRequest", ["module"]) - - def _get_scenario_decorator(feature, feature_name, scenario, scenario_name): # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception # when the decorator is misused. diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 9cb9b40c..034f9564 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -125,8 +125,9 @@ def test_nested_outlining(testdir): ), ) - testdir.makeconftest(textwrap.dedent( - """\ + testdir.makeconftest( + textwrap.dedent( + """\ from pytest_bdd import given, when, then from pytest_bdd.parsers import re, parse @@ -152,7 +153,8 @@ def should_have_left_cucumbers(cucumbers_count, left): assert isinstance(left, str) assert cucumbers_count["cucumbers_count"] == float(left) """ - )) + ) + ) testdir.makepyfile( textwrap.dedent( @@ -181,7 +183,7 @@ def test_outline(request): result.assert_outcomes(passed=2) -@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS]) +@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED]) def test_outline_has_subset_of_parameters(testdir, steps): """Test parametrized scenario when the test function has a subset of the parameters of the examples.""" @@ -220,7 +222,8 @@ def test_outline(request): assert_outcomes(result, passed=1) -def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): +@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS]) +def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir, steps): """Test parametrized scenario when the test function has a parameter set which is not a subset of those in the examples table.""" testdir.makefile( @@ -240,7 +243,7 @@ def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): """ ), ) - testdir.makeconftest(textwrap.dedent(STEPS)) + testdir.makeconftest(textwrap.dedent(steps)) testdir.makepyfile( textwrap.dedent(