Skip to content

Commit

Permalink
Test for maximum recursions in Preconditions
Browse files Browse the repository at this point in the history
  • Loading branch information
timofurrer committed Sep 1, 2019
1 parent 231210b commit d935c7b
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 14 deletions.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ license_file = LICENSE

[flake8]
max-line-length = 100
ignore = E203,W503
ignore = E203,W503,W504

[tool:pytest]
testpaths = tests/
Expand Down
17 changes: 14 additions & 3 deletions src/radish/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,20 @@ def cli(**kwargs):
features = []
for feature_file in config.feature_files:
logger.debug("Parsing Feature File %s", feature_file)
feature_ast = parser.parse(feature_file)
if feature_ast:
features.append(feature_ast)
try:
feature_ast = parser.parse(feature_file)
if feature_ast:
features.append(feature_ast)
except RadishError as exc:
print("", flush=True)
print(
"An error occured while parsing the Feature File {}:".format(
feature_file
),
flush=True,
)
print(exc, flush=True)
sys.exit(1)

logger.debug("Loading all modules from the basedirs")
loaded_modules = loader.load_modules(config.basedirs)
Expand Down
13 changes: 13 additions & 0 deletions src/radish/models/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ def __repr__(self) -> str:
line=self.line,
) # pragma: no cover

def __eq__(self, other):
if not isinstance(other, type(self)):
return False

return (
self.short_description == other.short_description
and self.path == other.path
and self.line == other.line
)

def __hash__(self):
return hash((self.short_description, self.path, self.line))

def set_feature(self, feature):
"""Set the Feature for this Scenario"""
self.feature = feature
Expand Down
14 changes: 12 additions & 2 deletions src/radish/parser/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

from lark import Lark, UnexpectedInput

from radish.models import PreconditionTag
from radish.parser.errors import (
RadishMisplacedBackground,
RadishMissingFeatureShortDescription,
RadishMissingRuleShortDescription,
RadishMissingScenarioShortDescription,
RadishMultipleBackgrounds,
RadishPreconditionScenarioDoesNotExist,
RadishPreconditionScenarioRecursion,
RadishScenarioLoopInvalidIterationsValue,
RadishScenarioLoopMissingIterations,
RadishScenarioOutlineExamplesInconsistentCellCount,
Expand All @@ -30,7 +32,6 @@
RadishStepDoesNotStartWithKeyword,
)
from radish.parser.transformer import RadishGherkinTransformer
from radish.models import PreconditionTag


class FeatureFileParser:
Expand Down Expand Up @@ -122,7 +123,7 @@ def _resolve_preconditions(self, features_rootdir, ast, visited_features):
if (
precondition_scenario.short_description
== precondition_tag.scenario_short_description
): # noqa
):
break
else:
raise RadishPreconditionScenarioDoesNotExist(
Expand All @@ -135,6 +136,15 @@ def _resolve_preconditions(self, features_rootdir, ast, visited_features):
),
)

# check if the precondition leads to a recursion
if (
precondition_scenario in preconditions
or precondition_scenario == scenario
):
raise RadishPreconditionScenarioRecursion(
scenario, precondition_scenario
)

preconditions.append(precondition_scenario)

# assign preconditions
Expand Down
12 changes: 12 additions & 0 deletions src/radish/parser/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,15 @@ def __init__(
featurefile_path,
", ".join(scenario_short_descriptions),
)


class RadishPreconditionScenarioRecursion(RadishSyntaxError):
examples = []

def __init__(self, scenario, precondition_scenario):
self.label = "Detected a Precondition Recursion in '{}' caused by '{}'".format(
scenario.short_description, precondition_scenario.short_description
)

def __str__(self):
return self.label
5 changes: 2 additions & 3 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
@pytest.fixture(name="radish_runner")
def setup_radish_runner(tmpdir):
"""Fixture to setup a radish command line runner"""

def __runner(features, basedir, radish_args):
cmdline = ["radish", *features, "-b", basedir, *radish_args]
proc = subprocess.Popen(
cmdline,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=str(tmpdir)
cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(tmpdir)
)
stdout, stderr = proc.communicate()
return proc.returncode, stdout, stderr
Expand Down
38 changes: 38 additions & 0 deletions tests/integration/features/Preconditions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,41 @@ Feature: Test the radish @precondition feature
"""
When the "precondition.feature" is run
Then the exit code should be 0

Scenario: Recursions in Precondition Scenarios should be detected
Given the Feature File "precondition.feature"
"""
Feature: User of a Precondition
@precondition(precondition.feature: Add user to database)
Scenario: Add user to database
When the users are added to the database
| barry |
| kara |
Then the database should have 2 users
"""
And the base dir module "steps.py"
"""
from radish import given, when, then
@given("the database is running")
def db_running(step):
step.context.db_running = True
@when("the users are added to the database")
def user_add(step):
assert step.context.db_running, "DB not running"
step.context.users = step.data_table
@then("the database should have {:int} users")
def expect_users(step, amount_users):
assert len(step.context.users) == amount_users
"""
When the "precondition.feature" is run
Then the run should fail with
"""
Detected a Precondition Recursion in 'Add user to database' caused by 'Add user to database'
"""
91 changes: 86 additions & 5 deletions tests/integration/radish/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
:license: MIT, see LICENSE for more details.
"""

