changeset 763:de466e590545 0.6.x

Port of [638], [639], [640] to 0.6.x
author wbell
date Sat, 24 Apr 2010 15:16:12 +0000
parents bfd5fd75d1d6
children b94682509d32
files bitten/htdocs/bitten.css bitten/master.py bitten/model.py bitten/queue.py bitten/templates/bitten_config.html bitten/tests/master.py bitten/tests/queue.py bitten/tests/upgrades.py bitten/upgrades.py bitten/web_ui.py
diffstat 10 files changed, 206 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/htdocs/bitten.css
+++ b/bitten/htdocs/bitten.css
@@ -93,9 +93,13 @@
   list-style-type: none; margin: .5em 0 0; padding: 0;
 }
 #content.build #builds ul.steps li.success,
+#content.build #builds ul.steps li.in-progress,
 #content.build #builds ul.steps li.failed {
   border: 1px solid; margin: 1px 0; padding: 0 2px 0 12px;
 }
+#content.build #builds ul.steps li.in-progress {
+  background: #dd9; border-color: #966; color: #993;
+}
 #content.build #builds ul.steps li.success {
   background: #9d9; border-color: #696; color: #393;
 }
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -55,7 +55,7 @@
     build_all = BoolOption('bitten', 'build_all', False, doc=
         """Whether to request builds of older revisions even if a younger
         revision has already been built.""")
-    
+
     stabilize_wait = IntOption('bitten', 'stabilize_wait', 0, doc=
         """The time in seconds to wait for the repository to stabilize before
         queuing up a new build.  This allows time for developers to check in
@@ -70,7 +70,7 @@
          """The directory on the server in which client log files will be stored.""")
 
     quick_status = BoolOption('bitten', 'quick_status', False, doc=
-         """Whether to show the current build status withing the Trac main 
+         """Whether to show the current build status withing the Trac main
             navigation bar""")
 
     def __init__(self):
@@ -151,7 +151,7 @@
         self._send_response(req, code, body=message, headers=headers)
 
     def _process_build_creation(self, req, slave_token):
