# HG changeset patch # User wbell # Date 1272116246 0 # Node ID 545be0c8f40572943c928508dbde8b64c119ac66 # Parent 673ec182679d7169b07b72abcd7e72efdf5d0feb Adding the ability to modify the default ''onerror'' property in the '''' element. If not specified, the behavior is unchanged; by default any step failure will result in the build failing and stopping. Added a new ''continue'' onerror specification-- it's similar to ''ignore'' except the results of ''continue'' steps are counted in the overall build status (in ''ignore'' they're ignored.) You'll need to upgrade both your master and slaves if you wish to use the '''' element override or the new ''continue'' value. Will update http://bitten.edgewall.org/wiki/Documentation/recipes.html . Thanks to jerith for comments. Closes #409. Refs #210. diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -353,7 +353,8 @@ for num, recipe_step in enumerate(recipe): step = BuildStep.fetch(self.env, build.id, recipe_step.id) if step.status == BuildStep.FAILURE: - if recipe_step.onerror != 'ignore': + if recipe_step.onerror == 'fail' or \ + recipe_step.onerror == 'continue': build.status = Build.FAILURE break else: diff --git a/bitten/recipe.py b/bitten/recipe.py --- a/bitten/recipe.py +++ b/bitten/recipe.py @@ -202,7 +202,7 @@ their keyword arguments. """ - def __init__(self, elem): + def __init__(self, elem, onerror_default): """Create the step. :param elem: the XML element representing the step @@ -211,7 +211,8 @@ self._elem = elem self.id = elem.attr['id'] self.description = elem.attr.get('description') - self.onerror = elem.attr.get('onerror', 'fail') + self.onerror = elem.attr.get('onerror', onerror_default) + assert self.onerror in ('fail', 'ignore', 'continue') def __repr__(self): return '<%s %r>' % (type(self).__name__, self.id) @@ -272,11 +273,13 @@ if not name.startswith('xmlns')]) self.ctxt = Context(basedir, config, vars) self._root = xml + self.onerror_default = vars.get('onerror', 'fail') + assert self.onerror_default in ('fail', 'ignore', 'continue') def __iter__(self): """Iterate over the individual steps of the recipe.""" for child in self._root.children('step'): - yield Step(child) + yield Step(child, self.onerror_default) def validate(self): """Validate the recipe. diff --git a/bitten/slave.py b/bitten/slave.py --- a/bitten/slave.py +++ b/bitten/slave.py @@ -303,10 +303,14 @@ os.mkdir(basedir) for step in recipe: - log.info('Executing build step %r', step.id) - if not self._execute_step(build_url, recipe, step): - log.warning('Stopping build due to failure') - break + try: + log.info('Executing build step %r, onerror = %s', step.id, step.onerror) + if not self._execute_step(build_url, recipe, step): + log.warning('Stopping build due to failure') + break + except Exception, e: + log.error('Exception raised processing step %s. Reraising %s', step.id, e) + raise else: log.info('Build completed') if self.dry_run: @@ -361,7 +365,6 @@ except KeyboardInterrupt: log.warning('Build interrupted') self._cancel_build(build_url) - return not failed or step.onerror != 'fail' def _cancel_build(self, build_url, exit_code=EX_OK): diff --git a/bitten/tests/master.py b/bitten/tests/master.py --- a/bitten/tests/master.py +++ b/bitten/tests/master.py @@ -831,6 +831,56 @@ self.assertEqual('foo', steps[0].name) self.assertEqual(BuildStep.FAILURE, steps[0].status) + + def test_process_build_step_failure_continue(self): + recipe = """ + + +""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.TOKEN] = '123'; + + build.insert() + + inbody = StringIO(""" +""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write, + incookie=Cookie('trac_auth=123')) + module = BuildMaster(self.env) + assert module.match_request(req) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.FAILURE, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.FAILURE, steps[0].status) + def test_process_build_step_invalid_xml(self): recipe = """ diff --git a/bitten/tests_slave/recipe.py b/bitten/tests_slave/recipe.py --- a/bitten/tests_slave/recipe.py +++ b/bitten/tests_slave/recipe.py @@ -175,6 +175,30 @@ recipe = Recipe(xml, basedir=self.basedir) recipe.validate() + def test_onerror_defaults(self): + xml = xmlio.parse('' + ' ' + '') + recipe = Recipe(xml, basedir=self.basedir) + steps = list(recipe) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].id) + self.assertEqual('Bar', steps[0].description) + self.assertEqual('continue', steps[0].onerror) + + + def test_onerror_override(self): + xml = xmlio.parse('' + ' ' + '') + recipe = Recipe(xml, basedir=self.basedir) + steps = list(recipe) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].id) + self.assertEqual('Bar', steps[0].description) + self.assertEqual('continue', steps[0].onerror) + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(ContextTestCase, 'test')) diff --git a/doc/recipes.txt b/doc/recipes.txt --- a/doc/recipes.txt +++ b/doc/recipes.txt @@ -36,9 +36,19 @@ these elements are declared in XML namespaces, where the namespace URI defines a collection of related commands. -A ```` element can additionally have an ``onerror`` attribute with -value of ``fail`` (terminate after step, default behaviour) or ``ignore`` -(fail, but run next step). + +The ```` element can optionally have an ``onerror`` attribute that +dictates how a build should proceed after the failure of a step. Allowable +values are: + ``fail``: failure of a step causes the build to terminate. (default) + ``continue``: builds continue after step failures. Failing steps + contribute to the overall build status. + ``ignore``: builds continue after step failures. Builds are marked + as successful even in the presence of failed steps with + onerror='ignore' + +```` elements can override the ```` ``onerror`` attribute with +their own ``onerror`` attributes. Commonly, the first step of any build recipe will perform the checkout from the repository.