Skip to content

Commit

Permalink
Various improvements in dry-run mode (use untested now instead of ski…
Browse files Browse the repository at this point in the history
…pped). pep8 cleanup, more tests.
  • Loading branch information
jenisys committed Apr 13, 2013
1 parent c87ee53 commit 0e34c78
Show file tree
Hide file tree
Showing 8 changed files with 607 additions and 64 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ Version: 1.2.3a19 (unreleased)

IMPROVEMENT:

* Dry-run mode: Detects now undefined steps.
* SummaryReporter: Summary shows untested items if one ore more exist.
* issue #103: sort feature file by name in a given directory (provided by: gurneyalex).
* issue #42: Show all undefined steps taking tags into account (provided by: roignac, jenisys)

CHANGES:

* Dry-run mode: Uses untested counts now (was using: skipped counts).

FIXED:

* issue #145: before_feature/after_feature should not be skipped (provided by: florentx).
Expand Down
21 changes: 17 additions & 4 deletions behave/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ def __init__(self, filename, line, keyword, name, tags=[], steps=[]):
self._row = None
self.stderr = None
self.stdout = None
self.was_dry_run = False

def __repr__(self):
return '<Scenario "%s">' % self.name
Expand All @@ -387,8 +388,16 @@ def __iter__(self):
@property
def status(self):
for step in self.steps:
if step.status == 'failed' or step.status == 'undefined':
if step.status == 'failed':
return 'failed'
elif step.status == 'undefined':
if self.was_dry_run:
# -- SPECIAL CASE: In dry-run with undefined-step discovery
# Undefined steps should not cause failed scenario.
return 'untested'
else:
# -- NORMALLY: Undefined steps cause failed scenario.
return 'failed'
elif step.status == 'skipped':
return 'skipped'
elif step.status == 'untested':
Expand All @@ -408,6 +417,8 @@ def run(self, runner):
tags = runner.feature.tags + self.tags
run_scenario = runner.config.tags.check(tags)
run_steps = run_scenario and not runner.config.dry_run
dry_run_scenario = run_scenario and runner.config.dry_run
self.was_dry_run = dry_run_scenario

if run_scenario or runner.config.show_skipped:
runner.formatter.scenario(self)
Expand All @@ -429,7 +440,6 @@ def run(self, runner):
for step in self:
runner.formatter.step(step)

dry_run_scenario = run_scenario and runner.config.dry_run
for step in self:
if run_steps:
if not step.run(runner):
Expand All @@ -440,6 +450,8 @@ def run(self, runner):
# -- SKIP STEPS: After failure/undefined-step occurred.
# BUT: Detect all remaining undefined steps.
step.status = 'skipped'
if dry_run_scenario:
step.status = 'untested'
found_step = step_registry.registry.find_match(step)
if not found_step:
step.status = 'undefined'
Expand Down Expand Up @@ -759,7 +771,7 @@ def run(self, runner, quiet=False, capture=True):
# -- ENSURE:
# * runner.context.text/.table attributes are reset (#66).
# * Even EMPTY multiline text is available in context.
runner.context.text = self.text
runner.context.text = self.text
runner.context.table = self.table
match.run(runner.context)
self.status = 'passed'
Expand Down Expand Up @@ -966,6 +978,7 @@ def as_dict(self):
from behave.compat.collections import OrderedDict
return OrderedDict(self.items())


class Tag(unicode):
'''Tags appear may be associated with Features or Scenarios.
Expand Down Expand Up @@ -1088,9 +1101,9 @@ def make_location(step_function):
location = '%s:%d' % (filename, step_function.func_code.co_firstlineno)
return location


class NoMatch(Match):
def __init__(self):
self.func = None
self.arguments = []
self.location = None

124 changes: 76 additions & 48 deletions behave/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def execute_steps(self, steps_text):
'''
assert isinstance(steps_text, unicode), "Steps must be unicode."
if not self.feature:
raise ValueError('execute_steps() called outside of feature context')
raise ValueError('execute_steps() called outside of feature')

steps = self.feature.parser.parse_steps(steps_text)
for step in steps:
Expand All @@ -279,6 +279,7 @@ def exec_file(filename, globals={}, locals=None):
else:
execfile(filename, globals, locals)


def path_getrootdir(path):
"""
Extract rootdir from path in a platform independent way.
Expand All @@ -288,7 +289,7 @@ def path_getrootdir(path):
assert rootdir == "/"
WINDOWS-PATH EXAMPLE:
rootdir = path_getrootdir(r"D:\foo\bar\one.feature")
rootdir = path_getrootdir("D:\\foo\\bar\\one.feature")
assert rootdir == r"D:\"
"""
drive, _ = os.path.splitdrive(path)
Expand Down Expand Up @@ -319,33 +320,31 @@ def add(self, path):
class Runner(object):
def __init__(self, config):
self.config = config

self.hooks = {}

