summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2014-02-05 15:37:01 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2014-02-11 11:50:45 +0000
commit711c89a85b52a987ea284d905c41bd5b52e5374a (patch)
treeaae9600a6fabf3f8f7bf2ff0343827a6b728f687
parent5e4610f0a687f4b308ac1283d4c2ad0b707e42f5 (diff)
downloadcmdtest-711c89a85b52a987ea284d905c41bd5b52e5374a.tar.gz
Factor scenario step and implementation connector into yarnlib
Rather than passing allow-missing-steps in, `missing_step_cb` can be passed to the ScenarioStepConnector, which if it returns true, will exclude that scenario, rather than raising an exception. This was chosen, as it also works as a status update callback.
-rwxr-xr-xyarn56
-rw-r--r--yarnlib/__init__.py5
-rw-r--r--yarnlib/scenario_step_connector.py108
-rw-r--r--yarnlib/scenario_step_connector_tests.py95
4 files changed, 217 insertions, 47 deletions
diff --git a/yarn b/yarn
index cc794f7..131ea2d 100755
--- a/yarn
+++ b/yarn
@@ -178,44 +178,18 @@ class YarnRunner(cliapp.Application):
return block_parser.scenarios, block_parser.implementations
def connect_implementations(self, scenarios, implementations):
- new_list = []
- for scenario in scenarios:
- missing_step = False
- for step in scenario.steps:
- self.connect_implementation(
- scenario, step, implementations)
- if step.implementation is None:
- missing_step = True
- if not missing_step:
- new_list.append(scenario)
- return new_list
-
- def connect_implementation(self, scenario, step, implementations):
- matching = [i for i in implementations
- if step.what == i.what and
- self.implements_matches_step(i, step)]
-
- if len(matching) == 0:
- if self.settings['allow-missing-steps']:
+ if self.settings['allow-missing-steps']:
+ def warn_missing(scenario, step):
self.warning(
'Scenario %s has missing step %s %s' %
(scenario.name, step.what, step.text))
- return
- raise cliapp.AppException(
- 'Scenario "%s", step "%s %s" has no matching '
- 'implementation' %
- (scenario.name, step.what, step.text))
- if len(matching) > 1:
- s = '\n'.join(
- 'IMPLEMENTS %s %s' % (i.what, i.regexp)
- for i in matching)
- raise cliapp.AppException(
- 'Scenario "%s", step "%s %s" has more than one '
- 'matching implementations:\n%s' %
- (scenario.name, step.what, step.text, s))
+ return True
+ step_connector = yarnlib.ScenarioStepConnector(
+ implementations, missing_step_cb=warn_missing)
+ else:
+ step_connector = yarnlib.ScenarioStepConnector(implementations)
- assert step.implementation is None
- step.implementation = matching[0]
+ return step_connector.connect_implementations(scenarios)
def load_shell_libraries(self):
if not self.settings['shell-library']:
@@ -366,7 +340,7 @@ class YarnRunner(cliapp.Application):
self.ts['step_name'] = '%s %s' % (step.what, step.text)
self.steps_run += 1
- m = self.implements_matches_step(step.implementation, step)
+ m = yarnlib.implements_matches_step(step.implementation, step)
assert m is not None
env = self.clean_env()
env['DATADIR'] = datadir
@@ -435,18 +409,6 @@ class YarnRunner(cliapp.Application):
nice = ''.join(nice)
return nice
- def implements_matches_step(self, implements, step):
- '''Return re.Match if implements matches the step.
-
- Otherwise, return None.
-
- '''
-
- m = re.match(implements.regexp, step.text, re.I)
- if m and m.end() != len(step.text):
- return None
- return m
-
def indent(self, s):
return ''.join(' %s\n' % line for line in s.splitlines())
diff --git a/yarnlib/__init__.py b/yarnlib/__init__.py
index b5b207d..e17223a 100644
--- a/yarnlib/__init__.py
+++ b/yarnlib/__init__.py
@@ -21,5 +21,10 @@ from elements import Scenario, ScenarioStep, Implementation
from block_parser import BlockParser, BlockError
from scenario_validator import (NoScenariosError, DuplicateScenariosError,
NoThensError, ScenarioValidator)
+from scenario_step_connector import (implements_matches_step,
+ ScenarioStepConnector,
+ StepNotImplementedError,
+ StepMultipleImplementationsError)
+
from shell_libraries import load_shell_libraries
diff --git a/yarnlib/scenario_step_connector.py b/yarnlib/scenario_step_connector.py
new file mode 100644
index 0000000..3e0712f
--- /dev/null
+++ b/yarnlib/scenario_step_connector.py
@@ -0,0 +1,108 @@
+# Copyright 2014 Lars Wirzenius and Codethink Limited
+#
+# 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/>.
+#
+# =*= License: GPL-3+ =*=
+
+
+import cliapp
+import re
+
+
+def implements_matches_step(implements, step):
+ '''Return re.Match if implements matches the step.
+
+ Otherwise, return None.
+
+ '''
+
+ m = re.match(implements.regexp, step.text, re.I)
+ if m and m.end() != len(step.text):
+ return None
+ return m
+
+
+class StepNotImplementedError(cliapp.AppException):
+
+ def __init__(self, scenario, step):
+ cliapp.AppException.__init__(
+ self, 'Scenario "%s", step "%s %s" has no matching '
+ 'implementation' %
+ (scenario.name, step.what, step.text))
+
+
+class StepMultipleImplementationsError(cliapp.AppException):
+
+ def __init__(self, scenario, step, matching):
+ s = '\n'.join(
+ 'IMPLEMENTS %s %s' % (i.what, i.regexp)
+ for i in matching)
+ cliapp.AppException.__init__(
+ self, 'Scenario "%s", step "%s %s" has more than one '
+ 'matching implementations:\n%s' %
+ (scenario.name, step.what, step.text, s))
+
+
+class ScenarioStepConnector(object):
+ '''Connect scenario steps to their implementations.'''
+
+ def __init__(self, implementations, missing_step_cb=lambda sc, st: None):
+ self.implementations = implementations
+ self.missing_step_cb = missing_step_cb
+
+ def connect_implementations(self, scenarios):
+ '''Connect scenario steps, returning implemented scenarios.
+
+ For each scenario given, connect known implementations to
+ the steps.
+
+ This returns only scenarios that were fully implemented.
+ Unless ``missing_step_cb`` is provided and returns true for all
+ missing steps, an exception is raised instead.
+
+ '''
+ implemented_scenarios = []
+ for scenario in scenarios:
+ missing_step = False
+ for step in scenario.steps:
+ self.connect_implementation(scenario, step)
+ if step.implementation is None:
+ missing_step = True
+ if not missing_step:
+ implemented_scenarios.append(scenario)
+ return implemented_scenarios
+
+ def connect_implementation(self, scenario, step):
+ '''Connect the step of a scenario to its implementation.
+
+ Always raises an exception if there are multiple matching
+ implementations.
+
+ Raises an exception if there are no matching implementations
+ unless ``missing_step_cb`` returns true.
+
+ '''
+ matching = [i for i in self.implementations
+ if step.what == i.what and
+ implements_matches_step(i, step)]
+
+ if len(matching) == 0:
+ if self.missing_step_cb(scenario, step):
+ return
+ raise StepNotImplementedError(scenario, step)
+ if len(matching) > 1:
+ raise StepMultipleImplementationsError(scenario, step, matching)
+
+ assert step.implementation is None
+ step.implementation = matching[0]
diff --git a/yarnlib/scenario_step_connector_tests.py b/yarnlib/scenario_step_connector_tests.py
new file mode 100644
index 0000000..e93d904
--- /dev/null
+++ b/yarnlib/scenario_step_connector_tests.py
@@ -0,0 +1,95 @@
+# Copyright 2014 Codethink Limited
+#
+# 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/>.
+#
+# =*= License: GPL-3+ =*=
+
+
+import unittest
+
+import yarnlib
+
+
+class ImplementsMatchesTests(unittest.TestCase):
+
+ def test_match(self):
+ i = yarnlib.Implementation('THEN', r'foo', 'echo foo\n')
+ step = yarnlib.ScenarioStep('THEN', 'foo')
+ self.assertTrue(yarnlib.implements_matches_step(i, step))
+
+ def test_no_match(self):
+ i = yarnlib.Implementation('THEN', r'foo', 'echo foo\n')
+ step = yarnlib.ScenarioStep('THEN', 'bar')
+ self.assertIs(yarnlib.implements_matches_step(i, step), None)
+
+ def test_not_match_all(self):
+ i = yarnlib.Implementation('THEN', r'foo', 'echo foo\n')
+ step = yarnlib.ScenarioStep('THEN', 'foo bar')
+ self.assertIs(yarnlib.implements_matches_step(i, step), None)
+
+
+class ScenarioStepConnectorTests(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def test_all_implemented(self):
+ impl = yarnlib.Implementation('THEN', r'foo', 'echo foo\n')
+ scenario = yarnlib.Scenario('foo')
+ scenario.steps = [
+ yarnlib.ScenarioStep('THEN', 'foo')
+ ]
+ ssc = yarnlib.ScenarioStepConnector([impl])
+ scenarios = ssc.connect_implementations([scenario])
+ self.assertIs(scenarios[0].steps[0].implementation, impl)
+
+ def test_multiple_impls(self):
+ ssc = yarnlib.ScenarioStepConnector([
+ yarnlib.Implementation('THEN', r'foo \S+', 'echo foo\n'),
+ yarnlib.Implementation('THEN', r'\S+ bar', 'echo bar\n'),
+ ])
+ scenario = yarnlib.Scenario('foo bar')
+ scenario.steps = [
+ yarnlib.ScenarioStep('THEN', 'foo bar')
+ ]
+ with self.assertRaises(yarnlib.StepMultipleImplementationsError):
+ scenarios = ssc.connect_implementations([scenario])
+
+ def test_noimpl_fail(self):
+ ssc = yarnlib.ScenarioStepConnector([
+ yarnlib.Implementation('THEN', r'foo', 'echo foo\n'),
+ ])
+ scenarios = []
+ for scenario_name, step_text in [('foo', 'foo'), ('bar', 'bar')]:
+ scenario = yarnlib.Scenario(scenario_name)
+ scenario.steps = [
+ yarnlib.ScenarioStep('THEN', step_text),
+ ]
+ scenarios.append(scenario)
+ with self.assertRaises(yarnlib.StepNotImplementedError):
+ scenarios = ssc.connect_implementations(scenarios)
+
+ def test_noimpl_ignore(self):
+ ssc = yarnlib.ScenarioStepConnector([
+ yarnlib.Implementation('THEN', r'foo', 'echo foo\n'),
+ ], lambda *args: True)
+ all_scenarios = []
+ for scenario_name, step_text in [('foo', 'foo'), ('bar', 'bar')]:
+ scenario = yarnlib.Scenario(scenario_name)
+ scenario.steps = [
+ yarnlib.ScenarioStep('THEN', step_text),
+ ]
+ all_scenarios.append(scenario)
+ implemented_scenarios = ssc.connect_implementations(all_scenarios)
+ self.assertTrue(implemented_scenarios)