Mercurial > bitten > bitten-test
view bitten/model.py @ 565:78109beea395
Type fixes for Postgres 8.x (we noticed issues on 8.3. Newer versions of Postgres don't auto massage integers from Python to string values in the database. In Bitten, this was causing issues with the 'id' field of bitten_rule, which was actually an integer (coming from the id field of the bitten_platform table.) Without explicitly converting it to a string, you'd see the error reported in #390. Added upgrade steps, tested on Postgres8.3. Closes #390. We may want to rename this field to 'platform' later.
author | wbell |
---|---|
date | Sat, 16 May 2009 22:39:21 +0000 |
parents | 98151eb9f1a5 |
children | e198a8df81e9 |
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.db import Table, Column, Index from trac.util.text import to_unicode 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') 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) 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) 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), 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, config=None, rev=None, platform=None, slave=None, started=0, stopped=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.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') 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) 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,status) " "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", (self.config, self.rev, int(self.rev_time), self.platform, self.slave or '', self.started or 0, self.stopped or 0, self.status)) self.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,status=%s WHERE id=%s", (self.slave or '', self.started or 0, self.stopped or 0, self.status, self.id)) cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) 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,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, status=row[7]) 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): """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", 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 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 config,rev_time DESC,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' 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') 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.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.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 stopped" % 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 = '' 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 cursor = db.cursor() cursor.execute("SELECT filename FROM bitten_log WHERE id=%s", (self.id,)) log_files = cursor.fetchall() or [] cursor.execute("DELETE FROM bitten_log WHERE id=%s", (self.id,)) for log_file in log_files: log_file = self.get_log_file(log_file[0]) if os.path.exists(log_file): try: os.remove(log_file) except Exception, e: self.env.log.warning("Error removing log file %s: %s" % (log_file, e)) level_file = self.get_log_file(log_file[1]) if os.path.exists(log_file): try: os.remove(level_file) except Exception, e: self.env.log.warning("Error removing log file %s: %s" % (log_file, e)) 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 + ".levels" 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 + ".levels") 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 = 9