import re
import os
import shutil
import tempfile
import textwrap
import subprocess

from radish import before, after, given, when, then
Expand All @@ -22,9 +24,39 @@ def create_temporary_directory_for_radish_run(scenario):
scenario.context.ctx_dir = tempfile.mkdtemp(prefix="radish-")
scenario.context.features_dir = os.path.join(scenario.context.ctx_dir, "features")
scenario.context.base_dir = os.path.join(scenario.context.ctx_dir, "radish")
scenario.context.results_dir = os.path.join(scenario.context.ctx_dir, "results")
scenario.context.failure_report_path = os.path.join(
scenario.context.results_dir, "failure_report"
)

# setup failure gather hooks
failure_gather_hooks = """
from radish import after
from radish.models.state import State
@after.each_step()
def gather_failure(step):
print("Step", step.text, step.state)
if step.state is not State.FAILED:
return
with open("{}", "w+", encoding="utf-8") as failure_report_file:
failure_report_file.write("{{}}\\n".format(step.failure_report.name))
failure_report_file.write("{{}}\\n".format(step.failure_report.reason))
""".format(
scenario.context.failure_report_path
)

os.makedirs(scenario.context.features_dir)
os.makedirs(scenario.context.base_dir)
os.makedirs(scenario.context.results_dir)

with open(
os.path.join(scenario.context.base_dir, "failure_report_hook.py"),
"w+",
encoding="utf-8",
) as failure_report_hook_file:
failure_report_hook_file.write(textwrap.dedent(failure_gather_hooks))


@after.each_scenario()
Expand All @@ -36,7 +68,9 @@ def remove_temporary_directory_for_radish_run(scenario):
@given("the Feature File {feature_filename:QuotedString}")
def create_feature_file(step, feature_filename):
"""Create the Feature File given in the doc string"""
assert step.doc_string is not None, "Please provide a Feature in the Step doc string"
assert (
step.doc_string is not None
), "Please provide a Feature in the Step doc string"

feature_contents = step.doc_string

Expand All @@ -48,7 +82,9 @@ def create_feature_file(step, feature_filename):
@given("the base dir module {module_filename:QuotedString}")
def create_base_dir_module(step, module_filename):
"""Create the base dir module given in the doc string"""
assert step.doc_string is not None, "Please provide a Python Module in the Step doc string"
assert (
step.doc_string is not None
), "Please provide a Python Module in the Step doc string"

module_contents = step.doc_string
module_path = os.path.join(step.context.base_dir, module_filename)
Expand All @@ -61,7 +97,9 @@ 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]
proc = subprocess.Popen(radish_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
proc = subprocess.Popen(
radish_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
stdout, _ = proc.communicate()

step.context.exit_code = proc.returncode
Expand All @@ -74,6 +112,49 @@ def expect_exit_code(step, exit_code):
"""Expect the exit code to be a certain integer"""
assert step.context.exit_code == exit_code, (
"Actual exit code was: {}\n".format(step.context.exit_code)
+ "stdout from radish run: '{}':\n".format(" ".join(step.context.command)) # noqa
+ step.context.stdout.decode("utf-8") # noqa
+ "stdout from radish run: '{}':\n".format(" ".join(step.context.command))
+ step.context.stdout.decode("utf-8")
)


@then("the run should fail with a {exc_type_name:word}")
def expect_fail_with_exc(step, exc_type_name):
"""Expect the run failed with an exception of the given type and message from doc string"""
assert step.context.exit_code != 0, +"stdout from radish run: '{}':\n".format(
" ".join(step.context.command)
) + step.context.stdout.decode("utf-8")

assert os.path.exists(step.context.failure_report_path), (
"No Step Failure Report was written\n"
+ "stdout from radish run: '{}':\n".format(
" ".join(step.context.command)
) # noqa
+ step.context.stdout.decode("utf-8")
)

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()

assert actual_exc_type_name == exc_type_name
if step.doc_string is not None:
assert actual_exc_reason == step.doc_string.strip()


@then("the run should fail with")
def expect_fail(step):
"""Expect the run failed with an exception"""
stdout = step.context.stdout.decode("utf-8")

assert step.context.exit_code != 0, (
"Actual exit code was: {}\n".format(step.context.exit_code)
+ "stdout from radish run: '{}':\n".format(" ".join(step.context.command))
+ stdout
)

match = re.search(step.doc_string, stdout)
assert match, (
"Searched for:\n" + step.doc_string + "\n" + "within stdout:\n" + stdout
)

0 comments on commit d935c7b

Please sign in to comment.