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."""
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
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):
step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None)
if step_func_context is None:

View File

@ -37,7 +37,8 @@ def _(article):
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, TypeVar
from itertools import count
from typing import Any, Callable, Iterable, TypeVar
import pytest
from _pytest.fixtures import FixtureDef, FixtureRequest
@ -52,7 +53,6 @@ TCallable = TypeVar("TCallable", bound=Callable[..., Any])
@dataclass
class StepFunctionContext:
name: str
type: Literal["given", "when", "then"]
step_func: Callable[..., Any]
parser: StepParser
@ -60,17 +60,6 @@ class StepFunctionContext:
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:
"""Get step fixture name.
@ -129,6 +118,23 @@ def then(
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(
step_type: Literal["given", "when", "then"],
step_name: str | StepParser,
@ -149,12 +155,8 @@ def _step_decorator(
def decorator(func: TCallable) -> TCallable:
parser = get_parser(step_name)
parsed_step_name = parser.name
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
context = StepFunctionContext(
name=fixture_step_name,
type=step_type,
step_func=func,
parser=parser,
@ -162,13 +164,13 @@ def _step_decorator(
target_fixture=target_fixture,
)
# TODO: Probably we can keep on returning None here instead
def step_function_marker() -> StepFunctionContext:
return context
step_function_marker._pytest_bdd_step_context = context
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)
return func

View File

@ -49,3 +49,51 @@ def test_step_function_multiple_target_fixtures(testdir):
[foo, bar] = collect_dumped_objects(result)
assert foo == "test foo"
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")