Fix bug when 2 steps have the same step name...

... but only one step impl was kept.
This commit is contained in:
Alessio Bogon 2022-07-27 17:02:35 +02:00
parent cc415d3d1c
commit 63a4268b9c
3 changed files with 69 additions and 19 deletions

View File

@ -48,7 +48,7 @@ def iter_argumented_step_function(
"""Iterate over argumented step functions.""" """Iterate over argumented step functions."""
# 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
fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items()) fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items())
for i, (fixturename, fixturedefs) in enumerate(reversed(fixture_def_by_name)): for i, (fixturename, fixturedefs) in enumerate(fixture_def_by_name):
for pos, fixturedef in enumerate(fixturedefs): for pos, fixturedef in enumerate(fixturedefs):
step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None) step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None)
if step_func_context is None: if step_func_context is None:

View File

@ -37,7 +37,8 @@ def _(article):
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Callable, TypeVar from itertools import count
from typing import Any, Callable, Iterable, TypeVar
import pytest import pytest
from _pytest.fixtures import FixtureDef, FixtureRequest from _pytest.fixtures import FixtureDef, FixtureRequest
@ -52,7 +53,6 @@ TCallable = TypeVar("TCallable", bound=Callable[..., Any])
@dataclass @dataclass
class StepFunctionContext: class StepFunctionContext:
name: str
type: Literal["given", "when", "then"] type: Literal["given", "when", "then"]
step_func: Callable[..., Any] step_func: Callable[..., Any]
parser: StepParser parser: StepParser
@ -60,17 +60,6 @@ class StepFunctionContext:
target_fixture: str | None = None target_fixture: str | None = None
def get_step_fixture_name(name: str, type_: str) -> str:
"""Get step fixture name.
:param name: string
:param type: step type
:return: step fixture name
:rtype: string
"""
return f"pytestbdd_{type_}_{name}"
def get_parsed_step_fixture_name(name: str, type_: str) -> str: def get_parsed_step_fixture_name(name: str, type_: str) -> str:
"""Get step fixture name. """Get step fixture name.
@ -129,6 +118,23 @@ def then(
return _step_decorator(THEN, name, converters=converters, target_fixture=target_fixture) return _step_decorator(THEN, name, converters=converters, target_fixture=target_fixture)
def find_unique_name(name: str, seen: Iterable[str]) -> str:
"""Find unique name.
:param name: string
:param seen: iterable of strings
:return: unique string
"""
seen = set(seen)
if name not in seen:
return name
for i in count(1):
new_name = f"{name}_{i}"
if new_name not in seen:
return new_name
def _step_decorator( def _step_decorator(
step_type: Literal["given", "when", "then"], step_type: Literal["given", "when", "then"],
step_name: str | StepParser, step_name: str | StepParser,
@ -149,12 +155,8 @@ def _step_decorator(
def decorator(func: TCallable) -> TCallable: def decorator(func: TCallable) -> TCallable:
parser = get_parser(step_name) parser = get_parser(step_name)
parsed_step_name = parser.name
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
context = StepFunctionContext( context = StepFunctionContext(
name=fixture_step_name,
type=step_type, type=step_type,
step_func=func, step_func=func,
parser=parser, parser=parser,
@ -162,13 +164,13 @@ def _step_decorator(
target_fixture=target_fixture, target_fixture=target_fixture,
) )
# TODO: Probably we can keep on returning None here instead
def step_function_marker() -> StepFunctionContext: def step_function_marker() -> StepFunctionContext:
return context return context
step_function_marker._pytest_bdd_step_context = context step_function_marker._pytest_bdd_step_context = context
caller_locals = get_caller_module_locals() caller_locals = get_caller_module_locals()
fixture_step_name = find_unique_name(f"pytestbdd_stepdef_{step_type}_{parser.name}", seen=caller_locals.keys())
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker) caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker)
return func return func

View File

@ -49,3 +49,51 @@ def test_step_function_multiple_target_fixtures(testdir):
[foo, bar] = collect_dumped_objects(result) [foo, bar] = collect_dumped_objects(result)
assert foo == "test foo" assert foo == "test foo"
assert bar == "test bar" assert bar == "test bar"
def test_step_functions_same_parser(testdir):
testdir.makefile(
".feature",
target_fixture=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given there is a foo with value "(?P<value>\\w+)"
And there is a foo with value "testfoo"
When pass
Then pass
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("target_fixture.feature")
STEP = 'there is a foo with value "(?P<value>\\w+)"'
@given(STEP)
def _():
dump_obj(('str',))
@given(parsers.re(STEP))
def _(value):
dump_obj(('re', value))
@when("pass")
@then("pass")
def _():
pass
"""
)
)
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1)
[first_given, second_given] = collect_dumped_objects(result)
assert first_given == ("str",)
assert second_given == ("re", "testfoo")