changeset 250:0271a2b1fc23

Improvements to the web interface: * The build configuration page will now display the result of the individual steps for each build. * Different icon for failed builds in the timeline. Closes #26. * The timeline events will now show which steps failed for failed builds. In the RSS feed, it'll also include the actual error messages. * The build view now displays more information about the build slave, and in a more structured way.
author cmlenz
date Tue, 04 Oct 2005 20:44:56 +0000
parents dcba83c01266
children d66359b298d4
files bitten/model.py bitten/trac_ext/htdocs/bitten.css bitten/trac_ext/htdocs/bitten_buildf.png bitten/trac_ext/htdocs/failure.png bitten/trac_ext/templates/bitten_build.cs bitten/trac_ext/templates/bitten_config.cs bitten/trac_ext/web_ui.py
diffstat 7 files changed, 238 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/model.py
+++ b/bitten/model.py
@@ -609,18 +609,22 @@
 
     fetch = classmethod(fetch)
 
-    def select(cls, env, build=None, name=None, db=None):
+    def select(cls, env, build=None, name=None, status=None, db=None):
         """Retrieve existing build steps from the database that match the
         specified criteria.
         """
         if not db:
             db = env.get_db_cnx()
 
+        assert status in (None, BuildStep.SUCCESS, BuildStep.FAILURE)
+
         where_clauses = []
         if build is not None:
             where_clauses.append(("build=%s", build))
         if name is not None:
             where_clauses.append(("name=%s", name))
+        if status is not None:
+            where_clauses.append(("status=%s", status))
         if where_clauses:
             where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses])
         else:
--- a/bitten/trac_ext/htdocs/bitten.css
+++ b/bitten/trac_ext/htdocs/bitten.css
@@ -1,15 +1,27 @@
 /* Timeline styles */
-#content.timeline dt.successbuild, #content.timeline dt.successbuild a,
+#content.timeline dt.successbuild, #content.timeline dt.successbuild a {
+ background-image: url(bitten_build.png) !important;
+}
 #content.timeline dt.failedbuild, #content.timeline dt.failedbuild a {
- background-image: url(bitten_build.png) !important
+ background-image: url(bitten_buildf.png) !important;
 }
 
+#content.build h2.config, #content.build h2.step {  background: #f7f7f7;
+ border-bottom: 1px solid #d7d7d7; margin: 2em 0 0;
+}
+#content.build h2.config :link, #content.build h2.config :visited {
+ color: #b00;
+ display: block;
+ border-bottom: none;
+}
 #content.build h2.deactivated { text-decoration: line-through; }
 #content.build #prefs { line-height: 1.4em; }
 
+#content.build form.config { margin-top: 1em; }
 #content.build form.config th { text-align: left; }
 #content.build form.config fieldset { margin-bottom: 1em; }
-#content.build form.platforms ul { list-style: none; padding-left: 1em; }
+#content.build div.platforms { margin-top: 2em; }
+#content.build form.platforms ul { list-style-type: none; padding-left: 1em; }
 
 #content.build #charts { clear: right; float: right; width: 44%; }
 
@@ -23,9 +35,61 @@
 #content.build #builds td :link, #content.build #builds td :visited {
  font-weight: bold;
 }
