Skip to content

Commit

Permalink
Implement Step Behave-like Feature
Browse files Browse the repository at this point in the history
  • Loading branch information
timofurrer committed Sep 1, 2019
1 parent cb08cad commit df3d33a
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 30 deletions.
9 changes: 9 additions & 0 deletions src/radish/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,12 @@ class StepImplementationPatternNotSupported(RadishError):

def __init__(self, step_impl):
self.step_impl = step_impl


class StepBehaveLikeRecursionError(RadishError):
"""Exception raised when a recursion is detected in behave-like calls of a Step"""

def __init__(self):
super().__init__(
"Detected a infinit recursion in your ``step.behave_like`` calls"
)
1 change: 1 addition & 0 deletions src/radish/matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,6 @@ def match_step(step: Step, registry):
# get best match
best_step_impl, best_match = min(potentional_matches, key=lambda x: x[1])[0]
step.assign_implementation(best_step_impl, best_match)
return

raise StepImplementationNotFoundError(step)
36 changes: 34 additions & 2 deletions src/radish/models/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import base64

import radish.utils as utils
from radish.errors import RadishError
from radish.errors import RadishError, StepBehaveLikeRecursionError
from radish.models.state import State
from radish.models.stepfailurereport import StepFailureReport
from radish.models.timed import Timed
Expand Down Expand Up @@ -50,6 +50,9 @@ def __init__(
self.step_impl = None
self.step_impl_match = None

#: Holds the behave-like runner
self._behave_like_runner = None

#: Holds information about the State of this Step
self.state = State.UNTESTED
self.failure_report = None
Expand Down Expand Up @@ -106,6 +109,17 @@ def __validate_if_runnable(self):
)
)

def with_behave_like_runner(func):
def __wrapper(self, behave_like_runner, *args, **kwargs):
self._behave_like_runner = behave_like_runner
try:
return func(self, *args, **kwargs)
finally:
self._behave_like_runner = None

return __wrapper

@with_behave_like_runner
def run(self):
"""Run this Step
Expand All @@ -127,6 +141,7 @@ def run(self):
self.state = State.PASSED
return self.state

@with_behave_like_runner
def debug(self):
"""Run this Step in a debugger"""
self.__validate_if_runnable()
Expand All @@ -144,6 +159,23 @@ def debug(self):
self.state = State.PASSED
return self.state

def behave_like(self, step_line):
"""Run this Step as if it would be the one given in ``step_line``
This function requires ``self._behave_like_runner`` to be set.
"""
if self._behave_like_runner is None:
raise RadishError(
"This Step is unable to use the `behave_like`-Feature because no runner is provided"
)

state, step = self._behave_like_runner(step_line)
if state is State.FAILED:
try:
raise step.failure_report.exception
except RecursionError:
raise StepBehaveLikeRecursionError()

def fail(self, exception):
"""Let this Step fail with the given exception"""
self.state = State.FAILED
Expand All @@ -159,7 +191,7 @@ def skip(self):
self.state = State.SKIPPED

def pending(self):
"""Skip this Step
"""Mark this Step as pending
All pending Steps will be reminded about in the Runs summary.
Expand Down
29 changes: 26 additions & 3 deletions src/radish/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
:license: MIT, see LICENSE for more details.
"""

import functools
import random

import radish.matcher as matcher
from radish.errors import RadishError
from radish.models import Feature, Rule, Scenario, ScenarioLoop, ScenarioOutline, Step
from radish.models.state import State
from radish.errors import RadishError


class Runner:
Expand Down Expand Up @@ -164,6 +165,9 @@ def __run_steps(steps):

@with_hooks("each_step")
def run_step(self, step: Step):
return self._run_step(step)

def _run_step(self, step: Step):
try:
# match the Step with a Step Implementation
matcher.match_step(step, self.step_registry)
Expand All @@ -172,8 +176,27 @@ def run_step(self, step: Step):
else:
if not self.config.dry_run_mode:
if self.config.debug_steps_mode:
step.debug()
step.debug(functools.partial(self._behave_like_runner, step))
else:
step.run()
step.run(functools.partial(self._behave_like_runner, step))

return step.state

def _behave_like_runner(self, step, step_line):
"""Wrapper function for a Step to run ``step.behave_like``"""
behave_like_step_keyword, behave_like_step_text = step_line.split(maxsplit=1)
behave_like_step = Step(
step.id,
behave_like_step_keyword,
behave_like_step_keyword,
behave_like_step_text,
None,
None,
step.path,
step.line,
)
behave_like_step.feature = step.feature
behave_like_step.rule = step.rule
behave_like_step.scenario = step.scenario

return self._run_step(behave_like_step), behave_like_step
93 changes: 93 additions & 0 deletions tests/integration/features/Step-Behave-Like.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Feature: Test the radish Step behave-like Feature
In order to call another Step from
within a Step Implementation Function radish
provides the behave-like Feature.

Scenario: Call another Step with the behave-like Feature
Given the Feature File "behave-like.feature"
"""
Feature: Behave-Like
Scenario: Behave-Like
When something is run
Then another thing was run, too
"""
And the base dir module "steps.py"
"""
from radish import when, then
@when("something is run")
def something_run(step):
step.context.something = True
step.behave_like("When another thing is run")
@when("another thing is run")
def anotherthing_run(step):
step.context.another_thing = True
@then("another thing was run")
def expect_anotherthing_ran(step):
assert step.context.something
assert step.context.another_thing
"""
When the "behave-like.feature" is run
Then the exit code should be 0

