Skip to content

Commit

Permalink
Scenario execution code cleanup
Browse files Browse the repository at this point in the history
* Remove a bunch of workarounds
* Left workarounds have same namings
* Removed potential defect with fixtures injection
* Prepare to fix #412, #438
  • Loading branch information
Kostiantyn Goloveshko authored and Kostiantyn Goloveshko committed Jan 3, 2022
1 parent 3c3c4d3 commit 79f5597
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 51 deletions.
9 changes: 7 additions & 2 deletions pytest_bdd/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from mako.lookup import TemplateLookup

from .feature import get_features
from .scenario import find_argumented_step_fixture_name, make_python_docstring, make_python_name, make_string_literal
from .scenario import (
find_argumented_step_fixture_name_and_step_alias_function,
make_python_docstring,
make_python_name,
make_string_literal,
)
from .steps import get_step_fixture_name
from .types import STEP_TYPES

Expand Down Expand Up @@ -121,7 +126,7 @@ def _find_step_fixturedef(fixturemanager, item, name, type_):
if fixturedefs is not None:
return fixturedefs

argumented_step_name = find_argumented_step_fixture_name(name, type_, fixturemanager)
argumented_step_name, _ = find_argumented_step_fixture_name_and_step_alias_function(name, type_, fixturemanager)
if argumented_step_name is not None:
return fixturemanager.getfixturedefs(argumented_step_name, item.nodeid)
return None
Expand Down
57 changes: 24 additions & 33 deletions pytest_bdd/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@
ALPHA_REGEX = re.compile(r"^\d+_*")


def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None):
def find_argumented_step_fixture_name_and_step_alias_function(name, type_, fixturemanager):
"""Find argumented step fixture name."""
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
for fixturename, fixturedefs in fixturemanager._arg2fixturedefs.items():
for fixturedef in fixturedefs:
parser = getattr(fixturedef.func, "parser", None)
if parser is None:
Expand All @@ -44,24 +43,13 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None)
if not match:
continue

# TODO: maybe `converters` should be part of the SterParser.__init__(),
# and used by StepParser.parse_arguments() method
converters = getattr(fixturedef.func, "converters", {})
for arg, value in parser.parse_arguments(name).items():
if arg in converters:
value = converters[arg](value)
if request:
inject_fixture(request, arg, value)
parser_name = get_step_fixture_name(parser.name, type_)
if request:
try:
request.getfixturevalue(parser_name)
except FixtureLookupError:
continue
return parser_name
step_alias_func = fixturedef.func
return parser_name, step_alias_func
return None, None


def _find_step_function(request, step, scenario):
def _find_step_and_step_alias_function(request, step, scenario):
"""Match the step defined by the regular expression pattern.
:param request: PyTest request object.
Expand All @@ -71,22 +59,24 @@ def _find_step_function(request, step, scenario):
:return: Function of the step.
:rtype: function
"""
name = step.name
try:
# Simple case where no parser is used for the step
return request.getfixturevalue(get_step_fixture_name(name, step.type))
name, fixture = find_argumented_step_fixture_name_and_step_alias_function(
step.name, step.type, request._fixturemanager
)
return request.getfixturevalue(name), fixture
except FixtureLookupError:
try:
# Could not find a fixture with the same name, let's see if there is a parser involved
name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request)
if name:
return request.getfixturevalue(name)
raise
except FixtureLookupError:
raise exceptions.StepDefinitionNotFoundError(
f"Step definition is not found: {step}. "
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
)
raise exceptions.StepDefinitionNotFoundError(
f"Step definition is not found: {step}. "
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
)


def _inject_step_fixtures(request, step, parser, converters):
# TODO: maybe `converters` should be part of the SterParser.__init__(),
# and used by StepParser.parse_arguments() method
for arg, value in parser.parse_arguments(step.name).items():
converted_value = converters.get(arg, lambda _: _)(value)
inject_fixture(request, arg, converted_value)


def _execute_step_function(request, scenario, step, step_func):
Expand Down Expand Up @@ -135,12 +125,13 @@ def _execute_scenario(feature: "Feature", scenario: "Scenario", request):
# Execute scenario steps
for step in scenario.steps:
try:
step_func = _find_step_function(request, step, scenario)
step_func, step_alias_func = _find_step_and_step_alias_function(request, step, scenario)
except exceptions.StepDefinitionNotFoundError as exception:
request.config.hook.pytest_bdd_step_func_lookup_error(
request=request, feature=feature, scenario=scenario, step=step, exception=exception
)
raise
_inject_step_fixtures(request, step, step_alias_func.parser, step_func.converters)
_execute_step_function(request, scenario, step, step_func)
finally:
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
Expand Down
33 changes: 17 additions & 16 deletions pytest_bdd/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def given_beautiful_article(article):
pass
"""
from typing import Optional, Dict

import pytest
from _pytest.fixtures import FixtureDef
Expand Down Expand Up @@ -93,7 +94,7 @@ def then(name, converters=None, target_fixture=None):
return _step_decorator(THEN, name, converters=converters, target_fixture=target_fixture)


def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
def _step_decorator(step_type, step_name, converters: Optional[Dict] = None, target_fixture=None):
"""Step decorator for the type and the name.
:param str step_type: Step type (GIVEN, WHEN or THEN).
Expand All @@ -104,34 +105,34 @@ def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
:return: Decorator function for the step.
"""

def decorator(func):
step_func = func
parser_instance = get_parser(step_name)
parsed_step_name = parser_instance.name
converters = converters or {}

def decorator(step_func):
parser = get_parser(step_name)
parsed_step_name = parser.name

step_func.__name__ = str(parsed_step_name)

def lazy_step_func():
def step_alias_func():
return step_func

step_func.step_type = step_type
lazy_step_func.step_type = step_type
step_func.step_type = step_alias_func.step_type = step_type

# Preserve the docstring
lazy_step_func.__doc__ = func.__doc__
step_alias_func.__doc__ = step_func.__doc__

step_func.parser = lazy_step_func.parser = parser_instance
if converters:
step_func.converters = lazy_step_func.converters = converters
step_func.parser = step_alias_func.parser = parser
step_func.converters = step_alias_func.converters = converters

step_func.target_fixture = lazy_step_func.target_fixture = target_fixture
step_func.target_fixture = step_alias_func.target_fixture = target_fixture

lazy_step_func = pytest.fixture()(lazy_step_func)
step_alias_fixture = pytest.fixture(step_alias_func)
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)

# Inject step alias fixture into module scope
caller_locals = get_caller_module_locals()
caller_locals[fixture_step_name] = lazy_step_func
return func
caller_locals[fixture_step_name] = step_alias_fixture
return step_func

return decorator

Expand Down

0 comments on commit 79f5597

Please sign in to comment.