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.
Copyright (C) 2012-2017 Edgewall Software