Mercurial > bitten > bitten-test
changeset 754:545be0c8f405
Adding the ability to modify the default ''onerror'' property in the ''<build>'' 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 ''<build>'' 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.
author | wbell |
---|---|
date | Sat, 24 Apr 2010 13:37:26 +0000 |
parents | 673ec182679d |
children | 3fbc7672640e |
files | bitten/master.py bitten/recipe.py bitten/slave.py bitten/tests/master.py bitten/tests_slave/recipe.py doc/recipes.txt |
diffstat | 6 files changed, 103 insertions(+), 12 deletions(-) [+] |
line wrap: on
line diff
--- 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:
--- 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.
--- 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):
--- 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 = """<build> + <step id="foo" onerror="continue"> + </step> +</build>""" + 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("""<result step="foo" status="failure" + time="2007-04-01T15:30:00.0000" + duration="3.45"> +</result>""") + 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 = """<build> <step id="foo">
--- 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('<build onerror="continue">' + ' <step id="foo" description="Bar"></step>' + '</build>') + 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('<build onerror="ignore">' + ' <step id="foo" description="Bar" onerror="continue"></step>' + '</build>') + 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'))
--- 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 ``<step>`` element can additionally have an ``onerror`` attribute with -value of ``fail`` (terminate after step, default behaviour) or ``ignore`` -(fail, but run next step). + +The ``<build>`` 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' + +``<step>`` elements can override the ``<build>`` ``onerror`` attribute with +their own ``onerror`` attributes. Commonly, the first step of any build recipe will perform the checkout from the repository.