changeset 76:ffa1ffd8c7db

* Implement basic slave selection based on configured target platforms. Closes #15. * Simple build status overview in config view.
author cmlenz
date Tue, 05 Jul 2005 17:52:15 +0000
parents 177d1c52ed48
children 8c7303f0b3d6
files bitten/master.py bitten/model.py bitten/tests/model.py bitten/trac_ext/web_ui.py htdocs/build.css templates/build.cs
diffstat 6 files changed, 159 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -20,6 +20,7 @@
 
 import logging
 import os.path
+import re
 import time
 
 from trac.env import Environment
@@ -52,25 +53,33 @@
     def _check_build_triggers(self, master, when):
         self.schedule(self.TRIGGER_INTERVAL, self._check_build_triggers)
 
-        logging.debug('Checking for build triggers...')
         repos = self.env.get_repository()
         try:
             repos.sync()
 
             for config in BuildConfig.select(self.env):
+                logging.debug('Checking for changes to "%s" at %s',
+                              config.label, config.path)
                 node = repos.get_node(config.path)
                 for path, rev, chg in node.get_history():
-                    # Check whether the latest revision of that configuration
-                    # has already been built
-                    builds = Build.select(self.env, config.name, rev)
-                    if not list(builds):
-                        logging.info('Enqueuing build of configuration "%s" as '
-                                     'of revision [%s]', config.name, rev)
-                        build = Build(self.env)
-                        build.config = config.name
-                        build.rev = rev
-                        build.rev_time = repos.get_changeset(rev).date
-                        build.insert()
+                    enqueued = False
+                    for platform in TargetPlatform.select(self.env, config.name):
+                        # Check whether the latest revision of the configuration
+                        # has already been built on this platform
+                        builds = Build.select(self.env, config.name, rev,
+                                              platform.id)
+                        if not list(builds):
+                            logging.info('Enqueuing build of configuration "%s"'
+                                         'at revision [%s] on %s', config.name,
+                                         rev, platform.name)
+                            build = Build(self.env)
+                            build.config = config.name
+                            build.rev = rev
+                            build.rev_time = repos.get_changeset(rev).date
+                            build.platform = platform.id
+                            build.insert()
+                            enqueued = True
+                    if enqueued:
                         break
         finally:
             repos.close()
@@ -83,11 +92,31 @@
         logging.debug('Checking for pending builds...')
         for build in Build.select(self.env, status=Build.PENDING):
             for slave in self.slaves.values():