-        queue = BuildQueue(self.env, build_all=self.build_all, 
+        queue = BuildQueue(self.env, build_all=self.build_all,
                            stabilize_wait=self.stabilize_wait,
                            timeout=self.slave_timeout)
         queue.populate()
@@ -224,6 +224,7 @@
         self.log.info('Build slave %r initiated build %d', build.slave,
                       build.id)
         build.started = int(time.time())
+        build.last_activity = build.started
         build.update()
 
         for listener in BuildSystem(self.env).listeners:
@@ -242,6 +243,14 @@
         self.log.info('Build slave %r initiated build %d', build.slave,
                       build.id)
 
+        # create the first step, mark it as in-progress.
+
+        recipe = Recipe(xmlio.parse(config.recipe))
+        stepname = recipe.__iter__().next().id
+
+        step = self._start_new_step(build, stepname)
+        step.insert()
+
         self._send_response(req, 200, body, headers={
                     'Content-Type': 'application/x-bitten+xml',
                     'Content-Length': str(len(body)),
@@ -258,9 +267,11 @@
             self._send_error(req, HTTP_BAD_REQUEST, 'XML parser error')
         stepname = elem.attr['step']
 
+        # we should have created this step previously; if it hasn't,
+        # the master and slave are processing steps out of order.
         step = BuildStep.fetch(self.env, build=build.id, name=stepname)
-        if step:
-            self._send_error(req, HTTP_CONFLICT, 'Build step already exists')
+        if not step:
+            self._send_error(req, HTTP_CONFLICT, 'Build step has not been created.')
 
         recipe = Recipe(xmlio.parse(config.recipe))
         index = None
@@ -280,14 +291,7 @@
 
         db = self.env.get_db_cnx()
 
-        step = BuildStep(self.env, build=build.id, name=stepname)
-
-        # not a great way to determine the start/stop time of the
-        # step, but it's a server time, which eliminates a bunch
-        # of skew issues.
-        now = int(time.time())
-        step.started = now - float(elem.attr['duration'])
-        step.stopped = now
+        step.stopped = int(time.time())
 
         if elem.attr['status'] == 'failure':
             self.log.warning('Build %s step %s failed', build.id, stepname)
@@ -297,6 +301,9 @@
         else:
             step.status = BuildStep.SUCCESS
         step.errors += [error.gettext() for error in elem.children('error')]
+
+        # TODO: step.update(db=db)
+        step.delete(db=db)
         step.insert(db=db)
 
         # Collect log messages from the request body
@@ -342,11 +349,12 @@
             attachment.insert(filename, fileobj, fileobj.len, db=db)
 
         # If this was the last step in the recipe we mark the build as
-        # completed
+        # completed otherwise just update last_activity
         if last_step:
             self.log.info('Slave %s completed build %d ("%s" as of [%s])',
                           build.slave, build.id, build.config, build.rev)
             build.stopped = step.stopped
+            build.last_activity = build.stopped
 
             # Determine overall outcome of the build by checking the outcome
             # of the individual steps against the "onerror" specification of
@@ -362,6 +370,20 @@
                 build.status = Build.SUCCESS
 
             build.update(db=db)
+        else:
+            build.last_activity = step.stopped
+            build.update(db=db)
+
+            # start the next step.
+            for num, recipe_step in enumerate(recipe):
+                if num == index + 1:
+                    next_step = recipe_step
+            if next_step is None:
+                self._send_error(req, HTTP_FORBIDDEN,
+                                 'Unable to find step after ' % stepname)
+
+            step = self._start_new_step(build, next_step.id)
+            step.insert(db=db)
 
         db.commit()
 
@@ -376,3 +398,13 @@
                             'Location': req.abs_href.builds(
                                     build.id, 'steps', stepname)})
 
+    def _start_new_step(self, build, stepname):
+        """Creates the in-memory representation for a newly started
+        step, ready to be persisted to the database.
+        """
+        step = BuildStep(self.env, build=build.id, name=stepname)
+        step.status = BuildStep.IN_PROGRESS
+        step.started = int(time.time())
+        step.stopped = 0
+
+        return step
--- a/bitten/model.py
+++ b/bitten/model.py
@@ -373,6 +373,7 @@
             Column('rev_time', type='int'), Column('platform', type='int'),
             Column('slave'), Column('started', type='int'),
             Column('stopped', type='int'), Column('status', size=1),
+            Column('last_activity', type='int'),
             Index(['config', 'rev', 'platform'], unique=True)
         ],
         Table('bitten_slave', key=('build', 'propname'))[
@@ -397,7 +398,8 @@
     TOKEN = 'token'
 
     def __init__(self, env, config=None, rev=None, platform=None, slave=None,
-                 started=0, stopped=0, rev_time=0, status=PENDING):
+                 started=0, stopped=0, last_activity=0, 
+                 rev_time=0, status=PENDING):
         """Initialize a new build with the specified attributes.
 
         To actually create this build in the database, the `insert` method needs
@@ -411,6 +413,7 @@
         self.slave = slave
         self.started = started or 0
         self.stopped = stopped or 0
+        self.last_activity = last_activity or 0
         self.rev_time = rev_time
         self.status = status
         self.slave_info = {}
@@ -466,11 +469,12 @@
 
         cursor = db.cursor()
         cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform,"
-                       "slave,started,stopped,status) "
-                       "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
+                       "slave,started,stopped,last_activity,status) "
+                       "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)",
                        (self.config, self.rev, int(self.rev_time),
                         self.platform, self.slave or '', self.started or 0,
-                        self.stopped or 0, self.status))
+                        self.stopped or 0, self.last_activity or 0,
+                        self.status))
         self.id = db.get_last_id(cursor, 'bitten_build')
         if self.slave_info:
             cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)",
@@ -497,9 +501,10 @@
 
         cursor = db.cursor()
         cursor.execute("UPDATE bitten_build SET slave=%s,started=%s,"
-                       "stopped=%s,status=%s WHERE id=%s",
+                       "stopped=%s,last_activity=%s,status=%s WHERE id=%s",
                        (self.slave or '', self.started or 0,
-                        self.stopped or 0, self.status, self.id))
+                        self.stopped or 0, self.last_activity or 0,
+                        self.status, self.id))
         cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,))
         if self.slave_info:
             cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)",
@@ -515,7 +520,8 @@
 
         cursor = db.cursor()
         cursor.execute("SELECT config,rev,rev_time,platform,slave,started,"
-                       "stopped,status FROM bitten_build WHERE id=%s", (id,))
+                       "stopped,last_activity,status FROM bitten_build WHERE "
+                       "id=%s", (id,))
         row = cursor.fetchone()
         if not row:
             return None
@@ -523,7 +529,9 @@
         build = Build(env, config=row[0], rev=row[1], rev_time=int(row[2]),
                       platform=int(row[3]), slave=row[4],
                       started=row[5] and int(row[5]) or 0,
-                      stopped=row[6] and int(row[6]) or 0, status=row[7])
+                      stopped=row[6] and int(row[6]) or 0, 
+                      last_activity=row[7] and int(row[7]) or 0,
+                      status=row[8])
         build.id = int(id)
         cursor.execute("SELECT propname,propvalue FROM bitten_slave "
                        "WHERE build=%s", (id,))
@@ -587,6 +595,7 @@
 
     # Step status codes
     SUCCESS = 'S'
+    IN_PROGRESS = 'I'
     FAILURE = 'F'
 
     def __init__(self, env, build=None, name=None, description=None,
@@ -610,7 +619,8 @@
                       doc='Whether this build step exists in the database')
     successful = property(fget=lambda self: self.status == BuildStep.SUCCESS,
                           doc='Whether the build step was successful')
-
+    completed = property(fget=lambda self: self.status == BuildStep.SUCCESS or self.status == BuildStep.FAILURE,
+                          doc='Whether this build step has completed processing')
     def delete(self, db=None):
         """Remove the build step from the database."""
         if not db:
@@ -645,7 +655,7 @@
             handle_ta = False
 
         assert self.build and self.name
-        assert self.status in (self.SUCCESS, self.FAILURE)
+        assert self.status in (self.SUCCESS, self.IN_PROGRESS, self.FAILURE)
 
         cursor = db.cursor()
         cursor.execute("INSERT INTO bitten_step (build,name,description,status,"
@@ -694,7 +704,7 @@
         if not db:
             db = env.get_db_cnx()
 
-        assert status in (None, BuildStep.SUCCESS, BuildStep.FAILURE)
+        assert status in (None, BuildStep.SUCCESS, BuildStep.IN_PROGRESS, BuildStep.FAILURE)
 
         where_clauses = []
         if build is not None:
@@ -709,7 +719,7 @@
             where = ""
 
         cursor = db.cursor()
-        cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY stopped"
+        cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY started"
                        % where, [wc[1] for wc in where_clauses])
         for build, name in cursor:
             yield BuildStep.fetch(env, build, name, db=db)
@@ -1031,4 +1041,4 @@
 
 schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \
          BuildStep._schema + BuildLog._schema + Report._schema
-schema_version = 11
+schema_version = 12
--- a/bitten/queue.py
+++ b/bitten/queue.py
@@ -24,6 +24,7 @@
 import time
 
 from trac.util.datefmt import to_timestamp
+from trac.util import pretty_timedelta, format_datetime
 from trac.attachment import Attachment
 
 
@@ -276,16 +277,21 @@
         db = self.env.get_db_cnx()
         now = int(time.time())
         for build in Build.select(self.env, status=Build.IN_PROGRESS, db=db):
-            if now - build.started < self.timeout:
+            if now - build.last_activity < self.timeout:
                 # This build has not reached the timeout yet, assume it's still
                 # being executed
-                # FIXME: ideally, we'd base this check on the last activity on
-                #        the build, not the start time
                 continue
+
+            self.log.info('Orphaning build %d. Last activity was %s (%s)' % \
+                              (build.id, format_datetime(build.last_activity),
+                               pretty_timedelta(build.last_activity)))
+
             build.status = Build.PENDING
             build.slave = None
             build.slave_info = {}
             build.started = 0
+            build.stopped = 0
+            build.last_activity = 0
             for step in list(BuildStep.select(self.env, build=build.id, db=db)):
                 step.delete(db=db)
             build.update(db=db)
--- a/bitten/templates/bitten_config.html
+++ b/bitten/templates/bitten_config.html
@@ -18,6 +18,17 @@
       </py:choose>
     </strong>
     
+    <div py:def="build_time(build)" class="system">
+      <py:choose>
+	<span py:when="build.stopped">
+	  <div>Duration: ${build.duration}</div>
+	</span>
+	<span py:otherwise="">
+	  <div>${build.started} (${build.started_delta} ago)</div>
+	</span>
+      </py:choose>
+    </div>
+
     <div py:def="slave_info(slave)" class="system">
       <strong>$slave.name</strong> ($slave.ipnr)<br />
       $slave.os_name $slave.os_version
@@ -27,12 +38,12 @@
     
     <ul py:def="build_steps(steps)" py:if="steps" class="steps">
       <li py:for="step in steps"
-          class="${step.failed and 'failed' or 'success'}">
+          class="${step.cls}">
         <span class="duration">$step.duration</span> 
         <a href="$step.href" title="${step.description or None}">
           $step.name
         </a>
-        <ul py:if="step.failed and step.errors">
+        <ul py:if="step.status is 'failed' and step.errors">
           <li py:for="error in step.errors">$error</li>
         </ul>
       </li>
@@ -65,8 +76,7 @@
         </py:if></py:for>)</i>
        </div></py:if><py:if test="config.builds_inprogress">
        <div>$config.builds_inprogress in-progress
-         build<py:if test="config.builds_inprogress > 1">s</py:if>&nbsp;<i>(<py:for each="platform in config.platforms">
-           <py:if test="platform.builds_inprogress">
+         build<py:if test="config.builds_inprogress > 1">s</py:if>&nbsp;<i>(<py:for each="platform in config.platforms"><py:if test="platform.builds_inprogress">
             $platform.name: $platform.builds_inprogress
           </py:if></py:for>)</i>
       </div></py:if>
@@ -137,9 +147,8 @@
           $platform.name: $platform.builds_pending
         </py:if></py:for>)</i>
        </div></py:if><py:if test="config.builds_inprogress">
-        <div>$config.builds_inprogress in-progress build<py:if test="config.builds_inprogress > 1">s</py:if>&nbsp;<i>(<py:for each="platform in config.platforms">
-          <py:if test="platform.builds_inprogress">
-            $platform.name: $platform.builds_inprogress
+	<div>$config.builds_inprogress in-progress build<py:if test="config.builds_inprogress > 1">s</py:if>&nbsp;<i>(<py:for each="platform in config.platforms"><py:if test="platform.builds_inprogress">
+	$platform.name: $platform.builds_inprogress
         </py:if></py:for>)</i>
       </div></py:if>
       <div id="charts"><py:for each="chart in config.charts">
@@ -173,6 +182,7 @@
                   ${build_status(build.status)}
                 </a>
                 ${slave_info(build.slave)}
+		${build_time(build)}
               </div>
               ${build_steps(build.steps)}
             </td>
@@ -201,6 +211,7 @@
                 $build.id: <strong class="status">$build.platform</strong>
               </a>
               ${slave_info(build.slave)}
+              ${build_time(build)}
             </div>
             ${build_steps(build.steps)}
           </td>
--- a/bitten/tests/master.py
+++ b/bitten/tests/master.py
@@ -261,7 +261,7 @@
 
     def test_initiate_build(self):
         config = BuildConfig(self.env, 'test', path='somepath', active=True,
-                             recipe='<build></build>')
+                             recipe='<build><step id="s1"></step></build>')
         config.insert()
         platform = TargetPlatform(self.env, config='test', name="Unix")
         platform.rules.append(('family', 'posix'))
@@ -288,14 +288,14 @@
         self.assertRaises(RequestDone, module.process_request, req)
 
         self.assertEqual(200, outheaders['Status'])
-        self.assertEqual('90', outheaders['Content-Length'])
+        self.assertEqual('112', outheaders['Content-Length'])
         self.assertEqual('application/x-bitten+xml',
                          outheaders['Content-Type'])
         self.assertEqual('attachment; filename=recipe_test_r123.xml',
                          outheaders['Content-Disposition'])
         self.assertEqual('<build build="1" config="test" name="hal"'
                          ' path="somepath" platform="Unix"'
-                         ' revision="123"/>',
+                         ' revision="123"><step id="s1"/></build>',
                          outbody.getvalue())
 
         # Make sure the started timestamp has been set
@@ -376,6 +376,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -428,6 +431,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -490,6 +496,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -563,6 +572,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -633,6 +645,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth='))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -646,7 +661,7 @@
         assert not build.stopped
 
         steps = list(BuildStep.select(self.env, build.id))
-        self.assertEqual(0, len(steps))
+        self.assertEqual(1, len(steps))
 
     def test_process_build_step_invalidated_build(self):
         recipe = """<build>
