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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from pprint import pformat
|
||||||
from typing import TYPE_CHECKING, Callable, cast
|
from typing import TYPE_CHECKING, Callable, cast
|
||||||
|
|
||||||
import pytest
|
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 . import exceptions
|
||||||
from .feature import get_feature, get_features
|
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
|
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -31,15 +33,22 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from .parser import Feature, Scenario, ScenarioTemplate, Step
|
from .parser import Feature, Scenario, ScenarioTemplate, Step
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||||
|
|
||||||
|
|
||||||
def find_argumented_step_function(name: str, type_: str, fixturemanager: FixtureManager) -> StepFunctionContext | None:
|
def iter_argumented_step_function(
|
||||||
"""Find argumented step fixture name."""
|
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
|
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
|
||||||
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
|
fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items())
|
||||||
for fixturedef in reversed(fixturedefs):
|
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)
|
step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None)
|
||||||
if step_func_context is None:
|
if step_func_context is None:
|
||||||
continue
|
continue
|
||||||
|
@ -51,8 +60,42 @@ def find_argumented_step_function(name: str, type_: str, fixturemanager: Fixture
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return step_func_context
|
yield fixturename, fixturedef, step_func_context, pos
|
||||||
return None
|
|
||||||
|
|
||||||
|
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(
|
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)
|
request.config.hook.pytest_bdd_before_scenario(request=request, feature=feature, scenario=scenario)
|
||||||
|
|
||||||
for step in scenario.steps:
|
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:
|
if context is None:
|
||||||
exc = exceptions.StepDefinitionNotFoundError(
|
exc = exceptions.StepDefinitionNotFoundError(
|
||||||
f"Step definition is not found: {step}. "
|
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}"
|
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(
|
def given(
|
||||||
name: str | StepParser,
|
name: str | StepParser,
|
||||||
converters: dict[str, Callable] | None = None,
|
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)
|
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
|
||||||
|
|
||||||
def step_function_marker() -> None:
|
context = StepFunctionContext(
|
||||||
return None
|
|
||||||
|
|
||||||
step_function_marker._pytest_bdd_step_context = StepFunctionContext(
|
|
||||||
name=fixture_step_name,
|
name=fixture_step_name,
|
||||||
type=step_type,
|
type=step_type,
|
||||||
step_func=func,
|
step_func=func,
|
||||||
|
@ -154,6 +162,12 @@ 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:
|
||||||
|
return context
|
||||||
|
|
||||||
|
step_function_marker._pytest_bdd_step_context = context
|
||||||
|
|
||||||
caller_locals = get_caller_module_locals()
|
caller_locals = get_caller_module_locals()
|
||||||
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
|
||||||
|
|
|
@ -256,10 +256,18 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
||||||
from pytest_bdd.utils import dump_obj
|
from pytest_bdd.utils import dump_obj
|
||||||
import pytest
|
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"))
|
@given(parsers.parse("I have a {thing} thing"))
|
||||||
def root_conftest(thing):
|
def root_conftest(thing):
|
||||||
dump_obj(thing + " root_conftest")
|
dump_obj(thing + " root_conftest")
|
||||||
|
|
||||||
|
@given("I have a specific thing")
|
||||||
|
def root_conftest_specific():
|
||||||
|
dump_obj("specific" + "(specific) root_conftest")
|
||||||
|
|
||||||
@then("pass")
|
@then("pass")
|
||||||
def _():
|
def _():
|
||||||
pass
|
pass
|
||||||
|
@ -281,8 +289,8 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
||||||
dump_obj(thing + " (catchall) test_a")
|
dump_obj(thing + " (catchall) test_a")
|
||||||
|
|
||||||
@given(parsers.parse("I have a specific thing"))
|
@given(parsers.parse("I have a specific thing"))
|
||||||
def in_root_test_a_specific(thing):
|
def in_root_test_a_specific():
|
||||||
dump_obj(thing + " (specific) test_a")
|
dump_obj("specific" + " (specific) test_a")
|
||||||
|
|
||||||
@given(parsers.parse("I have a {thing} thing"))
|
@given(parsers.parse("I have a {thing} thing"))
|
||||||
def in_root_test_a(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")
|
dump_obj(thing + " (catchall) test_c")
|
||||||
|
|
||||||
@given(parsers.parse("I have a specific thing"))
|
@given(parsers.parse("I have a specific thing"))
|
||||||
def in_root_test_c_specific(thing):
|
def in_root_test_c_specific():
|
||||||
dump_obj(thing + " (specific) test_c")
|
dump_obj("specific" + " (specific) test_c")
|
||||||
|
|
||||||
@given(parsers.parse("I have a {thing} thing"))
|
@given(parsers.parse("I have a {thing} thing"))
|
||||||
def in_root_test_c(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")
|
dump_obj(thing + " (catchall) test_b_test_a")
|
||||||
|
|
||||||
@given(parsers.parse("I have a specific thing"))
|
@given(parsers.parse("I have a specific thing"))
|
||||||
def in_test_b_test_a_specific(thing):
|
def in_test_b_test_a_specific():
|
||||||
dump_obj(thing + " (specific) test_b_test_a")
|
dump_obj("specific" + " (specific) test_b_test_a")
|
||||||
|
|
||||||
@given(parsers.parse("I have a {thing} thing"))
|
@given(parsers.parse("I have a {thing} thing"))
|
||||||
def in_test_b_test_a(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")
|
dump_obj(thing + " (catchall) test_b_test_c")
|
||||||
|
|
||||||
@given(parsers.parse("I have a specific thing"))
|
@given(parsers.parse("I have a specific thing"))
|
||||||
def in_test_b_test_c_specific(thing):
|
def in_test_b_test_c_specific():
|
||||||
dump_obj(thing + " (specific) test_a_test_c")
|
dump_obj("specific" + " (specific) test_a_test_c")
|
||||||
|
|
||||||
@given(parsers.parse("I have a {thing} thing"))
|
@given(parsers.parse("I have a {thing} thing"))
|
||||||
def in_test_b_test_c(thing):
|
def in_test_b_test_c(thing):
|
||||||
|
@ -365,18 +373,34 @@ def test_uses_correct_step_in_the_hierarchy(testdir):
|
||||||
scenarios("../specific.feature")
|
scenarios("../specific.feature")
|
||||||
|
|
||||||
|
|
||||||
# Important here to have the parse argument different from the others,
|
@given(parsers.parse("I have a {thing} thing"))
|
||||||
# otherwise test would succeed even if the wrong step was used.
|
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"))
|
@given(parsers.parse("I have a {t} thing"))
|
||||||
def in_test_b_test_b(t):
|
def in_test_b_test_b(t):
|
||||||
dump_obj(f"{t} test_b_test_b")
|
dump_obj(f"{t} test_b_test_b")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = testdir.runpytest("-s")
|
result = testdir.runpytest("-s")
|
||||||
result.assert_outcomes(passed=1)
|
result.assert_outcomes(passed=2)
|
||||||
|
|
||||||
[thing] = collect_dumped_objects(result)
|
[thing1, thing2] = collect_dumped_objects(result)
|
||||||
assert thing == "specific test_b_test_b"
|
assert thing1 == thing2 == "specific test_b_test_b"
|
||||||
|
|
Loading…
Reference in New Issue