Scenario: Call two other Steps with the behave-like Feature
Given the Feature File "behave-like.feature"
"""
Feature: Behave-Like
Scenario: Behave-Like
When something is run
Then the counter is 2
"""
And the base dir module "steps.py"
"""
from radish import before, when, then
@before.each_scenario()
def setup_counter(scenario):
scenario.context.counter = 0
@when("something is run")
def something_run(step):
step.behave_like("When the counter is increased")
step.behave_like("When the counter is increased")
@when("the counter is increased")
def increase_counter(step):
step.context.counter += 1
@then("the counter is {:int}")
def expect_counter(step, counter):
assert step.context.counter == counter
"""
When the "behave-like.feature" is run
Then the exit code should be 0

Scenario: Detect behave-like Call Recursions
Given the Feature File "behave-like.feature"
"""
Feature: Behave-Like
Scenario: Behave-Like
When something is run
"""
And the base dir module "steps.py"
"""
from radish import when, then
@when("something is run")
def something_run(step):
step.behave_like("When something is run")
"""
When the "behave-like.feature" is run
Then the run should fail with a StepBehaveLikeRecursionError
"""
Detected a infinit recursion in your ``step.behave_like`` calls
"""
15 changes: 10 additions & 5 deletions tests/integration/radish/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def create_base_dir_module(step, module_filename):
def run_feature_file(step, feature_filename):
"""Run the given Feature File"""
feature_path = os.path.join(step.context.features_dir, feature_filename)
radish_command = ["radish", "-b", step.context.base_dir, feature_path]
radish_command = ["radish", "-b", step.context.base_dir, feature_path, "-t"]
proc = subprocess.Popen(
radish_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
Expand Down Expand Up @@ -134,12 +134,17 @@ def expect_fail_with_exc(step, exc_type_name):
with open(
step.context.failure_report_path, encoding="utf-8"
) as failure_report_file:
actual_exc_type_name = failure_report_file.readline()
actual_exc_reason = failure_report_file.readline()
actual_exc_type_name = failure_report_file.readline().strip()
actual_exc_reason = failure_report_file.readline().strip()

assert (
actual_exc_type_name == exc_type_name
), "Exception types don't match '{}' == '{}'".format(
actual_exc_type_name, exc_type_name
)

assert actual_exc_type_name == exc_type_name
if step.doc_string is not None:
assert actual_exc_reason == step.doc_string.strip()
assert step.doc_string.strip() in actual_exc_reason, "Reasons don't match"


@then("the run should fail with")
Expand Down
22 changes: 14 additions & 8 deletions tests/unit/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,9 @@ def test_parser_assign_background_to_multiple_scenarios_outside_rule(parser):
assert ast.rules[0].scenarios[0].background is not None
assert ast.rules[0].scenarios[1].background is not None

assert ast.rules[0].scenarios[0].background is not ast.rules[0].scenarios[1].background
assert (
ast.rules[0].scenarios[0].background is not ast.rules[0].scenarios[1].background
)


def test_parser_assign_background_to_single_scenario_inside_rule(parser):
Expand Down Expand Up @@ -739,9 +741,15 @@ def test_parser_assign_background_to_every_scenario_inside_rules(parser):
assert ast.rules[1].scenarios[0].background is not None
assert ast.rules[1].scenarios[1].background is not None

assert ast.rules[0].scenarios[0].background is not ast.rules[0].scenarios[1].background
assert ast.rules[0].scenarios[1].background is not ast.rules[1].scenarios[0].background
assert ast.rules[1].scenarios[0].background is not ast.rules[1].scenarios[1].background
assert (
ast.rules[0].scenarios[0].background is not ast.rules[0].scenarios[1].background
)
assert (
ast.rules[0].scenarios[1].background is not ast.rules[1].scenarios[0].background
)
assert (
ast.rules[1].scenarios[0].background is not ast.rules[1].scenarios[1].background
)


@pytest.mark.xfail(
Expand Down Expand Up @@ -2194,15 +2202,13 @@ def test_parser_add_example_data_to_short_description_for_scenario_outline_examp
assert (
examples[0].short_description == "My Scenario Outline [hdr1: foo, hdr2: meh]"
# Python 3.5 has no dict ordering
or examples[0].short_description
== "My Scenario Outline [hdr2: meh, hdr1: foo]"
or examples[0].short_description == "My Scenario Outline [hdr2: meh, hdr1: foo]"
)

assert (
examples[1].short_description == "My Scenario Outline [hdr1: bar, hdr2: bla]"
# Python 3.5 has no dict ordering
or examples[1].short_description
== "My Scenario Outline [hdr2: bla, hdr1: bar]"
or examples[1].short_description == "My Scenario Outline [hdr2: bla, hdr1: bar]"
)


Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
:license: MIT, see LICENSE for more details.
"""

from unittest.mock import call
from unittest.mock import call, ANY

import pytest

Expand Down Expand Up @@ -894,7 +894,7 @@ def test_runner_should_run_step_after_being_matched_in_normal_mode(
runner.run_step(step_mock)

# then
step_mock.run.assert_called_once_with()
step_mock.run.assert_called_once_with(ANY)


def test_runner_should_debug_step_after_being_matched_in_debug_steps_mode(
Expand All @@ -917,7 +917,7 @@ def test_runner_should_debug_step_after_being_matched_in_debug_steps_mode(
runner.run_step(step_mock)

# then
step_mock.debug.assert_called_once_with()
step_mock.debug.assert_called_once_with(ANY)


def test_runner_should_not_run_nor_debug_step_after_being_matched_in_dry_run_mode(
Expand Down

0 comments on commit df3d33a

Please sign in to comment.