changeset 147:395b67aa072e

Build recipes are now stored in the database with the build configuration. This means that it is no longer necessary to store the recipe in the repository. Closes #41. At some point, there'll need to be a real user interface for creating/updating the recipe.
author cmlenz
date Sun, 21 Aug 2005 17:49:20 +0000
parents affd91b4c6fb
children f3f5895e373c
files bitten/master.py bitten/model.py bitten/recipe.py bitten/slave.py bitten/trac_ext/htdocs/bitten.css bitten/trac_ext/templates/bitten_config.cs bitten/trac_ext/tests/web_ui.py bitten/trac_ext/web_ui.py bitten/upgrades.py recipe.xml
diffstat 10 files changed, 183 insertions(+), 147 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -285,11 +285,11 @@
                 return
             self.send_snapshot(build, type, encoding)
 
-        xml = xmlio.Element('build', recipe='recipe.xml')
-        self.channel.send_msg(beep.Payload(xml), handle_reply=handle_reply)
+        config = BuildConfig.fetch(self.env, build.config)
+        self.channel.send_msg(beep.Payload(config.recipe),
+                              handle_reply=handle_reply)
 
     def send_snapshot(self, build, type, encoding):
-
         timestamp_delta = 0
         if self.master.adjust_timestamps:
             d = datetime.now() - timedelta(seconds=self.master.check_interval) \
--- a/bitten/model.py
+++ b/bitten/model.py
@@ -26,19 +26,23 @@
 
     _schema = [
         Table('bitten_config', key='name')[
-            Column('name'), Column('path'), Column('label'),
-            Column('active', type='int'), Column('description')
+            Column('name'), Column('path'), Column('active', type='int'),
+            Column('recipe'), Column('min_rev'), Column('max_rev'),
+            Column('label'), Column('description')
         ]
     ]
 
-    def __init__(self, env, name=None, path=None, label=None, active=False,
-                 description=None):
+    def __init__(self, env, name=None, path=None, active=False, recipe=None,
+                 min_rev=None, max_rev=None, label=None, description=None):
         self.env = env
         self._old_name = None
         self.name = name
         self.path = path or ''
+        self.active = bool(active)
+        self.recipe = recipe or ''
+        self.min_rev = min_rev or None
+        self.max_rev = max_rev or None
         self.label = label or ''
-        self.active = bool(active)
         self.description = description or ''
 
     exists = property(fget=lambda self: self._old_name is not None)
@@ -53,14 +57,16 @@
             handle_ta = False
 
         cursor = db.cursor()
-        cursor.execute("INSERT INTO bitten_config "
-                       "(name,path,label,active,description) "
-                       "VALUES (%s,%s,%s,%s,%s)",
-                       (self.name, self.path, self.label or '',
-                        int(self.active or 0), self.description or ''))
+        cursor.execute("INSERT INTO bitten_config (name,path,active,"
+                       "recipe,min_rev,max_rev,label,description) "
+                       "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
+                       (self.name, self.path, int(self.active or 0),
+                        self.recipe or '', self.min_rev, self.max_rev,
+                        self.label or '', self.description or ''))
 
         if handle_ta:
             db.commit()
+        self._old_name = self.name
 
     def update(self, db=None):
         assert self.exists, 'Cannot update a non-existing configuration'
@@ -72,21 +78,24 @@
             handle_ta = False
 
         cursor = db.cursor()
-        cursor.execute("UPDATE bitten_config SET name=%s,path=%s,label=%s,"
-                       "active=%s,description=%s WHERE name=%s",
-                       (self.name, self.path, self.label, int(self.active or 0),
-                        self.description, self._old_name))
+        cursor.execute("UPDATE bitten_config SET name=%s,path=%s,active=%s,"
+                       "recipe=%s,min_rev=%s,max_rev=%s,label=%s,"
+                       "description=%s WHERE name=%s",
+                       (self.name, self.path, int(self.active or 0),
+                        self.recipe, self.min_rev, self.max_rev,
+                        self.label, self.description, self._old_name))
 
         if handle_ta:
             db.commit()
+        self._old_name = self.name
 
     def fetch(cls, env, name, db=None):
         if not db:
             db = env.get_db_cnx()
 
         cursor = db.cursor()
-        cursor.execute("SELECT path,label,active,description "
-                       "FROM bitten_config WHERE name=%s", (name,))
+        cursor.execute("SELECT path,active,recipe,min_rev,max_rev,label,"
+                       "description FROM bitten_config WHERE name=%s", (name,))
         row = cursor.fetchone()
         if not row:
             return None
@@ -94,9 +103,12 @@
         config = BuildConfig(env)
         config.name = config._old_name = name
         config.path = row[0] or ''
-        config.label = row[1] or ''
-        config.active = row[2] and True or False
-        config.description = row[3] or ''
+        config.active = row[1] and True or False
+        config.recipe = row[2] or ''
+        config.min_rev = row[3] or ''
+        config.max_rev = row[4] or ''
+        config.label = row[5] or ''
+        config.description = row[6] or ''
         return config
 
     fetch = classmethod(fetch)
@@ -104,18 +116,21 @@
     def select(cls, env, include_inactive=False, db=None):
         if not db:
             db = env.get_db_cnx()
-        where = ''
+
         cursor = db.cursor()
         if include_inactive:
-            cursor.execute("SELECT name,path,label,active,description "
-                           "FROM bitten_config ORDER BY name")
+            cursor.execute("SELECT name,path,active,recipe,min_rev,max_rev,"
+                           "label,description FROM bitten_config ORDER BY name")
         else:
-            cursor.execute("SELECT name,path,label,active,description "
-                           "FROM bitten_config WHERE active=1 "
-                           "ORDER BY name")
-        for name, path, label, active, description in cursor:
+            cursor.execute("SELECT name,path,active,recipe,min_rev,max_rev,"
+                           "label,description FROM bitten_config "
+                           "WHERE active=1 ORDER BY name")
+        for name, path, active, recipe, min_rev, max_rev, label, description \
+                in cursor:
             config = BuildConfig(env, name=name, path=path or '',
-                                 label=label or '', active=bool(active),
+                                 active=bool(active), recipe=recipe or '',
+                                 min_rev=min_rev or None,
+                                 max_rev=max_rev or None, label=label or '',
                                  description=description or '')
             config._old_name = name
             yield config
@@ -643,4 +658,4 @@
 
 schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \
          BuildStep._schema + BuildLog._schema
-schema_version = 2
+schema_version = 3
--- a/bitten/recipe.py
+++ b/bitten/recipe.py
@@ -113,7 +113,7 @@
         func_name = self._translate_name(elem.name)
         try:
             module = __import__(elem.namespace[7:], globals(), locals(),
-                                func_name)
+                                [func_name])
             func = getattr(module, func_name)
             return func
         except (ImportError, AttributeError), e:
@@ -137,13 +137,17 @@
     LOG = 'log'
     REPORT = 'report'
 
-    def __init__(self, filename='recipe.xml', basedir=os.getcwd()):
+    def __init__(self, filename='recipe.xml', basedir=os.getcwd(),
+                 xml_elem=None):
         self.ctxt = Context(basedir)
-        fd = file(self.ctxt.resolve(filename), 'r')
-        try:
-            self._root = xmlio.parse(fd)
-        finally:
-            fd.close()
+        if filename:
+            fd = file(self.ctxt.resolve(filename), 'r')
+            try:
+                self._root = xmlio.parse(fd)
+            finally:
+                fd.close()
+        elif xml_elem:
+            self._root = xml_elem
         self.description = self._root.attr.get('description')
 
     def __iter__(self):
--- a/bitten/slave.py
+++ b/bitten/slave.py
@@ -58,7 +58,6 @@
 
     def handle_connect(self):
         """Register with the build master."""
-        self.recipe_path = None
 
         def handle_reply(cmd, msgno, ansno, payload):
             if cmd == 'ERR':
@@ -105,12 +104,12 @@
         self.channel.send_msg(beep.Payload(xml), handle_reply)
 
     def handle_msg(self, msgno, payload):
+        recipe_xml = None
         if payload.content_type == beep.BEEP_XML:
             elem = xmlio.parse(payload.body)
             if elem.name == 'build':
+                recipe_xml = elem
                 # Received a build request
-                self.recipe_path = elem.attr['recipe']
-
                 xml = xmlio.Element('proceed')[
                     xmlio.Element('accept', type='application/tar',
                                   encoding='bzip2'),
@@ -157,15 +156,12 @@
                 for filename in files:
                     os.chmod(os.path.join(root, filename), 0400)
 
-            self.execute_build(msgno, path, self.recipe_path)
+            self.execute_build(msgno, Recipe(basedir=path, xml_elem=recipe_xml))
 
-    def execute_build(self, msgno, basedir, recipe_path):
+    def execute_build(self, msgno, recipe):
         global log
-        log.info('Building in directory %s using recipe %s', basedir,
-                 recipe_path)
+        log.info('Building in directory %s', recipe.ctxt.basedir)
         try:
-            recipe = Recipe(recipe_path, basedir)
-
             xml = xmlio.Element('started', time=datetime.utcnow().isoformat())
             self.channel.send_ans(msgno, beep.Payload(xml))
 
--- a/bitten/trac_ext/htdocs/bitten.css
+++ b/bitten/trac_ext/htdocs/bitten.css
@@ -4,7 +4,10 @@
  background-image: url(bitten_build.png) !important
 }
 
-#content.build form.config td.active { vertical-align: bottom; }
+#content.build #prefs { line-height: 1.4em; }
+
+#content.build form.config th { text-align: left; }
+#content.build form.config fieldset { margin-bottom: 1em; }
 #content.build form.platforms ul { list-style: none; padding-left: 1em; }
 
 #content.build #builds { margin-top: 2em; }
--- a/bitten/trac_ext/templates/bitten_config.cs
+++ b/bitten/trac_ext/templates/bitten_config.cs
@@ -19,7 +19,7 @@
 
   elif:page.mode == 'edit_config' ?>
    <form class="config" method="post" action="">
-    <table><tr>
+    <table summary=""><tr>
      <td class="name"><label>Name:<br />
       <input type="text" name="name" value="<?cs var:config.name ?>" />
      </label></td>
@@ -28,14 +28,6 @@
         var:config.label ?>" />
      </label></td>
     </tr><tr>
-     <td class="active"><label><input type="checkbox" name="active"<?cs
-       if:config.active ?> checked="checked"<?cs /if ?> /> Active
-     </label></td>
-     <td class="path"><label>Repository path:<br />
-      <input type="text" name="path" size="48" value="<?cs
-        var:config.path ?>" />
-     </label></td>
-    </tr><tr>
      <td colspan="2"><fieldset class="iefix">
       <label for="description">Description (you may use <a tabindex="42" href="<?cs
         var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label>
@@ -45,6 +37,19 @@
         var:htdocs_location ?>js/wikitoolbar.js"></script>
      </fieldset></td>
     </tr></table>
+    <fieldset id="recipe">
+     <legend>Build Recipe</legend>
+     <textarea id="recipe" name="recipe" rows="8" cols="78"><?cs
+       var:config.recipe ?></textarea>
+    </fieldset>
+    <fieldset id="repos">
+     <legend>Repository Mapping</legend>
+     <table summary=""><tr>
+      <th><label for="path">Path:</label></th>
+      <td><input type="text" name="path" size="48" value="<?cs
+        var:config.path ?>" /></td>
+     </tr></table>
+    </fieldset>
     <div class="buttons">
      <input type="hidden" name="action" value="<?cs
        if:config.exists ?>edit<?cs else ?>new<?cs /if ?>" />
@@ -54,37 +59,53 @@
     </div>
    </form><?cs
    if:config.exists ?><div class="platforms">
-    <form class="platforms" method="post" action="">
-     <h2>Target Platforms</h2><?cs
-      if:len(config.platforms) ?><ul><?cs
-       each:platform = config.platforms ?>
-        <li><input type="checkbox" name="delete_platform" value="<?cs
-         var:platform.id ?>"> <a href="<?cs
-         var:platform.href ?>"><?cs var:platform.name ?></a>
-        </li><?cs
-       /each ?></ul><?cs
-      /if ?>
-     <div class="buttons">
-      <input type="submit" name="new" value="Add target platform" />
-      <input type="submit" name="delete" value="Delete selected platforms" />
-     </div>
-    </form>
-   </div><?cs
+     <form class="platforms" method="post" action="">
+      <h2>Target Platforms</h2><?cs
+       if:len(config.platforms) ?><ul><?cs
+        each:platform = config.platforms ?>
+         <li><input type="checkbox" name="delete_platform" value="<?cs
+          var:platform.id ?>"> <a href="<?cs
+          var:platform.href ?>"><?cs var:platform.name ?></a>
+         </li><?cs
+        /each ?></ul><?cs
+       /if ?>
+      <div class="buttons">
+       <input type="submit" name="new" value="Add target platform" />
+       <input type="submit" name="delete" value="Delete selected platforms" />
+      </div>
+     </form>
+    </div><?cs
    /if ?><?cs
 
-  elif:page.mode == 'view_config' ?><ul>
-   <li>Active: <?cs if:config.active ?>yes<?cs else ?>no<?cs /if ?></li>
-   <li>Path: <?cs if:config.path ?><a href="<?cs
+  elif:page.mode == 'view_config' ?><?cs
+   if:config.can_modify ?><form id="prefs" method="post" class="activation"><?cs
+    if:!config.active ?><div class="help">This build configuration is currently
+     inactive.<br /> No builds will be initiated for this configuration<br />
+     until it is activated.</div><?cs
+    else ?><div class="help">This configuration is currently active.</div><?cs
+    /if ?>
+    <div class="buttons">
+     <input type="hidden" name="action" value="edit" /><?cs
+     if:config.active ?>
+      <input type="submit" name="deactivate" value="Deactivate" /><?cs
+     else ?>
+      <input type="submit" name="activate" value="Activate" /><?cs
+     /if ?>
+    </div></form><?cs
+   /if ?>
+   <ul><li>Path: <?cs if:config.path ?><a href="<?cs
      var:config.browser_href ?>"><?cs
      var:config.path ?></a></li><?cs /if ?></ul><?cs
    if:config.description ?><div class="description"><?cs
      var:config.description ?></div><?cs
    /if ?><?cs
-   if:config.can_modify ?><div class="buttons">
-    <form method="get" action=""><div>
-     <input type="hidden" name="action" value="edit" />
-     <input type="submit" value="Edit configuration" />
-    </div></form><?cs
+   if:config.can_modify ?>
+    <div class="buttons">
+     <form method="get" action=""><div>
+      <input type="hidden" name="action" value="edit" />
+      <input type="submit" value="Edit configuration" />
+     </div></form>
+    </div><?cs
    /if ?><?cs
    if:len(config.platforms) ?>
     <table class="listing" id="builds"><thead><tr><th>Changeset</th><?cs
--- a/bitten/trac_ext/tests/web_ui.py
+++ b/bitten/trac_ext/tests/web_ui.py
@@ -109,9 +109,8 @@
         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'})
+                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
+                         'label': 'Test', 'description': 'Bla bla'})
         req.hdf['htdocs_location'] = '/htdocs'
 
         module = BuildConfigController(self.env)
@@ -119,12 +118,12 @@
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test', redirected_to[0])
 
-        build = BuildConfig.fetch(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)
+        config = BuildConfig.fetch(self.env, 'test')
+        assert config.exists
+        assert not config.active
+        self.assertEqual('Test', config.label)
+        self.assertEqual('test/trunk', config.path)
+        self.assertEqual('Bla bla', config.description)
 
     def test_new_config_cancel(self):
         PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
@@ -174,9 +173,8 @@
         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'})
+                   args={'action': 'edit', 'name': 'foo', 'path': 'test/trunk',
+                         'label': 'Test',  'description': 'Bla bla'})
         req.hdf['htdocs_location'] = '/htdocs'
 
         module = BuildConfigController(self.env)
@@ -186,12 +184,11 @@
 
         self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
 
-        build = BuildConfig.fetch(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)
+        config = BuildConfig.fetch(self.env, 'foo')
+        assert config.exists
+        self.assertEqual('Test', config.label)
+        self.assertEqual('test/trunk', config.path)
+        self.assertEqual('Bla bla', config.description)
 
     def test_edit_config_cancel(self):
         config = BuildConfig(self.env)
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -160,10 +160,18 @@
         if 'cancel' in req.args:
             req.redirect(self.env.href.build())
 
-        config = BuildConfig(self.env, name=req.args.get('name'),
+        config_name = req.args.get('name')
+
+        assert not BuildConfig.fetch(self.env, config_name), \
+            'A build configuration with the name "%s" already exists' \
+            % config_name
+
+        config = BuildConfig(self.env, name=config_name,
                              path=req.args.get('path', ''),
+                             recipe=req.args.get('recipe', ''),
+                             min_rev=req.args.get('min_rev', ''),
+                             max_rev=req.args.get('max_rev', ''),
                              label=req.args.get('label', ''),
-                             active=req.args.has_key('active'),
                              description=req.args.get('description'))
         config.insert()
 
@@ -178,13 +186,24 @@
 
         config = BuildConfig.fetch(self.env, config_name)
         assert config, 'Build configuration "%s" does not exist' % config_name
-        config.name = req.args.get('name')
-        config.active = req.args.has_key('active')
-        config.label = req.args.get('label', '')
-        config.path = req.args.get('path', '')
-        config.description = req.args.get('description', '')
+
+        if 'activate' in req.args:
+            config.active = True
+
+        elif 'deactivate' in req.args:
+            config.active = False
+
+        else:
+            # TODO: Validate recipe, repository path, etc
+            config.name = req.args.get('name')
+            config.path = req.args.get('path', '')
+            config.recipe = req.args.get('recipe', '')
+            config.min_rev = req.args.get('min_rev')
+            config.max_rev = req.args.get('max_rev')
+            config.label = req.args.get('label', '')
+            config.description = req.args.get('description', '')
+
         config.update()
-
         req.redirect(self.env.href.build(config.name))
 
     def _do_create_platform(self, req, config_name):
@@ -330,9 +349,11 @@
         if config:
             req.perm.assert_permission('BUILD_MODIFY')
             req.hdf['config'] = {
-                'name': config.name, 'label': config.label, 'path': config.path,
-                'active': config.active, 'description': config.description,
-                'exists': config.exists
+                'name': config.name, 'exists': config.exists,
+                'path': config.path, 'active': config.active,
+                'recipe': config.recipe, 'min_rev': config.min_rev,
+                'max_rev': config.max_rev, 'label': config.label,
+                'description': config.description
             }
 
             req.hdf['title'] = 'Edit Build Configuration "%s"' \
--- a/bitten/upgrades.py
+++ b/bitten/upgrades.py
@@ -42,6 +42,21 @@
                    "started,stopped) SELECT build,name,description,status,"
                    "started,stopped FROM old_step")
 
+def add_recipe_to_config(env, db):
+    from bitten.model import BuildConfig
+    cursor = db.cursor()
+
+    cursor.execute("CREATE TEMP TABLE old_config AS "
+                   "SELECT * FROM bitten_config")
+    cursor.execute("DROP TABLE bitten_config")
+    for table in BuildConfig._schema:
+        for stmt in db.to_sql(table):
+            cursor.execute(stmt)
+    cursor.execute("INSERT INTO bitten_config (name,path,active,recipe,min_rev,"
+                   "max_rev,label,description) SELECT name,path,0,'',NULL,"
+                   "NULL,label,description FROM old_config")
+
 map = {
-    2: [add_log_table]
+    2: [add_log_table],
+    3: [add_recipe_to_config]
 }
deleted file mode 100644
--- a/recipe.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0"?>
-<build description="My project"
-    xmlns:c="bitten:bitten.build.ctools"
-    xmlns:python="bitten:bitten.build.pythontools">
-
-    <step id="build" title="Let Distutils build the python code">
-        <python:distutils command="build"/>
-    </step>
-
-    <step id="test" title="Unit tests"
-          description="Run unit tests and record code coverage">
-        <python:distutils command="unittest"/>
-        <reports>
-            <python:unittest file="build/test-results.xml"/>
-            <python:trace summary="build/test-coverage.txt" 
-                coverdir="build/coverage" include="trac*" exclude="*.tests.*"/>
-        </reports>
-    </step>
-
-    <step id="lint" title="Run Pylint" onerror="ignore"
-          description="Run Pylint to check for bad style and potential errors">
-        <python:exec module="logilab.pylint.lint"
-            output="build/pylint-results.txt"
-            args="--parseable=yes --include-ids=yes
-                  --disable-msg=C0101,E0201,E0213,W0103,W0704,R0921,R0923
-                  --ignore=tests bitten"/>
-        <reports>
-            <python:pylint file="build/pylint-results.txt"/>
-        </reports>
-    </step>
-
-    <step id="dist" title="Package up distributions">
-        <python:distutils command="sdist"/>
-    </step>
-
-</build>
Copyright (C) 2012-2017 Edgewall Software