Skip to content
This repository
Browse code

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
@@ -23,8 +23,6 @@
23 23 import traceback
24 24 from datetime import datetime
25 25
26   -from lettuce import fs
27   -
28 26 from lettuce.core import Feature, TotalResult
29 27
30 28 from lettuce.terrain import after
@@ -37,7 +35,7 @@
37 35 from lettuce.registry import CALLBACK_REGISTRY
38 36 from lettuce.exceptions import StepLoadingError
39 37 from lettuce.plugins import xunit_output
40   -
  38 +from lettuce import fs
41 39 from lettuce import exceptions
42 40
43 41 __all__ = [
@@ -71,12 +69,14 @@ class Runner(object):
71 69 features and step definitions on there.
72 70 """
73 71 def __init__(self, base_path, scenarios=None, verbosity=0,
74   - enable_xunit=False, xunit_filename=None):
  72 + enable_xunit=False, xunit_filename=None, tags=None):
75 73 """ lettuce.Runner will try to find a terrain.py file and
76 74 import it from within `base_path`
77 75 """
78 76
  77 + self.tags = tags
79 78 self.single_feature = None
  79 +
80 80 if os.path.isfile(base_path) and os.path.exists(base_path):
81 81 self.single_feature = base_path
82 82 base_path = os.path.dirname(base_path)
@@ -134,7 +134,7 @@ def run(self):
134 134 for filename in features_files:
135 135 feature = Feature.from_file(filename)
136 136 results.append(
137   - feature.run(self.scenarios))
  137 + feature.run(self.scenarios, tags=self.tags))
138 138
139 139 except exceptions.LettuceSyntaxError, e:
140 140 sys.stderr.write(e.msg)
112 lettuce/core.py
@@ -17,6 +17,7 @@
17 17
18 18 import re
19 19 import codecs
  20 +from fuzzywuzzy import fuzz
20 21 import unicodedata
21 22 from itertools import chain
22 23 from copy import deepcopy
@@ -496,8 +497,11 @@ class Scenario(object):
496 497 indentation = 2
497 498 table_indentation = indentation + 2
498 499
499   - def __init__(self, name, remaining_lines, keys, outlines, with_file=None,
500   - original_string=None, language=None):
  500 + def __init__(self, name, remaining_lines, keys, outlines,
  501 + with_file=None,
  502 + original_string=None,
  503 + language=None,
  504 + previous_scenario=None):
501 505
502 506 if not language:
503 507 language = language()
@@ -513,6 +517,8 @@ def __init__(self, name, remaining_lines, keys, outlines, with_file=None,
513 517 self.with_file = with_file
514 518 self.original_string = original_string
515 519
  520 + self.previous_scenario = previous_scenario
  521 +
516 522 if with_file and original_string:
517 523 scenario_definition = ScenarioDescription(self, with_file,
518 524 original_string,
@@ -526,7 +532,7 @@ def __init__(self, name, remaining_lines, keys, outlines, with_file=None,
526 532 if original_string and '@' in self.original_string:
527 533 self.tags = self._find_tags_in(original_string)
528 534 else:
529   - self.tags = []
  535 + self.tags = None
530 536
531 537 @property
532 538 def max_length(self):
@@ -572,6 +578,46 @@ def _calc_value_length(self, data):
572 578 def __repr__(self):
573 579 return u'<Scenario: "%s">' % self.name
574 580
  581 + def matches_tags(self, tags):
  582 + if tags is None:
  583 + return True
  584 +
  585 + if not self.tags:
  586 + return False
  587 +
  588 + matched = []
  589 +
  590 + for tag in self.tags:
  591 + if tag in tags:
  592 + return True
  593 +
  594 + for tag in tags:
  595 + exclude = tag.startswith('-')
  596 + if exclude:
  597 + tag = tag[1:]
  598 +
  599 + fuzzable = tag.startswith('~')
  600 + if fuzzable:
  601 + tag = tag[1:]
  602 +
  603 + result = tag in self.tags
  604 + if fuzzable:
  605 + fuzzed = []
  606 + for internal_tag in self.tags:
  607 + ratio = fuzz.ratio(tag, internal_tag)
  608 + if exclude:
  609 + fuzzed.append(ratio <= 80)
  610 + else:
  611 + fuzzed.append(ratio > 80)
  612 +
  613 + result = any(fuzzed)
  614 + elif exclude:
  615 + result = tag not in self.tags
  616 +
  617 + matched.append(result)
  618 +
  619 + return all(matched)
  620 +
575 621 @property
576 622 def evaluated(self):
577 623 for outline in self.outlines:
@@ -639,21 +685,34 @@ def _add_myself_to_steps(self):
639 685 step.scenario = self
640 686
641 687 def _find_tags_in(self, original_string):
642   - previous_scenario_re = re.compile(ur"(?:%s.*)([@].*)%s: (%s)" % (
643   - self.language.non_capturable_scenario_separator,
  688 + broad_regex = re.compile(ur"([@].*)%s: (%s)" % (
644 689 self.language.scenario_separator,
645 690 self.name), re.DOTALL)
646 691
647   - first_of_scenario_re = re.compile(ur"([@].*)%s: (%s)" % (
648   - self.language.scenario_separator,
649   - self.name), re.DOTALL)
  692 + regexes = []
  693 + if not self.previous_scenario:
  694 + regexes.append(broad_regex)
650 695
651   - for regex in (previous_scenario_re, first_of_scenario_re):
  696 + else:
  697 + regexes.append(re.compile(ur"(?:%s: %s.*)([@]?.*)%s: (%s)" % (
  698 + self.language.non_capturable_scenario_separator,
  699 + self.previous_scenario.name,
  700 + self.language.scenario_separator,
  701 + self.name), re.DOTALL))
  702 +
  703 + def try_finding_with(regex):
652 704 found = regex.search(original_string)
653 705 if found:
654 706 tag_lines = found.group().splitlines()
655 707 return list(chain(*map(self._extract_tag, tag_lines)))
656 708
  709 + for regex in regexes:
  710 + found = try_finding_with(regex)
  711 + if found:
  712 + return found
  713 +
  714 + return []
  715 +
657 716 def _extract_tag(self, item):
658 717 regex = re.compile(r'[@](\S+)')
659 718 return regex.findall(item)
@@ -693,8 +752,13 @@ def represent_examples(self):
693 752 return "\n".join([(u" " * self.table_indentation) + line for line in lines]) + '\n'
694 753
695 754 @classmethod
696   - def from_string(new_scenario, string, with_file=None, original_string=None, language=None):
  755 + def from_string(new_scenario, string,
  756 + with_file=None,
  757 + original_string=None,
  758 + language=None,
  759 + previous_scenario=None):
697 760 """ Creates a new scenario from string"""
  761 +
698 762 # ignoring comments
699 763 string = "\n".join(strings.get_stripped_lines(string, ignore_lines_starting_with='#'))
700 764
@@ -724,6 +788,7 @@ def from_string(new_scenario, string, with_file=None, original_string=None, lang
724 788 with_file=with_file,
725 789 original_string=original_string,
726 790 language=language,
  791 + previous_scenario=previous_scenario,
727 792 )
728 793
729 794 return scenario
@@ -854,8 +919,9 @@ def _parse_remaining_lines(self, lines, original_string, with_file=None):
854 919 description = parts[0]
855 920 parts.pop(0)
856 921
857   - scenario_strings = [u"%s: %s" % (self.language.first_of_scenario, s) \
858   - for s in parts if s.strip()]
  922 + prefix = self.language.first_of_scenario
  923 + upcoming_scenarios = [
  924 + u"%s: %s" % (prefix, s) for s in parts if s.strip()]
859 925
860 926 kw = dict(
861 927 original_string=original_string,
@@ -863,11 +929,26 @@ def _parse_remaining_lines(self, lines, original_string, with_file=None):
863 929 language=self.language,
864 930 )
865 931
866   - scenarios = [Scenario.from_string(s, **kw) for s in scenario_strings]
  932 + scenarios = []
  933 + while upcoming_scenarios:
  934 + current = upcoming_scenarios.pop(0)
  935 + previous_scenario = None
  936 + has_previous = len(scenarios) > 0
  937 +
  938 + if has_previous:
  939 + previous_scenario = scenarios[-1]
  940 +
  941 + params = dict(
  942 + previous_scenario=previous_scenario,
  943 + )
  944 +
  945 + params.update(kw)
  946 + current_scenario = Scenario.from_string(current, **params)
  947 + scenarios.append(current_scenario)
867 948
868 949 return scenarios, description
869 950
870   - def run(self, scenarios=None, ignore_case=True):
  951 + def run(self, scenarios=None, ignore_case=True, tags=None):
871 952 call_hook('before_each', 'feature', self)
872 953 scenarios_ran = []
873 954
@@ -881,6 +962,9 @@ def run(self, scenarios=None, ignore_case=True):
881 962 if scenarios_to_run and (index + 1) not in scenarios_to_run:
882 963 continue
883 964
  965 + if not scenario.matches_tags(tags):
  966 + continue
  967 +
884 968 scenarios_ran.extend(scenario.run(ignore_case))
885 969
886 970 call_hook('after_each', 'feature', self)
15 tests/functional/tag_features/timebound/timebound.feature
... ... @@ -0,0 +1,15 @@
  1 +Feature: ignore slow steps
  2 + As a python developer
  3 + I want to run only the fast tests
  4 + So that I can be really happy
  5 +
  6 + @slow-ish
  7 + Scenario: this one is kinda slow
  8 + Given I wait for 60 seconds
  9 + Then the time passed is 1 minute
  10 +
  11 +
  12 + @fast-ish
  13 + Scenario: this one is fast!!
  14 + Given I wait for 0 seconds
  15 + Then the time passed is 0 seconds
54 tests/functional/test_runner.py
@@ -16,7 +16,7 @@
16 16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 17 import os
18 18 import lettuce
19   -
  19 +from mock import Mock
20 20 from StringIO import StringIO
21 21 from os.path import dirname, join, abspath
22 22 from nose.tools import assert_equals, with_setup, assert_raises
@@ -38,15 +38,21 @@
38 38 lettuce_dir = abspath(dirname(lettuce.__file__))
39 39 ojoin = lambda *x: join(current_dir, 'output_features', *x)
40 40 sjoin = lambda *x: join(current_dir, 'syntax_features', *x)
  41 +tjoin = lambda *x: join(current_dir, 'tag_features', *x)
  42 +
41 43 lettuce_path = lambda *x: fs.relpath(join(lettuce_dir, *x))
42 44
43 45 call_line = StepDefinition.__call__.im_func.func_code.co_firstlineno + 5
44 46
45   -def feature_name(name):
46   - return join(abspath(dirname(__file__)), 'output_features', name, "%s.feature" % name)
47 47
48   -def syntax_feature_name(name):
49   - return sjoin(name, "%s.feature" % name)
  48 +def joiner(callback, name):
  49 + return callback(name, "%s.feature" % name)
  50 +
  51 +
  52 +feature_name = lambda name: joiner(ojoin, name)
  53 +syntax_feature_name = lambda name: joiner(sjoin, name)
  54 +tag_feature_name = lambda name: joiner(tjoin, name)
  55 +
50 56
51 57 @with_setup(prepare_stderr)
52 58 def test_try_to_import_terrain():
@@ -1052,3 +1058,41 @@ def append_2_more(step):
1052 1058 "1 scenario (1 passed)\n"
1053 1059 "4 steps (4 passed)\n"
1054 1060 )
  1061 +
  1062 +
  1063 +@with_setup(prepare_stdout)
  1064 +def test_run_only_fast_tests():
  1065 + "Runner can filter by tags"
  1066 +
  1067 + from lettuce import step
  1068 +
  1069 + good_one = Mock()
  1070 + bad_one = Mock()
  1071 +
  1072 + @step('I wait for 0 seconds')
  1073 + def wait_for_0_seconds(step):
  1074 + good_one(step.sentence)
  1075 +
  1076 + @step('the time passed is 0 seconds')
  1077 + def time_passed_0_sec(step):
  1078 + good_one(step.sentence)
  1079 +
  1080 + @step('I wait for 60 seconds')
  1081 + def wait_for_60_seconds(step):
  1082 + bad_one(step.sentence)
  1083 +
  1084 + @step('the time passed is 1 minute')
  1085 + def time_passed_1_min(step):
  1086 + bad_one(step.sentence)
  1087 +
  1088 + filename = tag_feature_name('timebound')
  1089 + runner = Runner(filename, verbosity=1, tags=['fast-ish'])
  1090 + runner.run()
  1091 +
  1092 + assert_stdout_lines(
  1093 + ".."
  1094 + "\n"
  1095 + "1 feature (1 passed)\n"
  1096 + "1 scenario (1 passed)\n"
  1097 + "2 steps (2 passed)\n"
  1098 + )
37 tests/unit/test_feature_parser.py
@@ -175,6 +175,27 @@
175 175
176 176 """
177 177
  178 +FEATURE13 = """
  179 +Feature: correct matching
  180 + @runme
  181 + Scenario: Holy tag, Batman
  182 + Given this scenario has tags
  183 + Then it can be inspected from within the object
  184 +
  185 + Scenario: This has no tags
  186 + Given this scenario has tags
  187 + Then it can be inspected from within the object
  188 +
  189 + @slow
  190 + Scenario: this is slow
  191 + Given this scenario has tags
  192 + Then it can be inspected from within the object
  193 +
  194 + Scenario: Also without tags
  195 + Given this scenario has tags
  196 + Then it can be inspected from within the object
  197 +"""
  198 +
178 199
179 200 def test_feature_has_repr():
180 201 "Feature implements __repr__ nicely"
@@ -349,15 +370,17 @@ def test_single_scenario_single_scenario():
349 370
350 371
351 372 def test_single_scenario_many_scenarios():
352   - "Features should have their scenarios parsed with its respective tags"
353   - feature = Feature.from_string(FEATURE12)
  373 + "Untagged scenario following a tagged one should have no tags"
  374 + feature = Feature.from_string(FEATURE13)
354 375
355 376 first_scenario = feature.scenarios[0]
  377 + assert that(first_scenario.tags).equals(['runme'])
356 378
357   - assert that(first_scenario.tags).deep_equals([
358   - 'many', 'other', 'basic', 'tags', 'here', ':)'])
  379 + second_scenario = feature.scenarios[1]
  380 + assert that(second_scenario.tags).equals([])
359 381
360   - last_scenario = feature.scenarios[1]
  382 + third_scenario = feature.scenarios[2]
  383 + assert that(third_scenario.tags).equals(['slow'])
361 384
362   - assert that(last_scenario.tags).deep_equals([
363   - 'only', 'a-few', 'tags'])
  385 + last_scenario = feature.scenarios[3]
  386 + assert that(last_scenario.tags).equals([])
47 tests/unit/test_scenario_parsing.py
@@ -459,3 +459,50 @@ def test_scenario_has_tags_singleline():
459 459 'another',
460 460 '$%^&even-weird_chars',
461 461 ])
  462 +
  463 +
  464 +def test_scenario_matches_tags():
  465 + ("A scenario with tags should respond with True when "
  466 + ".matches_tags() is called with a valid list of tags")
  467 +
  468 + scenario = Scenario.from_string(
  469 + SCENARIO1,
  470 + original_string=('@onetag\n@another-one\n' + SCENARIO1.strip()))
  471 +
  472 + assert that(scenario.tags).deep_equals(['onetag','another-one'])
  473 + assert scenario.matches_tags(['onetag'])
  474 + assert scenario.matches_tags(['another-one'])
  475 +
  476 +
  477 +def test_scenario_matches_tags_fuzzywuzzy():
  478 + ("When Scenario#matches_tags is called with a member starting with ~ "
  479 + "it will consider a fuzzywuzzy match")
  480 +
  481 + scenario = Scenario.from_string(
  482 + SCENARIO1,
  483 + original_string=('@anothertag\n@another-tag\n' + SCENARIO1.strip()))
  484 +
  485 + assert scenario.matches_tags(['~another'])
  486 +
  487 +
  488 +def test_scenario_matches_tags_excluding():
  489 + ("When Scenario#matches_tags is called with a member starting with - "
  490 + "it will exclude that tag from the matching")
  491 +
  492 + scenario = Scenario.from_string(
  493 + SCENARIO1,
  494 + original_string=('@anothertag\n@another-tag\n' + SCENARIO1.strip()))
  495 +
  496 + assert not scenario.matches_tags(['-anothertag'])
  497 + assert scenario.matches_tags(['-foobar'])
  498 +
  499 +
  500 +def test_scenario_matches_tags_excluding_fuzzywuzzy():
  501 + ("When Scenario#matches_tags is called with a member starting with -~ "
  502 + "it will exclude that tag from that fuzzywuzzy match")
  503 +
  504 + scenario = Scenario.from_string(
  505 + SCENARIO1,
  506 + original_string=('@anothertag\n@another-tag\n' + SCENARIO1.strip()))
  507 +
  508 + assert not scenario.matches_tags(['-~anothertag'])
