Skip to content

Commit

Permalink
Add teardown actions and stable action order pybuilder#169
Browse files Browse the repository at this point in the history
Add unit and integration tests

fixes pybuilder#169, connected to pybuilder#169
  • Loading branch information
arcivanov committed Sep 5, 2015
1 parent 0161e4f commit 7d40aa4
Show file tree
Hide file tree
Showing 9 changed files with 643 additions and 26 deletions.
@@ -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()
@@ -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()
@@ -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()
@@ -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()
11 changes: 7 additions & 4 deletions src/main/python/pybuilder/core.py
Expand Up @@ -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"

Expand Down Expand Up @@ -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


Expand All @@ -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"):
Expand Down
74 changes: 58 additions & 16 deletions src/main/python/pybuilder/execution.py
Expand Up @@ -24,19 +24,27 @@
"""

import inspect
import copy
import traceback
import sys

import re
import types
import copy

from pybuilder.errors import (CircularTaskDependencyException,
DependenciesNotResolvedException,
InvalidNameException,
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 = []
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = []

Expand Down Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions src/main/python/pybuilder/reactor.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = []
Expand Down

0 comments on commit 7d40aa4

Please sign in to comment.