Skip to content

Commit 8a5b324

Browse files
Merge pull request #14 from npilon/invoke-test-by-name
Add Support to Invoke Tests By Name
2 parents b4ad393 + 9602ebb commit 8a5b324

File tree

5 files changed

+655
-18
lines changed

5 files changed

+655
-18
lines changed

README.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,25 @@ scenario multiple times with different values. For example:
132132
133133
Your ``'before'`` and ``'after'`` ``'scenario'`` hooks will only run
134134
once for the entire scenario outline.
135+
136+
Invoking Tests
137+
--------------
138+
139+
You can run tests by allowing nose2's autodiscovery to find all of your tests,
140+
or you can specify specific tests to be run on the command line. When
141+
specifying specific tests, you can either specify an entire package,
142+
an entire feature, or individual scenarios. Individual scenarios can be
143+
specified either by index (from 0) or by name.
144+
145+
.. code::
146+
147+
nose2 planterbox.tests.test_feature
148+
nose2 planterbox.tests.test_feature:basic.feature planterbox.tests.test_hooks:hooks.feature
149+
nose2 planterbox.tests.test_feature:basic.feature:1
150+
nose2 planterbox.tests.test_feature:basic.feature:0
151+
nose2 planterbox.tests.test_feature:basic.feature:"I need to verify basic arithmetic"
152+
nose2 planterbox.tests.test_feature:basic.feature:"I need to verify basic arithmetic."
153+
154+
If your feature includes multiple scenarios with the same name, all will be
155+
run when that name is given. Names with a trailing period can be specified with
156+
or without the trailing period.

planterbox/feature.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""TestCase subclass for executing the scenarios from a feature"""
22

3+
from cStringIO import StringIO
4+
import csv
35
import codecs
46
from importlib import import_module
57
from itertools import (
@@ -53,11 +55,11 @@ def from_exc_info(cls, exc_info, scenario_index,
5355
class FeatureTestCase(TestCase):
5456
"""A test case generated from the scenarios in a feature file."""
5557

56-
def __init__(self, feature_path, scenario_indexes=None, feature_text=None,
58+
def __init__(self, feature_path, scenarios_to_run=None, feature_text=None,
5759
config=None):
5860
super(FeatureTestCase, self).__init__('nota')
5961
self.feature_path = feature_path
60-
self.scenario_indexes = scenario_indexes
62+
self.scenarios_to_run = scenarios_to_run
6163
self.config = config
6264

6365
if feature_text is None:
@@ -71,10 +73,12 @@ def __init__(self, feature_path, scenario_indexes=None, feature_text=None,
7173
self.feature_doc = [doc.strip() for doc in header_text[1:]]
7274

7375
def id(self):
74-
if self.scenario_indexes:
75-
return self.feature_id() + ':' + ','.join(
76-
[unicode(i) for i in self.scenario_indexes]
77-
)
76+
if self.scenarios_to_run:
77+
scenario_string = StringIO()
78+
csv.writer(
79+
scenario_string, quoting=csv.QUOTE_NONNUMERIC
80+
).writerow(list(self.scenarios_to_run))
81+
return self.feature_id() + ':' + scenario_string.getvalue().strip()
7882
else:
7983
return self.feature_id()
8084

@@ -133,8 +137,8 @@ def run(self, result=None):
133137
try:
134138
for i, scenario in enumerate(self.scenarios):
135139
if (
136-
self.scenario_indexes and
137-
i not in self.scenario_indexes
140+
self.scenarios_to_run and
141+
not self.should_run_scenario(i, scenario)
138142
):
139143
continue
140144

@@ -177,6 +181,22 @@ def run(self, result=None):
177181
except HookFailedException:
178182
return # Failure already registered.
179183

184+
def should_run_scenario(self, i, scenario):
185+
"""Decide whether to run this scenario when running a subset"""
186+
scenario_name = scenario[0].partition(':')[2].strip()
187+
188+
no_trailing_period = (
189+
scenario_name[:-1]
190+
if scenario_name[-1] == '.'
191+
else scenario_name
192+
)
193+
194+
return (
195+
scenario_name in self.scenarios_to_run or
196+
no_trailing_period in self.scenarios_to_run or
197+
i in self.scenarios_to_run
198+
)
199+
180200
def run_scenario(self, module, index, scenario, result):
181201
completed_steps = []
182202
self.scenario_index = index

planterbox/plugin.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""nose2 plugin for discovering and running planterbox features as tests"""
22