-#content.build #builds td.completed { background: #9d9; }
-#content.build #builds td.failed { background: #d99; }
-#content.build #builds td.in-progress { background: #ff9; }
+#content.build #builds tbody td { background-position: 2px .5em;
+ background-repeat: no-repeat;
+}
+#content.build #builds td.completed {
+ background-color: #e8f6e8; background-image: url(bitten_build.png);
+}
+#content.build #builds td.failed {
+ background-color: #fbe8e7; background-image: url(bitten_buildf.png);
+}
+#content.build #builds td.in-progress {
+ background-color: #f6fae0; background-image: url(bitten_build.png);
+}
+#content.build #builds .info { margin-left: 16px; }
+#content.build #builds :link, #content.build #builds :visited {
+ text-decoration: none;
+}
+#content.build #builds .info .status { color: #000; }
+#content.build #builds .info .system { color: #999; font-size: smaller;
+ line-height: 1.2em; margin-top: .5em;
+}
+#content.build #builds ul.steps {
+ list-style-type: none; margin: .5em 0 0; padding: 0;
+}
+#content.build #builds ul.steps li.success,
+#content.build #builds ul.steps li.failed {
+ border: 1px solid; margin: 1px 0; padding: 0 2px 0 12px;
+}
+#content.build #builds ul.steps li.success {
+ background: #9d9; border-color: #696; color: #393;
+}
+#content.build #builds ul.steps li.failed {
+ background: #d99 url(failure.png) 2px .3em no-repeat; border-color: #966;
+ color: #933;
+}
+#content.build #builds ul.steps li :link,
+#content.build #builds ul.steps li :visited { border: none; color: inherit;
+ font-weight: bold; text-decoration: none;
+}
+#content.build #builds ul.steps li .duration { float: right;
+ font-size: smaller;
+}
+#content.build #builds ul.steps li.success .duration { color: #696; }
+#content.build #builds ul.steps li.failed .duration { color: #966; }
+#content.build #builds ul.steps li.failed ul { font-size: smaller;
+ line-height: 1.2em; list-style-type: square; margin: 0;
+ padding: 0 0 .5em 1.5em;
+}
+
+#content.build #overview { line-height: 130%; margin-top: 1em; padding: .5em; }
+#content.build #overview dt { font-weight: bold; padding-right: .25em;
+ position: absolute; left: 0; text-align: right; width: 11.5em;
+}
+#content.build #overview dd { margin-left: 12em; }
+#content.build #overview .slave { margin-top: 1em; }
+#content.build #overview .time { margin-top: 1em; }
 
 #content.build .tabs { list-style: none; float: left; width: 100%; margin: 0;
  padding: 0; }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a5b902f0caf9aa53a97b38b3cc0436f483f24d92