@@ -683,6 +698,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -692,7 +710,7 @@
         assert not build.stopped
 
         steps = list(BuildStep.select(self.env, build.id))
-        self.assertEqual(1, len(steps))
+        self.assertEqual(2, len(steps))
 
         # invalidate the build. 
 
@@ -723,6 +741,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -763,6 +784,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -812,6 +836,9 @@
                    write=outbody.write,
                    incookie=Cookie('trac_auth=123'))
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -906,6 +933,9 @@
                    incookie=Cookie('trac_auth='))
 
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
 
         self.assertRaises(RequestDone, module.process_request, req)
@@ -933,6 +963,9 @@
                    incookie=Cookie('trac_auth='))
 
         module = BuildMaster(self.env)
+
+        module._start_new_step(build, 'foo').insert()
+
         assert module.match_request(req)
         
         self.assertRaises(RequestDone, module.process_request, req)
--- a/bitten/tests/queue.py
+++ b/bitten/tests/queue.py
@@ -392,12 +392,12 @@
         platform.insert()
         build1 = Build(self.env, config='test', platform=platform.id, rev=123,
                       rev_time=42, status=Build.IN_PROGRESS, slave='heinz',
-                      started=time.time() - 600) # Started ten minutes ago
+                      last_activity=time.time() - 600) # active ten minutes ago
         build1.insert()
 
         build2 = Build(self.env, config='test', platform=platform.id, rev=124,
                        rev_time=42, status=Build.IN_PROGRESS, slave='heinz',
-                       started=time.time() - 60) # Started a minute ago
+                       last_activity=time.time() - 60) # active a minute ago
         build2.insert()
 
         queue = BuildQueue(self.env, timeout=300) # 5 minutes timeout
