changeset 245:a22ec8fce6c9

Store the reason(s) for build step failure in the database. Requires DB upgrade.
author cmlenz
date Mon, 03 Oct 2005 21:15:31 +0000
parents 1aa624af9ebb
children ebfe660f7cb1
files bitten/build/javatools.py bitten/build/pythontools.py bitten/master.py bitten/model.py bitten/tests/model.py bitten/trac_ext/templates/bitten_build.cs bitten/trac_ext/web_ui.py bitten/upgrades.py scripts/build.py
diffstat 9 files changed, 87 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/build/javatools.py
+++ b/bitten/build/javatools.py
@@ -41,6 +41,7 @@
         if err is not None:
             log.error(err)
 
+    error_logged = False
     log_elem = xmlio.Fragment()
     try:
         xml_log = xmlio.parse(logfile)
@@ -57,9 +58,14 @@
                 else:
                     collect_log_messages(child)
         collect_log_messages(xml_log)
+
+        if 'error' in xml_log.attr:
+            ctxt.error(xml_log.attr['error'])
+            error_logged = True
+
     except xmlio.ParseError, e:
         log.warning('Error parsing Ant XML log file (%s)', e)
     ctxt.log(log_elem)
 
-    if cmdline.returncode != 0:
+    if not error_logged and cmdline.returncode != 0:
         ctxt.error('Ant failed (%s)' % cmdline.returncode)
--- a/bitten/build/pythontools.py
+++ b/bitten/build/pythontools.py
@@ -38,6 +38,7 @@
     cmdline = CommandLine(_python_path(ctxt), [ctxt.resolve(file_), command],
                           cwd=ctxt.basedir)
     log_elem = xmlio.Fragment()
+    error_logged = False
     for out, err in cmdline.execute():
         if out is not None:
             log.info(out)
@@ -48,11 +49,15 @@
                 err = err[9:]
                 level = 'warning'
                 log.warning(err)
+            elif err.startswith('error: '):
+                ctxt.error(err[7:])
+                error_logged = True
             else:
                 log.error(err)
             log_elem.append(xmlio.Element('message', level=level)[err])
     ctxt.log(log_elem)
-    if cmdline.returncode != 0:
+
+    if not error_logged and cmdline.returncode != 0:
         ctxt.error('distutils failed (%s)' % cmdline.returncode)
 
 def exec_(ctxt, file_=None, module=None, function=None, output=None, args=None):
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -267,6 +267,7 @@
             step.status = BuildStep.FAILURE
         else:
             step.status = BuildStep.SUCCESS
+        step.errors += [err.gettext() for err in elem.children('error')]
         step.insert(db=db)
 
         for idx, log_elem in enumerate(elem.children('log')):
--- a/bitten/model.py
+++ b/bitten/model.py
@@ -501,6 +501,10 @@
             Column('build', type='int'), Column('name'), Column('description'),
             Column('status', size=1), Column('started', type='int'),
             Column('stopped', type='int')
+        ],
+        Table('bitten_error', key=('build', 'step', 'orderno'))[
+            Column('build', type='int'), Column('step'), Column('message'),
+            Column('orderno', type='int')
         ]
     ]
 
@@ -509,7 +513,7 @@
     FAILURE = 'F'
 
     def __init__(self, env, build=None, name=None, description=None,
-                 status=None, started=None, stopped=None):
+                 status=None, started=None, stopped=None, errors=None):
         """Initialize a new build step with the specified attributes.
 
         To actually create this build step in the database, the `insert` method
@@ -522,8 +526,10 @@
         self.status = status
         self.started = started
         self.stopped = stopped
+        self.errors = errors or []
+        self._exists = False
 
-    exists = property(fget=lambda self: self.build is not None)
+    exists = property(fget=lambda self: self._exists)
     successful = property(fget=lambda self: self.status == BuildStep.SUCCESS)
 
     def delete(self, db=None):
@@ -544,8 +550,12 @@
         cursor = db.cursor()
         cursor.execute("DELETE FROM bitten_step WHERE build=%s AND name=%s",
                        (self.build, self.name))
+        cursor.execute("DELETE FROM bitten_error WHERE build=%s AND step=%s",
+                       (self.build, self.name))
+
         if handle_ta:
             db.commit()
+        self._exists = False
 
     def insert(self, db=None):
         """Insert a new build step into the database."""
@@ -563,8 +573,16 @@
                        "started,stopped) VALUES (%s,%s,%s,%s,%s,%s)",
                        (self.build, self.name, self.description or '',
                         self.status, self.started or 0, self.stopped or 0))
+        step_id = db.get_last_id(cursor, 'bitten_step')
+        if self.errors:
+            cursor.executemany("INSERT INTO bitten_error (build,step,message,"
+                               "orderno) VALUES (%s,%s,%s,%s)",
+                               [(self.build, self.name, message, idx)
+                                for idx, message in enumerate(self.errors)])
+
         if handle_ta:
             db.commit()
+        self._exists = True
 
     def fetch(cls, env, build, name, db=None):
         """Retrieve an existing build from the database by build ID and step
@@ -579,9 +597,16 @@
         row = cursor.fetchone()
         if not row:
             return None
