From 7d40aa4ad43f188cab3d139026e94590347d52d5 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Mon, 31 Aug 2015 17:39:23 -0400 Subject: [PATCH] Add teardown actions and stable action order #169 Add unit and integration tests fixes #169, connected to #169 --- ...xecute_teardown_on_before_failure_tests.py | 51 ++++ ..._execute_teardown_on_task_failure_tests.py | 48 +++ ...skip_non_teardown_on_task_failure_tests.py | 52 ++++ ...ld_suppress_error_during_teardown_tests.py | 51 ++++ src/main/python/pybuilder/core.py | 11 +- src/main/python/pybuilder/execution.py | 74 ++++- src/main/python/pybuilder/reactor.py | 8 +- src/main/python/pybuilder/utils.py | 273 +++++++++++++++++- src/unittest/python/execution_tests.py | 101 ++++++- 9 files changed, 643 insertions(+), 26 deletions(-) create mode 100644 src/integrationtest/python/should_execute_teardown_on_before_failure_tests.py create mode 100644 src/integrationtest/python/should_execute_teardown_on_task_failure_tests.py create mode 100644 src/integrationtest/python/should_skip_non_teardown_on_task_failure_tests.py create mode 100644 src/integrationtest/python/should_suppress_error_during_teardown_tests.py diff --git a/src/integrationtest/python/should_execute_teardown_on_before_failure_tests.py b/src/integrationtest/python/should_execute_teardown_on_before_failure_tests.py new file mode 100644 index 000000000..a789cd363 --- /dev/null +++ b/src/integrationtest/python/should_execute_teardown_on_before_failure_tests.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# This file is part of PyBuilder +# +# Copyright 2011-2015 PyBuilder Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from integrationtest_support import IntegrationTestSupport + +__author__ = 'arcivanov' + + +class Test(IntegrationTestSupport): + def test(self): + self.write_build_file(""" +from pybuilder.core import task, before, after + +@task +def foo(): pass + +@before("foo") +def before_foo(): + raise ValueError("simulated before failure") + +@after(["foo"], teardown=True) +def teardown_foo(project): + project.set_property("teardown_foo completed", True) + + """) + reactor = self.prepare_reactor() + project = reactor.project + + self.assertRaises(ValueError, reactor.build, "foo") + self.assertTrue(project.get_property("teardown_foo completed")) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/integrationtest/python/should_execute_teardown_on_task_failure_tests.py b/src/integrationtest/python/should_execute_teardown_on_task_failure_tests.py new file mode 100644 index 000000000..3d8e4c5bb --- /dev/null +++ b/src/integrationtest/python/should_execute_teardown_on_task_failure_tests.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +# This file is part of PyBuilder +# +# Copyright 2011-2015 PyBuilder Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from integrationtest_support import IntegrationTestSupport + +__author__ = 'arcivanov' + + +class Test(IntegrationTestSupport): + def test(self): + self.write_build_file(""" +from pybuilder.core import task, before, after + +@task +def foo(): + raise ValueError("simulated task failure") + +@after(["foo"], teardown=True) +def teardown_foo(project): + project.set_property("teardown_foo completed", True) + + """) + reactor = self.prepare_reactor() + project = reactor.project + + self.assertRaises(ValueError, reactor.build, "foo") + self.assertTrue(project.get_property("teardown_foo completed")) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/integrationtest/python/should_skip_non_teardown_on_task_failure_tests.py b/src/integrationtest/python/should_skip_non_teardown_on_task_failure_tests.py new file mode 100644 index 000000000..1f5440c09 --- /dev/null +++ b/src/integrationtest/python/should_skip_non_teardown_on_task_failure_tests.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# This file is part of PyBuilder +# +# Copyright 2011-2015 PyBuilder Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from integrationtest_support import IntegrationTestSupport + +__author__ = 'arcivanov' + + +class Test(IntegrationTestSupport): + def test(self): + self.write_build_file(""" +from pybuilder.core import task, before, after + +@task +def foo(): + raise ValueError("simulated task failure") + +@after(["foo"]) +def non_teardown_foo(project): + project.set_property("non_teardown_foo ran", True) + +@after(["foo"], teardown=True) +def teardown_foo(project): + project.set_property("teardown_foo completed", True) + + """) + reactor = self.prepare_reactor() + project = reactor.project + + self.assertRaises(ValueError, reactor.build, "foo") + self.assertTrue(project.get_property("non_teardown_foo ran") is None) + self.assertTrue(project.get_property("teardown_foo completed")) + +if __name__ == "__main__": + unittest.main() diff --git a/src/integrationtest/python/should_suppress_error_during_teardown_tests.py b/src/integrationtest/python/should_suppress_error_during_teardown_tests.py new file mode 100644 index 000000000..19d853977 --- /dev/null +++ b/src/integrationtest/python/should_suppress_error_during_teardown_tests.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# This file is part of PyBuilder +# +# Copyright 2011-2015 PyBuilder Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from integrationtest_support import IntegrationTestSupport + +__author__ = 'arcivanov' + + +class Test(IntegrationTestSupport): + def test(self): + self.write_build_file(""" +from pybuilder.core import task, before, after + +@task +def foo(): + raise ValueError("simulated task failure") + +@after(["foo"], teardown=True) +def teardown1_foo(project): + raise ArithmeticError("simulated teardown error") + +@after(["foo"], teardown=True) +def teardown2_foo(project): + project.set_property("teardown2_foo completed", True) + + """) + reactor = self.prepare_reactor() + project = reactor.project + + self.assertRaises(ValueError, reactor.build, "foo") + self.assertTrue(project.get_property("teardown2_foo completed")) + +if __name__ == "__main__": + unittest.main() diff --git a/src/main/python/pybuilder/core.py b/src/main/python/pybuilder/core.py index 70ec689d2..410729124 100644 --- a/src/main/python/pybuilder/core.py +++ b/src/main/python/pybuilder/core.py @@ -38,6 +38,7 @@ NAME_ATTRIBUTE = "_python_builder_name" ACTION_ATTRIBUTE = "_python_builder_action" ONLY_ONCE_ATTRIBUTE = "_python_builder_action_only_once" +TEARDOWN_ATTRIBUTE = "_python_builder_action_teardown" BEFORE_ATTRIBUTE = "_python_builder_before" AFTER_ATTRIBUTE = "_python_builder_after" @@ -134,17 +135,19 @@ def __call__(self, callable): class BaseAction(object): - def __init__(self, attribute, only_once, tasks): + def __init__(self, attribute, only_once, tasks, teardown=False): self.tasks = tasks self.attribute = attribute self.only_once = only_once + self.teardown = teardown def __call__(self, callable): setattr(callable, ACTION_ATTRIBUTE, True) setattr(callable, self.attribute, self.tasks) if self.only_once: setattr(callable, ONLY_ONCE_ATTRIBUTE, True) - + if self.teardown: + setattr(callable, TEARDOWN_ATTRIBUTE, True) return callable @@ -154,8 +157,8 @@ def __init__(self, tasks, only_once=False): class after(BaseAction): - def __init__(self, tasks, only_once=False): - super(after, self).__init__(AFTER_ATTRIBUTE, only_once, tasks) + def __init__(self, tasks, only_once=False, teardown=False): + super(after, self).__init__(AFTER_ATTRIBUTE, only_once, tasks, teardown) def use_bldsup(build_support_dir="bldsup"): diff --git a/src/main/python/pybuilder/execution.py b/src/main/python/pybuilder/execution.py index 087487b2d..47a53d440 100644 --- a/src/main/python/pybuilder/execution.py +++ b/src/main/python/pybuilder/execution.py @@ -24,9 +24,12 @@ """ import inspect +import copy +import traceback +import sys + import re import types -import copy from pybuilder.errors import (CircularTaskDependencyException, DependenciesNotResolvedException, @@ -34,9 +37,14 @@ MissingTaskDependencyException, MissingActionDependencyException, NoSuchTaskException) -from pybuilder.utils import as_list, Timer +from pybuilder.utils import as_list, Timer, odict from pybuilder.graph_utils import Graph, GraphHasCycles +if sys.version_info[0] < 3: # if major is less than 3 + from .excp_util_2 import raise_exception +else: + from .excp_util_3 import raise_exception + def as_task_name_list(mixed): result = [] @@ -83,11 +91,12 @@ def execute(self, argument_dict): class Action(Executable): - def __init__(self, name, callable, before=None, after=None, description="", only_once=False): + def __init__(self, name, callable, before=None, after=None, description="", only_once=False, teardown=False): super(Action, self).__init__(name, callable, description) self.execute_before = as_task_name_list(before) self.execute_after = as_task_name_list(after) self.only_once = only_once + self.teardown = teardown class Task(object): @@ -148,12 +157,12 @@ class ExecutionManager(object): def __init__(self, logger): self.logger = logger - self._tasks = {} - self._task_dependencies = {} + self._tasks = odict() + self._task_dependencies = odict() - self._actions = {} - self._execute_before = {} - self._execute_after = {} + self._actions = odict() + self._execute_before = odict() + self._execute_after = odict() self._initializers = [] @@ -216,16 +225,49 @@ def execute_task(self, task, **keyword_arguments): self._current_task = task - for action in self._execute_before[task.name]: - if self.execute_action(action, keyword_arguments): - number_of_actions += 1 + suppressed_errors = [] + task_error = None - task.execute(self.logger, keyword_arguments) - - for action in self._execute_after[task.name]: - if self.execute_action(action, keyword_arguments): - number_of_actions += 1 + has_teardown_tasks = False + after_actions = self._execute_after[task.name] + for action in after_actions: + if action.teardown: + has_teardown_tasks = True + break + try: + for action in self._execute_before[task.name]: + if self.execute_action(action, keyword_arguments): + number_of_actions += 1 + + task.execute(self.logger, keyword_arguments) + except: + if not has_teardown_tasks: + raise + else: + task_error = sys.exc_info() + + for action in after_actions: + try: + if not task_error or action.teardown: + if self.execute_action(action, keyword_arguments): + number_of_actions += 1 + except: + if not has_teardown_tasks: + raise + elif task_error: + suppressed_errors.append((action, sys.exc_info())) + else: + task_error = sys.exc_info() + + for suppressed_error in suppressed_errors: + action = suppressed_error[0] + action_error = suppressed_error[1] + self.logger.error("Executing action '%s' from '%s' resulted in an error that was suppressed:\n%s", + action.name, action.source, + "".join(traceback.format_exception(action_error[0], action_error[1], action_error[2]))) + if task_error: + raise_exception(task_error[1], task_error[2]) self._current_task = None if task not in self._tasks_executed: self._tasks_executed.append(task) diff --git a/src/main/python/pybuilder/reactor.py b/src/main/python/pybuilder/reactor.py index 0d45d7ed8..12181b571 100644 --- a/src/main/python/pybuilder/reactor.py +++ b/src/main/python/pybuilder/reactor.py @@ -23,12 +23,13 @@ """ import imp + import os.path from pybuilder.core import (TASK_ATTRIBUTE, DEPENDS_ATTRIBUTE, DESCRIPTION_ATTRIBUTE, AFTER_ATTRIBUTE, BEFORE_ATTRIBUTE, INITIALIZER_ATTRIBUTE, - ACTION_ATTRIBUTE, ONLY_ONCE_ATTRIBUTE, + ACTION_ATTRIBUTE, ONLY_ONCE_ATTRIBUTE, TEARDOWN_ATTRIBUTE, Project, NAME_ATTRIBUTE, ENVIRONMENTS_ATTRIBUTE) from pybuilder.errors import PyBuilderException, ProjectValidationFailedException from pybuilder.pluginloader import (BuiltinPluginLoader, @@ -221,10 +222,13 @@ def collect_tasks_and_actions_and_initializers(self, project_module): only_once = False if hasattr(candidate, ONLY_ONCE_ATTRIBUTE): only_once = getattr(candidate, ONLY_ONCE_ATTRIBUTE) + teardown = False + if hasattr(candidate, TEARDOWN_ATTRIBUTE): + teardown = getattr(candidate, TEARDOWN_ATTRIBUTE) self.logger.debug("Found action %s", name) self.execution_manager.register_action( - Action(name, candidate, before, after, description, only_once)) + Action(name, candidate, before, after, description, only_once, teardown)) elif hasattr(candidate, INITIALIZER_ATTRIBUTE) and getattr(candidate, INITIALIZER_ATTRIBUTE): environments = [] diff --git a/src/main/python/pybuilder/utils.py b/src/main/python/pybuilder/utils.py index 35b621b88..a06e5b34c 100644 --- a/src/main/python/pybuilder/utils.py +++ b/src/main/python/pybuilder/utils.py @@ -21,10 +21,7 @@ Provides generic utilities that can be used by plugins. """ -import fnmatch import json -import os -import re import sys import subprocess import tempfile @@ -33,6 +30,10 @@ from subprocess import Popen, PIPE from multiprocessing import Process +import fnmatch +import os +import re + try: from multiprocessing import SimpleQueue except ImportError: @@ -304,3 +305,269 @@ def instrumented_target(*args, **kwargs): msg += "\n" + chained_message ex = Exception(msg) raise_exception(ex, result.args[1]) + + +if sys.version_info[0] == 2 and sys.version_info[1] == 6: # if Python is 2.6 + # Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. + # Passes Python2.7's test suite and incorporates all the latest updates. + + try: + from thread import get_ident as _get_ident + except ImportError: + from dummy_thread import get_ident as _get_ident + + try: + from _abcoll import KeysView, ValuesView, ItemsView + except ImportError: + pass + + class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as for regular dictionaries. + + # The internal self.__map dictionary maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. Signature is the same as for + regular dictionaries, but keyword arguments are not recommended + because their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link which goes at the end of the linked + # list, and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which is + # then removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, key = self.__map.pop(key) + link_prev[1] = link_next + link_next[0] = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + root = self.__root + curr = root[0] + while curr is not root: + yield curr[2] + curr = curr[0] + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + try: + for node in self.__map.itervalues(): + del node[:] + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + except AttributeError: + pass + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root[0] + link_prev = link[0] + link_prev[1] = root + root[0] = link_prev + else: + link = root[1] + link_next = link[1] + root[1] = link_next + link_next[0] = root + key = link[2] + del self.__map[key] + value = dict.pop(self, key) + return key, value + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) items in od' + for k in self: + yield (k, self[k]) + + def update(*args, **kwds): + '''od.update(E, **F) -> None. Update od from dict/iterable E and F. + + If E is a dict instance, does: for k in E: od[k] = E[k] + If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] + Or if E is an iterable of items, does: for k, v in E: od[k] = v + In either case, this is followed by: for k, v in F.items(): od[k] = v + + ''' + if len(args) > 2: + raise TypeError('update() takes at most 2 positional ' + 'arguments (%d given)' % (len(args),)) + elif not args: + raise TypeError('update() takes at least 1 argument (0 given)') + self = args[0] + # Make progressively weaker assumptions about "other" + other = () + if len(args) == 2: + other = args[1] + if isinstance(other, dict): + for key in other: + self[key] = other[key] + elif hasattr(other, 'keys'): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S + and values equal to v (which defaults to None). + + ''' + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self) == len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other + + # -- the following methods are only used in Python 2.7 -- + + def viewkeys(self): + "od.viewkeys() -> a set-like object providing a view on od's keys" + return KeysView(self) + + def viewvalues(self): + "od.viewvalues() -> an object providing a view on od's values" + return ValuesView(self) + + def viewitems(self): + "od.viewitems() -> a set-like object providing a view on od's items" + return ItemsView(self) + + odict = OrderedDict +else: + from collections import OrderedDict + + odict = OrderedDict diff --git a/src/unittest/python/execution_tests.py b/src/unittest/python/execution_tests.py index 86220f3de..772737b96 100644 --- a/src/unittest/python/execution_tests.py +++ b/src/unittest/python/execution_tests.py @@ -251,7 +251,7 @@ def test_should_return_true_when_invoking_is_applicable_with_environment_and_ini class ExecutionManagerTestBase(unittest.TestCase): def setUp(self): - self.execution_manager = ExecutionManager(Logger()) + self.execution_manager = ExecutionManager(mock(Logger)) def tearDown(self): unstub() @@ -346,6 +346,105 @@ def test_ensure_after_action_is_executed_when_task_is_executed(self): verify(action).execute({}) verify(task).execute(any(), {}) + def test_ensure_after_action_teardown_is_executed_when_task_fails(self): + task = mock(name="task", dependencies=[]) + when(task).execute(any(), {}).thenRaise(ValueError("simulated task error")) + action = mock(name="action", execute_before=[], execute_after=["task"], teardown=True) + + self.execution_manager.register_action(action) + self.execution_manager.register_task(task) + self.execution_manager.resolve_dependencies() + + try: + self.execution_manager.execute_task(task) + self.assertTrue(False, "should not have reached here") + except Exception as e: + self.assertEquals(type(e), ValueError) + self.assertEquals(str(e), "simulated task error") + + verify(action).execute({}) + verify(task).execute(any(), {}) + + def test_ensure_after_action_teardown_is_executed_when_action_fails(self): + task = mock(name="task", dependencies=[]) + action_regular = mock(name="action_regular", execute_before=[], execute_after=["task"], teardown=False) + when(action_regular).execute({}).thenRaise(ValueError("simulated action error")) + action_teardown = mock(name="action_teardown", execute_before=[], execute_after=["task"], teardown=True) + action_after_teardown = mock(name="action_after_teardown", execute_before=[], execute_after=["task"], + teardown=False) + + self.execution_manager.register_action(action_regular) + self.execution_manager.register_action(action_teardown) + self.execution_manager.register_action(action_after_teardown) + self.execution_manager.register_task(task) + self.execution_manager.resolve_dependencies() + + try: + self.execution_manager.execute_task(task) + self.assertTrue(False, "should not have reached here") + except Exception as e: + self.assertEquals(type(e), ValueError) + self.assertEquals(str(e), "simulated action error") + + verify(task).execute(any(), {}) + verify(action_regular).execute({}) + verify(action_teardown).execute({}) + verify(action_after_teardown, times(0)).execute({}) + + def test_ensure_after_action_teardown_suppression_works_when_action_fails(self): + task = mock(name="task", dependencies=[]) + action_regular = mock(name="action_regular", execute_before=[], execute_after=["task"], teardown=False) + when(action_regular).execute({}).thenRaise(ValueError("simulated action error")) + action_teardown = mock(name="action_teardown", execute_before=[], execute_after=["task"], teardown=True) + action_after_teardown = mock(name="action_after_teardown", execute_before=[], execute_after=["task"], + teardown=False) + + self.execution_manager.register_action(action_regular) + self.execution_manager.register_action(action_teardown) + self.execution_manager.register_action(action_after_teardown) + self.execution_manager.register_task(task) + self.execution_manager.resolve_dependencies() + + try: + self.execution_manager.execute_task(task) + self.assertTrue(False, "should not have reached here") + except Exception as e: + self.assertEquals(type(e), ValueError) + self.assertEquals(str(e), "simulated action error") + + verify(task).execute(any(), {}) + verify(action_regular).execute({}) + verify(action_teardown).execute({}) + verify(action_after_teardown, times(0)).execute({}) + + def test_ensure_after_action_teardown_is_executed_and_suppresses(self): + task = mock(name="task", dependencies=[]) + when(task).execute(any(), {}).thenRaise(ValueError("simulated task error")) + action_teardown1 = mock(name="action_teardown1", execute_before=[], execute_after=["task"], teardown=True, + source="task") + when(action_teardown1).execute({}).thenRaise(ValueError("simulated action error teardown1")) + action_teardown2 = mock(name="action_teardown2", execute_before=[], execute_after=["task"], teardown=True, + source="task") + + self.execution_manager.register_action(action_teardown1) + self.execution_manager.register_action(action_teardown2) + self.execution_manager.register_task(task) + self.execution_manager.resolve_dependencies() + + try: + self.execution_manager.execute_task(task) + self.assertTrue(False, "should not have reached here") + except Exception as e: + self.assertEquals(type(e), ValueError) + self.assertEquals(str(e), "simulated task error") + + verify(task).execute(any(), {}) + verify(action_teardown1).execute({}) + verify(action_teardown2).execute({}) + verify(self.execution_manager.logger).error( + "Executing action '%s' from '%s' resulted in an error that was suppressed:\n%s", "action_teardown1", + "task", any()) + def test_should_return_single_task_name(self): self.execution_manager.register_task(mock(name="spam")) self.assertEquals(["spam"], self.execution_manager.task_names)