-                active_builds = Build.select(self.env, slave=slave.name,
-                                             status=Build.IN_PROGRESS)
-                if not list(active_builds):
-                    slave.send_initiation(build)
-                    return
+                matches_platform = True
+                platform = TargetPlatform(self.env, build.platform)
+                logging.debug('Matching slave %s against rules: %s',
+                              slave.name, platform.rules)
+                for property, pattern in platform.rules:
+                    if not property or not pattern:
+                        continue
+                    try:
+                        if not re.match(pattern, slave.props.get(property)):
+                            logging.debug('Rule (%s, %s) did not match "%s"',
+                                          property, pattern, slave.props.get(property))
+                            matches_platform = False
+                            break
+                    except re.error, e:
+                        logging.error('Invalid platform matching pattern "%s"',
+                                      pattern, exc_info=True)
+                        matches_platform = False
+                        break
+
+                if matches_platform:
+                    active_builds = Build.select(self.env, slave=slave.name,
+                                                 status=Build.IN_PROGRESS)
+                    if not list(active_builds):
+                        slave.send_initiation(build)
+                        return
 
     def get_snapshot(self, build, type, encoding):
         formats = {
@@ -212,7 +241,7 @@
                 if elem.name == 'started':
                     self.steps = []
                     build.slave = self.name
-                    build.slave_info = self.props
+                    build.slave_info.update(self.props)
                     build.started = int(time.time())
                     build.status = Build.IN_PROGRESS
                     build.update()
--- a/bitten/model.py
+++ b/bitten/model.py
@@ -242,9 +242,10 @@
     _schema = [
         Table('bitten_build', key='id')[
             Column('id', auto_increment=True), Column('config'), Column('rev'),
-            Column('rev_time', type='int'), Column('slave'),
-            Column('started', type='int'), Column('stopped', type='int'),
-            Column('status', size='1'),  Index(['config', 'rev', 'slave'])
+            Column('rev_time', type='int'), Column('platform', type='int'),
+            Column('slave'), Column('started', type='int'),
+            Column('stopped', type='int'), Column('status', size='1'),
+            Index(['config', 'rev', 'slave'])
         ],
         Table('bitten_slave', key=('build', 'propname'))[
             Column('build', type='int'), Column('propname'), Column('propvalue')
@@ -272,7 +273,7 @@
         if id is not None:
             self._fetch(id, db)
         else:
-            self.id = self.config = self.rev = self.slave = None
+            self.id = self.config = self.rev = self.platform = self.slave = None
             self.started = self.stopped = self.rev_time = 0
             self.status = self.PENDING
 
@@ -281,8 +282,8 @@
             db = self.env.get_db_cnx()
 
         cursor = db.cursor()
-        cursor.execute("SELECT config,rev,rev_time,slave,started,stopped,"
-                       "status FROM bitten_build WHERE id=%s", (id,))
+        cursor.execute("SELECT config,rev,rev_time,platform,slave,started,"
+                       "stopped,status FROM bitten_build WHERE id=%s", (id,))
         row = cursor.fetchone()
         if not row:
             raise Exception, "Build %s not found" % id
@@ -290,10 +291,11 @@
         self.config = row[0]
         self.rev = row[1]
         self.rev_time = int(row[2])
-        self.slave = row[3]
-        self.started = row[4] and int(row[4])
-        self.stopped = row[5] and int(row[5])
-        self.status = row[6]
+        self.platform = int(row[3])
+        self.slave = row[4]
+        self.started = row[5] and int(row[5])
+        self.stopped = row[6] and int(row[6])
+        self.status = row[7]
 
         cursor.execute("SELECT propname,propvalue FROM bitten_slave "
                        "WHERE build=%s", (self.id,))
@@ -325,18 +327,19 @@
         else:
             handle_ta = False
 
-        assert self.config and self.rev and self.rev_time
+        assert self.config and self.rev and self.rev_time and self.platform
         assert self.status in (self.PENDING, self.IN_PROGRESS, self.SUCCESS,
                                self.FAILURE)
         if not self.slave:
             assert self.status == self.PENDING
 
         cursor = db.cursor()
-        cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,slave,"
-                       "started,stopped,status) "
-                       "VALUES (%s,%s,%s,%s,%s,%s,%s)",
-                       (self.config, self.rev, self.rev_time, self.slave or '',
-                        self.started or 0, self.stopped or 0, self.status))
+        cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform,"
+                       "slave,started,stopped,status) "
+                       "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
+                       (self.config, self.rev, self.rev_time, self.platform,
+                        self.slave or '', self.started or 0, self.stopped or 0,
+                        self.status))
         self.id = db.get_last_id('bitten_build')
         cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)",
                            [(self.id, name, value) for name, value
@@ -363,11 +366,15 @@
                        "stopped=%s,status=%s WHERE id=%s",
                        (self.slave or '', self.started or 0,
                         self.stopped or 0, self.status, self.id))
+        cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id))
+        cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)",
+                           [(self.id, name, value) for name, value
+                            in self.slave_info.items()])
         if handle_ta:
             db.commit()
 
-    def select(cls, env, config=None, rev=None, slave=None, status=None,
-               db=None):
+    def select(cls, env, config=None, rev=None, platform=None, slave=None,
+               status=None, db=None):
         if not db:
             db = env.get_db_cnx()
 
@@ -376,6 +383,8 @@
             where_clauses.append(("config=%s", config))
         if rev is not None:
             where_clauses.append(("rev=%s", rev))
+        if platform is not None:
+            where_clauses.append(("platform=%s", platform))
         if slave is not None:
             where_clauses.append(("slave=%s", slave))
         if status is not None:
@@ -386,16 +395,17 @@
             where = ""
 
         cursor = db.cursor()