22 tests/unit/test_step_runner.py
@@ -68,12 +68,15 @@
68 68
69 69 FEATURE7 = """
70 70 Feature: Many scenarios
  71 +
  72 + @first
71 73 Scenario: 1st one
72 74 Given I have a defined step
73 75
74 76 Scenario: 2nd one
75 77 Given I have a defined step
76 78
  79 + @third
77 80 Scenario: 3rd one
78 81 Given I have a defined step
79 82
@@ -223,7 +226,7 @@ def test_steps_are_aware_of_its_definitions():
223 226
224 227 step1 = scenario_result.steps_passed[0]
225 228
226   - assert_equals(step1.defined_at.line, 109)
  229 + assert_equals(step1.defined_at.line, 112)
227 230 assert_equals(step1.defined_at.file, core.fs.relpath(__file__.rstrip("c")))
228 231
229 232 @with_setup(step_runner_environ)
@@ -303,6 +306,23 @@ def just_register(scenario):
303 306
304 307
305 308 @with_setup(step_runner_environ)
  309 +def test_feature_can_run_only_specified_scenarios_in_tags():
  310 + "Features can run only specified scenarios, by tags"
  311 + feature = Feature.from_string(FEATURE7)
  312 +
  313 + scenarios_ran = []
  314 +
  315 + @after.each_scenario
  316 + def just_register(scenario):
  317 + scenarios_ran.append(scenario.name)
  318 +
  319 + result = feature.run(tags=['first', 'third'])
  320 + assert result.scenario_results
  321 +
  322 + assert_equals(scenarios_ran, ['1st one', '3rd one'])
  323 +
  324 +
  325 +@with_setup(step_runner_environ)
306 326 def test_count_raised_exceptions_as_failing_steps():
307 327 "When a step definition raises an exception, it is marked as a failed step. "
308 328
18 tests/unit/test_terrain.py
@@ -39,6 +39,7 @@
39 39 Given I append "during" to states
40 40 '''
41 41
  42 +
42 43 def test_world():
43 44 "lettuce.terrain.world can be monkey patched at will"
44 45
@@ -57,9 +58,11 @@ def test_does_have():
57 58 set_world()
58 59 test_does_have()
59 60
  61 +
60 62 def test_after_each_step_is_executed_before_each_step():
61 63 "terrain.before.each_step and terrain.after.each_step decorators"
62 64 world.step_states = []
  65 +
63 66 @before.each_step
64 67 def set_state_to_before(step):
65 68 world.step_states.append('before')
@@ -83,6 +86,7 @@ def set_state_to_after(step):
83 86
84 87 assert_equals(world.step_states, ['before', 'during', 'after'])
85 88
  89 +
86 90 def test_after_each_scenario_is_executed_before_each_scenario():
87 91 "terrain.before.each_scenario and terrain.after.each_scenario decorators"
88 92 world.scenario_steps = []
@@ -104,9 +108,10 @@ def set_state_to_after(scenario):
104 108
105 109 assert_equals(
106 110 world.scenario_steps,
107   - ['before', 'during', 'after', 'before', 'during', 'after']
  111 + ['before', 'during', 'after', 'before', 'during', 'after'],
108 112 )
109 113
  114 +
110 115 def test_after_each_feature_is_executed_before_each_feature():
111 116 "terrain.before.each_feature and terrain.after.each_feature decorators"
112 117 world.feature_steps = []
@@ -128,9 +133,10 @@ def set_state_to_after(feature):
128 133
129 134 assert_equals(
130 135 world.feature_steps,
131   - ['before', 'during', 'during', 'after']
  136 + ['before', 'during', 'during', 'after'],
132 137 )
133 138
  139 +
134 140 def test_after_each_all_is_executed_before_each_all():
135 141 "terrain.before.each_all and terrain.after.each_all decorators"
136 142 import lettuce
@@ -159,6 +165,7 @@ def test_after_each_all_is_executed_before_each_all():
159 165
160 166 runner = lettuce.Runner('some_basepath')
161 167 CALLBACK_REGISTRY.clear()
  168 +
162 169 @before.all
163 170 def set_state_to_before():
164 171 world.all_steps.append('before')
@@ -178,11 +185,12 @@ def set_state_to_after(total):
178 185
179 186 assert_equals(
180 187 world.all_steps,
181   - ['before', 'during', 'during', 'after']
  188 + ['before', 'during', 'during', 'after'],
182 189 )
183 190
184 191 mox.UnsetStubs()
185 192
  193 +
186 194 def test_world_should_be_able_to_absorb_functions():
187 195 u"world should be able to absorb functions"
188 196 assert not hasattr(world, 'function1')
@@ -200,6 +208,7 @@ def function1():
200 208
201 209 assert not hasattr(world, 'function1')
202 210
  211 +
203 212 def test_world_should_be_able_to_absorb_lambdas():
204 213 u"world should be able to absorb lambdas"
205 214 assert not hasattr(world, 'named_func')
@@ -215,8 +224,9 @@ def test_world_should_be_able_to_absorb_lambdas():
215 224
216 225 assert not hasattr(world, 'named_func')
217 226
  227 +
218 228 def test_world_should_be_able_to_absorb_classs():
219   - u"world should be able to absorb classs"
  229 + u"world should be able to absorb class"
220 230 assert not hasattr(world, 'MyClass')
221 231
222 232 @world.absorb

0 comments on commit 5bfee60

Please sign in to comment.
Something went wrong with that request. Please try again.