--- a/bitten/tests/upgrades.py
+++ b/bitten/tests/upgrades.py
@@ -170,6 +170,7 @@
         'old_log_v5',
         'old_log_v8',
         'old_rule',
+        'old_build_v11',
     ]
 
     basic_data = [
--- a/bitten/upgrades.py
+++ b/bitten/upgrades.py
@@ -125,6 +125,34 @@
                    "max_rev,label,description) SELECT name,path,0,'',NULL,"
                    "NULL,label,description FROM old_config")
 
+def add_last_activity_to_build(env, db):
+    """Add a column for storing the last activity to the build table."""
+    from trac.db import Table, Column, Index
+    cursor = db.cursor()
+
+    build_table_schema_v12 = Table('bitten_build', key='id')[
+            Column('id', auto_increment=True), Column('config'), Column('rev'),
+            Column('rev_time', type='int'), Column('platform', type='int'),
+            Column('slave'), Column('started', type='int'),
+            Column('stopped', type='int'), Column('status', size=1),
+            Column('last_activity', type='int'),
+            Index(['config', 'rev', 'platform'], unique=True)
+        ]
+
+    cursor.execute("CREATE TEMPORARY TABLE old_build_v11 AS "
+                   "SELECT * FROM bitten_build")
+    cursor.execute("DROP TABLE bitten_build")
+
+    connector, _ = DatabaseManager(env)._get_connector()
+    for stmt in connector.to_sql(build_table_schema_v12):
+        cursor.execute(stmt)
+
+    # it's safe to make the last activity the stop time of the build
+    cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform,"
+                   "slave,started,stopped,last_activity,status) "
+                   "SELECT config,rev,rev_time,platform,"
+                   "slave,started,stopped,stopped,status FROM old_build_v11")
+
 def add_config_to_reports(env, db):
     """Add the name of the build configuration as metadata to report documents
     stored in the BDB XML database."""
