Stub fix for finding the right fixture for step definition

This commit is contained in:
Alessio Bogon 2022-07-24 00:02:52 +02:00
parent 2c8d1552e0
commit 020c443941
3 changed files with 108 additions and 27 deletions

View File

@ -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,7 +60,41 @@ def find_argumented_step_function(name: str, type_: str, fixturemanager: Fixture
if not match:
continue
return step_func_context
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
@ -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}. "

View File

@ -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

View File

@ -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"