+        step = BuildStep(env, build, name, row[0] or '', row[1],
+                         row[2] and int(row[2]), row[3] and int(row[3]))
+        step._exists = True
 
-        return BuildStep(env, build, name, row[0] or '', row[1],
-                         row[2] and int(row[2]), row[3] and int(row[3]))
+        cursor.execute("SELECT message FROM bitten_error WHERE build=%s "
+                       "AND step=%s ORDER BY orderno", (build, name))
+        for row in cursor:
+            step.errors.append(row[0] or '')
+        return step
+
     fetch = classmethod(fetch)
 
     def select(cls, env, build=None, name=None, db=None):
@@ -602,12 +627,11 @@
             where = ""
 
         cursor = db.cursor()
-        cursor.execute("SELECT build,name,description,status,started,stopped "
-                       "FROM bitten_step %s ORDER BY stopped"
+        cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY stopped"
                        % where, [wc[1] for wc in where_clauses])
-        for build, name, description, status, started, stopped in cursor:
-            yield BuildStep(env, build, name, description or '', status,
-                            started and int(started), stopped and int(stopped))
+        for build, name in cursor:
+            yield BuildStep.fetch(env, build, name, db=db)
+
     select = classmethod(select)
 
 
@@ -883,4 +907,4 @@
 
 schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \
          BuildStep._schema + BuildLog._schema + Report._schema
-schema_version = 6
+schema_version = 7
--- a/bitten/tests/model.py
+++ b/bitten/tests/model.py
@@ -340,6 +340,21 @@
         self.assertEqual('Foo bar', step.description)
         self.assertEqual(BuildStep.SUCCESS, step.status)
 
+    def test_fetch_with_errors(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("INSERT INTO bitten_step VALUES (%s,%s,%s,%s,%s,%s)",
+                       (1, 'test', 'Foo bar', BuildStep.SUCCESS, 0, 0))
+        cursor.executemany("INSERT INTO bitten_error VALUES (%s,%s,%s,%s)",
+                           [(1, 'test', 'Foo', 0), (1, 'test', 'Bar', 1)])
+
+        step = BuildStep.fetch(self.env, build=1, name='test')
+        self.assertEqual(1, step.build)
+        self.assertEqual('test', step.name)
+        self.assertEqual('Foo bar', step.description)
+        self.assertEqual(BuildStep.SUCCESS, step.status)
+        self.assertEqual(['Foo', 'Bar'], step.errors)
+
 
 class BuildLogTestCase(unittest.TestCase):
 
--- a/bitten/trac_ext/templates/bitten_build.cs
+++ b/bitten/trac_ext/templates/bitten_build.cs
@@ -25,7 +25,14 @@
   each:step = build.steps ?>
    <h2 id="<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs
      var:step.duration ?>)</h2>
-   <p><?cs var:step.description ?></p>
+   <p><?cs var:step.description ?></p><?cs
+   if:len(step.errors) ?>
+    <div class="errors">
+     <h3>Errors:</h3><ul class="errors"><?cs
+     each:error = step.errors ?><li><?cs var:error ?></li><?cs
+     /each ?></ul>
+    </div><?cs
+   /if ?>
    <div id="<?cs var:step.name ?>_tabs">
     <div class="tab"><h3>Log</h3><div class="log"><?cs
      each:item = step.log ?><code class="<?cs var:item.level ?>"><?cs
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -483,6 +483,7 @@
                 'name': step.name, 'description': step.description,
                 'duration': pretty_timedelta(step.started, step.stopped),
                 'failed': step.status == BuildStep.FAILURE,
+                'errors': step.errors,
                 'log': self._render_log(req, build, step),
                 'reports': self._render_reports(req, config, build, step)
             })
--- a/bitten/upgrades.py
+++ b/bitten/upgrades.py
@@ -252,10 +252,23 @@
         cursor.execute("UPDATE bitten_report SET generator=%s "
                        "WHERE id=%s", (mapping[generator], report_id))
 
+def add_error_table(env, db):
+    """Add the bitten_error table for recording step failure reasons."""
+    from trac.db import Table, Column
+
+    table = Table('bitten_error', key=('build', 'step', 'orderno'))[
+                Column('build', type='int'), Column('step'), Column('message'),
+                Column('orderno', type='int')
+            ]
+    cursor = db.cursor()
+    for stmt in db.to_sql(table):
+        cursor.execute(stmt)
+
 map = {
     2: [add_log_table],
     3: [add_recipe_to_config],
     4: [add_config_to_reports],
     5: [add_order_to_log, add_report_tables, xmldb_to_db],
-    6: [normalize_file_paths, fixup_generators]
+    6: [normalize_file_paths, fixup_generators],
+    7: [add_error_table]
 }
--- a/scripts/build.py
+++ b/scripts/build.py
@@ -56,7 +56,7 @@
                 print '-->', step.id
                 for type, category, generator, output in step.execute(recipe.ctxt):
                     if type == Recipe.ERROR:
-                        log.error('Failure in step "%s": %s', step.id, output)
+                        log.error(output)
                     elif type == Recipe.LOG and options.print_logs:
                         output.write(sys.stdout, newlines=True)
                     elif type == Recipe.REPORT and options.print_reports:
Copyright (C) 2012-2017 Edgewall Software