Revert "Allows use <params> in parsers defined steps"

This reverts commit 7850f218b7.
This commit is contained in:
Oleg Pidsadnyi 2021-08-18 09:20:58 +02:00 committed by GitHub
parent 4f1a0f8707
commit f75a57b22b
4 changed files with 42 additions and 209 deletions

View File

@ -1,3 +1,4 @@
import io
import os.path import os.path
import re import re
import textwrap import textwrap
@ -456,5 +457,4 @@ def get_tags(line):
return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1} return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1}
STEP_PARAM_TEMPLATE = "<{param}>" STEP_PARAM_RE = re.compile(r"\<(.+?)\>")
STEP_PARAM_RE = re.compile(STEP_PARAM_TEMPLATE.format(param="((?<=<)[^<>]+(?=>))"))

View File

@ -10,16 +10,15 @@ test_publish_article = scenario(
scenario_name="Publishing the article", scenario_name="Publishing the article",
) )
""" """
import collections
import os import os
import re import re
from itertools import chain, product
import pytest import pytest
from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupError
from . import exceptions from . import exceptions
from .feature import get_feature, get_features 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 .steps import get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path 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+_*") 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): def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None):
"""Find argumented step fixture name.""" """Find argumented step fixture name."""
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy # 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())) converters = getattr(fixturedef.func, "converters", {})
fixturedef_funcs = (fixturedef.func for fixturedef in fixturedefs) for arg, value in parser.parse_arguments(name).items():
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():
if arg in converters: if arg in converters:
value = converters[arg](value) value = converters[arg](value)
try: if request:
overridable_fixture_value = request.getfixturevalue(arg)
except FixtureLookupError:
inject_fixture(request, arg, value) inject_fixture(request, arg, value)
else: parser_name = get_step_fixture_name(parser.name, type_)
if overridable_fixture_value != value: if request:
inject_fixture(request, arg, value) try:
return get_step_fixture_name(parser.name, type_) request.getfixturevalue(parser_name)
except FixtureLookupError:
continue
return parser_name
def _find_step_function(request, step, scenario): 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) 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): 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 # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
# when the decorator is misused. # when the decorator is misused.

View File

