forked from test_framework/pytest-bdd
Stub fix for finding the right fixture for step definition
This commit is contained in:
parent
2c8d1552e0
commit
020c443941
|
@ -12,16 +12,18 @@ test_publish_article = scenario(
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from pprint import pformat
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureManager, FixtureRequest, call_fixture_func
|
||||
from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func
|
||||
|
||||
from . import exceptions
|
||||
from .feature import get_feature, get_features
|
||||
from .steps import StepFunctionContext, inject_fixture
|
||||
from .steps import StepFunctionContext, get_parsed_step_fixture_name, inject_fixture
|
||||
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -31,15 +33,22 @@ if TYPE_CHECKING:
|
|||
|
||||
from .parser import Feature, Scenario, ScenarioTemplate, Step
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||
|
||||
|
||||
def find_argumented_step_function(name: str, type_: str, fixturemanager: FixtureManager) -> StepFunctionContext | None:
|
||||
"""Find argumented step fixture name."""
|
||||
def iter_argumented_step_function(
|
||||
name: str, type_: str, fixturemanager: FixtureManager
|
||||
) -> Iterable[tuple[str, FixtureDef[Any], StepFunctionContext, int]]:
|
||||
"""Iterate over argumented step functions."""
|
||||
# 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 reversed(fixturedefs):
|
||||
fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items())
|
||||
for i, (fixturename, fixturedefs) in enumerate(reversed(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:
|
||||
continue
|
||||
|
@ -51,8 +60,42 @@ def find_argumented_step_function(name: str, type_: str, fixturemanager: Fixture
|
|||
if not match:
|
||||
continue
|
||||
|
||||
return step_func_context
|
||||
return None
|
||||
yield fixturename, fixturedef, step_func_context, pos
|
||||
|
||||
|
||||
def rewrite_argumented_step_functions(name: str, type_, fixturemanager) -> None:
|
||||
bdd_name = get_parsed_step_fixture_name(name, type_)
|
||||
|
||||
l = list(iter_argumented_step_function(name=name, type_=type_, fixturemanager=fixturemanager))
|
||||
l = [f for _, f, _, _ in l]
|
||||
# TODO: quick n dirty way to give the right priority to the fixtures, but we should be more sophisticated than this.
|
||||
resorted = sorted(l, key=lambda x: x.baseid)
|
||||
# l are all the fixture names that parse the current step
|
||||
added = {}
|
||||
# TODO: Remove all the injected bdd_name we did here after the execution of the step
|
||||
# or maybe not, it could be used as a cache? would it poison steps for other scenarios?
|
||||
for fixturedef in resorted:
|
||||
existing_defs = fixturemanager._arg2fixturedefs.setdefault(bdd_name, [])
|
||||
if fixturedef not in existing_defs:
|
||||
existing_defs.append(fixturedef)
|
||||
added.setdefault(bdd_name, []).append(fixturedef)
|
||||
else:
|
||||
logger.warning("%r already added to bdd name %r, SKIPING", fixturedef, bdd_name)
|
||||
pass
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(f"Added the following fixtures:\n{pformat(added)}")
|
||||
|
||||
|
||||
def find_argumented_step_function(
|
||||
request, name: str, type_: str, fixturemanager: FixtureManager
|
||||
) -> StepFunctionContext | None:
|
||||
"""Find argumented step fixture name."""
|
||||
rewrite_argumented_step_functions(name=name, type_=type_, fixturemanager=fixturemanager)
|
||||
bdd_name = get_parsed_step_fixture_name(name, type_)
|
||||
try:
|
||||
return request.getfixturevalue(bdd_name)
|
||||
except pytest.FixtureLookupError:
|
||||
return None
|
||||
|
||||
|
||||
def _execute_step_function(
|
||||
|
@ -108,7 +151,7 @@ def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequ
|
|||
request.config.hook.pytest_bdd_before_scenario(request=request, feature=feature, scenario=scenario)
|
||||
|
||||
for step in scenario.steps:
|
||||
context = find_argumented_step_function(step.name, step.type, request._fixturemanager)
|
||||
context = find_argumented_step_function(request, step.name, step.type, request._fixturemanager)
|
||||
if context is None:
|
||||
exc = exceptions.StepDefinitionNotFoundError(
|
||||
f"Step definition is not found: {step}. "
|
||||
|
|
|
@ -71,6 +71,17 @@ def get_step_fixture_name(name: str, type_: str) -> str:
|
|||
return f"pytestbdd_{type_}_{name}"
|
||||
|
||||
|
||||
def get_parsed_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_parsed_{type_}_{name}"
|
||||
|
||||
|
||||
def given(
|
||||
name: str | StepParser,
|
||||
converters: dict[str, Callable] | None = None,
|
||||
|
@ -142,10 +153,7 @@ def _step_decorator(
|
|||
|
||||
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
|
||||
|
||||
def step_function_marker() -> None:
|
||||
return None
|
||||
|
||||
step_function_marker._pytest_bdd_step_context = StepFunctionContext(
|
||||
context = StepFunctionContext(
|
||||
name=fixture_step_name,
|
||||
type=step_type,
|
||||
step_func=func,
|
||||
|
@ -154,6 +162,12 @@ 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()
|
||||
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker)
|
||||
return func
|
||||
|
|
|
@ -256,10 +256,18 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
|||
from pytest_bdd.utils import dump_obj
|
||||
import pytest
|
||||
|
||||
@given(parsers.re("(?P<thing>.*)"))
|
||||
def root_conftest_catchall(thing):
|
||||
dump_obj(thing + " (catchall) root_conftest")
|
||||
|
||||
@given(parsers.parse("I have a {thing} thing"))
|
||||
def root_conftest(thing):
|
||||
dump_obj(thing + " root_conftest")
|
||||
|
||||
@given("I have a specific thing")
|
||||
def root_conftest_specific():
|
||||
dump_obj("specific" + "(specific) root_conftest")
|
||||
|
||||
@then("pass")
|
||||
def _():
|
||||
pass
|
||||
|
@ -281,8 +289,8 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
|||
dump_obj(thing + " (catchall) test_a")
|
||||
|
||||
@given(parsers.parse("I have a specific thing"))
|
||||
def in_root_test_a_specific(thing):
|
||||
dump_obj(thing + " (specific) test_a")
|
||||
def in_root_test_a_specific():
|
||||
dump_obj("specific" + " (specific) test_a")
|
||||
|
||||
@given(parsers.parse("I have a {thing} thing"))
|
||||
def in_root_test_a(thing):
|
||||
|
@ -299,8 +307,8 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
|||
dump_obj(thing + " (catchall) test_c")
|
||||
|
||||
@given(parsers.parse("I have a specific thing"))
|
||||
def in_root_test_c_specific(thing):
|
||||
dump_obj(thing + " (specific) test_c")
|
||||
def in_root_test_c_specific():
|
||||
dump_obj("specific" + " (specific) test_c")
|
||||
|
||||
@given(parsers.parse("I have a {thing} thing"))
|
||||
def in_root_test_c(thing):
|
||||
|
@ -322,8 +330,8 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
|||
dump_obj(thing + " (catchall) test_b_test_a")
|
||||
|
||||
@given(parsers.parse("I have a specific thing"))
|
||||
def in_test_b_test_a_specific(thing):
|
||||
dump_obj(thing + " (specific) test_b_test_a")
|
||||
def in_test_b_test_a_specific():
|
||||
dump_obj("specific" + " (specific) test_b_test_a")
|
||||
|
||||
@given(parsers.parse("I have a {thing} thing"))
|
||||
def in_test_b_test_a(thing):
|
||||
|
@ -343,8 +351,8 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
|||
dump_obj(thing + " (catchall) test_b_test_c")
|
||||
|
||||
@given(parsers.parse("I have a specific thing"))
|
||||
def in_test_b_test_c_specific(thing):
|
||||
dump_obj(thing + " (specific) test_a_test_c")
|
||||
def in_test_b_test_c_specific():
|
||||
dump_obj("specific" + " (specific) test_a_test_c")
|
||||
|
||||
@given(parsers.parse("I have a {thing} thing"))
|
||||
def in_test_b_test_c(thing):
|
||||
|
@ -365,18 +373,34 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
|||
scenarios("../specific.feature")
|
||||
|
||||
|
||||
# Important here to have the parse argument different from the others,
|
||||
# otherwise test would succeed even if the wrong step was used.
|
||||
@given(parsers.parse("I have a {thing} thing"))
|
||||
def in_test_b_test_b(thing):
|
||||
dump_obj(f"{thing} test_b_test_b")
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
test_b_folder.join("test_b_alternative.py").write(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd import scenarios, given, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
|
||||
scenarios("../specific.feature")
|
||||
|
||||
|
||||
# Here we try to use an argument different from the others,
|
||||
# to make sure it doesn't matter if a new step parser string is encountered.
|
||||
@given(parsers.parse("I have a {t} thing"))
|
||||
def in_test_b_test_b(t):
|
||||
dump_obj(f"{t} test_b_test_b")
|
||||
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
result.assert_outcomes(passed=2)
|
||||
|
||||
[thing] = collect_dumped_objects(result)
|
||||
assert thing == "specific test_b_test_b"
|
||||
[thing1, thing2] = collect_dumped_objects(result)
|
||||
assert thing1 == thing2 == "specific test_b_test_b"
|
||||
|
|
Loading…
Reference in New Issue