@@ -199,7 +227,7 @@
 
 def xmldb_to_db(env, db):
     """Migrate report data from Berkeley DB XML to SQL database.
-    
+
     Depending on the number of reports stored, this might take rather long.
     After the upgrade is done, the bitten.dbxml file (and any BDB XML log files)
     may be deleted. BDB XML is no longer used by Bitten.
@@ -562,4 +590,5 @@
     9: [recreate_rule_with_int_id],
    10: [add_config_platform_rev_index_to_build, fix_sequences],
    11: [fix_log_levels_misnaming, remove_stray_log_levels_files],
+   12: [add_last_activity_to_build],
 }
--- a/bitten/web_ui.py
+++ b/bitten/web_ui.py
@@ -12,6 +12,7 @@
 
 import posixpath
 import re
+import time
 from StringIO import StringIO
 from datetime import datetime
 
@@ -45,6 +46,9 @@
                  Build.IN_PROGRESS: 'In Progress',
                  Build.SUCCESS: 'Success',
                  Build.FAILURE: 'Failure'}
+_step_status_label = {BuildStep.SUCCESS: 'success',
+                      BuildStep.FAILURE: 'failed',
+                      BuildStep.IN_PROGRESS: 'in progress'}
 
 def _get_build_data(env, req, build):
     data = {'id': build.id, 'name': build.slave, 'rev': build.rev,
@@ -89,7 +93,7 @@
             status = ''
             if BuildMaster(self.env).quick_status:
                 repos = self.env.get_repository(req.authname)
-                for config in BuildConfig.select(self.env, 
+                for config in BuildConfig.select(self.env,
                                                  include_inactive=False):
                     prev_rev = None
                     for platform, rev, build in collect_changes(repos, config):
@@ -106,11 +110,11 @@
                                 status='bitteninprogress'
                             elif not status:
                                 if (build_data['status'] == 'completed'):
-                                    status='bittencompleted'  
+                                    status='bittencompleted'
                 if not status:
                     status='bittenpending'
             yield ('mainnav', 'build',
-                   tag.a('Build Status', href=req.href.build(), accesskey=5, 
+                   tag.a('Build Status', href=req.href.build(), accesskey=5,
                          class_=status))
 
     # ITemplatesProvider methods
@@ -164,16 +168,16 @@
         return 'bitten_config.html', data, None
 
     # IRequestHandler methods
-    
+
     def pre_process_request(self, req, handler):
         return handler
 
     def post_process_request(self, req, template, data, content_type):
         if template:
             add_stylesheet(req, 'bitten/bitten.css')
-        
+
         return template, data, content_type
-        
+
     # Internal methods
 
     def _render_overview(self, req):
@@ -301,9 +305,10 @@
                     build_data['steps'].append({
                         'name': step.name,
                         'description': step.description,
-                        'duration': to_datetime(step.stopped, utc) - \
+                        'duration': to_datetime(step.stopped or int(time.time()), utc) - \
                                     to_datetime(step.started, utc),
-                        'failed': not step.successful,
+                        'status': _step_status_label[step.status],
+                        'cls': _step_status_label[step.status].replace(' ', '-'),
                         'errors': step.errors,
                         'href': build_data['href'] + '#step_' + step.name
                     })
@@ -311,7 +316,7 @@
                 builds.append(build_data)
                 current_builds += 1
 
-            if current_builds == 0: 
+            if current_builds == 0:
                 continue
 
             description = config.description
@@ -325,7 +330,7 @@
                 'builds': builds
             })
 
-        data['configs'] = configs
+        data['configs'] = sorted(configs, key=lambda x:x['label'].lower())
         return data
 
     def _render_config(self, req, config_name):
@@ -372,7 +377,7 @@
                                                db=db))
         data['config']['platforms'] = [
             { 'name': platform.name,
-              'id': platform.id, 
+              'id': platform.id,
               'builds_pending': len(list(Build.select(self.env,
                                                     config=config.name,
                                                     status=Build.PENDING,
@@ -393,13 +398,13 @@
         if has_reports:
             chart_generators = []
             report_categories = list(self._report_categories_for_config(config))
-            for generator in ReportChartController(self.env).generators: 
-                for category in generator.get_supported_categories(): 
+            for generator in ReportChartController(self.env).generators:
+                for category in generator.get_supported_categories():
                     if category in report_categories:
                         chart_generators.append({
-                            'href': req.href.build(config.name, 'chart/' + category) 
+                            'href': req.href.build(config.name, 'chart/' + category)
                         })
-            data['config']['charts'] = chart_generators 
+            data['config']['charts'] = chart_generators
             charts_license = self.config.get('bitten', 'charts_license')
             if charts_license:
                 data['config']['charts_license'] = charts_license
@@ -428,9 +433,11 @@
                         build_data['steps'].append({
                             'name': step.name,
                             'description': step.description,
-                            'duration': to_datetime(step.stopped, utc) - \
+                            'duration': to_datetime(step.stopped or int(time.time()), utc) - \
                                         to_datetime(step.started, utc),
-                            'failed': not step.successful,
+                            'status': _step_status_label[step.status],
+                            'cls': _step_status_label[step.status].replace(' ', '-'),
+
                             'errors': step.errors,
                             'href': build_data['href'] + '#step_' + step.name
                         })
@@ -457,16 +464,16 @@
         """Yields the categories of reports that exist for active builds
         of this configuration.
         """
-           
+
         db = self.env.get_db_cnx()
         repos = self.env.get_repository()
         cursor = db.cursor()
-        
+
         cursor.execute("""SELECT DISTINCT report.category as category