@ -1,28 +1,25 @@
"""Scenario Outline tests.""" """Scenario Outline tests."""
import textwrap import textwrap
from pytest import mark
from tests.utils import assert_outcomes from tests.utils import assert_outcomes
FLOAT_NUMBER_PATTERN = r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?" STEPS = """\
STEPS_TEMPLATE = """\
from pytest_bdd import given, when, then from pytest_bdd import given, when, then
from pytest_bdd.parsers import re
{given_decorator_definition}
@given("there are <start> cucumbers", target_fixture="start_cucumbers")
def start_cucumbers(start): def start_cucumbers(start):
assert isinstance(start, int) assert isinstance(start, int)
return dict(start=start) return dict(start=start)
{when_decorator_definition} @when("I eat <eat> cucumbers")
def eat_cucumbers(start_cucumbers, eat): def eat_cucumbers(start_cucumbers, eat):
assert isinstance(eat, float) assert isinstance(eat, float)
start_cucumbers["eat"] = eat start_cucumbers["eat"] = eat
{then_decorator_definition} @then("I should have <left> cucumbers")
def should_have_left_cucumbers(start_cucumbers, start, eat, left): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert isinstance(left, str) assert isinstance(left, str)
assert start - eat == int(left) assert start - eat == int(left)
@ -32,31 +29,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left):
""" """
STRING_STEPS = STEPS_TEMPLATE.format( def test_outlined(testdir):
given_decorator_definition='@given("there are <start> cucumbers", target_fixture="start_cucumbers")',
when_decorator_definition='@when("I eat <eat> cucumbers")',
then_decorator_definition='@then("I should have <left> cucumbers")',
)
PARSER_STEPS = STEPS_TEMPLATE.format(
given_decorator_definition=f'@given(re("there are (?P<start>{FLOAT_NUMBER_PATTERN}) cucumbers"), '
f'target_fixture="start_cucumbers")',
when_decorator_definition=f'@when(re("I eat (?P<eat>{FLOAT_NUMBER_PATTERN}) cucumbers"))',
then_decorator_definition=f'@then(re("I should have (?P<left>{FLOAT_NUMBER_PATTERN}) cucumbers"))',
)
PARSER_STEPS_CONVERTED = STEPS_TEMPLATE.format(
given_decorator_definition=f'@given(re("there are (?P<start>{FLOAT_NUMBER_PATTERN}) cucumbers"), '
f'target_fixture="start_cucumbers", converters=dict(start=int))',
when_decorator_definition=f'@when(re("I eat (?P<eat>{FLOAT_NUMBER_PATTERN}) cucumbers"), '
f"converters=dict(eat=float))",
then_decorator_definition=f'@then(re("I should have (?P<left>{FLOAT_NUMBER_PATTERN}) cucumbers"), '
f"converters=dict(left=str))",
)
@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED])
def test_outlined(testdir, steps):
testdir.makefile( testdir.makefile(
".feature", ".feature",
outline=textwrap.dedent( outline=textwrap.dedent(
@ -76,7 +49,7 @@ def test_outlined(testdir, steps):
), ),
) )
testdir.makeconftest(textwrap.dedent(steps)) testdir.makeconftest(textwrap.dedent(STEPS))
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
@ -105,9 +78,8 @@ def test_outlined(testdir, steps):
result.assert_outcomes(passed=2) result.assert_outcomes(passed=2)
@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED]) def test_wrongly_outlined(testdir):
def test_outline_has_subset_of_parameters(testdir, steps): """Test parametrized scenario when the test function lacks parameters."""
"""Test parametrized scenario when the test function has a subset of the parameters of the examples."""
testdir.makefile( testdir.makefile(
".feature", ".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( testdir.makepyfile(
textwrap.dedent( 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'[]].*") 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):
def test_wrong_vertical_examples_scenario(testdir, steps):
"""Test parametrized scenario vertical example table has wrong format.""" """Test parametrized scenario vertical example table has wrong format."""
testdir.makefile( testdir.makefile(
".feature", ".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( testdir.makepyfile(
textwrap.dedent( 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):
def test_wrong_vertical_examples_feature(testdir, steps):
"""Test parametrized feature vertical example table has wrong format.""" """Test parametrized feature vertical example table has wrong format."""
testdir.makefile( testdir.makefile(
".feature", ".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( testdir.makepyfile(
textwrap.dedent( 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):
def test_outlined_with_other_fixtures(testdir, steps):
"""Test outlined scenario also using other parametrized fixture.""" """Test outlined scenario also using other parametrized fixture."""
testdir.makefile( testdir.makefile(
".feature", ".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( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
@ -282,8 +251,7 @@ def test_outlined_with_other_fixtures(testdir, steps):
result.assert_outcomes(passed=6) result.assert_outcomes(passed=6)
@mark.parametrize("steps", [STRING_STEPS, PARSER_STEPS_CONVERTED]) def test_vertical_example(testdir):
def test_vertical_example(testdir, steps):
"""Test outlined scenario with vertical examples table.""" """Test outlined scenario with vertical examples table."""
testdir.makefile( testdir.makefile(
".feature", ".feature",
@ -304,7 +272,7 @@ def test_vertical_example(testdir, steps):
), ),
) )
testdir.makeconftest(textwrap.dedent(steps)) testdir.makeconftest(textwrap.dedent(STEPS))
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
@ -333,62 +301,6 @@ def test_vertical_example(testdir, steps):
result.assert_outcomes(passed=2) 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 <first> <foods>
When I ate <consume> <foods>
Then I should have had <remaining> <foods>
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): def test_outlined_feature(testdir):
testdir.makefile( testdir.makefile(
".feature", ".feature",

View File

@ -59,54 +59,3 @@ def test_parametrized(testdir):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.assert_outcomes(passed=3) 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 <start> gherkins
When I eat <eat> gherkins
Then I should have <left> 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 <start> (?P<vegetables>\\\\w+)"), target_fixture="start_vegetables")
def start_vegetables(start, vegetables):
return dict(start=start)
@when("I eat <eat> gherkins")
def eat_cucumbers(start_vegetables, start, eat):
start_vegetables["eat"] = eat
@then(re("I should have (?P<left>\\\\d+) (?P<vegetables>\\\\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)