Navigation Menu

Skip to content

Commit

Permalink
Implementing --failfast option. closes #317 #224
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Jan 28, 2013
1 parent d380179 commit dabe2cd
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 11 deletions.
21 changes: 16 additions & 5 deletions lettuce/__init__.py
Expand Up @@ -83,7 +83,8 @@ class Runner(object):
features and step definitions on there.
"""
def __init__(self, base_path, scenarios=None, verbosity=0, random=False,
enable_xunit=False, xunit_filename=None, tags=None):
enable_xunit=False, xunit_filename=None, tags=None,
failfast=False):
""" lettuce.Runner will try to find a terrain.py file and
import it from within `base_path`
"""
Expand All @@ -99,6 +100,7 @@ def __init__(self, base_path, scenarios=None, verbosity=0, random=False,
self.loader = fs.FeatureLoader(base_path)
self.verbosity = verbosity
self.scenarios = scenarios and map(int, scenarios.split(",")) or None
self.failfast = failfast

sys.path.remove(base_path)

Expand Down Expand Up @@ -152,15 +154,24 @@ def run(self):
for filename in features_files:
feature = Feature.from_file(filename)
results.append(
feature.run(self.scenarios, tags=self.tags, random=self.random))
feature.run(self.scenarios,
tags=self.tags,
random=self.random,
failfast=self.failfast))

except exceptions.LettuceSyntaxError, e:
sys.stderr.write(e.msg)
failed = True
except:
e = sys.exc_info()[1]
print "Died with %s" % str(e)
traceback.print_exc()
if not self.failfast:
e = sys.exc_info()[1]
print "Died with %s" % str(e)
traceback.print_exc()
else:
print
print ("Lettuce aborted running any more tests "
"because was called with the `--failfast` option")

failed = True

finally:
Expand Down
7 changes: 7 additions & 0 deletions lettuce/bin.py
Expand Up @@ -66,6 +66,12 @@ def main(args=sys.argv[1:]):
help='Write JUnit XML to this file. Defaults to '
'lettucetests.xml')

parser.add_option("--failfast",
dest="failfast",
default=False,
action="store_true",
help='Stop running in the first failure')

options, args = parser.parse_args(args)
if args:
base_path = os.path.abspath(args[0])
Expand All @@ -86,6 +92,7 @@ def main(args=sys.argv[1:]):
random=options.random,
enable_xunit=options.enable_xunit,
xunit_filename=options.xunit_file,
failfast=options.failfast,
tags=tags,
)

Expand Down
12 changes: 7 additions & 5 deletions lettuce/core.py
Expand Up @@ -425,7 +425,7 @@ def _handle_inline_comments(klass, line):
return line

@staticmethod
def run_all(steps, outline=None, run_callbacks=False, ignore_case=True):
def run_all(steps, outline=None, run_callbacks=False, ignore_case=True, failfast=False):
"""Runs each step in the given list of steps.
Returns a tuple of five lists:
Expand Down Expand Up @@ -460,6 +460,8 @@ def run_all(steps, outline=None, run_callbacks=False, ignore_case=True):
steps_undefined.append(e.step)

except Exception, e:
if failfast:
raise
steps_failed.append(step)
reasons_to_fail.append(step.why)

Expand Down Expand Up @@ -685,15 +687,15 @@ def passed(self):
def failed(self):
return any([step.failed for step in self.steps])

def run(self, ignore_case):
def run(self, ignore_case, failfast=False):
"""Runs a scenario, running each of its steps. Also call
before_each and after_each callbacks for steps and scenario"""

results = []
call_hook('before_each', 'scenario', self)

def run_scenario(almost_self, order=-1, outline=None, run_callbacks=False):
all_steps, steps_passed, steps_failed, steps_undefined, reasons_to_fail = Step.run_all(self.steps, outline, run_callbacks, ignore_case)
all_steps, steps_passed, steps_failed, steps_undefined, reasons_to_fail = Step.run_all(self.steps, outline, run_callbacks, ignore_case, failfast=failfast)
skip = lambda x: x not in steps_passed and x not in steps_undefined and x not in steps_failed

steps_skipped = filter(skip, all_steps)
Expand Down Expand Up @@ -1181,7 +1183,7 @@ def _parse_remaining_lines(self, lines, original_string, with_file=None):

return background, scenarios, description

def run(self, scenarios=None, ignore_case=True, tags=None, random=False):
def run(self, scenarios=None, ignore_case=True, tags=None, random=False, failfast=False):
call_hook('before_each', 'feature', self)
scenarios_ran = []