-FROM bitten_build AS build 
+FROM bitten_build AS build
 JOIN bitten_report AS report ON (report.build=build.id)
-WHERE build.config=%s AND build.rev_time >= %s AND build.rev_time <= %s""", 
-                       (config.name, 
+WHERE build.config=%s AND build.rev_time >= %s AND build.rev_time <= %s""",
+                       (config.name,
                         config.min_rev_time(self.env),
                         config.max_rev_time(self.env)))
 
@@ -545,8 +552,9 @@
         for step in BuildStep.select(self.env, build=build.id, db=db):
             steps.append({
                 'name': step.name, 'description': step.description,
-                'duration': pretty_timedelta(step.started, step.stopped),
-                'failed': step.status == BuildStep.FAILURE,
+                'duration': pretty_timedelta(step.started, step.stopped or int(time.time())),
+                'status': _step_status_label[step.status],
+                'cls': _step_status_label[step.status].replace(' ', '-'),
                 'errors': step.errors,
                 'log': self._render_log(req, build, formatters, step),
                 'reports': self._render_reports(req, config, build, summarizers,
@@ -668,7 +676,9 @@
             step.delete(db=db)
 
         build.slave = None
-        build.started = build.stopped = 0
+        build.started = 0
+        build.stopped = 0
+        build.last_activity = 0
         build.status = Build.PENDING
         build.slave_info = {}
         build.update()
Copyright (C) 2012-2017 Edgewall Software