forked from test_framework/pytest-bdd
Fix bug when 2 steps have the same step name...
... but only one step impl was kept.
This commit is contained in:
parent
cc415d3d1c
commit
63a4268b9c
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue