# HG changeset patch # User cmlenz # Date 1120425119 0 # Node ID 6d7753ea179811aad43229fd40a616b45438e858 # Parent b2d371dac2702419b7524c1ec22b995f330ecef8 Implemented basic management of target platforms. Closes #14. diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -23,7 +23,7 @@ import time from trac.env import Environment -from bitten.model import Build, BuildConfig, SlaveInfo +from bitten.model import Build, BuildConfig, TargetPlatform from bitten.util import archive, beep, xmlio @@ -147,13 +147,13 @@ self.name = elem.attr['name'] for child in elem.children(): if child.name == 'platform': - self.props[SlaveInfo.MACHINE] = child.gettext() - self.props[SlaveInfo.PROCESSOR] = child.attr.get('processor') + self.props[Build.MACHINE] = child.gettext() + self.props[Build.PROCESSOR] = child.attr.get('processor') elif child.name == 'os': - self.props[SlaveInfo.OS_NAME] = child.gettext() - self.props[SlaveInfo.OS_FAMILY] = child.attr.get('family') - self.props[SlaveInfo.OS_VERSION] = child.attr.get('version') - self.props[SlaveInfo.IP_ADDRESS] = self.session.addr[0] + self.props[Build.OS_NAME] = child.gettext() + self.props[Build.OS_FAMILY] = child.attr.get('family') + self.props[Build.OS_VERSION] = child.attr.get('version') + self.props[Build.IP_ADDRESS] = self.session.addr[0] self.name = elem.attr['name'] self.master.slaves[self.name] = self @@ -212,6 +212,7 @@ if elem.name == 'started': self.steps = [] build.slave = self.name + build.slave_info = self.props build.started = int(time.time()) build.status = Build.IN_PROGRESS build.update() @@ -247,13 +248,6 @@ build.started = 0 build.update() - # Insert slave info - slave_info = SlaveInfo(self.env) - slave_info.build = build.id - for name, value in self.props.items(): - slave_info[name] = value - slave_info.insert() - # TODO: should not block while reading the file; rather stream it using # asyncore push_with_producer() snapshot_path = self.master.get_snapshot(build, type, encoding) diff --git a/bitten/model.py b/bitten/model.py --- a/bitten/model.py +++ b/bitten/model.py @@ -20,15 +20,15 @@ from trac.db_default import Table, Column, Index -schema_version = 1 - class BuildConfig(object): """Representation of a build configuration.""" - _table = Table('bitten_config', key='name')[ - Column('name'), Column('path'), Column('label'), - Column('active', type='int'), Column('description') + _schema = [ + Table('bitten_config', key='name')[ + Column('name'), Column('path'), Column('label'), + Column('active', type='int'), Column('description') + ] ] def __init__(self, env, name=None, db=None): @@ -116,23 +116,146 @@ select = classmethod(select) +class TargetPlatform(object): + """Target platform for a build configuration.""" + + _schema = [ + Table('bitten_platform', key='id')[ + Column('id', auto_increment=True), Column('config'), Column('name') + ], + Table('bitten_rule', key=('id', 'propname'))[ + Column('id'), Column('propname'), Column('pattern'), + Column('orderno', type='int') + ] + ] + + def __init__(self, env, id=None, db=None): + self.env = env + self.rules = [] + if id is not None: + self._fetch(id, db) + else: + self.id = self.config = self.name = None + + def _fetch(self, id, db=None): + if not db: + db = self.env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT config,name FROM bitten_platform " + "WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + raise Exception, 'Target platform %s does not exist' % id + self.id = id + self.config = row[0] + self.name = row[1] + + cursor.execute("SELECT propname,pattern FROM bitten_rule " + "WHERE id=%s ORDER BY orderno", (id,)) + for propname, pattern in cursor: + self.rules.append((propname, pattern)) + + exists = property(fget=lambda self: self.id is not None) + + def insert(self, db=None): + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert not self.exists, 'Cannot insert existing target platform' + assert self.config, 'Target platform needs to be associated with a ' \ + 'configuration' + assert self.name, 'Target platform requires a name' + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", (self.config, self.name)) + self.id = db.get_last_id('bitten_platform') + cursor.executemany("INSERT INTO bitten_rule VALUES (%s,%s,%s,%s)", + [(self.id, propname, pattern, idx) for + idx, (propname, pattern) in enumerate(self.rules)]) + + if handle_ta: + db.commit() + + def update(self, db=None): + assert self.exists, 'Cannot update a non-existing platform' + assert self.config, 'Target platform needs to be associated with a ' \ + 'configuration' + assert self.name, 'Target platform requires a name' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("UPDATE bitten_platform SET name=%s WHERE id=%s", + (self.name, self.id)) + cursor.execute("DELETE FROM bitten_rule WHERE id=%s", (self.id)) + cursor.executemany("INSERT INTO bitten_rule VALUES (%s,%s,%s,%s)", + [(self.id, propname, pattern, idx) for + idx, (propname, pattern) in enumerate(self.rules)]) + + if handle_ta: + db.commit() + + def select(cls, env, config=None, db=None): + if not db: + db = env.get_db_cnx() + + where_clauses = [] + if config is not None: + where_clauses.append(("config=%s", config)) + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + cursor = db.cursor() + cursor.execute("SELECT id FROM bitten_platform %s ORDER BY name" + % where, [wc[1] for wc in where_clauses]) + for (id,) in cursor: + yield TargetPlatform(env, id) + select = classmethod(select) + + class Build(object): """Representation of a build.""" - _table = 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']) + _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']) + ], + Table('bitten_slave', key=('build', 'propname'))[ + Column('build', type='int'), Column('propname'), Column('propvalue') + ] ] + # Build status codes PENDING = 'P' IN_PROGRESS = 'I' SUCCESS = 'S' FAILURE = 'F' + # Standard slave properties + IP_ADDRESS = 'ipnr' + MAINTAINER = 'owner' + OS_NAME = 'os' + OS_FAMILY = 'family' + OS_VERSION = 'version' + MACHINE = 'machine' + PROCESSOR = 'processor' + def __init__(self, env, id=None, db=None): self.env = env + self.slave_info = {} if id is not None: self._fetch(id, db) else: @@ -159,6 +282,11 @@ self.stopped = row[5] and int(row[5]) self.status = row[6] + cursor.execute("SELECT propname,propvalue FROM bitten_slave " + "WHERE build=%s", (self.id,)) + for propname, propvalue in cursor: + self.slave_info[propname] = propvalue + exists = property(fget=lambda self: self.id is not None) completed = property(fget=lambda self: self.status != Build.IN_PROGRESS) successful = property(fget=lambda self: self.status == Build.SUCCESS) @@ -197,6 +325,10 @@ (self.config, self.rev, self.rev_time, 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 + in self.slave_info.items()]) + if handle_ta: db.commit() @@ -260,59 +392,5 @@ select = classmethod(select) -class SlaveInfo(object): - _table = Table('bitten_slave', key=('build', 'propname'))[ - Column('build', type='int'), Column('propname'), Column('propvalue') - ] - - # Standard properties - IP_ADDRESS = 'ipnr' - MAINTAINER = 'owner' - OS_NAME = 'os' - OS_FAMILY = 'family' - OS_VERSION = 'version' - MACHINE = 'machine' - PROCESSOR = 'processor' - - def __init__(self, env, build=None, db=None): - self.env = env - self.properties = {} - if build: - self._fetch(build, db) - else: - self.build = None - - def _fetch(self, build, db=None): - if not db: - db = self.env.get_db_cnx() - - cursor = db.cursor() - cursor.execute("SELECT propname,propvalue FROM bitten_slave " - "WHERE build=%s", (build,)) - for propname, propvalue in cursor: - self.properties[propname] = propvalue - if not self.properties: - raise Exception, "Slave info for %s not found" % build - self.build = build - - def __getitem__(self, name): - return self.properties[name] - - def __setitem__(self, name, value): - self.properties[name] = value - - def insert(self, db=None): - if not db: - db = self.env.get_db_cnx() - handle_ta = True - else: - handle_ta = False - - assert self.build - - cursor = db.cursor() - cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", - [(self.build, name, value) for name, value - in self.properties.items()]) - if handle_ta: - db.commit() +schema = BuildConfig._schema + TargetPlatform._schema + Build._schema +schema_version = 1 diff --git a/bitten/tests/__init__.py b/bitten/tests/__init__.py --- a/bitten/tests/__init__.py +++ b/bitten/tests/__init__.py @@ -22,11 +22,13 @@ from bitten.tests import model, recipe from bitten.util import tests as util +from bitten.trac_ext import tests as trac_ext def suite(): suite = unittest.TestSuite() suite.addTest(model.suite()) suite.addTest(recipe.suite()) + suite.addTest(trac_ext.suite()) suite.addTest(util.suite()) return suite diff --git a/bitten/tests/model.py b/bitten/tests/model.py --- a/bitten/tests/model.py +++ b/bitten/tests/model.py @@ -21,7 +21,7 @@ import unittest from trac.test import EnvironmentStub -from bitten.model import Build, BuildConfig, SlaveInfo +from bitten.model import Build, BuildConfig, TargetPlatform class BuildConfigTestCase(unittest.TestCase): @@ -30,7 +30,8 @@ self.env = EnvironmentStub() db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute(db.to_sql(BuildConfig._table)) + for table in BuildConfig._schema: + cursor.execute(db.to_sql(table)) db.commit() def test_new_config(self): @@ -55,27 +56,86 @@ self.assertRaises(AssertionError, config.insert) +class TargetPlatformTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + db = self.env.get_db_cnx() + cursor = db.cursor() + for table in TargetPlatform._schema: + cursor.execute(db.to_sql(table)) + db.commit() + + def test_new(self): + platform = TargetPlatform(self.env) + self.assertEqual(False, platform.exists) + self.assertEqual([], platform.rules) + + def test_insert(self): + platform = TargetPlatform(self.env) + platform.config = 'test' + platform.name = 'Windows XP' + platform.rules.append((Build.OS_NAME, 'Windows')) + platform.rules.append((Build.OS_VERSION, 'XP')) + platform.insert() + + assert platform.exists + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT config,name FROM bitten_platform " + "WHERE id=%s", (platform.id,)) + self.assertEqual(('test', 'Windows XP'), cursor.fetchone()) + + cursor.execute("SELECT propname,pattern,orderno FROM bitten_rule " + "WHERE id=%s", (platform.id,)) + self.assertEqual((Build.OS_NAME, 'Windows', 0), cursor.fetchone()) + self.assertEqual((Build.OS_VERSION, 'XP', 1), cursor.fetchone()) + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", ('test', 'Windows')) + id = db.get_last_id('bitten_platform') + platform = TargetPlatform(self.env, id) + assert platform.exists + self.assertEqual('test', platform.config) + self.assertEqual('Windows', platform.name) + + def test_select(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.executemany("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", [('test', 'Windows'), + ('test', 'Mac OS X')]) + platforms = list(TargetPlatform.select(self.env, config='test')) + self.assertEqual(2, len(platforms)) + + class BuildTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute(db.to_sql(Build._table)) + for table in Build._schema: + cursor.execute(db.to_sql(table)) db.commit() - def test_new_build(self): + def test_new(self): build = Build(self.env) self.assertEqual(None, build.id) self.assertEqual(Build.PENDING, build.status) self.assertEqual(0, build.stopped) self.assertEqual(0, build.started) - def test_insert_build(self): + def test_insert(self): build = Build(self.env) build.config = 'test' build.rev = '42' build.rev_time = 12039 + 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() @@ -84,7 +144,13 @@ "FROM bitten_build WHERE id=%s" % build.id) self.assertEqual(('test', '42', '', 0, 0, 'P'), cursor.fetchone()) - def test_insert_build_no_config_or_rev_or_rev_time(self): + cursor.execute("SELECT propname,propvalue FROM bitten_slave") + expected = {Build.IP_ADDRESS: '127.0.0.1', + Build.MAINTAINER: 'joe@example.org'} + for propname, propvalue in cursor: + self.assertEqual(expected[propname], propvalue) + + def test_insert_no_config_or_rev_or_rev_time(self): build = Build(self.env) self.assertRaises(AssertionError, build.insert) @@ -103,7 +169,7 @@ build.rev = '42' self.assertRaises(AssertionError, build.insert) - def test_insert_build_no_slave(self): + def test_insert_no_slave(self): build = Build(self.env) build.config = 'test' build.rev = '42' @@ -125,49 +191,29 @@ build.status = 'DUNNO' self.assertRaises(AssertionError, build.insert) - -class SlaveInfoTestCase(unittest.TestCase): - - def setUp(self): - self.env = EnvironmentStub() + def test_fetch(self): db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute(db.to_sql(SlaveInfo._table)) - db.commit() - - def test_insert_slave_info(self): - slave_info = SlaveInfo(self.env) - slave_info.build = 42 - slave_info[SlaveInfo.IP_ADDRESS] = '127.0.0.1' - slave_info[SlaveInfo.MAINTAINER] = 'joe@example.org' - slave_info.insert() + 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, + Build.SUCCESS)) + build_id = db.get_last_id('bitten_build') + cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", + [(build_id, Build.IP_ADDRESS, '127.0.0.1'), + (build_id, Build.MAINTAINER, 'joe@example.org')]) - db = self.env.get_db_cnx() - cursor = db.cursor() - cursor.execute("SELECT propname,propvalue FROM bitten_slave") - expected = {SlaveInfo.IP_ADDRESS: '127.0.0.1', - SlaveInfo.MAINTAINER: 'joe@example.org'} - for propname, propvalue in cursor: - self.assertEqual(expected[propname], propvalue) - - def test_fetch_slave_info(self): - db = self.env.get_db_cnx() - cursor = db.cursor() - cursor.executemany("INSERT INTO bitten_slave VALUES (42,%s,%s)", - [(SlaveInfo.IP_ADDRESS, '127.0.0.1'), - (SlaveInfo.MAINTAINER, 'joe@example.org')]) - - slave_info = SlaveInfo(self.env, 42) - self.assertEquals(42, slave_info.build) - self.assertEquals('127.0.0.1', slave_info[SlaveInfo.IP_ADDRESS]) - self.assertEquals('joe@example.org', slave_info[SlaveInfo.MAINTAINER]) + build = Build(self.env, build_id) + self.assertEquals(build_id, build.id) + self.assertEquals('127.0.0.1', build.slave_info[Build.IP_ADDRESS]) + self.assertEquals('joe@example.org', build.slave_info[Build.MAINTAINER]) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(BuildConfigTestCase, 'test')) + suite.addTest(unittest.makeSuite(TargetPlatformTestCase, 'test')) suite.addTest(unittest.makeSuite(BuildTestCase, 'test')) - suite.addTest(unittest.makeSuite(SlaveInfoTestCase, 'test')) return suite if __name__ == '__main__': diff --git a/bitten/trac_ext/main.py b/bitten/trac_ext/main.py --- a/bitten/trac_ext/main.py +++ b/bitten/trac_ext/main.py @@ -23,7 +23,7 @@ from trac.core import * from trac.env import IEnvironmentSetupParticipant from trac.perm import IPermissionRequestor -from bitten.model import Build, BuildConfig, SlaveInfo, schema_version +from bitten.model import * from bitten.trac_ext import web_ui class BuildSystem(Component): @@ -36,7 +36,8 @@ # Create the required tables db = self.env.get_db_cnx() cursor = db.cursor() - for table in [Build._table, BuildConfig._table, SlaveInfo._table]: + for table in [Build._table, BuildConfig._table, SlaveInfo._table] \ + + TargetPlatform._schema: cursor.execute(db.to_sql(table)) tarballs_dir = os.path.join(self.env.path, 'snapshots') diff --git a/bitten/trac_ext/tests/__init__.py b/bitten/trac_ext/tests/__init__.py new file mode 100644 --- /dev/null +++ b/bitten/trac_ext/tests/__init__.py @@ -0,0 +1,31 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# +# Bitten is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Trac is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# Author: Christopher Lenz + +import unittest + +from bitten.trac_ext.tests import web_ui + +def suite(): + suite = unittest.TestSuite() + suite.addTest(web_ui.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/bitten/trac_ext/tests/web_ui.py b/bitten/trac_ext/tests/web_ui.py new file mode 100644 --- /dev/null +++ b/bitten/trac_ext/tests/web_ui.py @@ -0,0 +1,337 @@ +import unittest + +from trac.perm import PermissionCache, PermissionSystem +from trac.test import EnvironmentStub, Mock +from trac.versioncontrol import Repository +from trac.web.clearsilver import HDFWrapper +from trac.web.main import Request, RequestDone +from bitten.model import BuildConfig, TargetPlatform, Build, schema +from bitten.trac_ext.main import BuildSystem +from bitten.trac_ext.web_ui import BuildModule + + +class BuildModuleTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + + # Create tables + db = self.env.get_db_cnx() + cursor = db.cursor() + for table in schema: + cursor.execute(db.to_sql(table)) + + # Set up permissions + self.env.config.set('trac', 'permission_store', + 'DefaultPermissionStore') + + # Hook up a dummy repository + repos = Mock(get_node=lambda path: Mock(get_history=lambda: [])) + self.env.get_repository = lambda x: repos + + def test_overview(self): + PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW') + req = Mock(Request, path_info='/build', args={}, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('overview', req.hdf['build.mode']) + self.assertEqual('0', req.hdf.get('build.can_create', '0')) + + def test_overview_admin(self): + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + req = Mock(Request, path_info='/build', args={}, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('1', req.hdf.get('build.can_create')) + + def test_view_config(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW') + req = Mock(Request, path_info='/build/test', args={}, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('view_config', req.hdf['build.mode']) + self.assertEqual('0', req.hdf.get('build.config.can_modify', '0')) + + def test_view_config_admin(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + req = Mock(Request, path_info='/build/test', args={}, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('1', req.hdf.get('build.config.can_modify')) + + def test_new_config(self): + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + req = Mock(Request, path_info='/build', args={'action': 'new'}, + hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('edit_config', req.hdf['build.mode']) + + def test_new_config_submit(self): + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build', + redirect=redirect, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe'), + args={'action': 'new', 'name': 'test', 'active': 'on', + 'label': 'Test', 'path': 'test/trunk', + 'description': 'Bla bla'}) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/test', redirected_to[0]) + + build = BuildConfig(self.env, 'test') + assert build.exists + assert build.active + self.assertEqual('Test', build.label) + self.assertEqual('test/trunk', build.path) + self.assertEqual('Bla bla', build.description) + + def test_new_config_cancel(self): + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build', + redirect=redirect, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe'), + args={'action': 'new', 'cancel': '1', 'name': 'test'}) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build', redirected_to[0]) + + self.assertRaises(Exception, BuildConfig, self.env, 'test') + + def test_edit_config(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + req = Mock(Request, path_info='/build/test', args={'action': 'edit'}, + hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('edit_config', req.hdf['build.mode']) + + def test_edit_config_submit(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build/test', + redirect=redirect, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe'), + args={'action': 'edit', 'name': 'foo', 'active': 'on', + 'label': 'Test', 'path': 'test/trunk', + 'description': 'Bla bla'}) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/foo', redirected_to[0]) + + self.assertRaises(Exception, BuildConfig, self.env, 'test') + + build = BuildConfig(self.env, 'foo') + assert build.exists + assert build.active + self.assertEqual('Test', build.label) + self.assertEqual('test/trunk', build.path) + self.assertEqual('Bla bla', build.description) + + def test_edit_config_cancel(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build/test', + redirect=redirect, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe'), + args={'action': 'edit', 'cancel': '1'}) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/test', redirected_to[0]) + + def test_new_platform(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + req = Mock(Request, path_info='/build/test', args={'action': 'new'}, + hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('edit_platform', req.hdf['build.mode']) + + def test_new_platform_submit(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build/test', + redirect=redirect, args={'action': 'new', 'name': 'Test'}, + hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0]) + + def test_new_platform_cancel(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build/test', + redirect=redirect, args={'action': 'new', 'cancel': '1'}, + hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0]) + + def test_edit_platform(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + platform = TargetPlatform(self.env) + platform.config = 'test' + platform.name = 'linux' + platform.rules.append(('os', 'linux?')) + platform.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + req = Mock(Request, path_info='/build/test', + args={'action': 'edit', 'platform': platform.id}, + hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + module.process_request(req) + + self.assertEqual('edit_platform', req.hdf['build.mode']) + + def test_edit_platform_submit(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + platform = TargetPlatform(self.env) + platform.config = 'test' + platform.name = 'linux' + platform.rules.append(('os', 'linux?')) + platform.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build/test', + args={'action': 'edit', 'platform': platform.id, + 'name': 'Test'}, + redirect=redirect, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0]) + + def test_edit_platform_cancel(self): + config = BuildConfig(self.env) + config.name = 'test' + config.insert() + platform = TargetPlatform(self.env) + platform.config = 'test' + platform.name = 'linux' + platform.rules.append(('os', 'linux')) + platform.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(Request, method='POST', path_info='/build/test', + args={'action': 'edit', 'platform': platform.id, + 'cancel': '1'}, + redirect=redirect, hdf=HDFWrapper(), + perm=PermissionCache(self.env, 'joe')) + + module = BuildModule(self.env) + assert module.match_request(req) + self.assertRaises(RequestDone, module.process_request, req) + self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0]) + + +def suite(): + return unittest.makeSuite(BuildModuleTestCase, 'test') + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/bitten/trac_ext/web_ui.py b/bitten/trac_ext/web_ui.py --- a/bitten/trac_ext/web_ui.py +++ b/bitten/trac_ext/web_ui.py @@ -24,10 +24,10 @@ from trac.core import * from trac.Timeline import ITimelineEventProvider from trac.util import escape, pretty_timedelta -from trac.web.chrome import INavigationContributor +from trac.web.chrome import INavigationContributor, add_link from trac.web.main import IRequestHandler from trac.wiki import wiki_to_html -from bitten.model import Build, BuildConfig, SlaveInfo +from bitten.model import Build, BuildConfig, TargetPlatform class BuildModule(Component): @@ -50,34 +50,38 @@ if:build.can_create ?>
- -
+
-
-
-
-
-
-
- -

- -
+ + + + + + + + + +
+ +

+ +
@@ -86,6 +90,21 @@ if:build.config.exists ?>Save changesCreate" />
+

Target Platforms

+
+
+ + +
+
+
+ if:build.config.can_modify ?>
+
+
+

Rules

+ + + + + + + +
Property nameMatch pattern
+
+
+
+ + + + +
+
+

Triggered by: Changeset [] of

-

Built by: ( on )

+

Built by: ( on )

Completed: ( ago)
Took:

@@ -135,7 +190,7 @@ # IRequestHandler methods def match_request(self, req): - match = re.match(r'/build(?:/([\w.-]+))?(?:/([\w.-]+))?', req.path_info) + match = re.match(r'/build(?:/([\w.-]+))?(?:/([\d]+))?', req.path_info) if match: if match.group(1): req.args['config'] = match.group(1) @@ -151,23 +206,41 @@ id = req.args.get('id') if req.method == 'POST': - if not config and action == 'new': - self._do_create(req) - elif config and action == 'edit': - self._do_save(req, config) + if config: + if action == 'new': + self._do_create_platform(req, config) + else: + platform_id = req.args.get('platform') + if platform_id: + if action == 'edit': + self._do_save_platform(req, config, platform_id) + else: + self._do_save_config(req, config) + else: + if action == 'new': + self._do_create_config(req) else: - if not config: + if id: + self._render_build(req, id) + elif config: + if action == 'edit': + platform_id = req.args.get('platform') + if platform_id: + platform = TargetPlatform(self.env, int(platform_id)) + self._render_platform_form(req, platform) + else: + self._render_config_form(req, config) + elif action == 'new': + platform = TargetPlatform(self.env) + platform.config = config + self._render_platform_form(req, platform) + else: + self._render_config(req, config) + else: if action == 'new': self._render_config_form(req) else: self._render_overview(req) - elif not id: - if action == 'edit': - self._render_config_form(req, config) - else: - self._render_config(req, config) - else: - self._render_build(req, config, id) return req.hdf.parse(self.build_cs), None @@ -198,7 +271,7 @@ # Internal methods - def _do_create(self, req): + def _do_create_config(self, req): """Create a new build configuration.""" req.perm.assert_permission('BUILD_CREATE') @@ -215,7 +288,7 @@ req.redirect(self.env.href.build(config.name)) - def _do_save(self, req, config_name): + def _do_save_config(self, req, config_name): """Save changes to a build configuration.""" req.perm.assert_permission('BUILD_MODIFY') @@ -232,6 +305,81 @@ req.redirect(self.env.href.build(config.name)) + def _do_create_platform(self, req, config_name): + """Create a new target platform.""" + req.perm.assert_permission('BUILD_MODIFY') + + if 'cancel' in req.args.keys(): + req.redirect(self.env.href.build(config_name, action='edit')) + + platform = TargetPlatform(self.env) + platform.config = config_name + platform.name = req.args.get('name') + + properties = [int(key[9:]) for key in req.args.keys() + if key.startswith('property_')] + properties.sort() + patterns = [int(key[8:]) for key in req.args.keys() + if key.startswith('pattern_')] + patterns.sort() + platform.rules = [(req.args.get('property_%d' % property), + req.args.get('pattern_%d' % pattern)) + for property, pattern in zip(properties, patterns)] + + add_rules = [int(key[9:]) for key in req.args.keys() + if key.startswith('add_rule_')] + if add_rules: + platform.rules.insert(add_rules[0] + 1, ('', '')) + self._render_platform_form(req, platform) + return + rm_rules = [int(key[8:]) for key in req.args.keys() + if key.startswith('rm_rule_')] + if rm_rules: + del platform.rules[rm_rules[0]] + self._render_platform_form(req, platform) + return + + platform.insert() + + req.redirect(self.env.href.build(config_name, action='edit')) + + def _do_save_platform(self, req, config_name, platform_id): + """Save changes to a target platform.""" + req.perm.assert_permission('BUILD_MODIFY') + + if 'cancel' in req.args.keys(): + req.redirect(self.env.href.build(config_name, action='edit')) + + platform = TargetPlatform(self.env, platform_id) + platform.name = req.args.get('name') + + properties = [int(key[9:]) for key in req.args.keys() + if key.startswith('property_')] + properties.sort() + patterns = [int(key[8:]) for key in req.args.keys() + if key.startswith('pattern_')] + patterns.sort() + platform.rules = [(req.args.get('property_%d' % property), + req.args.get('pattern_%d' % pattern)) + for property, pattern in zip(properties, patterns)] + + add_rules = [int(key[9:]) for key in req.args.keys() + if key.startswith('add_rule_')] + if add_rules: + platform.rules.insert(add_rules[0] + 1, ('', '')) + self._render_platform_form(req, platform) + return + rm_rules = [int(key[8:]) for key in req.args.keys() + if key.startswith('rm_rule_')] + if rm_rules: + del platform.rules[rm_rules[0]] + self._render_platform_form(req, platform) + return + + platform.update() + + req.redirect(self.env.href.build(config_name, action='edit')) + def _render_overview(self, req): req.hdf['title'] = 'Build Status' configurations = BuildConfig.select(self.env, include_inactive=True) @@ -251,37 +399,73 @@ config = BuildConfig(self.env, config_name) req.hdf['title'] = 'Build Configuration "%s"' \ % escape(config.label or config.name) + add_link(req, 'up', self.env.href.build(), 'Build Status') description = config.description if description: description = wiki_to_html(description, self.env, req) req.hdf['build.config'] = { 'name': config.name, 'label': config.label, 'path': config.path, 'active': config.active, 'description': description, - 'browser_href': self.env.href.browser(config.path) + 'browser_href': self.env.href.browser(config.path), + 'can_modify': req.perm.has_permission('BUILD_MODIFY') } + req.hdf['build.mode'] = 'view_config' - req.hdf['build.mode'] = 'view_config' - req.hdf['build.can_modify'] = req.perm.has_permission('BUILD_MODIFY') + 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 + 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: + break def _render_config_form(self, req, config_name=None): config = BuildConfig(self.env, config_name) if config.exists: req.perm.assert_permission('BUILD_MODIFY') - req.hdf['title'] = 'Edit Build Configuration "%s"' \ - % escape(config.label or config.name) req.hdf['build.config'] = { 'name': config.name, 'label': config.label, 'path': config.path, 'active': config.active, 'description': config.description, 'exists': config.exists } + + if 'new' in req.args.keys() or 'platform' in req.args.keys(): + self._render_platform_form(req, config_name, + req.args.get('platform')) + return + + req.hdf['title'] = 'Edit Build Configuration "%s"' \ + % escape(config.label or config.name) + for idx, platform in enumerate(TargetPlatform.select(self.env, + config_name)): + req.hdf['build.platforms.%d' % idx] = { + 'id': platform.id, 'name': platform.name, + 'href': self.env.href.build(config_name, action='edit', + platform=platform.id) + } else: req.perm.assert_permission('BUILD_CREATE') req.hdf['title'] = 'Create Build Configuration' req.hdf['build.mode'] = 'edit_config' - def _render_build(self, req, config_name, build_id): + def _render_platform_form(self, req, platform): + req.perm.assert_permission('BUILD_MODIFY') + req.hdf['title'] = 'Edit Target Platform "%s"' \ + % escape(platform.name) + req.hdf['build.platform'] = { + 'name': platform.name, 'id': platform.id, 'exists': platform.exists, + 'rules': [{'property': propname, 'pattern': pattern} + for propname, pattern in platform.rules] + } + req.hdf['build.mode'] = 'edit_platform' + + def _render_build(self, req, build_id): build = Build(self.env, build_id) assert build.exists + add_link(req, 'up', self.env.href.build(build.config), + 'Build Configuration') status2title = {Build.SUCCESS: 'Success', Build.FAILURE: 'Failure'} req.hdf['title'] = 'Build %s - %s' % (build_id, status2title[build.status]) @@ -294,17 +478,6 @@ 'href': self.env.href.build(config.name) } - slave_info = SlaveInfo(self.env, build.id) - req.hdf['build.slave'] = { - 'name': build.slave, - 'ip_address': slave_info.properties.get(SlaveInfo.IP_ADDRESS), - 'os': slave_info.properties.get(SlaveInfo.OS_NAME), - 'os.family': slave_info.properties.get(SlaveInfo.OS_FAMILY), - 'os.version': slave_info.properties.get(SlaveInfo.OS_VERSION), - 'machine': slave_info.properties.get(SlaveInfo.MACHINE), - 'processor': slave_info.properties.get(SlaveInfo.PROCESSOR) - } - def _build_to_hdf(self, build): hdf = {'name': build.slave, 'status': self._status_label[build.status], 'rev': build.rev, @@ -316,4 +489,13 @@ hdf['stopped'] = strftime('%x %X', localtime(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), + '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), + 'processor': build.slave_info.get(Build.PROCESSOR) + } return hdf