Expand All @@ -1204,7 +1206,7 @@ def run(self, scenarios=None, ignore_case=True, tags=None, random=False):
if self.background:
self.background.run(ignore_case)

scenarios_ran.extend(scenario.run(ignore_case))
scenarios_ran.extend(scenario.run(ignore_case, failfast=failfast))

call_hook('after_each', 'feature', self)
return FeatureResult(self, *scenarios_ran)
Expand Down
7 changes: 6 additions & 1 deletion lettuce/django/management/commands/harvest.py
Expand Up @@ -73,6 +73,9 @@ class Command(BaseCommand):

make_option('--xunit-file', action='store', dest='xunit_file', default=None,
help='Write JUnit XML to this file. Defaults to lettucetests.xml'),

make_option("--failfast", dest="failfast", default=False,
action="store_true", help='Stop running in the first failure'),
)

def stopserver(self, failed=False):
Expand Down Expand Up @@ -101,6 +104,8 @@ def handle(self, *args, **options):
apps_to_avoid = tuple(options.get('avoid_apps', '').split(","))
run_server = not options.get('no_server', False)
tags = options.get('tags', None)
failfast = options.get('failfast', False)

server = Server(port=options['port'])

paths = self.get_paths(args, apps_to_run, apps_to_avoid)
Expand Down Expand Up @@ -129,7 +134,7 @@ def handle(self, *args, **options):
runner = Runner(path, options.get('scenarios'), verbosity,
enable_xunit=options.get('enable_xunit'),
xunit_filename=options.get('xunit_file'),
tags=tags)
tags=tags, failfast=failfast)

result = runner.run()
if app_module is not None:
Expand Down
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions tests/integration/django/celeries/leaves/features/foobar-steps.py
@@ -0,0 +1,16 @@
from lettuce import step


@step(r'Given I say foo bar')
def given_i_say_foo_bar(step):
pass


@step(r'Then it works')
def then_it_works(step):
pass


@step(r'Then it fails')
def then_it_fails(step):
assert False
@@ -0,0 +1,8 @@
Feature: Test the django app FOO BAR
Scenario: This one is present
Given I say foo bar
Then it fails

Scenario: This one is never called
Given I say foo bar
Then it works
Empty file.
20 changes: 20 additions & 0 deletions tests/integration/django/celeries/leaves/views.py
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# <Lettuce - Behaviour Driven Development for python>
# Copyright (C) <2010-2012> Gabriel Falcão <gabriel@nacaolivre.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.http import HttpResponse

def index(request):
return HttpResponse('OK')
11 changes: 11 additions & 0 deletions tests/integration/django/celeries/manage.py
@@ -0,0 +1,11 @@
#!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)

if __name__ == "__main__":
execute_manager(settings)
17 changes: 17 additions & 0 deletions tests/integration/django/celeries/settings.py
@@ -0,0 +1,17 @@
DEBUG = True

ROOT_URLCONF = 'couves.urls'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': '', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
}
}
INSTALLED_APPS = (
'lettuce.django',
'leaves',
)
26 changes: 26 additions & 0 deletions tests/integration/django/celeries/terrain.py
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# <Lettuce - Behaviour Driven Development for python>
# Copyright (C) <2010-2012> Gabriel Falcão <gabriel@nacaolivre.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from lettuce import before, after

@before.all
def couves_before():
print "Couves before all"

@after.all
def couves_after(total):
print "Couves after all"
5 changes: 5 additions & 0 deletions tests/integration/django/celeries/urls.py
@@ -0,0 +1,5 @@
from django.conf.urls.defaults import *

urlpatterns = patterns('',
url(r'', 'couves.leaves.views.index'),
)
33 changes: 33 additions & 0 deletions tests/integration/test_celeries.py
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# <Lettuce - Behaviour Driven Development for python>
# Copyright (C) <2010-2012> Gabriel Falcão <gabriel@nacaolivre.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import commands
from lettuce.fs import FileSystem
from sure import expect

current_directory = FileSystem.dirname(__file__)


def test_failfast():
'passing --failfast to the harvest command will cause lettuce to stop in the first failure'

FileSystem.pushd(current_directory, "django", "celeries")

status, out = commands.getstatusoutput("python manage.py harvest --verbosity=3 --failfast")

expect("This one is present").to.be.within(out)
expect("This one is never called").to.not_be.within(out)
FileSystem.popd()

0 comments on commit dabe2cd

Please sign in to comment.