Mercurial > bitten > bitten-test
view bitten/model.py @ 831:cc428947a283 0.6.x
0.6dev: Merged [908] from trunk.
author | osimons |
---|---|
date | Sun, 26 Sep 2010 16:47:12 +0000 |
parents | 9a4deb714478 |
children | f4d07544722b |
line wrap: on
line source
# -*- coding: utf-8 -*- # # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> # Copyright (C) 2007 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://bitten.edgewall.org/wiki/License. """Model classes for objects persisted in the database.""" from trac.attachment import Attachment from trac.db import Table, Column, Index from trac.resource import Resource from trac.util.text import to_unicode from trac.util.datefmt import to_timestamp, utcmin, utcmax from datetime import datetime import codecs import os __docformat__ = 'restructuredtext en' class BuildConfig(object): """Representation of a build configuration.""" _schema = [ Table('bitten_config', key='name')[ 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, active=False, recipe=None, min_rev=None, max_rev=None, label=None, description=None): """Initialize a new build configuration with the specified attributes. To actually create this configuration in the database, the `insert` method needs to be called. """ 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.description = description or '' def __repr__(self): return '<%s %r>' % (type(self).__name__, self.name) exists = property(fget=lambda self: self._old_name is not None, doc='Whether this configuration exists in the database') resource = property(fget=lambda self: Resource('build', '%s' % self.name), doc='Build Config resource identification') def delete(self, db=None): """Remove a build configuration and all dependent objects from the database.""" assert self.exists, 'Cannot delete non-existing configuration' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False for platform in list(TargetPlatform.select(self.env, self.name, db=db)): platform.delete(db=db) for build in list(Build.select(self.env, config=self.name, db=db)): build.delete(db=db) # Delete attachments Attachment.delete_all(self.env, 'build', self.resource.id, db) cursor = db.cursor() cursor.execute("DELETE FROM bitten_config WHERE name=%s", (self.name,)) if handle_ta: db.commit() self._old_name = None def insert(self, db=None): """Insert a new configuration into the database.""" assert not self.exists, 'Cannot insert existing configuration' assert self.name, 'Configuration requires a name' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False cursor = db.cursor() 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): """Save changes to an existing build configuration.""" assert self.exists, 'Cannot update a non-existing configuration' assert self.name, 'Configuration 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_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 self.name != self._old_name: cursor.execute("UPDATE bitten_platform SET config=%s " "WHERE config=%s", (self.name, self._old_name)) cursor.execute("UPDATE bitten_build SET config=%s " "WHERE config=%s", (self.name, self._old_name)) if handle_ta: db.commit() self._old_name = self.name def fetch(cls, env, name, db=None): """Retrieve an existing build configuration from the database by name. """ if not db: db = env.get_db_cnx() cursor = db.cursor() 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 config = BuildConfig(env) config.name = config._old_name = name config.path = row[0] or '' config.active = bool(row[1]) config.recipe = row[2] or '' config.min_rev = row[3] or None config.max_rev = row[4] or None config.label = row[5] or '' config.description = row[6] or '' return config fetch = classmethod(fetch) def select(cls, env, include_inactive=False, db=None): """Retrieve existing build configurations from the database that match the specified criteria. """ if not db: db = env.get_db_cnx() cursor = db.cursor() if include_inactive: 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,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 '', 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 select = classmethod(select) def min_rev_time(self, env): """Returns the time of the minimum revision being built for this configuration. Returns utcmin if not specified. """ repos = env.get_repository() assert repos, 'No "(default)" Repository: Add a repository or alias ' \ 'named "(default)" to Trac.' min_time = utcmin if self.min_rev: min_time = repos.get_changeset(self.min_rev).date if isinstance(min_time, datetime): # Trac>=0.11 min_time = to_timestamp(min_time) return min_time def max_rev_time(self, env): """Returns the time of the maximum revision being built for this configuration. Returns utcmax if not specified. """ repos = env.get_repository() assert repos, 'No "(default)" Repository: Add a repository or alias ' \ 'named "(default)" to Trac.' max_time = utcmax if self.max_rev: max_time = repos.get_changeset(self.max_rev).date if isinstance(max_time, datetime): # Trac>=0.11 max_time = to_timestamp(max_time) return max_time 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', type='int'), Column('propname'), Column('pattern'), Column('orderno', type='int') ] ] def __init__(self, env, config=None, name=None): """Initialize a new target platform with the specified attributes. To actually create this platform in the database, the `insert` method needs to be called. """ self.env = env self.id = None self.config = config self.name = name self.rules = [] def __repr__(self): return '<%s %r>' % (type(self).__name__, self.id) exists = property(fget=lambda self: self.id is not None, doc='Whether this target platform exists in the database') def delete(self, db=None): """Remove the target platform from the database.""" if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False for build in Build.select(self.env, platform=self.id, status=Build.PENDING, db=db): build.delete() cursor = db.cursor() cursor.execute("DELETE FROM bitten_rule WHERE id=%s", (self.id,)) cursor.execute("DELETE FROM bitten_platform WHERE id=%s", (self.id,)) if handle_ta: db.commit() def insert(self, db=None): """Insert a new target platform into the database.""" 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(cursor, 'bitten_platform') if self.rules: 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): """Save changes to an existing target platform.""" 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,)) if self.rules: 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 fetch(cls, env, id, db=None): """Retrieve an existing target platform from the database by ID.""" if not db: db = 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: return None platform = TargetPlatform(env, config=row[0], name=row[1]) platform.id = id cursor.execute("SELECT propname,pattern FROM bitten_rule " "WHERE id=%s ORDER BY orderno", (id,)) for propname, pattern in cursor: platform.rules.append((propname, pattern)) return platform fetch = classmethod(fetch) def select(cls, env, config=None, db=None): """Retrieve existing target platforms from the database that match the specified criteria. """ 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.fetch(env, id) select = classmethod(select) class Build(object): """Representation of a build.""" _schema = [ 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) ], 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' TOKEN = 'token' def __init__(self, env, config=None, rev=None, platform=None, slave=None, 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 to be called. """ self.env = env self.id = None self.config = config self.rev = rev and str(rev) or None self.platform = platform 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 = {} def __repr__(self): return '<%s %r>' % (type(self).__name__, self.id) exists = property(fget=lambda self: self.id is not None, doc='Whether this build exists in the database') completed = property(fget=lambda self: self.status != Build.IN_PROGRESS, doc='Whether the build has been completed') successful = property(fget=lambda self: self.status == Build.SUCCESS, doc='Whether the build was successful') resource = property(fget=lambda self: Resource('build', '%s/%s' % (self.config, self.id)), doc='Build resource identification') def delete(self, db=None): """Remove the build from the database.""" assert self.exists, 'Cannot delete a non-existing build' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False for step in list(BuildStep.select(self.env, build=self.id)): step.delete(db=db) # Delete attachments Attachment.delete_all(self.env, 'build', self.resource.id, db) cursor = db.cursor() cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) cursor.execute("DELETE FROM bitten_build WHERE id=%s", (self.id,)) if handle_ta: db.commit() def insert(self, db=None): """Insert a new build into the database.""" assert not self.exists, 'Cannot insert an existing build' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False 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,platform," "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.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)", [(self.id, name, value) for name, value in self.slave_info.items()]) if handle_ta: db.commit() def update(self, db=None): """Save changes to an existing build.""" assert self.exists, 'Cannot update a non-existing build' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False assert self.config and self.rev 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("UPDATE bitten_build SET slave=%s,started=%s," "stopped=%s,last_activity=%s,status=%s WHERE id=%s", (self.slave or '', self.started or 0, 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)", [(self.id, name, value) for name, value in self.slave_info.items()]) if handle_ta: db.commit() def fetch(cls, env, id, db=None): """Retrieve an existing build from the database by ID.""" if not db: db = env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT config,rev,rev_time,platform,slave,started," "stopped,last_activity,status FROM bitten_build WHERE " "id=%s", (id,)) row = cursor.fetchone() if not row: return None 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, 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,)) for propname, propvalue in cursor: build.slave_info[propname] = propvalue return build fetch = classmethod(fetch) def select(cls, env, config=None, rev=None, platform=None, slave=None, status=None, db=None, min_rev_time=None, max_rev_time=None): """Retrieve existing builds from the database that match the specified criteria. """ if not db: db = env.get_db_cnx() where_clauses = [] if config is not None: where_clauses.append(("config=%s", config)) if rev is not None: where_clauses.append(("rev=%s", str(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: where_clauses.append(("status=%s", status)) if min_rev_time is not None: where_clauses.append(("rev_time>=%s", min_rev_time)) if max_rev_time is not None: where_clauses.append(("rev_time<=%s", max_rev_time)) 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_build %s " "ORDER BY rev_time DESC,config,slave" % where, [wc[1] for wc in where_clauses]) for (id,) in cursor: yield Build.fetch(env, id) select = classmethod(select) class BuildStep(object): """Represents an individual step of an executed build.""" _schema = [ Table('bitten_step', key=('build', 'name'))[ Column('build', type='int'), Column('name'), Column('description'), Column('status', size=1), Column('started', type='int'), Column('stopped', type='int') ], Table('bitten_error', key=('build', 'step', 'orderno'))[ Column('build', type='int'), Column('step'), Column('message'), Column('orderno', type='int') ] ] # Step status codes SUCCESS = 'S' IN_PROGRESS = 'I' FAILURE = 'F' def __init__(self, env, build=None, name=None, description=None, status=None, started=None, stopped=None): """Initialize a new build step with the specified attributes. To actually create this build step in the database, the `insert` method needs to be called. """ self.env = env self.build = build self.name = name self.description = description self.status = status self.started = started self.stopped = stopped self.errors = [] self._exists = False exists = property(fget=lambda self: self._exists, 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: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False for log in list(BuildLog.select(self.env, build=self.build, step=self.name, db=db)): log.delete(db=db) for report in list(Report.select(self.env, build=self.build, step=self.name, db=db)): report.delete(db=db) cursor = db.cursor() cursor.execute("DELETE FROM bitten_step WHERE build=%s AND name=%s", (self.build, self.name)) cursor.execute("DELETE FROM bitten_error WHERE build=%s AND step=%s", (self.build, self.name)) if handle_ta: db.commit() self._exists = False def insert(self, db=None): """Insert a new build step into the database.""" if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False assert self.build and self.name assert self.status in (self.SUCCESS, self.IN_PROGRESS, self.FAILURE) cursor = db.cursor() cursor.execute("INSERT INTO bitten_step (build,name,description,status," "started,stopped) VALUES (%s,%s,%s,%s,%s,%s)", (self.build, self.name, self.description or '', self.status, self.started or 0, self.stopped or 0)) if self.errors: cursor.executemany("INSERT INTO bitten_error (build,step,message," "orderno) VALUES (%s,%s,%s,%s)", [(self.build, self.name, message, idx) for idx, message in enumerate(self.errors)]) if handle_ta: db.commit() self._exists = True def fetch(cls, env, build, name, db=None): """Retrieve an existing build from the database by build ID and step name.""" if not db: db = env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT description,status,started,stopped " "FROM bitten_step WHERE build=%s AND name=%s", (build, name)) row = cursor.fetchone() if not row: return None step = BuildStep(env, build, name, row[0] or '', row[1], row[2] and int(row[2]), row[3] and int(row[3])) step._exists = True cursor.execute("SELECT message FROM bitten_error WHERE build=%s " "AND step=%s ORDER BY orderno", (build, name)) for row in cursor: step.errors.append(row[0] or '') return step fetch = classmethod(fetch) def select(cls, env, build=None, name=None, status=None, db=None): """Retrieve existing build steps from the database that match the specified criteria. """ if not db: db = env.get_db_cnx() assert status in (None, BuildStep.SUCCESS, BuildStep.IN_PROGRESS, BuildStep.FAILURE) where_clauses = [] if build is not None: where_clauses.append(("build=%s", build)) if name is not None: where_clauses.append(("name=%s", name)) if status is not None: where_clauses.append(("status=%s", status)) if where_clauses: where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) else: where = "" cursor = db.cursor() 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) select = classmethod(select) class BuildLog(object): """Represents a build log.""" _schema = [ Table('bitten_log', key='id')[ Column('id', auto_increment=True), Column('build', type='int'), Column('step'), Column('generator'), Column('orderno', type='int'), Column('filename'), Index(['build', 'step']) ], ] # Message levels DEBUG = 'D' INFO = 'I' WARNING = 'W' ERROR = 'E' UNKNOWN = '' LEVELS_SUFFIX = '.levels' def __init__(self, env, build=None, step=None, generator=None, orderno=None, filename=None): """Initialize a new build log with the specified attributes. To actually create this build log in the database, the `insert` method needs to be called. """ self.env = env self.id = None self.build = build self.step = step self.generator = generator or '' self.orderno = orderno and int(orderno) or 0 self.filename = filename or None self.messages = [] self.logs_dir = env.config.get('bitten', 'logs_dir', 'log/bitten') if not os.path.isabs(self.logs_dir): self.logs_dir = os.path.join(env.path, self.logs_dir) if not os.path.exists(self.logs_dir): os.makedirs(self.logs_dir) exists = property(fget=lambda self: self.id is not None, doc='Whether this build log exists in the database') def get_log_file(self, filename): """Returns the full path to the log file""" if filename != os.path.basename(filename): raise ValueError("Filename may not contain path: %s" % (filename,)) return os.path.join(self.logs_dir, filename) def delete(self, db=None): """Remove the build log from the database.""" assert self.exists, 'Cannot delete a non-existing build log' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False if self.filename: log_file = self.get_log_file(self.filename) if os.path.exists(log_file): try: self.env.log.debug("Deleting log file: %s" % log_file) os.remove(log_file) except Exception, e: self.env.log.warning("Error removing log file %s: %s" % (log_file, e)) level_file = log_file + self.LEVELS_SUFFIX if os.path.exists(level_file): try: self.env.log.debug("Deleting level file: %s" % level_file) os.remove(level_file) except Exception, e: self.env.log.warning("Error removing level file %s: %s" \ % (level_file, e)) cursor = db.cursor() cursor.execute("DELETE FROM bitten_log WHERE id=%s", (self.id,)) if handle_ta: db.commit() self.id = None def insert(self, db=None): """Insert a new build log into the database.""" if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False assert self.build and self.step cursor = db.cursor() cursor.execute("INSERT INTO bitten_log (build,step,generator,orderno) " "VALUES (%s,%s,%s,%s)", (self.build, self.step, self.generator, self.orderno)) id = db.get_last_id(cursor, 'bitten_log') log_file = "%s.log" % (id,) cursor.execute("UPDATE bitten_log SET filename=%s WHERE id=%s", (log_file, id)) if self.messages: log_file_name = self.get_log_file(log_file) level_file_name = log_file_name + self.LEVELS_SUFFIX codecs.open(log_file_name, "wb", "UTF-8").writelines([to_unicode(msg[1]+"\n") for msg in self.messages]) codecs.open(level_file_name, "wb", "UTF-8").writelines([to_unicode(msg[0]+"\n") for msg in self.messages]) if handle_ta: db.commit() self.id = id def fetch(cls, env, id, db=None): """Retrieve an existing build from the database by ID.""" if not db: db = env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT build,step,generator,orderno,filename FROM bitten_log " "WHERE id=%s", (id,)) row = cursor.fetchone() if not row: return None log = BuildLog(env, int(row[0]), row[1], row[2], row[3], row[4]) log.id = id if log.filename: log_filename = log.get_log_file(log.filename) if os.path.exists(log_filename): log_lines = codecs.open(log_filename, "rb", "UTF-8").readlines() else: log_lines = [] level_filename = log.get_log_file(log.filename + cls.LEVELS_SUFFIX) if os.path.exists(level_filename): log_levels = dict(enumerate(codecs.open(level_filename, "rb", "UTF-8").readlines())) else: log_levels = {} log.messages = [(log_levels.get(line_num, BuildLog.UNKNOWN).rstrip("\n"), line.rstrip("\n")) for line_num, line in enumerate(log_lines)] else: log.messages = [] return log fetch = classmethod(fetch) def select(cls, env, build=None, step=None, generator=None, db=None): """Retrieve existing build logs from the database that match the specified criteria. """ if not db: db = env.get_db_cnx() where_clauses = [] if build is not None: where_clauses.append(("build=%s", build)) if step is not None: where_clauses.append(("step=%s", step)) if generator is not None: where_clauses.append(("generator=%s", generator)) 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_log %s ORDER BY orderno" % where, [wc[1] for wc in where_clauses]) for (id, ) in cursor: yield BuildLog.fetch(env, id, db=db) select = classmethod(select) class Report(object): """Represents a generated report.""" _schema = [ Table('bitten_report', key='id')[ Column('id', auto_increment=True), Column('build', type='int'), Column('step'), Column('category'), Column('generator'), Index(['build', 'step', 'category']) ], Table('bitten_report_item', key=('report', 'item', 'name'))[ Column('report', type='int'), Column('item', type='int'), Column('name'), Column('value') ] ] def __init__(self, env, build=None, step=None, category=None, generator=None): """Initialize a new report with the specified attributes. To actually create this build log in the database, the `insert` method needs to be called. """ self.env = env self.id = None self.build = build self.step = step self.category = category self.generator = generator or '' self.items = [] exists = property(fget=lambda self: self.id is not None, doc='Whether this report exists in the database') def delete(self, db=None): """Remove the report from the database.""" assert self.exists, 'Cannot delete a non-existing report' if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False cursor = db.cursor() cursor.execute("DELETE FROM bitten_report_item WHERE report=%s", (self.id,)) cursor.execute("DELETE FROM bitten_report WHERE id=%s", (self.id,)) if handle_ta: db.commit() self.id = None def insert(self, db=None): """Insert a new build log into the database.""" if not db: db = self.env.get_db_cnx() handle_ta = True else: handle_ta = False assert self.build and self.step and self.category # Enforce uniqueness of build-step-category. # This should be done by the database, but the DB schema helpers in Trac # currently don't support UNIQUE() constraints assert not list(Report.select(self.env, build=self.build, step=self.step, category=self.category, db=db)), 'Report already exists' cursor = db.cursor() cursor.execute("INSERT INTO bitten_report " "(build,step,category,generator) VALUES (%s,%s,%s,%s)", (self.build, self.step, self.category, self.generator)) id = db.get_last_id(cursor, 'bitten_report') for idx, item in enumerate([item for item in self.items if item]): cursor.executemany("INSERT INTO bitten_report_item " "(report,item,name,value) VALUES (%s,%s,%s,%s)", [(id, idx, key, value) for key, value in item.items()]) if handle_ta: db.commit() self.id = id def fetch(cls, env, id, db=None): """Retrieve an existing build from the database by ID.""" if not db: db = env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT build,step,category,generator " "FROM bitten_report WHERE id=%s", (id,)) row = cursor.fetchone() if not row: return None report = Report(env, int(row[0]), row[1], row[2] or '', row[3] or '') report.id = id cursor.execute("SELECT item,name,value FROM bitten_report_item " "WHERE report=%s ORDER BY item", (id,)) items = {} for item, name, value in cursor: items.setdefault(item, {})[name] = value report.items = items.values() return report fetch = classmethod(fetch) def select(cls, env, config=None, build=None, step=None, category=None, db=None): """Retrieve existing reports from the database that match the specified criteria. """ where_clauses = [] joins = [] if config is not None: where_clauses.append(("config=%s", config)) joins.append("INNER JOIN bitten_build ON (bitten_build.id=build)") if build is not None: where_clauses.append(("build=%s", build)) if step is not None: where_clauses.append(("step=%s", step)) if category is not None: where_clauses.append(("category=%s", category)) if where_clauses: where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) else: where = "" if not db: db = env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT bitten_report.id FROM bitten_report %s %s " "ORDER BY category" % (' '.join(joins), where), [wc[1] for wc in where_clauses]) for (id, ) in cursor: yield Report.fetch(env, id, db=db) select = classmethod(select) schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \ BuildStep._schema + BuildLog._schema + Report._schema schema_version = 12