GIT binary patch
literal 289
zc%17D@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OSkfJR9T^y|-MHc(VFct$mbgZg
z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$J4)6(ajf#pYj?%Q$;+<XN^m*E}
z^GhQ7rp>XkvO0SF^s{Hr?q_Cp^Ygc~wA^4|`2YWZT3Q;AJlGg74b(1J666>B9}O_5
zuAP|#lnnQDaSW-rWpXB3=zxHLOJK{4^e_LyXB!GGJ!LDumLWmuQAOaQw`RtEZ{{7@
zGI8&O<x?d09%JvmDl+jiqr%--{zdac4hcVYkJ;;%cV&0>YE#Cm5hi<>co>u_*nX^-
SuDb?k1%s!npUXO@geCxb9(I)g
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..702258c3d72fb128148646894daed3c9facc0513
GIT binary patch
literal 206
zc%17D@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxfSkfJR9T^y|-MHc(VFct$mbgZg
z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-#;3h)VW{XA{j{mjg6e*XXe|KDI>
z0Fnm{m_SCdl?3?({|5nv&HI<^2Z|VYx;TbZ+;TZ($ala%fXPwe&Hw#xK5I_OEaN(2
t-}tumYMQ~{bx-Un99R8yFnY>iz@R9@@I-illP6FwgQu&X%Q~loCIE_oN?HH_
--- a/bitten/trac_ext/templates/bitten_build.cs
+++ b/bitten/trac_ext/templates/bitten_build.cs
@@ -4,16 +4,42 @@
  <div id="ctxtnav" class="nav"></div>
  <div id="content" class="build">
   <h1><?cs var:title ?></h1>
-  <p class="trigger">Triggered by: Changeset <a href="<?cs
-    var:build.chgset_href ?>">[<?cs var:build.rev ?>]</a> of <a href="<?cs
-    var:build.config.href ?>"><?cs var:build.config.name ?></a></p>
-  <p class="slave">Built by: <strong title="<?cs
-    var:build.slave.ip_address ?>"><?cs var:build.slave.name ?></strong> (<?cs
-    var:build.slave.os ?> <?cs var:build.slave.os.version ?><?cs
-    if:build.slave.machine ?> on <?cs var:build.slave.machine ?><?cs
-    /if ?>)</p>
-  <p class="time">Completed: <?cs var:build.started ?> (<?cs
-    var:build.started_delta ?> ago)<br />Took: <?cs var:build.duration ?></p><?cs
+  <dl id="overview">
+   <dt class="config">Configuration:</dt>
+   <dd class="config"><a href="<?cs var:build.config.href ?>"><?cs
+    var:build.config.name ?></a>
+   </dd>
+   <dt class="trigger">Triggered by:</dt>
+   <dd class="trigger">Changeset <a href="<?cs
+    var:build.chgset_href ?>">[<?cs var:build.rev ?>]</a> by <?cs
+    var:build.chgset_author ?>
+   </dd>
+   <dt class="slave">Built by:</dt>
+   <dd class="slave"><code><?cs var:build.slave.name ?></code> (<?cs
+    var:build.slave.ipnr ?>)
+   </dd>
+   <dt class="os">Operating system:</dt>
+   <dd><?cs var:build.slave.os.name ?> <?cs var:build.slave.os.version ?> (<?cs
+    var:build.slave.os.family ?>)
+   </dd><?cs
+   if:build.slave.machine ?>
+    <dt class="machine">Hardware:</dt>
+    <dd class="machine"><?cs
+     var:build.slave.machine ?><?cs
+     if:build.slave.processor ?> (<?cs
+      var:build.slave.processor ?>)<?cs
+     /if ?>
+    </dd><?cs
+   /if ?>
+   <dt class="time">Completed:</dt>
+   <dd class="time"><?cs var:build.started ?> (<?cs
+    var:build.started_delta ?> ago)
+   </dd>
+   <dt class="duration">Took:</dt>
+   <dd class="duration"><?cs
+    var:build.duration ?>
+   </dd>
+  </dl><?cs
   if:build.can_delete ?>
    <div class="buttons">
     <form method="post" action=""><div>
@@ -23,7 +49,7 @@
    </div><?cs
   /if ?><?cs
   each:step = build.steps ?>
-   <h2 id="<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs
+   <h2 class="step" id="step_<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs
      var:step.duration ?>)</h2>
    <p><?cs var:step.description ?></p><?cs
    if:len(step.errors) ?>
--- a/bitten/trac_ext/templates/bitten_config.cs
+++ b/bitten/trac_ext/templates/bitten_config.cs
@@ -33,8 +33,10 @@
     </div>
    </form><?cs
    each:config = configs ?>
-    <h2<?cs if:!config.active ?> class="deactivated"<?cs /if ?>><a href="<?cs
-      var:config.href ?>"><?cs var:config.label ?></a></h2><?cs
+    <h2 class="config <?cs
+     if:!config.active ?>deactivated<?cs
+     /if ?>"><a href="<?cs var:config.href ?>"><?cs
+     var:config.label ?></a></h2><?cs
     if:config.description ?><div class="description"><?cs
      var:config.description ?></div><?cs
     /if ?><?cs
@@ -139,9 +141,20 @@
      /if ?>
     </div></form><?cs
    /if ?>
-   <ul><li>Path: <?cs if:config.path ?><a href="<?cs
+   <p class="path">
+    Repository path: <?cs if:config.path ?><a href="<?cs
      var:config.browser_href ?>"><?cs
-     var:config.path ?></a></li><?cs /if ?></ul><?cs
+     var:config.path ?></a><?cs else ?>&mdash;<?cs /if ?><?cs
+     if:config.min_rev || config.max_rev ?> (<?cs
+      if:config.min_rev ?>starting at <a href="<?cs
+       var:config.min_rev_href ?>">[<?cs var:config.min_rev ?>]</a><?cs
+      /if ?><?cs
+      if:config.min_rev && config.max_rev ?>, <?cs /if ?><?cs
+      if:config.max_rev ?>up to <a href="<?cs
+       var:config.max_rev_href ?>">[<?cs var:config.max_rev ?>]</a><?cs
+      /if ?>)<?cs
+     /if ?>
+   </p><?cs
    if:config.description ?><div class="description"><?cs
      var:config.description ?></div><?cs
    /if ?>
@@ -183,16 +196,38 @@
       each:platform = config.platforms ?><?cs
        if:len(rev[platform.id]) ?><?cs
         with:build = rev[platform.id] ?><td class="<?cs
-         var:build.cls ?>"><a href="<?cs
-         var:build.href ?>" title="View build results"><?cs
-         var:build.slave.name ?></a> (<?cs
-         var:build.slave.os ?> <?cs
-         var:build.slave.os.version ?>)<br /><?cs
-         if:build.status == 'in progress' ?>started <?cs
-          var:build.started_delta ?> ago<?cs
-         else ?>took <?cs
-          var:build.duration ?></td><?cs
-         /if ?><?cs
+         var:build.cls ?>"><div class="info"><a href="<?cs
+         var:build.href ?>" title="View build results"><?cs var:build.id ?>:
+         <strong class="status"><?cs
+         if:build.status == 'completed' ?>Success<?cs
+         elif:build.status == 'failed' ?>Failed<?cs
+         else ?>In-progress<?cs
+         /if ?></strong></a>
+         <div class="system">
+          <strong class="ipnr"><?cs var:build.slave.name ?></strong> (<?cs
+           var:build.slave.ipnr ?>)<br />
+          <?cs var:build.slave.os.name ?> <?cs var:build.slave.os.version ?><?cs
+          if:build.slave.machine || build.slave.processor ?> / <?cs
+           alt:build.slave.processor ?><?cs
+            var:build.slave.machine ?><?cs
+           /alt ?><?cs
+          /if ?></div></div><?cs
+         if:len(build.steps) ?><ul class="steps"><?cs
+          each:step = build.steps ?><li class="<?cs
+           if:step.failed ?>failed<?cs else ?>success<?cs /if ?>">
+           <span class="duration"><?cs var:step.duration ?></span> <a href="<?cs
+           var:step.href ?>"<?cs
+           if:step.description ?> title="<?cs
+            var:step.description ?>"<?cs
+           /if ?>><?cs
+           var:name(step) ?></a><?cs
+           if:step.failed && len(step.errors) ?><ul><?cs
+            each:error = step.errors ?><li><?cs
+             var:error ?></li><?cs
+            /each ?></ul><?cs
+           /if ?></li><?cs
+          /each ?></ul><?cs
+         /if ?></td><?cs
         /with ?><?cs
        else ?><td>&mdash;</td><?cs
        /if ?><?cs
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -7,13 +7,18 @@
 # you should have received as part of this distribution. The terms
 # are also available at http://bitten.cmlenz.net/wiki/License.
 
+from datetime import datetime, timedelta
 import re
-from time import localtime, strftime
+try:
+    set
+except NameError:
+    from sets import Set as set
+from StringIO import StringIO
 
 import pkg_resources
 from trac.core import *
 from trac.Timeline import ITimelineEventProvider
-from trac.util import escape, pretty_timedelta
+from trac.util import escape, pretty_timedelta, format_date, format_datetime
 from trac.web import IRequestHandler
 from trac.web.chrome import INavigationContributor, ITemplateProvider, \
                             add_link, add_stylesheet
@@ -36,16 +41,16 @@
            'href': env.href.build(build.config, build.id),
            'chgset_href': env.href.changeset(build.rev)}
     if build.started:
-        hdf['started'] = strftime('%x %X', localtime(build.started))
+        hdf['started'] = format_datetime(build.started)
         hdf['started_delta'] = pretty_timedelta(build.started)
     if build.stopped:
-        hdf['stopped'] = strftime('%x %X', localtime(build.stopped))
+        hdf['stopped'] = format_datetime(build.stopped)
         hdf['stopped_delta'] = pretty_timedelta(build.stopped)
         hdf['duration'] = pretty_timedelta(build.stopped, build.started)
     hdf['slave'] = {
         'name': build.slave,
-        'ip_address': build.slave_info.get(Build.IP_ADDRESS),
-        'os': build.slave_info.get(Build.OS_NAME),
+        'ipnr': build.slave_info.get(Build.IP_ADDRESS),
+        'os.name': build.slave_info.get(Build.OS_NAME),
         'os.family': build.slave_info.get(Build.OS_FAMILY),
         'os.version': build.slave_info.get(Build.OS_VERSION),
         'machine': build.slave_info.get(Build.MACHINE),
@@ -339,7 +344,9 @@
         req.hdf['config.can_create'] = req.perm.has_permission('BUILD_CREATE')
 
     def _render_config(self, req, config_name):
-        config = BuildConfig.fetch(self.env, config_name)
+        db = self.env.get_db_cnx()
+
+        config = BuildConfig.fetch(self.env, config_name, db=db)
         req.hdf['title'] = 'Build Configuration "%s"' \
                            % escape(config.label or config.name)
         add_link(req, 'up', self.env.href.build(), 'Build Status')
@@ -348,6 +355,10 @@
             description = wiki_to_html(description, self.env, req)
         req.hdf['config'] = {
             'name': config.name, 'label': config.label, 'path': config.path,
+            'min_rev': config.min_rev,
+            'min_rev_href': self.env.href.changeset(config.min_rev),
+            'max_rev': config.max_rev,
+            'max_rev_href': self.env.href.changeset(config.max_rev),
             'active': config.active, 'description': description,
             'browser_href': self.env.href.browser(config.path),
             'can_modify': req.perm.has_permission('BUILD_MODIFY'),
@@ -355,13 +366,14 @@
         }
         req.hdf['page.mode'] = 'view_config'
 
-        platforms = list(TargetPlatform.select(self.env, config=config_name))
+        platforms = list(TargetPlatform.select(self.env, config=config_name,
+                                               db=db))
         req.hdf['config.platforms'] = [
             {'name': platform.name, 'id': platform.id} for platform in platforms
         ]
 
         has_reports = False
-        for report in Report.select(self.env, config=config.name):
+        for report in Report.select(self.env, config=config.name, db=db):
             has_reports = True
             break
 
@@ -392,6 +404,17 @@
                 if build and build.status != Build.PENDING:
                     build_hdf = _build_to_hdf(self.env, req, build)
                     req.hdf['%s.%s' % (prefix, platform.id)] = build_hdf
+                    for step in BuildStep.select(self.env, build=build.id,
+                                                 db=db):
+                        req.hdf['%s.%s.steps.%s' % (prefix, platform.id,
+                                                    step.name)] = {
+                            'description': escape(step.description),
+                            'duration': datetime.fromtimestamp(step.stopped) - \
+                                        datetime.fromtimestamp(step.started),
+                            'failed': not step.successful,
+                            'errors': step.errors,
+                            'href': build_hdf['href'] + '#step_' + step.name,
+                        }
             idx += 1
 
         if page > 1:
@@ -519,6 +542,10 @@
         req.hdf['build.steps'] = steps
         req.hdf['build.can_delete'] = req.perm.has_permission('BUILD_DELETE')
 
+        repos = self.env.get_repository(req.authname)
+        chgset = repos.get_changeset(build.rev)
+        req.hdf['build.chgset_author'] = chgset.author
+
         add_stylesheet(req, 'bitten/bitten.css')
         return 'bitten_build.cs', None
 
@@ -531,26 +558,65 @@
     def get_timeline_events(self, req, start, stop, filters):
         if 'build' in filters:
             add_stylesheet(req, 'bitten/bitten.css')
+
             db = self.env.get_db_cnx()
             cursor = db.cursor()
             cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name,"
                            "b.stopped,b.status FROM bitten_build AS b"
-                           "  INNER JOIN bitten_config AS c ON (c.name=b.config)"
+                           "  INNER JOIN bitten_config AS c ON (c.name=b.config) "
                            "  INNER JOIN bitten_platform AS p ON (p.id=b.platform) "
                            "WHERE b.stopped>=%s AND b.stopped<=%s "
                            "AND b.status IN (%s, %s) ORDER BY b.stopped",
                            (start, stop, Build.SUCCESS, Build.FAILURE))
+
             event_kinds = {Build.SUCCESS: 'successbuild',
                            Build.FAILURE: 'failedbuild'}
             for id, config, label, rev, platform, stopped, status in cursor:
+
+                errors = []
+                if status == Build.FAILURE:
+                    for step in BuildStep.select(self.env, build=id,
+                                                 status=BuildStep.FAILURE,
+                                                 db=db):
+                        errors += [(escape(step.name), escape(error)) for error
+                                   in step.errors]
+
                 title = 'Build of <em>%s [%s]</em> on %s %s' \
                         % (escape(label), escape(rev), escape(platform),
                            _status_label[status])
+                message = ''
                 if req.args.get('format') == 'rss':
                     href = self.env.abs_href.build(config, id)
+                    if errors:
+                        buf = StringIO()
+                        prev_step = None
+                        for step, error in errors:
+                            if step != prev_step:
+                                if prev_step is not None:
+                                    buf.write('</ul>')
+                                buf.write('<p>Step %s failed:</p><ul>' % step)
+                                prev_step = step
+                            buf.write('<li>%s</li>' % escape(error))
+                        buf.write('</ul>')
+                        message = buf.getvalue()
                 else:
                     href = self.env.href.build(config, id)
-                yield event_kinds[status], href, title, stopped, None, ''
+                    if errors:
+                        steps = []
+                        for step, error in errors:
+                            if step not in steps:
+                                steps.append(step)
+                        steps = ['<em>%s</em>' % step for step in steps]
+                        if len(steps) < 2:
+                            message = steps[0]
+                        elif len(steps) == 2:
+                            message = ' and '.join(steps)
+                        elif len(steps) > 2:
+                            message = ', '.join(steps[:-1]) + ', and ' + \
+                                      steps[-1]
+                        message = 'Step%s ' % (len(steps) != 1 and 's' or '') \
+                                  + message + ' failed'
+                yield event_kinds[status], href, title, stopped, None, message
 
     # Internal methods
 
Copyright (C) 2012-2017 Edgewall Software