-        cursor.execute("SELECT id,config,rev,slave,started,stopped,status,"
-                       "rev_time FROM bitten_build %s "
+        cursor.execute("SELECT id,config,rev,platform,slave,started,stopped,"
+                       "status,rev_time FROM bitten_build %s "
                        "ORDER BY config,rev_time DESC,slave"
                        % where, [wc[1] for wc in where_clauses])
-        for id, config, rev, slave, started, stopped, status, rev_time \
-                in cursor:
+        for id, config, rev, platform, slave, started, stopped, status, \
+                rev_time in cursor:
             build = Build(env)
             build.id = id
             build.config = config
             build.rev = rev
+            build.platform = platform
             build.slave = slave
             build.started = started and int(started) or 0
             build.stopped = stopped and int(stopped) or 0
--- a/bitten/tests/model.py
+++ b/bitten/tests/model.py
@@ -134,15 +134,16 @@
         build.config = 'test'
         build.rev = '42'
         build.rev_time = 12039
+        build.platform = 1
         build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'
         build.slave_info[Build.MAINTAINER] = 'joe@example.org'
         build.insert()
 
         db = self.env.get_db_cnx()
         cursor = db.cursor()
-        cursor.execute("SELECT config,rev,slave,started,stopped,status "
-                       "FROM bitten_build WHERE id=%s" % build.id)
-        self.assertEqual(('test', '42', '', 0, 0, 'P'), cursor.fetchone())
+        cursor.execute("SELECT config,rev,platform,slave,started,stopped,status"
+                       " FROM bitten_build WHERE id=%s" % build.id)
+        self.assertEqual(('test', '42', 1, '', 0, 0, 'P'), cursor.fetchone())
 
         cursor.execute("SELECT propname,propvalue FROM bitten_slave")
         expected = {Build.IP_ADDRESS: '127.0.0.1',
@@ -150,23 +151,36 @@
         for propname, propvalue in cursor:
             self.assertEqual(expected[propname], propvalue)
 
-    def test_insert_no_config_or_rev_or_rev_time(self):
+    def test_insert_no_config_or_rev_or_rev_time_or_platform(self):
         build = Build(self.env)
         self.assertRaises(AssertionError, build.insert)
 
+        # No config
+        build = Build(self.env)
+        build.rev = '42'
+        build.rev_time = 12039
+        build.platform = 1
+        self.assertRaises(AssertionError, build.insert)
+
+        # No rev
         build = Build(self.env)
         build.config = 'test'
         build.rev_time = 12039
+        build.platform = 1
         self.assertRaises(AssertionError, build.insert)
 
-        build = Build(self.env)
-        build.rev = '42'
-        build.rev_time = 12039
-        self.assertRaises(AssertionError, build.insert)
-
+        # No rev time
         build = Build(self.env)
         build.config = 'test'
         build.rev = '42'
+        build.platform = 1
+        self.assertRaises(AssertionError, build.insert)
+
+        # No platform
+        build = Build(self.env)
+        build.config = 'test'
+        build.rev = '42'
+        build.rev_time = 12039
         self.assertRaises(AssertionError, build.insert)
 
     def test_insert_no_slave(self):
@@ -174,6 +188,7 @@
         build.config = 'test'
         build.rev = '42'
         build.rev_time = 12039
+        build.platform = 1
         build.status = Build.SUCCESS
         self.assertRaises(AssertionError, build.insert)
         build.status = Build.FAILURE
@@ -194,9 +209,10 @@
     def test_fetch(self):
         db = self.env.get_db_cnx()
         cursor = db.cursor()
-        cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,slave,"
-                       "started,stopped,status) VALUES (%s,%s,%s,%s,%s,%s,%s)",
-                       ('test', '42', 12039, 'tehbox', 15006, 16007,
+        cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform,"
+                       "slave,started,stopped,status) "
+                       "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
+                       ('test', '42', 12039, 1, 'tehbox', 15006, 16007,
                         Build.SUCCESS))
         build_id = db.get_last_id('bitten_build')
         cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)",
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -136,18 +136,19 @@
             add_stylesheet(req, 'build.css')
             db = self.env.get_db_cnx()
             cursor = db.cursor()
-            cursor.execute("SELECT id,config,label,rev,slave,stopped,status "
-                           "FROM bitten_build "
-                           "  INNER JOIN bitten_config ON (name=config) "
-                           "WHERE stopped>=%s AND stopped<=%s "
-                           "AND status IN (%s, %s) ORDER BY stopped",
+            cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name,b.slave,"
+                           "b.stopped,b.status FROM bitten_build AS b"
+                           "  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, slave, stopped, status in cursor:
-                title = 'Build <em title="[%s] of %s">%s</em> by %s %s' \
-                        % (escape(rev), escape(label), escape(id),
-                           escape(slave), self._status_label[status])
+            for id, config, label, rev, platform, slave, stopped, status in cursor:
+                title = 'Build of <em>%s [%s]</em> by %s (%s) %s' \
+                        % (escape(label), escape(rev), escape(slave),
+                           escape(platform), self._status_label[status])
                 href = self.env.href.build(config, id)
                 yield event_kinds[status], href, title, stopped, None, ''
 
@@ -307,16 +308,21 @@
         req.hdf['build.mode'] = 'view_config'
 
         platforms = TargetPlatform.select(self.env, config=config_name)
-        req.hdf['build.platforms'] = [platform.name for platform in platforms]
+        req.hdf['build.platforms'] = [
+            {'name': platform.name, 'id': platform.id} for platform in platforms
+        ]
 
         repos = self.env.get_repository(req.authname)
         root = repos.get_node(config.path)
         num = 0
         for idx, (path, rev, chg) in enumerate(root.get_history()):
-            prefix = 'build.config.builds.%d' % rev
+            prefix = 'build.builds.%d' % rev
+            req.hdf[prefix + '.href'] = self.env.href.changeset(rev)
             for build in Build.select(self.env, config=config.name, rev=rev):
-                req.hdf[prefix + '.' + build.slave] = self._build_to_hdf(build)
-            if idx > 5:
+                if build.status == Build.PENDING:
+                    continue
+                req.hdf['%s.%s' % (prefix, build.platform)] = self._build_to_hdf(build)
+            if idx > 4:
                 break
 
     def _render_config_form(self, req, config_name=None):
@@ -375,8 +381,10 @@
         }
 
     def _build_to_hdf(self, build):
-        hdf = {'name': build.slave, 'status': self._status_label[build.status],
-               'rev': build.rev,
+        hdf = {'id': build.id, 'name': build.slave, 'rev': build.rev,
+               'status': self._status_label[build.status],
+               'cls': self._status_label[build.status].replace(' ', '-'),
+               'href': self.env.href.build(build.config, build.id),
                'chgset_href': self.env.href.changeset(build.rev)}
         if build.started:
             hdf['started'] = strftime('%x %X', localtime(build.started))
--- a/htdocs/build.css
+++ b/htdocs/build.css
@@ -8,3 +8,10 @@
 #content.build form.platforms ul { list-style: none; padding-left: 1em; }
 
 #content.build #builds { margin-top: 2em; }
+#content.build #builds th.rev { width: 6em; }
+#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; }
--- a/templates/build.cs
+++ b/templates/build.cs
@@ -87,9 +87,28 @@
     </div></form><?cs
    /if ?><?cs
    if:len(build.platforms) ?>
-    <table class="listing" id="builds"><thead><tr><th>Build</th><?cs
-    each:platform = build.platforms ?><th><?cs var:platform ?><?cs /each ?>
-   </tr></thead></table><?cs
+    <table class="listing" id="builds"><thead><tr><th>Changeset</th><?cs
+    each:platform = build.platforms ?><th><?cs var:platform.name ?><?cs
+    /each ?></tr></thead><?cs
+    if:len(build.builds) ?><tbody><?cs
+     each:rev = build.builds ?><tr>
+      <th class="rev" scope="row"><a href="<?cs
+        var:rev.href ?>" title="View Changeset">[<?cs
+        var:name(rev) ?>]</a></th><?cs
+      each:platform = build.platforms ?><?cs
+       if:len(rev[platform.id]) ?><td class="<?cs
+        var:rev[platform.id].cls ?>"><a href="<?cs
+        var:rev[platform.id].href ?>" title="View build results"><?cs
+        var:rev[platform.id].slave.name ?></a><br /><?cs
+        if:rev[platform.id].status == 'in progress' ?>started <?cs var:rev[platform.id].started_delta ?> ago<?cs
+        else ?>took <?cs
+         var:rev[platform.id].duration ?></td><?cs
+        /if ?><?cs
+       else ?><td>&mdash;</td><?cs
+       /if ?><?cs
+      /each ?></tr><?cs
+     /each ?></tbody><?cs
+    /if ?></table><?cs
    /if ?></div><?cs
 
   elif:build.mode == 'edit_platform' ?>
Copyright (C) 2012-2017 Edgewall Software