diff options
author | Richard Maw <richard.maw@codethink.co.uk> | 2014-02-05 15:37:01 +0000 |
---|---|---|
committer | Richard Maw <richard.maw@codethink.co.uk> | 2014-02-11 11:50:45 +0000 |
commit | 711c89a85b52a987ea284d905c41bd5b52e5374a (patch) | |
tree | aae9600a6fabf3f8f7bf2ff0343827a6b728f687 | |
parent | 5e4610f0a687f4b308ac1283d4c2ad0b707e42f5 (diff) | |
download | cmdtest-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-x | yarn | 56 | ||||
-rw-r--r-- | yarnlib/__init__.py | 5 | ||||
-rw-r--r-- | yarnlib/scenario_step_connector.py | 108 | ||||
-rw-r--r-- | yarnlib/scenario_step_connector_tests.py | 95 |
4 files changed, 217 insertions, 47 deletions
@@ -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) |