changeset 755:ab91df14f670 0.6.x

Merge of [832] from trunk.
author wbell
date Sat, 24 Apr 2010 13:38:25 +0000
parents d9f06a314db5
children ab1f18d0e98c
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