diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 344c181..25d5490 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -1,3 +1,4 @@ +import io import os.path import re import textwrap @@ -456,5 +457,4 @@ def get_tags(line): return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1} -STEP_PARAM_TEMPLATE = "<{param}>" -STEP_PARAM_RE = re.compile(STEP_PARAM_TEMPLATE.format(param="((?<=<)[^<>]+(?=>))")) +STEP_PARAM_RE = re.compile(r"\<(.+?)\>") diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index c8bf75f..5a8729d 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -10,16 +10,15 @@ test_publish_article = scenario( scenario_name="Publishing the article", ) """ +import collections import os import re -from itertools import chain, product import pytest from _pytest.fixtures import FixtureLookupError from . import exceptions from .feature import get_feature, get_features -from .parser import STEP_PARAM_RE, STEP_PARAM_TEMPLATE from .steps import get_step_fixture_name, inject_fixture from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path @@ -27,61 +26,31 @@ PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") -def generate_partial_substituted_step_parameters(name: str, request): - """\ - Returns step name with substituted parameters from fixtures, example tables, param marks starting - from most substituted to less, so giving chance to most specific parser - """ - matches = re.finditer(STEP_PARAM_RE, name) - for match in matches: - param_name = match.group(1) - try: - sub_name = re.sub( - STEP_PARAM_TEMPLATE.format(param=re.escape(param_name)), - str(request.getfixturevalue(param_name)), - name, - count=1, - ) - except FixtureLookupError: - continue - else: - yield from generate_partial_substituted_step_parameters(sub_name, request) - yield name - - 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 + for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()): + for fixturedef in fixturedefs: + parser = getattr(fixturedef.func, "parser", None) + if parser is None: + continue + match = parser.is_matching(name) + if not match: + 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") - ) - - 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) - ) - - for step_name, parser, converters in matched_steps_with_parsers: - if request: - for arg, value in parser.parse_arguments(step_name).items(): + converters = getattr(fixturedef.func, "converters", {}) + for arg, value in parser.parse_arguments(name).items(): if arg in converters: value = converters[arg](value) - try: - overridable_fixture_value = request.getfixturevalue(arg) - except FixtureLookupError: + if request: inject_fixture(request, arg, value) - else: - if overridable_fixture_value != value: - inject_fixture(request, arg, value) - return get_step_fixture_name(parser.name, type_) + parser_name = get_step_fixture_name(parser.name, type_) + if request: + try: + request.getfixturevalue(parser_name) + except FixtureLookupError: + continue + return parser_name def _find_step_function(request, step, scenario): @@ -169,6 +138,9 @@ 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 802bc40..92a1d6d 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -1,28 +1,25 @@ """Scenario Outline tests.""" import textwrap -from pytest import mark - from tests.utils import assert_outcomes -FLOAT_NUMBER_PATTERN = r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?" -STEPS_TEMPLATE = """\ +STEPS = """\ from pytest_bdd import given, when, then -from pytest_bdd.parsers import re -{given_decorator_definition} + +@given("there are cucumbers", target_fixture="start_cucumbers") def start_cucumbers(start): assert isinstance(start, int) return dict(start=start) -{when_decorator_definition} +@when("I eat cucumbers") def eat_cucumbers(start_cucumbers, eat): assert isinstance(eat, float) start_cucumbers["eat"] = eat -{then_decorator_definition} +@then("I should have cucumbers") def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert isinstance(left, str) assert start - eat == int(left) @@ -32,31 +29,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): """ -STRING_STEPS = STEPS_TEMPLATE.format( - given_decorator_definition='@given("there are cucumbers", target_fixture="start_cucumbers")', - when_decorator_definition='@when("I eat cucumbers")', - then_decorator_definition='@then("I should have cucumbers")', -) - -PARSER_STEPS = STEPS_TEMPLATE.format( - given_decorator_definition=f'@given(re("there are (?P{FLOAT_NUMBER_PATTERN}) cucumbers"), ' - f'target_fixture="start_cucumbers")', - when_decorator_definition=f'@when(re("I eat (?P{FLOAT_NUMBER_PATTERN}) cucumbers"))', - then_decorator_definition=f'@then(re("I should have (?P{FLOAT_NUMBER_PATTERN}) cucumbers"))', -) - -PARSER_STEPS_CONVERTED = STEPS_TEMPLATE.format( - given_decorator_definition=f'@given(re("there are (?P{FLOAT_NUMBER_PATTERN}) cucumbers"), ' - f'target_fixture="start_cucumbers", converters=dict(start=int))', - when_decorator_definition=f'@when(re("I eat (?P{FLOAT_NUMBER_PATTERN}) cucumbers"), ' - f"converters=dict(eat=float))", - then_decorator_definition=f'@then(re("I should have (?P{FLOAT_NUMBER_PATTERN}) cucumbers"), ' - f"converters=dict(left=str))", -) - - -@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED]) -def test_outlined(testdir, steps): +def test_outlined(testdir): testdir.makefile( ".feature", outline=textwrap.dedent( @@ -76,7 +49,7 @@ def test_outlined(testdir, steps): ), ) - testdir.makeconftest(textwrap.dedent(steps)) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( @@ -105,9 +78,8 @@ def test_outlined(testdir, steps): result.assert_outcomes(passed=2) -@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.""" +def test_wrongly_outlined(testdir): + """Test parametrized scenario when the test function lacks parameters.""" testdir.makefile( ".feature", @@ -126,7 +98,7 @@ def test_outline_has_subset_of_parameters(testdir, steps): """ ), ) - testdir.makeconftest(textwrap.dedent(steps)) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( @@ -147,8 +119,7 @@ def test_outline_has_subset_of_parameters(testdir, steps): result.stdout.fnmatch_lines("*should match set of example values [[]'eat', 'left', 'start', 'unknown_param'[]].*") -@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS]) -def test_wrong_vertical_examples_scenario(testdir, steps): +def test_wrong_vertical_examples_scenario(testdir): """Test parametrized scenario vertical example table has wrong format.""" testdir.makefile( ".feature", @@ -167,7 +138,7 @@ def test_wrong_vertical_examples_scenario(testdir, steps): """ ), ) - testdir.makeconftest(textwrap.dedent(steps)) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( @@ -188,8 +159,7 @@ def test_wrong_vertical_examples_scenario(testdir, steps): ) -@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS]) -def test_wrong_vertical_examples_feature(testdir, steps): +def test_wrong_vertical_examples_feature(testdir): """Test parametrized feature vertical example table has wrong format.""" testdir.makefile( ".feature", @@ -209,7 +179,7 @@ def test_wrong_vertical_examples_feature(testdir, steps): """ ), ) - testdir.makeconftest(textwrap.dedent(steps)) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( @@ -230,8 +200,7 @@ def test_wrong_vertical_examples_feature(testdir, steps): ) -@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED]) -def test_outlined_with_other_fixtures(testdir, steps): +def test_outlined_with_other_fixtures(testdir): """Test outlined scenario also using other parametrized fixture.""" testdir.makefile( ".feature", @@ -252,7 +221,7 @@ def test_outlined_with_other_fixtures(testdir, steps): ), ) - testdir.makeconftest(textwrap.dedent(steps)) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( @@ -282,8 +251,7 @@ def test_outlined_with_other_fixtures(testdir, steps): result.assert_outcomes(passed=6) -@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED]) -def test_vertical_example(testdir, steps): +def test_vertical_example(testdir): """Test outlined scenario with vertical examples table.""" testdir.makefile( ".feature", @@ -304,7 +272,7 @@ def test_vertical_example(testdir, steps): ), ) - testdir.makeconftest(textwrap.dedent(steps)) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( @@ -333,62 +301,6 @@ def test_vertical_example(testdir, steps): result.assert_outcomes(passed=2) -def test_outlined_paramaters_parsed_indirectly(testdir): - testdir.makefile( - ".feature", - outline=textwrap.dedent( - """\ - Feature: Outline - - Examples: - | first | consume | remaining | - | 12 | 5 | 7 | - | 5 | 4 | 1 | - - Scenario Outline: Outlined modern given, when, thens - Given there were - When I ate - Then I should have had - - Examples: - | foods | - | ice-creams | - | almonds | - """ - ), - ) - - testdir.makepyfile( - textwrap.dedent( - """\ - from pytest_bdd import scenarios, given, when, then - from pytest_bdd.parsers import parse - - @given(parse('there were {start:d} {fruits}'), target_fixture='context') - def started_fruits(start, fruits): - assert isinstance(start, int) - return {fruits: dict(start=start)} - - @when(parse('I ate {eat:g} {fruits}')) - def ate_fruits(start, eat, fruits, context): - assert isinstance(eat, float) - context[fruits]['eat'] = eat - - @then(parse('I should have had {left} {fruits}')) - def should_have_had_left_fruits(start, eat, left, fruits, context): - assert isinstance(left, str) - assert start - eat == int(left) - assert context[fruits]['start'] == start - assert context[fruits]['eat'] == eat - - scenarios('outline.feature') - """ - ) - ) - result = testdir.runpytest() - result.assert_outcomes(passed=4) - - def test_outlined_feature(testdir): testdir.makefile( ".feature", diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index 41744a5..d650f4f 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -59,54 +59,3 @@ def test_parametrized(testdir): ) result = testdir.runpytest() result.assert_outcomes(passed=3) - - -def test_parametrized_with_parsers(testdir): - """Test parametrized scenario.""" - testdir.makefile( - ".feature", - parametrized=textwrap.dedent( - """\ - Feature: Parametrized scenario - Scenario: Parametrized given, when, thens with parsers invocation - Given there are gherkins - When I eat gherkins - Then I should have gherkins - """ - ), - ) - - testdir.makepyfile( - textwrap.dedent( - """\ - import pytest - from pytest_bdd import given, when, then, scenario - from pytest_bdd.parsers import re, parse - - @pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)]) - @scenario("parametrized.feature", "Parametrized given, when, thens with parsers invocation") - def test_parametrized(request, start, eat, left): - pass - - - @given(re("there are (?P\\\\w+)"), target_fixture="start_vegetables") - def start_vegetables(start, vegetables): - return dict(start=start) - - - @when("I eat gherkins") - def eat_cucumbers(start_vegetables, start, eat): - start_vegetables["eat"] = eat - - - @then(re("I should have (?P\\\\d+) (?P\\\\w+)"), converters=dict(left=int)) - def should_have_left_vegetables(start_vegetables, start, eat, left, vegetables): - assert start - eat == left - assert start_vegetables["start"] == start - assert start_vegetables["eat"] == eat - - """ - ) - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1)