33
from collections import defaultdict
4+
import csv
45
from datetime import datetime
56
from functools import partial
67
from itertools import (
@@ -31,7 +32,7 @@
3132

3233

3334
EXAMPLE_TO_FORMAT = re.compile(r'<(.+?)>')
34-
FEATURE_NAME = re.compile(r'\.feature(?:\:[\d,]+)?$')
35+
FEATURE_NAME = re.compile(r'\.feature(?:\:.+)?$')
3536

3637

3738
class Planterbox(Plugin):
@@ -55,7 +56,7 @@ def register(self):
5556
start_datetime.strftime("%H_%M_%S")))
5657

5758
def makeSuiteFromFeature(self, module, feature_path,
58-
scenario_indexes=None):
59+
scenarios_to_run=None):
5960
MyTestSuite = transplant_class(TestSuite, module.__name__)
6061

6162
MyFeatureTestCase = transplant_class(FeatureTestCase, module.__name__)
@@ -64,7 +65,7 @@ def makeSuiteFromFeature(self, module, feature_path,
6465
tests=[
6566
MyFeatureTestCase(
6667
feature_path=feature_path,
67-
scenario_indexes=scenario_indexes,
68+
scenarios_to_run=scenarios_to_run,
6869
config=self.config,
6970
),
7071
],
@@ -121,7 +122,7 @@ def _from_names(self, names):
121122

122123
for (
123124
feature_package_name, feature_filename
124-
), scenario_indexes in sorted(by_feature.iteritems()):
125+
), scenarios_to_run in sorted(by_feature.iteritems()):
125126
feature_module = object_from_name(feature_package_name)[1]
126127
feature_path = os.path.join(
127128
os.path.dirname(feature_module.__file__), feature_filename
@@ -130,7 +131,7 @@ def _from_names(self, names):
130131
suite = self.makeSuiteFromFeature(
131132
module=feature_module,
132133
feature_path=feature_path,
133-
scenario_indexes=scenario_indexes,
134+
scenarios_to_run=scenarios_to_run,
134135
)
135136
yield suite
136137

@@ -148,15 +149,36 @@ def normalize_names(names):
148149
for name in sorted(names):
149150
name_parts = name.split(':')
150151
if len(name_parts) == 3:
151-
scenario_indexes = {int(s) for s in name_parts.pop(-1).split(',')}
152+
scenario_string = name_parts.pop(-1)
153+
scenarios_to_run = resolve_scenarios(scenario_string)
152154
name_parts = tuple(name_parts)
153155
if name_parts not in by_feature or by_feature[name_parts]:
154-
by_feature[name_parts].update(scenario_indexes)
156+
# Avoid adding specific scenarios if we've explicitly listed
157+
# an entire feature
158+
by_feature[name_parts].update(scenarios_to_run)
155159
elif len(name_parts) == 2:
156160
name_parts = tuple(name_parts)
157-
scenario_indexes = None
161+
scenarios_to_run = None # So... All!
158162
by_feature[name_parts] = set()
159163
else:
160164
continue
161165

162166
return by_feature
167+
168+
169+
def resolve_scenarios(scenario_string):
170+
"""Convert a comma-separated string of scenarios into a set of scenarios
171+
172+
Scenarios can be specified as either scenario names (optionally quoted)
173+
or scenario indexes."""
174+
175+
scenario_string = scenario_string.strip()
176+
if not scenario_string:
177+
return []
178+
179+
scenario_parser = csv.reader([scenario_string])
180+
scenarios = scenario_parser.next()
181+
scenarios = {
182+
int(s) if s.isdigit() else s for s in scenarios
183+
}
184+
return scenarios

0 commit comments

Comments
 (0)