self.features = []
self.passed = []
self.failed = []
self.undefined = []
self.skipped = []
# -- XXX-JE-UNUSED:
# self.passed = []
# self.failed = []
# self.skipped = []

self.path_manager = PathManager()

self.feature = None

self.stdout_capture = None
self.stderr_capture = None
self.log_capture = None
self.old_stdout = None

self.base_dir = None
self.context = None
self.formatter = None
self.base_dir = None
self.context = None
self.formatter = None

def setup_paths(self):
if self.config.paths:
if self.config.verbose:
print 'Supplied path:', ', '.join('"%s"' % path
for path in self.config.paths)
print 'Supplied path:', \
', '.join('"%s"' % path for path in self.config.paths)
base_dir = self.config.paths[0]
if base_dir.startswith('@'):
# -- USE: behave @features.txt
Expand Down Expand Up @@ -457,17 +456,25 @@ def run_hook(self, name, context, *args):
self.hooks[name](context, *args)

@staticmethod
def parse_features_file(features_filename):
def parse_features_configfile(features_configfile):
"""
Read textual file, ala '@features.txt'
:param features_filename: Name of features file.
:return: List of feature names.
Read textual file, ala '@features.txt'. This file contains:
* a feature filename in each line
* empty lines (skipped)
* comment lines (skipped)
Relative path names are evaluated relative to the configfile directory.
A leading '@' (AT) character is removed from the configfile name.
:param features_configfile: Name of features configfile.
:return: List of feature filenames.
"""
if features_filename.startswith('@'):
features_filename = features_filename[1:]
here = os.path.dirname(features_filename) or "."
if features_configfile.startswith('@'):
features_configfile = features_configfile[1:]
here = os.path.dirname(features_configfile) or "."
files = []
for line in open(features_filename).readlines():
for line in open(features_configfile).readlines():
line = line.strip()
if not line:
continue # SKIP: Over empty line(s).
Expand All @@ -487,13 +494,33 @@ def feature_files(self):
files.append(os.path.join(dirpath, filename))
elif path.startswith('@'):
# -- USE: behave @list_of_features.txt
files.extend(self.parse_features_file(path[1:]))
files.extend(self.parse_features_configfile(path[1:]))
elif os.path.exists(path):
files.append(path)
else:
raise Exception("Can't find path: " + path)
return files

def parse_features(self, feature_files):
"""
Parse feature files and return list of Feature model objects.
:param feature_files: List of feature files to parse.
:return: List of feature objects.
"""
features = []
for filename in feature_files:
if self.config.exclude(filename):
continue

filename2 = os.path.abspath(filename)
feature = parser.parse_file(filename2, language=self.config.lang)
if not feature:
# -- CORNER-CASE: Feature file without any feature(s).
continue
features.append(feature)
return features

def run(self):
with self.path_manager:
self.setup_paths()
Expand All @@ -507,42 +534,43 @@ def run_with_paths(self):
# -- ENSURE: context.execute_steps() works in weird cases (hooks, ...)
self.setup_capture()
stream = self.config.output
failed = False
failed_count = 0

self.run_hook('before_all', context)

for filename in self.feature_files():
if self.config.exclude(filename):
continue

feature = parser.parse_file(os.path.abspath(filename),
language=self.config.lang)
if not feature:
# -- CORNER-CASE: Feature file without any feature(s).
continue
self.features.append(feature)
self.feature = feature

self.formatter = formatters.get_formatter(self.config, stream)
self.formatter.uri(filename)

failed = feature.run(self)
if failed:
failed_count += 1

self.formatter.close()
# -- STEP: Parse all feature files.
features = self.parse_features(self.feature_files())
self.features.extend(features)

# -- STEP: Run all features.
undefined_steps_initial_size = len(self.undefined)
run_feature = True
for feature in features:
if run_feature:
self.feature = feature
self.formatter = formatters.get_formatter(self.config, stream)
self.formatter.uri(feature.filename)

failed = feature.run(self)
if failed:
failed_count += 1
if self.config.stop:
# -- FAIL-EARLY: After first failure.
run_feature = False

self.formatter.close()

# -- ALWAYS: Report run/not-run feature to reporters.
# REQUIRED-FOR: Summary to keep track of untested features.
for reporter in self.config.reporters:
reporter.feature(feature)

if failed and self.config.stop:
break

self.run_hook('after_all', context)
for reporter in self.config.reporters:
reporter.end()

failed = (failed_count > 0)
failed = ((failed_count > 0) or
(len(self.undefined) > undefined_steps_initial_size))
return failed

def setup_capture(self):
Expand Down Expand Up @@ -591,7 +619,7 @@ def make_undefined_step_snippet(step, language=None):
if isinstance(step, types.StringTypes):
step_text = step
steps = parser.parse_steps(step_text, language=language)
step = steps[0]
step = steps[0]
assert step, "ParseError: %s" % step_text
prefix = u""
if sys.version_info[0] == 2:
Expand Down
Loading

0 comments on commit 0e34c78

Please sign in to comment.