Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

A few changes in this commit:

 * Scenario instances now have a `self.previous_scenario` attribute pointing to the last full-featured scenario (if any)
 * the runner now takes tag names
 * Scenario have now a method that matches strings
  • Loading branch information...
commit 5bfee60283af31f1408cb45ad722821a0083bdc3 1 parent 0131a00
Gabriel Falcão authored
10 lettuce/__init__.py
View
@@ -23,8 +23,6 @@
import traceback
from datetime import datetime
-from lettuce import fs
-
from lettuce.core import Feature, TotalResult
from lettuce.terrain import after
@@ -37,7 +35,7 @@
from lettuce.registry import CALLBACK_REGISTRY
from lettuce.exceptions import StepLoadingError
from lettuce.plugins import xunit_output
-
+from lettuce import fs
from lettuce import exceptions
__all__ = [
@@ -71,12 +69,14 @@ class Runner(object):
features and step definitions on there.
"""
def __init__(self, base_path, scenarios=None, verbosity=0,
- enable_xunit=False, xunit_filename=None):
+ enable_xunit=False, xunit_filename=None, tags=None):
""" lettuce.Runner will try to find a terrain.py file and
import it from within `base_path`
"""
+ self.tags = tags
self.single_feature = None
+
if os.path.isfile(base_path) and os.path.exists(base_path):
self.single_feature = base_path
base_path = os.path.dirname(base_path)
@@ -134,7 +134,7 @@ def run(self):
for filename in features_files:
feature = Feature.from_file(filename)
results.append(
- feature.run(self.scenarios))
+ feature.run(self.scenarios, tags=self.tags))
except exceptions.LettuceSyntaxError, e:
sys.stderr.write(e.msg)
112 lettuce/core.py
View
@@ -17,6 +17,7 @@
import re
import codecs
+from fuzzywuzzy import fuzz
import unicodedata
from itertools import chain
from copy import deepcopy
@@ -496,8 +497,11 @@ class Scenario(object):
indentation = 2
table_indentation = indentation + 2
- def __init__(self, name, remaining_lines, keys, outlines, with_file=None,
- original_string=None, language=None):
+ def __init__(self, name, remaining_lines, keys, outlines,
+ with_file=None,
+ original_string=None,
+ language=None,
+ previous_scenario=None):
if not language:
language = language()
@@ -513,6 +517,8 @@ def __init__(self, name, remaining_lines, keys, outlines, with_file=None,
self.with_file = with_file
self.original_string = original_string
+ self.previous_scenario = previous_scenario
+
if with_file and original_string:
scenario_definition = ScenarioDescription(self, with_file,
original_string,
@@ -526,7 +532,7 @@ def __init__(self, name, remaining_lines, keys, outlines, with_file=None,
if original_string and '@' in self.original_string:
self.tags = self._find_tags_in(original_string)
else:
- self.tags = []
+ self.tags = None
@property
def max_length(self):
@@ -572,6 +578,46 @@ def _calc_value_length(self, data):
def __repr__(self):
return u'<Scenario: "%s">' % self.name
+ def matches_tags(self, tags):
+ if tags is None:
+ return True
+
+ if not self.tags:
+ return False
+
+ matched = []
+
+ for tag in self.tags:
+ if tag in tags:
+ return True
+
+ for tag in tags:
+ exclude = tag.startswith('-')
+ if exclude:
+ tag = tag[1:]
+
+ fuzzable = tag.startswith('~')
+ if fuzzable:
+ tag = tag[1:]
+
+ result = tag in self.tags
+ if fuzzable:
+ fuzzed = []
+ for internal_tag in self.tags:
+ ratio = fuzz.ratio(tag, internal_tag)
+ if exclude:
+ fuzzed.append(ratio <= 80)
+ else:
+ fuzzed.append(ratio > 80)
+
+ result = any(fuzzed)
+ elif exclude:
+ result = tag not in self.tags
+
+ matched.append(result)
+
+ return all(matched)
+
@property
def evaluated(self):
for outline in self.outlines:
@@ -639,21 +685,34 @@ def _add_myself_to_steps(self):
step.scenario = self
def _find_tags_in(self, original_string):
- previous_scenario_re = re.compile(ur"(?:%s.*)([@].*)%s: (%s)" % (
- self.language.non_capturable_scenario_separator,
+ broad_regex = re.compile(ur"([@].*)%s: (%s)" % (
self.language.scenario_separator,
self.name), re.DOTALL)
- first_of_scenario_re = re.compile(ur"([@].*)%s: (%s)" % (
- self.language.scenario_separator,
- self.name), re.DOTALL)
+ regexes = []
+ if not self.previous_scenario:
+ regexes.append(broad_regex)
- for regex in (previous_scenario_re, first_of_scenario_re):
+ else:
+ regexes.append(re.compile(ur"(?:%s: %s.*)([@]?.*)%s: (%s)" % (
+ self.language.non_capturable_scenario_separator,
+ self.previous_scenario.name,
+ self.language.scenario_separator,
+ self.name), re.DOTALL))
+
+ def try_finding_with(regex):
found = regex.search(original_string)
if found:
tag_lines = found.group().splitlines()
return list(chain(*map(self._extract_tag, tag_lines)))
+ for regex in regexes:
+ found = try_finding_with(regex)
+ if found:
+ return found
+
+ return []
+
def _extract_tag(self, item):
regex = re.compile(r'[@](\S+)')
return regex.findall(item)
@@ -693,8 +752,13 @@ def represent_examples(self):
return "\n".join([(u" " * self.table_indentation) + line for line in lines]) + '\n'
@classmethod
- def from_string(new_scenario, string, with_file=None, original_string=None, language=None):
+ def from_string(new_scenario, string,
+ with_file=None,
+ original_string=None,
+ language=None,
+ previous_scenario=None):
""" Creates a new scenario from string"""
+
# ignoring comments
string = "\n".join(strings.get_stripped_lines(string, ignore_lines_starting_with='#'))
@@ -724,6 +788,7 @@ def from_string(new_scenario, string, with_file=None, original_string=None, lang
with_file=with_file,
original_string=original_string,
language=language,
+ previous_scenario=previous_scenario,
)
return scenario
@@ -854,8 +919,9 @@ def _parse_remaining_lines(self, lines, original_string, with_file=None):
description = parts[0]
parts.pop(0)
- scenario_strings = [u"%s: %s" % (self.language.first_of_scenario, s) \
- for s in parts if s.strip()]
+ prefix = self.language.first_of_scenario
+ upcoming_scenarios = [
+ u"%s: %s" % (prefix, s) for s in parts if s.strip()]
kw = dict(
original_string=original_string,
@@ -863,11 +929,26 @@ def _parse_remaining_lines(self, lines, original_string, with_file=None):
language=self.language,
)
- scenarios = [Scenario.from_string(s, **kw) for s in scenario_strings]
+ scenarios = []
+ while upcoming_scenarios:
+ current = upcoming_scenarios.pop(0)
+ previous_scenario = None
+ has_previous = len(scenarios) > 0
+
+ if has_previous:
+ previous_scenario = scenarios[-1]
+
+ params = dict(
+ previous_scenario=previous_scenario,
+ )
+
+ params.update(kw)
+ current_scenario = Scenario.from_string(current, **params)
+ scenarios.append(current_scenario)
return scenarios, description
- def run(self, scenarios=None, ignore_case=True):
+ def run(self, scenarios=None, ignore_case=True, tags=None):
call_hook('before_each', 'feature', self)
scenarios_ran = []
@@ -881,6 +962,9 @@ def run(self, scenarios=None, ignore_case=True):
if scenarios_to_run and (index + 1) not in scenarios_to_run:
continue
+ if not scenario.matches_tags(tags):
+ continue
+
scenarios_ran.extend(scenario.run(ignore_case))
call_hook('after_each', 'feature', self)
15 tests/functional/tag_features/timebound/timebound.feature
View
@@ -0,0 +1,15 @@
+Feature: ignore slow steps
+ As a python developer
+ I want to run only the fast tests
+ So that I can be really happy
+
+ @slow-ish
+ Scenario: this one is kinda slow
+ Given I wait for 60 seconds
+ Then the time passed is 1 minute
+
+
+ @fast-ish
+ Scenario: this one is fast!!
+ Given I wait for 0 seconds
+ Then the time passed is 0 seconds
54 tests/functional/test_runner.py
View
@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import lettuce
-
+from mock import Mock
from StringIO import StringIO
from os.path import dirname, join, abspath
from nose.tools import assert_equals, with_setup, assert_raises
@@ -38,15 +38,21 @@
lettuce_dir = abspath(dirname(lettuce.__file__))
ojoin = lambda *x: join(current_dir, 'output_features', *x)
sjoin = lambda *x: join(current_dir, 'syntax_features', *x)
+tjoin = lambda *x: join(current_dir, 'tag_features', *x)
+
lettuce_path = lambda *x: fs.relpath(join(lettuce_dir, *x))
call_line = StepDefinition.__call__.im_func.func_code.co_firstlineno + 5
-def feature_name(name):
- return join(abspath(dirname(__file__)), 'output_features', name, "%s.feature" % name)
-def syntax_feature_name(name):
- return sjoin(name, "%s.feature" % name)
+def joiner(callback, name):
+ return callback(name, "%s.feature" % name)
+
+
+feature_name = lambda name: joiner(ojoin, name)
+syntax_feature_name = lambda name: joiner(sjoin, name)
+tag_feature_name = lambda name: joiner(tjoin, name)
+
@with_setup(prepare_stderr)
def test_try_to_import_terrain():
@@ -1052,3 +1058,41 @@ def append_2_more(step):
"1 scenario (1 passed)\n"
"4 steps (4 passed)\n"
)
+
+
+@with_setup(prepare_stdout)
+def test_run_only_fast_tests():
+ "Runner can filter by tags"
+
+ from lettuce import step
+
+ good_one = Mock()
+ bad_one = Mock()
+
+ @step('I wait for 0 seconds')
+ def wait_for_0_seconds(step):
+ good_one(step.sentence)
+
+ @step('the time passed is 0 seconds')
+ def time_passed_0_sec(step):
+ good_one(step.sentence)
+
+ @step('I wait for 60 seconds')
+ def wait_for_60_seconds(step):
+ bad_one(step.sentence)
+
+ @step('the time passed is 1 minute')
+ def time_passed_1_min(step):
+ bad_one(step.sentence)
+
+ filename = tag_feature_name('timebound')
+ runner = Runner(filename, verbosity=1, tags=['fast-ish'])
+ runner.run()
+
+ assert_stdout_lines(
+ ".."
+ "\n"
+ "1 feature (1 passed)\n"
+ "1 scenario (1 passed)\n"
+ "2 steps (2 passed)\n"
+ )
37 tests/unit/test_feature_parser.py
View
@@ -175,6 +175,27 @@
"""
+FEATURE13 = """
+Feature: correct matching
+ @runme
+ Scenario: Holy tag, Batman
+ Given this scenario has tags
+ Then it can be inspected from within the object
+
+ Scenario: This has no tags
+ Given this scenario has tags
+ Then it can be inspected from within the object
+
+ @slow
+ Scenario: this is slow
+ Given this scenario has tags
+ Then it can be inspected from within the object
+
+ Scenario: Also without tags
+ Given this scenario has tags
+ Then it can be inspected from within the object
+"""
+
def test_feature_has_repr():
"Feature implements __repr__ nicely"
@@ -349,15 +370,17 @@ def test_single_scenario_single_scenario():
def test_single_scenario_many_scenarios():
- "Features should have their scenarios parsed with its respective tags"
- feature = Feature.from_string(FEATURE12)
+ "Untagged scenario following a tagged one should have no tags"
+ feature = Feature.from_string(FEATURE13)
first_scenario = feature.scenarios[0]
+ assert that(first_scenario.tags).equals(['runme'])
- assert that(first_scenario.tags).deep_equals([
- 'many', 'other', 'basic', 'tags', 'here', ':)'])
+ second_scenario = feature.scenarios[1]
+ assert that(second_scenario.tags).equals([])
- last_scenario = feature.scenarios[1]
+ third_scenario = feature.scenarios[2]
+ assert that(third_scenario.tags).equals(['slow'])
- assert that(last_scenario.tags).deep_equals([
- 'only', 'a-few', 'tags'])
+ last_scenario = feature.scenarios[3]
+ assert that(last_scenario.tags).equals([])
47 tests/unit/test_scenario_parsing.py
View
@@ -459,3 +459,50 @@ def test_scenario_has_tags_singleline():
'another',
'$%^&even-weird_chars',
])
+
+
+def test_scenario_matches_tags():
+ ("A scenario with tags should respond with True when "
+ ".matches_tags() is called with a valid list of tags")
+
+ scenario = Scenario.from_string(
+ SCENARIO1,
+ original_string=('@onetag\n@another-one\n' + SCENARIO1.strip()))
+
+ assert that(scenario.tags).deep_equals(['onetag','another-one'])
+ assert scenario.matches_tags(['onetag'])
+ assert scenario.matches_tags(['another-one'])
+
+
+def test_scenario_matches_tags_fuzzywuzzy():
+ ("When Scenario#matches_tags is called with a member starting with ~ "
+ "it will consider a fuzzywuzzy match")
+
+ scenario = Scenario.from_string(
+ SCENARIO1,
+ original_string=('@anothertag\n@another-tag\n' + SCENARIO1.strip()))
+
+ assert scenario.matches_tags(['~another'])
+
+
+def test_scenario_matches_tags_excluding():
+ ("When Scenario#matches_tags is called with a member starting with - "
+ "it will exclude that tag from the matching")
+
+ scenario = Scenario.from_string(
+ SCENARIO1,
+ original_string=('@anothertag\n@another-tag\n' + SCENARIO1.strip()))
+
+ assert not scenario.matches_tags(['-anothertag'])
+ assert scenario.matches_tags(['-foobar'])
+
+
+def test_scenario_matches_tags_excluding_fuzzywuzzy():
+ ("When Scenario#matches_tags is called with a member starting with -~ "
+ "it will exclude that tag from that fuzzywuzzy match")
+
+ scenario = Scenario.from_string(
+ SCENARIO1,
+ original_string=('@anothertag\n@another-tag\n' + SCENARIO1.strip()))
+
+ assert not scenario.matches_tags(['-~anothertag'])
22 tests/unit/test_step_runner.py
View
@@ -68,12 +68,15 @@
FEATURE7 = """
Feature: Many scenarios
+
+ @first
Scenario: 1st one
Given I have a defined step
Scenario: 2nd one
Given I have a defined step
+ @third
Scenario: 3rd one
Given I have a defined step
@@ -223,7 +226,7 @@ def test_steps_are_aware_of_its_definitions():
step1 = scenario_result.steps_passed[0]
- assert_equals(step1.defined_at.line, 109)
+ assert_equals(step1.defined_at.line, 112)
assert_equals(step1.defined_at.file, core.fs.relpath(__file__.rstrip("c")))
@with_setup(step_runner_environ)
@@ -303,6 +306,23 @@ def just_register(scenario):
@with_setup(step_runner_environ)
+def test_feature_can_run_only_specified_scenarios_in_tags():
+ "Features can run only specified scenarios, by tags"
+ feature = Feature.from_string(FEATURE7)
+
+ scenarios_ran = []
+
+ @after.each_scenario
+ def just_register(scenario):
+ scenarios_ran.append(scenario.name)
+
+ result = feature.run(tags=['first', 'third'])
+ assert result.scenario_results
+
+ assert_equals(scenarios_ran, ['1st one', '3rd one'])
+
+
+@with_setup(step_runner_environ)
def test_count_raised_exceptions_as_failing_steps():
"When a step definition raises an exception, it is marked as a failed step. "
18 tests/unit/test_terrain.py
View
@@ -39,6 +39,7 @@
Given I append "during" to states
'''
+
def test_world():
"lettuce.terrain.world can be monkey patched at will"
@@ -57,9 +58,11 @@ def test_does_have():
set_world()
test_does_have()
+
def test_after_each_step_is_executed_before_each_step():
"terrain.before.each_step and terrain.after.each_step decorators"
world.step_states = []
+
@before.each_step
def set_state_to_before(step):
world.step_states.append('before')
@@ -83,6 +86,7 @@ def set_state_to_after(step):
assert_equals(world.step_states, ['before', 'during', 'after'])
+
def test_after_each_scenario_is_executed_before_each_scenario():
"terrain.before.each_scenario and terrain.after.each_scenario decorators"
world.scenario_steps = []
@@ -104,9 +108,10 @@ def set_state_to_after(scenario):
assert_equals(
world.scenario_steps,
- ['before', 'during', 'after', 'before', 'during', 'after']
+ ['before', 'during', 'after', 'before', 'during', 'after'],
)
+
def test_after_each_feature_is_executed_before_each_feature():
"terrain.before.each_feature and terrain.after.each_feature decorators"
world.feature_steps = []
@@ -128,9 +133,10 @@ def set_state_to_after(feature):
assert_equals(
world.feature_steps,
- ['before', 'during', 'during', 'after']
+ ['before', 'during', 'during', 'after'],
)
+
def test_after_each_all_is_executed_before_each_all():
"terrain.before.each_all and terrain.after.each_all decorators"
import lettuce
@@ -159,6 +165,7 @@ def test_after_each_all_is_executed_before_each_all():
runner = lettuce.Runner('some_basepath')
CALLBACK_REGISTRY.clear()
+
@before.all
def set_state_to_before():
world.all_steps.append('before')
@@ -178,11 +185,12 @@ def set_state_to_after(total):
assert_equals(
world.all_steps,
- ['before', 'during', 'during', 'after']
+ ['before', 'during', 'during', 'after'],
)
mox.UnsetStubs()
+
def test_world_should_be_able_to_absorb_functions():
u"world should be able to absorb functions"
assert not hasattr(world, 'function1')
@@ -200,6 +208,7 @@ def function1():
assert not hasattr(world, 'function1')
+
def test_world_should_be_able_to_absorb_lambdas():
u"world should be able to absorb lambdas"
assert not hasattr(world, 'named_func')
@@ -215,8 +224,9 @@ def test_world_should_be_able_to_absorb_lambdas():
assert not hasattr(world, 'named_func')
+
def test_world_should_be_able_to_absorb_classs():
- u"world should be able to absorb classs"
+ u"world should be able to absorb class"
assert not hasattr(world, 'MyClass')
@world.absorb
Please sign in to comment.
Something went wrong with that request. Please try again.