Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pytest_bdd/parser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
import os.path
import re
import textwrap
Expand Down Expand Up @@ -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"\<(.+?)\>")
72 changes: 22 additions & 50 deletions pytest_bdd/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,78 +10,47 @@
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

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

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():
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

converters = getattr(fixturedef.func, "converters", {})
for arg, value in parser.parse_arguments(name).items():
if arg in converters:
value = converters[arg](value)
if request:
inject_fixture(request, arg, value)
parser_name = get_step_fixture_name(parser.name, type_)
if request:
try:
overridable_fixture_value = request.getfixturevalue(arg)
request.getfixturevalue(parser_name)
except FixtureLookupError:
inject_fixture(request, arg, value)
else:
if overridable_fixture_value != value:
inject_fixture(request, arg, value)
return get_step_fixture_name(parser.name, type_)
continue
return parser_name


def _find_step_function(request, step, scenario):
Expand Down Expand Up @@ -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.
Expand Down
164 changes: 18 additions & 146 deletions tests/feature/test_outline.py
Original file line number Diff line number Diff line change
@@ -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 <start> cucumbers", target_fixture="start_cucumbers")
def start_cucumbers(start):
assert isinstance(start, int)
return dict(start=start)


{when_decorator_definition}
@when("I eat <eat> cucumbers")
def eat_cucumbers(start_cucumbers, eat):
assert isinstance(eat, float)
start_cucumbers["eat"] = eat


{then_decorator_definition}
@then("I should have <left> cucumbers")
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert isinstance(left, str)
assert start - eat == int(left)
Expand All @@ -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 <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):
def test_outlined(testdir):
testdir.makefile(
".feature",
outline=textwrap.dedent(
Expand All @@ -76,7 +49,7 @@ def test_outlined(testdir, steps):
),
)

testdir.makeconftest(textwrap.dedent(steps))
testdir.makeconftest(textwrap.dedent(STEPS))

testdir.makepyfile(
textwrap.dedent(
Expand Down Expand Up @@ -105,9 +78,8 @@ def test_outline(request):
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",
Expand All @@ -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(
Expand All @@ -147,8 +119,7 @@ def test_outline(request):
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",
Expand All @@ -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(
Expand All @@ -188,8 +159,7 @@ def test_outline(request):
)


@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",
Expand All @@ -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(
Expand All @@ -230,8 +200,7 @@ def test_outline(request):
)


@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",
Expand All @@ -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(
Expand Down Expand Up @@ -282,8 +251,7 @@ def test_outline(other_fixture):
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",
Expand All @@ -304,7 +272,7 @@ def test_vertical_example(testdir, steps):
),
)

testdir.makeconftest(textwrap.dedent(steps))
testdir.makeconftest(textwrap.dedent(STEPS))

testdir.makepyfile(
textwrap.dedent(
Expand Down Expand Up @@ -333,102 +301,6 @@ def test_outline(request):
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_unsubstitutable_indirect_parameters(testdir):
testdir.makefile(
".feature",
unsubstitutable=textwrap.dedent(
"""\
Feature:

Scenario Outline: Unsubstitutable parameter
Given there is <substitutable> parameter

Examples:
| substitutable |
| no |
| <yes> |
"""
),
)

testdir.makepyfile(
textwrap.dedent(
"""\
from pytest_bdd import scenarios, given, when, then
from pytest_bdd.parsers import parse, re

@given(parse('there is <{is_substitutable}> parameter'))
def _(is_substitutable):
assert is_substitutable == "yes"

@given(re('there is (?P<is_substitutable>[^<].*[^>]) parameter'))
def _(is_substitutable):
assert is_substitutable == "no"

scenarios('unsubstitutable.feature')
"""
)
)
result = testdir.runpytest()
result.assert_outcomes(passed=2)


def test_outlined_feature(testdir):
testdir.makefile(
".feature",
Expand Down
Loading