| 1 | # -*- coding: utf-8 -*- |
---|
| 2 | # |
---|
| 3 | # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> |
---|
| 4 | # Copyright (C) 2007-2010 Edgewall Software |
---|
| 5 | # All rights reserved. |
---|
| 6 | # |
---|
| 7 | # This software is licensed as described in the file COPYING, which |
---|
| 8 | # you should have received as part of this distribution. The terms |
---|
| 9 | # are also available at http://bitten.edgewall.org/wiki/License. |
---|
| 10 | |
---|
| 11 | """Model classes for objects persisted in the database.""" |
---|
| 12 | |
---|
| 13 | from trac.attachment import Attachment |
---|
| 14 | from trac.db import Table, Column, Index |
---|
| 15 | from trac.resource import Resource |
---|
| 16 | from trac.util.text import to_unicode |
---|
| 17 | from trac.util.datefmt import to_timestamp, utcmin, utcmax |
---|
| 18 | from datetime import datetime |
---|
| 19 | import codecs |
---|
| 20 | import os |
---|
| 21 | |
---|
| 22 | __docformat__ = 'restructuredtext en' |
---|
| 23 | |
---|
| 24 | |
---|
| 25 | class BuildConfig(object): |
---|
| 26 | """Representation of a build configuration.""" |
---|
| 27 | |
---|
| 28 | _schema = [ |
---|
| 29 | Table('bitten_config', key='name')[ |
---|
| 30 | Column('name'), Column('path'), Column('active', type='int'), |
---|
| 31 | Column('recipe'), Column('min_rev'), Column('max_rev'), |
---|
| 32 | Column('label'), Column('description') |
---|
| 33 | ] |
---|
| 34 | ] |
---|
| 35 | |
---|
| 36 | def __init__(self, env, name=None, path=None, active=False, recipe=None, |
---|
| 37 | min_rev=None, max_rev=None, label=None, description=None): |
---|
| 38 | """Initialize a new build configuration with the specified attributes. |
---|
| 39 | |
---|
| 40 | To actually create this configuration in the database, the `insert` |
---|
| 41 | method needs to be called. |
---|
| 42 | """ |
---|
| 43 | self.env = env |
---|
| 44 | self._old_name = None |
---|
| 45 | self.name = name |
---|
| 46 | self.path = path or '' |
---|
| 47 | self.active = bool(active) |
---|
| 48 | self.recipe = recipe or '' |
---|
| 49 | self.min_rev = min_rev or None |
---|
| 50 | self.max_rev = max_rev or None |
---|
| 51 | self.label = label or '' |
---|
| 52 | self.description = description or '' |
---|
| 53 | |
---|
| 54 | def __repr__(self): |
---|
| 55 | return '<%s %r>' % (type(self).__name__, self.name) |
---|
| 56 | |
---|
| 57 | exists = property(fget=lambda self: self._old_name is not None, |
---|
| 58 | doc='Whether this configuration exists in the database') |
---|
| 59 | resource = property(fget=lambda self: Resource('build', '%s' % self.name), |
---|
| 60 | doc='Build Config resource identification') |
---|
| 61 | |
---|
| 62 | def delete(self, db=None): |
---|
| 63 | """Remove a build configuration and all dependent objects from the |
---|
| 64 | database.""" |
---|
| 65 | assert self.exists, 'Cannot delete non-existing configuration' |
---|
| 66 | if not db: |
---|
| 67 | db = self.env.get_db_cnx() |
---|
| 68 | handle_ta = True |
---|
| 69 | else: |
---|
| 70 | handle_ta = False |
---|
| 71 | |
---|
| 72 | for platform in list(TargetPlatform.select(self.env, self.name, db=db)): |
---|
| 73 | platform.delete(db=db) |
---|
| 74 | |
---|
| 75 | for build in list(Build.select(self.env, config=self.name, db=db)): |
---|
| 76 | build.delete(db=db) |
---|
| 77 | |
---|
| 78 | # Delete attachments |
---|
| 79 | Attachment.delete_all(self.env, 'build', self.resource.id, db) |
---|
| 80 | |
---|
| 81 | cursor = db.cursor() |
---|
| 82 | cursor.execute("DELETE FROM bitten_config WHERE name=%s", (self.name,)) |
---|
| 83 | |
---|
| 84 | if handle_ta: |
---|
| 85 | db.commit() |
---|
| 86 | self._old_name = None |
---|
| 87 | |
---|
| 88 | def insert(self, db=None): |
---|
| 89 | """Insert a new configuration into the database.""" |
---|
| 90 | assert not self.exists, 'Cannot insert existing configuration' |
---|
| 91 | assert self.name, 'Configuration requires a name' |
---|
| 92 | if not db: |
---|
| 93 | db = self.env.get_db_cnx() |
---|
| 94 | handle_ta = True |
---|
| 95 | else: |
---|
| 96 | handle_ta = False |
---|
| 97 | |
---|
| 98 | cursor = db.cursor() |
---|
| 99 | cursor.execute("INSERT INTO bitten_config (name,path,active," |
---|
| 100 | "recipe,min_rev,max_rev,label,description) " |
---|
| 101 | "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", |
---|
| 102 | (self.name, self.path, int(self.active or 0), |
---|
| 103 | self.recipe or '', self.min_rev, self.max_rev, |
---|
| 104 | self.label or '', self.description or '')) |
---|
| 105 | |
---|
| 106 | if handle_ta: |
---|
| 107 | db.commit() |
---|
| 108 | self._old_name = self.name |
---|
| 109 | |
---|
| 110 | def update(self, db=None): |
---|
| 111 | """Save changes to an existing build configuration.""" |
---|
| 112 | assert self.exists, 'Cannot update a non-existing configuration' |
---|
| 113 | assert self.name, 'Configuration requires a name' |
---|
| 114 | if not db: |
---|
| 115 | db = self.env.get_db_cnx() |
---|
| 116 | handle_ta = True |
---|
| 117 | else: |
---|
| 118 | handle_ta = False |
---|
| 119 | |
---|
| 120 | cursor = db.cursor() |
---|
| 121 | cursor.execute("UPDATE bitten_config SET name=%s,path=%s,active=%s," |
---|
| 122 | "recipe=%s,min_rev=%s,max_rev=%s,label=%s," |
---|
| 123 | "description=%s WHERE name=%s", |
---|
| 124 | (self.name, self.path, int(self.active or 0), |
---|
| 125 | self.recipe, self.min_rev, self.max_rev, |
---|
| 126 | self.label, self.description, self._old_name)) |
---|
| 127 | if self.name != self._old_name: |
---|
| 128 | cursor.execute("UPDATE bitten_platform SET config=%s " |
---|
| 129 | "WHERE config=%s", (self.name, self._old_name)) |
---|
| 130 | cursor.execute("UPDATE bitten_build SET config=%s " |
---|
| 131 | "WHERE config=%s", (self.name, self._old_name)) |
---|
| 132 | |
---|
| 133 | if handle_ta: |
---|
| 134 | db.commit() |
---|
| 135 | self._old_name = self.name |
---|
| 136 | |
---|
| 137 | def fetch(cls, env, name, db=None): |
---|
| 138 | """Retrieve an existing build configuration from the database by |
---|
| 139 | name. |
---|
| 140 | """ |
---|
| 141 | if not db: |
---|
| 142 | db = env.get_db_cnx() |
---|
| 143 | |
---|
| 144 | cursor = db.cursor() |
---|
| 145 | cursor.execute("SELECT path,active,recipe,min_rev,max_rev,label," |
---|
| 146 | "description FROM bitten_config WHERE name=%s", (name,)) |
---|
| 147 | row = cursor.fetchone() |
---|
| 148 | if not row: |
---|
| 149 | return None |
---|
| 150 | |
---|
| 151 | config = BuildConfig(env) |
---|
| 152 | config.name = config._old_name = name |
---|
| 153 | config.path = row[0] or '' |
---|
| 154 | config.active = bool(row[1]) |
---|
| 155 | config.recipe = row[2] or '' |
---|
| 156 | config.min_rev = row[3] or None |
---|
| 157 | config.max_rev = row[4] or None |
---|
| 158 | config.label = row[5] or '' |
---|
| 159 | config.description = row[6] or '' |
---|
| 160 | return config |
---|
| 161 | |
---|
| 162 | fetch = classmethod(fetch) |
---|
| 163 | |
---|
| 164 | def select(cls, env, include_inactive=False, db=None): |
---|
| 165 | """Retrieve existing build configurations from the database that match |
---|
| 166 | the specified criteria. |
---|
| 167 | """ |
---|
| 168 | if not db: |
---|
| 169 | db = env.get_db_cnx() |
---|
| 170 | |
---|
| 171 | cursor = db.cursor() |
---|
| 172 | if include_inactive: |
---|
| 173 | cursor.execute("SELECT name,path,active,recipe,min_rev,max_rev," |
---|
| 174 | "label,description FROM bitten_config ORDER BY name") |
---|
| 175 | else: |
---|
| 176 | cursor.execute("SELECT name,path,active,recipe,min_rev,max_rev," |
---|
| 177 | "label,description FROM bitten_config " |
---|
| 178 | "WHERE active=1 ORDER BY name") |
---|
| 179 | for name, path, active, recipe, min_rev, max_rev, label, description \ |
---|
| 180 | in cursor: |
---|
| 181 | config = BuildConfig(env, name=name, path=path or '', |
---|
| 182 | active=bool(active), recipe=recipe or '', |
---|
| 183 | min_rev=min_rev or None, |
---|
| 184 | max_rev=max_rev or None, label=label or '', |
---|
| 185 | description=description or '') |
---|
| 186 | config._old_name = name |
---|
| 187 | yield config |
---|
| 188 | |
---|
| 189 | select = classmethod(select) |
---|
| 190 | |
---|
| 191 | def min_rev_time(self, env): |
---|
| 192 | """Returns the time of the minimum revision being built for this |
---|
| 193 | configuration. Returns utcmin if not specified. |
---|
| 194 | """ |
---|
| 195 | repos = env.get_repository() |
---|
| 196 | assert repos, 'No "(default)" Repository: Add a repository or alias ' \ |
---|
| 197 | 'named "(default)" to Trac.' |
---|
| 198 | |
---|
| 199 | min_time = utcmin |
---|
| 200 | if self.min_rev: |
---|
| 201 | min_time = repos.get_changeset(self.min_rev).date |
---|
| 202 | |
---|
| 203 | if isinstance(min_time, datetime): # Trac>=0.11 |
---|
| 204 | min_time = to_timestamp(min_time) |
---|
| 205 | |
---|
| 206 | return min_time |
---|
| 207 | |
---|
| 208 | def max_rev_time(self, env): |
---|
| 209 | """Returns the time of the maximum revision being built for this |
---|
| 210 | configuration. Returns utcmax if not specified. |
---|
| 211 | """ |
---|
| 212 | repos = env.get_repository() |
---|
| 213 | assert repos, 'No "(default)" Repository: Add a repository or alias ' \ |
---|
| 214 | 'named "(default)" to Trac.' |
---|
| 215 | |
---|
| 216 | max_time = utcmax |
---|
| 217 | if self.max_rev: |
---|
| 218 | max_time = repos.get_changeset(self.max_rev).date |
---|
| 219 | |
---|
| 220 | if isinstance(max_time, datetime): # Trac>=0.11 |
---|
| 221 | max_time = to_timestamp(max_time) |
---|
| 222 | |
---|
| 223 | return max_time |
---|
| 224 | |
---|
| 225 | |
---|
| 226 | class TargetPlatform(object): |
---|
| 227 | """Target platform for a build configuration.""" |
---|
| 228 | |
---|
| 229 | _schema = [ |
---|
| 230 | Table('bitten_platform', key='id')[ |
---|
| 231 | Column('id', auto_increment=True), Column('config'), Column('name') |
---|
| 232 | ], |
---|
| 233 | Table('bitten_rule', key=('id', 'propname'))[ |
---|
| 234 | Column('id', type='int'), Column('propname'), Column('pattern'), |
---|
| 235 | Column('orderno', type='int') |
---|
| 236 | ] |
---|
| 237 | ] |
---|
| 238 | |
---|
| 239 | def __init__(self, env, config=None, name=None): |
---|
| 240 | """Initialize a new target platform with the specified attributes. |
---|
| 241 | |
---|
| 242 | To actually create this platform in the database, the `insert` method |
---|
| 243 | needs to be called. |
---|
| 244 | """ |
---|
| 245 | self.env = env |
---|
| 246 | self.id = None |
---|
| 247 | self.config = config |
---|
| 248 | self.name = name |
---|
| 249 | self.rules = [] |
---|
| 250 | |
---|
| 251 | def __repr__(self): |
---|
| 252 | return '<%s %r>' % (type(self).__name__, self.id) |
---|
| 253 | |
---|
| 254 | exists = property(fget=lambda self: self.id is not None, |
---|
| 255 | doc='Whether this target platform exists in the database') |
---|
| 256 | |
---|
| 257 | def delete(self, db=None): |
---|
| 258 | """Remove the target platform from the database.""" |
---|
| 259 | if not db: |
---|
| 260 | db = self.env.get_db_cnx() |
---|
| 261 | handle_ta = True |
---|
| 262 | else: |
---|
| 263 | handle_ta = False |
---|
| 264 | |
---|
| 265 | for build in Build.select(self.env, platform=self.id, status=Build.PENDING, db=db): |
---|
| 266 | build.delete() |
---|
| 267 | |
---|
| 268 | cursor = db.cursor() |
---|
| 269 | cursor.execute("DELETE FROM bitten_rule WHERE id=%s", (self.id,)) |
---|
| 270 | cursor.execute("DELETE FROM bitten_platform WHERE id=%s", (self.id,)) |
---|
| 271 | if handle_ta: |
---|
| 272 | db.commit() |
---|
| 273 | |
---|
| 274 | def insert(self, db=None): |
---|
| 275 | """Insert a new target platform into the database.""" |
---|
| 276 | if not db: |
---|
| 277 | db = self.env.get_db_cnx() |
---|
| 278 | handle_ta = True |
---|
| 279 | else: |
---|
| 280 | handle_ta = False |
---|
| 281 | |
---|
| 282 | assert not self.exists, 'Cannot insert existing target platform' |
---|
| 283 | assert self.config, 'Target platform needs to be associated with a ' \ |
---|
| 284 | 'configuration' |
---|
| 285 | assert self.name, 'Target platform requires a name' |
---|
| 286 | |
---|
| 287 | cursor = db.cursor() |
---|
| 288 | cursor.execute("INSERT INTO bitten_platform (config,name) " |
---|
| 289 | "VALUES (%s,%s)", (self.config, self.name)) |
---|
| 290 | self.id = db.get_last_id(cursor, 'bitten_platform') |
---|
| 291 | if self.rules: |
---|
| 292 | cursor.executemany("INSERT INTO bitten_rule VALUES (%s,%s,%s,%s)", |
---|
| 293 | [(self.id, propname, pattern, idx) |
---|
| 294 | for idx, (propname, pattern) |
---|
| 295 | in enumerate(self.rules)]) |
---|
| 296 | |
---|
| 297 | if handle_ta: |
---|
| 298 | db.commit() |
---|
| 299 | |
---|
| 300 | def update(self, db=None): |
---|
| 301 | """Save changes to an existing target platform.""" |
---|
| 302 | assert self.exists, 'Cannot update a non-existing platform' |
---|
| 303 | assert self.config, 'Target platform needs to be associated with a ' \ |
---|
| 304 | 'configuration' |
---|
| 305 | assert self.name, 'Target platform requires a name' |
---|
| 306 | if not db: |
---|
| 307 | db = self.env.get_db_cnx() |
---|
| 308 | handle_ta = True |
---|
| 309 | else: |
---|
| 310 | handle_ta = False |
---|
| 311 | |
---|
| 312 | cursor = db.cursor() |
---|
| 313 | cursor.execute("UPDATE bitten_platform SET name=%s WHERE id=%s", |
---|
| 314 | (self.name, self.id)) |
---|
| 315 | cursor.execute("DELETE FROM bitten_rule WHERE id=%s", (self.id,)) |
---|
| 316 | if self.rules: |
---|
| 317 | cursor.executemany("INSERT INTO bitten_rule VALUES (%s,%s,%s,%s)", |
---|
| 318 | [(self.id, propname, pattern, idx) |
---|
| 319 | for idx, (propname, pattern) |
---|
| 320 | in enumerate(self.rules)]) |
---|
| 321 | |
---|
| 322 | if handle_ta: |
---|
| 323 | db.commit() |
---|
| 324 | |
---|
| 325 | def fetch(cls, env, id, db=None): |
---|
| 326 | """Retrieve an existing target platform from the database by ID.""" |
---|
| 327 | if not db: |
---|
| 328 | db = env.get_db_cnx() |
---|
| 329 | |
---|
| 330 | cursor = db.cursor() |
---|
| 331 | cursor.execute("SELECT config,name FROM bitten_platform " |
---|
| 332 | "WHERE id=%s", (id,)) |
---|
| 333 | row = cursor.fetchone() |
---|
| 334 | if not row: |
---|
| 335 | return None |
---|
| 336 | |
---|
| 337 | platform = TargetPlatform(env, config=row[0], name=row[1]) |
---|
| 338 | platform.id = id |
---|
| 339 | cursor.execute("SELECT propname,pattern FROM bitten_rule " |
---|
| 340 | "WHERE id=%s ORDER BY orderno", (id,)) |
---|
| 341 | for propname, pattern in cursor: |
---|
| 342 | platform.rules.append((propname, pattern)) |
---|
| 343 | return platform |
---|
| 344 | |
---|
| 345 | fetch = classmethod(fetch) |
---|
| 346 | |
---|
| 347 | def select(cls, env, config=None, db=None): |
---|
| 348 | """Retrieve existing target platforms from the database that match the |
---|
| 349 | specified criteria. |
---|
| 350 | """ |
---|
| 351 | if not db: |
---|
| 352 | db = env.get_db_cnx() |
---|
| 353 | |
---|
| 354 | where_clauses = [] |
---|
| 355 | if config is not None: |
---|
| 356 | where_clauses.append(("config=%s", config)) |
---|
| 357 | if where_clauses: |
---|
| 358 | where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) |
---|
| 359 | else: |
---|
| 360 | where = "" |
---|
| 361 | |
---|
| 362 | cursor = db.cursor() |
---|
| 363 | cursor.execute("SELECT id FROM bitten_platform %s ORDER BY name" |
---|
| 364 | % where, [wc[1] for wc in where_clauses]) |
---|
| 365 | for (id,) in cursor: |
---|
| 366 | yield TargetPlatform.fetch(env, id) |
---|
| 367 | |
---|
| 368 | select = classmethod(select) |
---|
| 369 | |
---|
| 370 | |
---|
| 371 | class Build(object): |
---|
| 372 | """Representation of a build.""" |
---|
| 373 | |
---|
| 374 | _schema = [ |
---|
| 375 | Table('bitten_build', key='id')[ |
---|
| 376 | Column('id', auto_increment=True), Column('config'), Column('rev'), |
---|
| 377 | Column('rev_time', type='int'), Column('platform', type='int'), |
---|
| 378 | Column('slave'), Column('started', type='int'), |
---|
| 379 | Column('stopped', type='int'), Column('status', size=1), |
---|
| 380 | Column('last_activity', type='int'), |
---|
| 381 | Index(['config', 'rev', 'platform'], unique=True) |
---|
| 382 | ], |
---|
| 383 | Table('bitten_slave', key=('build', 'propname'))[ |
---|
| 384 | Column('build', type='int'), Column('propname'), Column('propvalue') |
---|
| 385 | ] |
---|
| 386 | ] |
---|
| 387 | |
---|
| 388 | # Build status codes |
---|
| 389 | PENDING = 'P' |
---|
| 390 | IN_PROGRESS = 'I' |
---|
| 391 | SUCCESS = 'S' |
---|
| 392 | FAILURE = 'F' |
---|
| 393 | |
---|
| 394 | # Standard slave properties |
---|
| 395 | IP_ADDRESS = 'ipnr' |
---|
| 396 | MAINTAINER = 'owner' |
---|
| 397 | OS_NAME = 'os' |
---|
| 398 | OS_FAMILY = 'family' |
---|
| 399 | OS_VERSION = 'version' |
---|
| 400 | MACHINE = 'machine' |
---|
| 401 | PROCESSOR = 'processor' |
---|
| 402 | TOKEN = 'token' |
---|
| 403 | |
---|
| 404 | def __init__(self, env, config=None, rev=None, platform=None, slave=None, |
---|
| 405 | started=0, stopped=0, last_activity=0, |
---|
| 406 | rev_time=0, status=PENDING): |
---|
| 407 | """Initialize a new build with the specified attributes. |
---|
| 408 | |
---|
| 409 | To actually create this build in the database, the `insert` method needs |
---|
| 410 | to be called. |
---|
| 411 | """ |
---|
| 412 | self.env = env |
---|
| 413 | self.id = None |
---|
| 414 | self.config = config |
---|
| 415 | self.rev = rev and str(rev) or None |
---|
| 416 | self.platform = platform |
---|
| 417 | self.slave = slave |
---|
| 418 | self.started = started or 0 |
---|
| 419 | self.stopped = stopped or 0 |
---|
| 420 | self.last_activity = last_activity or 0 |
---|
| 421 | self.rev_time = rev_time |
---|
| 422 | self.status = status |
---|
| 423 | self.slave_info = {} |
---|
| 424 | |
---|
| 425 | def __repr__(self): |
---|
| 426 | return '<%s %r>' % (type(self).__name__, self.id) |
---|
| 427 | |
---|
| 428 | exists = property(fget=lambda self: self.id is not None, |
---|
| 429 | doc='Whether this build exists in the database') |
---|
| 430 | completed = property(fget=lambda self: self.status != Build.IN_PROGRESS, |
---|
| 431 | doc='Whether the build has been completed') |
---|
| 432 | successful = property(fget=lambda self: self.status == Build.SUCCESS, |
---|
| 433 | doc='Whether the build was successful') |
---|
| 434 | resource = property(fget=lambda self: Resource('build', '%s/%s' % (self.config, self.id)), |
---|
| 435 | doc='Build resource identification') |
---|
| 436 | |
---|
| 437 | def delete(self, db=None): |
---|
| 438 | """Remove the build from the database.""" |
---|
| 439 | assert self.exists, 'Cannot delete a non-existing build' |
---|
| 440 | if not db: |
---|
| 441 | db = self.env.get_db_cnx() |
---|
| 442 | handle_ta = True |
---|
| 443 | else: |
---|
| 444 | handle_ta = False |
---|
| 445 | |
---|
| 446 | for step in list(BuildStep.select(self.env, build=self.id)): |
---|
| 447 | step.delete(db=db) |
---|
| 448 | |
---|
| 449 | # Delete attachments |
---|
| 450 | Attachment.delete_all(self.env, 'build', self.resource.id, db) |
---|
| 451 | |
---|
| 452 | cursor = db.cursor() |
---|
| 453 | cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) |
---|
| 454 | cursor.execute("DELETE FROM bitten_build WHERE id=%s", (self.id,)) |
---|
| 455 | |
---|
| 456 | if handle_ta: |
---|
| 457 | db.commit() |
---|
| 458 | |
---|
| 459 | def insert(self, db=None): |
---|
| 460 | """Insert a new build into the database.""" |
---|
| 461 | assert not self.exists, 'Cannot insert an existing build' |
---|
| 462 | if not db: |
---|
| 463 | db = self.env.get_db_cnx() |
---|
| 464 | handle_ta = True |
---|
| 465 | else: |
---|
| 466 | handle_ta = False |
---|
| 467 | |
---|
| 468 | assert self.config and self.rev and self.rev_time and self.platform |
---|
| 469 | assert self.status in (self.PENDING, self.IN_PROGRESS, self.SUCCESS, |
---|
| 470 | self.FAILURE) |
---|
| 471 | if not self.slave: |
---|
| 472 | assert self.status == self.PENDING |
---|
| 473 | |
---|
| 474 | cursor = db.cursor() |
---|
| 475 | cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform," |
---|
| 476 | "slave,started,stopped,last_activity,status) " |
---|
| 477 | "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)", |
---|
| 478 | (self.config, self.rev, int(self.rev_time), |
---|
| 479 | self.platform, self.slave or '', self.started or 0, |
---|
| 480 | self.stopped or 0, self.last_activity or 0, |
---|
| 481 | self.status)) |
---|
| 482 | self.id = db.get_last_id(cursor, 'bitten_build') |
---|
| 483 | if self.slave_info: |
---|
| 484 | cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", |
---|
| 485 | [(self.id, name, value) for name, value |
---|
| 486 | in self.slave_info.items()]) |
---|
| 487 | |
---|
| 488 | if handle_ta: |
---|
| 489 | db.commit() |
---|
| 490 | |
---|
| 491 | def update(self, db=None): |
---|
| 492 | """Save changes to an existing build.""" |
---|
| 493 | assert self.exists, 'Cannot update a non-existing build' |
---|
| 494 | if not db: |
---|
| 495 | db = self.env.get_db_cnx() |
---|
| 496 | handle_ta = True |
---|
| 497 | else: |
---|
| 498 | handle_ta = False |
---|
| 499 | |
---|
| 500 | assert self.config and self.rev |
---|
| 501 | assert self.status in (self.PENDING, self.IN_PROGRESS, self.SUCCESS, |
---|
| 502 | self.FAILURE) |
---|
| 503 | if not self.slave: |
---|
| 504 | assert self.status == self.PENDING |
---|
| 505 | |
---|
| 506 | cursor = db.cursor() |
---|
| 507 | cursor.execute("UPDATE bitten_build SET slave=%s,started=%s," |
---|
| 508 | "stopped=%s,last_activity=%s,status=%s WHERE id=%s", |
---|
| 509 | (self.slave or '', self.started or 0, |
---|
| 510 | self.stopped or 0, self.last_activity or 0, |
---|
| 511 | self.status, self.id)) |
---|
| 512 | cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) |
---|
| 513 | if self.slave_info: |
---|
| 514 | cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", |
---|
| 515 | [(self.id, name, value) for name, value |
---|
| 516 | in self.slave_info.items()]) |
---|
| 517 | if handle_ta: |
---|
| 518 | db.commit() |
---|
| 519 | |
---|
| 520 | def fetch(cls, env, id, db=None): |
---|
| 521 | """Retrieve an existing build from the database by ID.""" |
---|
| 522 | if not db: |
---|
| 523 | db = env.get_db_cnx() |
---|
| 524 | |
---|
| 525 | cursor = db.cursor() |
---|
| 526 | cursor.execute("SELECT config,rev,rev_time,platform,slave,started," |
---|
| 527 | "stopped,last_activity,status FROM bitten_build WHERE " |
---|
| 528 | "id=%s", (id,)) |
---|
| 529 | row = cursor.fetchone() |
---|
| 530 | if not row: |
---|
| 531 | return None |
---|
| 532 | |
---|
| 533 | build = Build(env, config=row[0], rev=row[1], rev_time=int(row[2]), |
---|
| 534 | platform=int(row[3]), slave=row[4], |
---|
| 535 | started=row[5] and int(row[5]) or 0, |
---|
| 536 | stopped=row[6] and int(row[6]) or 0, |
---|
| 537 | last_activity=row[7] and int(row[7]) or 0, |
---|
| 538 | status=row[8]) |
---|
| 539 | build.id = int(id) |
---|
| 540 | cursor.execute("SELECT propname,propvalue FROM bitten_slave " |
---|
| 541 | "WHERE build=%s", (id,)) |
---|
| 542 | for propname, propvalue in cursor: |
---|
| 543 | build.slave_info[propname] = propvalue |
---|
| 544 | return build |
---|
| 545 | |
---|
| 546 | fetch = classmethod(fetch) |
---|
| 547 | |
---|
| 548 | def select(cls, env, config=None, rev=None, platform=None, slave=None, |
---|
| 549 | status=None, db=None, min_rev_time=None, max_rev_time=None): |
---|
| 550 | """Retrieve existing builds from the database that match the specified |
---|
| 551 | criteria. |
---|
| 552 | """ |
---|
| 553 | if not db: |
---|
| 554 | db = env.get_db_cnx() |
---|
| 555 | |
---|
| 556 | where_clauses = [] |
---|
| 557 | if config is not None: |
---|
| 558 | where_clauses.append(("config=%s", config)) |
---|
| 559 | if rev is not None: |
---|
| 560 | where_clauses.append(("rev=%s", str(rev))) |
---|
| 561 | if platform is not None: |
---|
| 562 | where_clauses.append(("platform=%s", platform)) |
---|
| 563 | if slave is not None: |
---|
| 564 | where_clauses.append(("slave=%s", slave)) |
---|
| 565 | if status is not None: |
---|
| 566 | where_clauses.append(("status=%s", status)) |
---|
| 567 | if min_rev_time is not None: |
---|
| 568 | where_clauses.append(("rev_time>=%s", min_rev_time)) |
---|
| 569 | if max_rev_time is not None: |
---|
| 570 | where_clauses.append(("rev_time<=%s", max_rev_time)) |
---|
| 571 | if where_clauses: |
---|
| 572 | where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) |
---|
| 573 | else: |
---|
| 574 | where = "" |
---|
| 575 | |
---|
| 576 | cursor = db.cursor() |
---|
| 577 | cursor.execute("SELECT id FROM bitten_build %s " |
---|
| 578 | "ORDER BY rev_time DESC,config,slave" |
---|
| 579 | % where, [wc[1] for wc in where_clauses]) |
---|
| 580 | for (id,) in cursor: |
---|
| 581 | yield Build.fetch(env, id) |
---|
| 582 | select = classmethod(select) |
---|
| 583 | |
---|
| 584 | |
---|
| 585 | class BuildStep(object): |
---|
| 586 | """Represents an individual step of an executed build.""" |
---|
| 587 | |
---|
| 588 | _schema = [ |
---|
| 589 | Table('bitten_step', key=('build', 'name'))[ |
---|
| 590 | Column('build', type='int'), Column('name'), Column('description'), |
---|
| 591 | Column('status', size=1), Column('started', type='int'), |
---|
| 592 | Column('stopped', type='int') |
---|
| 593 | ], |
---|
| 594 | Table('bitten_error', key=('build', 'step', 'orderno'))[ |
---|
| 595 | Column('build', type='int'), Column('step'), Column('message'), |
---|
| 596 | Column('orderno', type='int') |
---|
| 597 | ] |
---|
| 598 | ] |
---|
| 599 | |
---|
| 600 | # Step status codes |
---|
| 601 | SUCCESS = 'S' |
---|
| 602 | IN_PROGRESS = 'I' |
---|
| 603 | FAILURE = 'F' |
---|
| 604 | |
---|
| 605 | def __init__(self, env, build=None, name=None, description=None, |
---|
| 606 | status=None, started=None, stopped=None): |
---|
| 607 | """Initialize a new build step with the specified attributes. |
---|
| 608 | |
---|
| 609 | To actually create this build step in the database, the `insert` method |
---|
| 610 | needs to be called. |
---|
| 611 | """ |
---|
| 612 | self.env = env |
---|
| 613 | self.build = build |
---|
| 614 | self.name = name |
---|
| 615 | self.description = description |
---|
| 616 | self.status = status |
---|
| 617 | self.started = started |
---|
| 618 | self.stopped = stopped |
---|
| 619 | self.errors = [] |
---|
| 620 | self._exists = False |
---|
| 621 | |
---|
| 622 | exists = property(fget=lambda self: self._exists, |
---|
| 623 | doc='Whether this build step exists in the database') |
---|
| 624 | successful = property(fget=lambda self: self.status == BuildStep.SUCCESS, |
---|
| 625 | doc='Whether the build step was successful') |
---|
| 626 | completed = property(fget=lambda self: self.status == BuildStep.SUCCESS or self.status == BuildStep.FAILURE, |
---|
| 627 | doc='Whether this build step has completed processing') |
---|
| 628 | def delete(self, db=None): |
---|
| 629 | """Remove the build step from the database.""" |
---|
| 630 | if not db: |
---|
| 631 | db = self.env.get_db_cnx() |
---|
| 632 | handle_ta = True |
---|
| 633 | else: |
---|
| 634 | handle_ta = False |
---|
| 635 | |
---|
| 636 | for log in list(BuildLog.select(self.env, build=self.build, |
---|
| 637 | step=self.name, db=db)): |
---|
| 638 | log.delete(db=db) |
---|
| 639 | for report in list(Report.select(self.env, build=self.build, |
---|
| 640 | step=self.name, db=db)): |
---|
| 641 | report.delete(db=db) |
---|
| 642 | |
---|
| 643 | cursor = db.cursor() |
---|
| 644 | cursor.execute("DELETE FROM bitten_step WHERE build=%s AND name=%s", |
---|
| 645 | (self.build, self.name)) |
---|
| 646 | cursor.execute("DELETE FROM bitten_error WHERE build=%s AND step=%s", |
---|
| 647 | (self.build, self.name)) |
---|
| 648 | |
---|
| 649 | if handle_ta: |
---|
| 650 | db.commit() |
---|
| 651 | self._exists = False |
---|
| 652 | |
---|
| 653 | def insert(self, db=None): |
---|
| 654 | """Insert a new build step into the database.""" |
---|
| 655 | if not db: |
---|
| 656 | db = self.env.get_db_cnx() |
---|
| 657 | handle_ta = True |
---|
| 658 | else: |
---|
| 659 | handle_ta = False |
---|
| 660 | |
---|
| 661 | assert self.build and self.name |
---|
| 662 | assert self.status in (self.SUCCESS, self.IN_PROGRESS, self.FAILURE) |
---|
| 663 | |
---|
| 664 | cursor = db.cursor() |
---|
| 665 | cursor.execute("INSERT INTO bitten_step (build,name,description,status," |
---|
| 666 | "started,stopped) VALUES (%s,%s,%s,%s,%s,%s)", |
---|
| 667 | (self.build, self.name, self.description or '', |
---|
| 668 | self.status, self.started or 0, self.stopped or 0)) |
---|
| 669 | if self.errors: |
---|
| 670 | cursor.executemany("INSERT INTO bitten_error (build,step,message," |
---|
| 671 | "orderno) VALUES (%s,%s,%s,%s)", |
---|
| 672 | [(self.build, self.name, message, idx) |
---|
| 673 | for idx, message in enumerate(self.errors)]) |
---|
| 674 | |
---|
| 675 | if handle_ta: |
---|
| 676 | db.commit() |
---|
| 677 | self._exists = True |
---|
| 678 | |
---|
| 679 | def fetch(cls, env, build, name, db=None): |
---|
| 680 | """Retrieve an existing build from the database by build ID and step |
---|
| 681 | name.""" |
---|
| 682 | if not db: |
---|
| 683 | db = env.get_db_cnx() |
---|
| 684 | |
---|
| 685 | cursor = db.cursor() |
---|
| 686 | cursor.execute("SELECT description,status,started,stopped " |
---|
| 687 | "FROM bitten_step WHERE build=%s AND name=%s", |
---|
| 688 | (build, name)) |
---|
| 689 | row = cursor.fetchone() |
---|
| 690 | if not row: |
---|
| 691 | return None |
---|
| 692 | step = BuildStep(env, build, name, row[0] or '', row[1], |
---|
| 693 | row[2] and int(row[2]), row[3] and int(row[3])) |
---|
| 694 | step._exists = True |
---|
| 695 | |
---|
| 696 | cursor.execute("SELECT message FROM bitten_error WHERE build=%s " |
---|
| 697 | "AND step=%s ORDER BY orderno", (build, name)) |
---|
| 698 | for row in cursor: |
---|
| 699 | step.errors.append(row[0] or '') |
---|
| 700 | return step |
---|
| 701 | |
---|
| 702 | fetch = classmethod(fetch) |
---|
| 703 | |
---|
| 704 | def select(cls, env, build=None, name=None, status=None, db=None): |
---|
| 705 | """Retrieve existing build steps from the database that match the |
---|
| 706 | specified criteria. |
---|
| 707 | """ |
---|
| 708 | if not db: |
---|
| 709 | db = env.get_db_cnx() |
---|
| 710 | |
---|
| 711 | assert status in (None, BuildStep.SUCCESS, BuildStep.IN_PROGRESS, BuildStep.FAILURE) |
---|
| 712 | |
---|
| 713 | where_clauses = [] |
---|
| 714 | if build is not None: |
---|
| 715 | where_clauses.append(("build=%s", build)) |
---|
| 716 | if name is not None: |
---|
| 717 | where_clauses.append(("name=%s", name)) |
---|
| 718 | if status is not None: |
---|
| 719 | where_clauses.append(("status=%s", status)) |
---|
| 720 | if where_clauses: |
---|
| 721 | where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) |
---|
| 722 | else: |
---|
| 723 | where = "" |
---|
| 724 | |
---|
| 725 | cursor = db.cursor() |
---|
| 726 | cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY started" |
---|
| 727 | % where, [wc[1] for wc in where_clauses]) |
---|
| 728 | for build, name in cursor: |
---|
| 729 | yield BuildStep.fetch(env, build, name, db=db) |
---|
| 730 | |
---|
| 731 | select = classmethod(select) |
---|
| 732 | |
---|
| 733 | |
---|
| 734 | class BuildLog(object): |
---|
| 735 | """Represents a build log.""" |
---|
| 736 | |
---|
| 737 | _schema = [ |
---|
| 738 | Table('bitten_log', key='id')[ |
---|
| 739 | Column('id', auto_increment=True), Column('build', type='int'), |
---|
| 740 | Column('step'), Column('generator'), Column('orderno', type='int'), |
---|
| 741 | Column('filename'), |
---|
| 742 | Index(['build', 'step']) |
---|
| 743 | ], |
---|
| 744 | ] |
---|
| 745 | |
---|
| 746 | # Message levels |
---|
| 747 | DEBUG = 'D' |
---|
| 748 | INFO = 'I' |
---|
| 749 | WARNING = 'W' |
---|
| 750 | ERROR = 'E' |
---|
| 751 | UNKNOWN = '' |
---|
| 752 | LEVELS_SUFFIX = '.levels' |
---|
| 753 | |
---|
| 754 | def __init__(self, env, build=None, step=None, generator=None, |
---|
| 755 | orderno=None, filename=None): |
---|
| 756 | """Initialize a new build log with the specified attributes. |
---|
| 757 | |
---|
| 758 | To actually create this build log in the database, the `insert` method |
---|
| 759 | needs to be called. |
---|
| 760 | """ |
---|
| 761 | self.env = env |
---|
| 762 | self.id = None |
---|
| 763 | self.build = build |
---|
| 764 | self.step = step |
---|
| 765 | self.generator = generator or '' |
---|
| 766 | self.orderno = orderno and int(orderno) or 0 |
---|
| 767 | self.filename = filename or None |
---|
| 768 | self.messages = [] |
---|
| 769 | self.logs_dir = env.config.get('bitten', 'logs_dir', 'log/bitten') |
---|
| 770 | if not os.path.isabs(self.logs_dir): |
---|
| 771 | self.logs_dir = os.path.join(env.path, self.logs_dir) |
---|
| 772 | if not os.path.exists(self.logs_dir): |
---|
| 773 | os.makedirs(self.logs_dir) |
---|
| 774 | |
---|
| 775 | exists = property(fget=lambda self: self.id is not None, |
---|
| 776 | doc='Whether this build log exists in the database') |
---|
| 777 | |
---|
| 778 | def get_log_file(self, filename): |
---|
| 779 | """Returns the full path to the log file""" |
---|
| 780 | if filename != os.path.basename(filename): |
---|
| 781 | raise ValueError("Filename may not contain path: %s" % (filename,)) |
---|
| 782 | return os.path.join(self.logs_dir, filename) |
---|
| 783 | |
---|
| 784 | def delete(self, db=None): |
---|
| 785 | """Remove the build log from the database.""" |
---|
| 786 | assert self.exists, 'Cannot delete a non-existing build log' |
---|
| 787 | if not db: |
---|
| 788 | db = self.env.get_db_cnx() |
---|
| 789 | handle_ta = True |
---|
| 790 | else: |
---|
| 791 | handle_ta = False |
---|
| 792 | |
---|
| 793 | if self.filename: |
---|
| 794 | log_file = self.get_log_file(self.filename) |
---|
| 795 | if os.path.exists(log_file): |
---|
| 796 | try: |
---|
| 797 | self.env.log.debug("Deleting log file: %s" % log_file) |
---|
| 798 | os.remove(log_file) |
---|
| 799 | except Exception, e: |
---|
| 800 | self.env.log.warning("Error removing log file %s: %s" % (log_file, e)) |
---|
| 801 | level_file = log_file + self.LEVELS_SUFFIX |
---|
| 802 | if os.path.exists(level_file): |
---|
| 803 | try: |
---|
| 804 | self.env.log.debug("Deleting level file: %s" % level_file) |
---|
| 805 | os.remove(level_file) |
---|
| 806 | except Exception, e: |
---|
| 807 | self.env.log.warning("Error removing level file %s: %s" \ |
---|
| 808 | % (level_file, e)) |
---|
| 809 | |
---|
| 810 | cursor = db.cursor() |
---|
| 811 | cursor.execute("DELETE FROM bitten_log WHERE id=%s", (self.id,)) |
---|
| 812 | |
---|
| 813 | if handle_ta: |
---|
| 814 | db.commit() |
---|
| 815 | self.id = None |
---|
| 816 | |
---|
| 817 | def insert(self, db=None): |
---|
| 818 | """Insert a new build log into the database.""" |
---|
| 819 | if not db: |
---|
| 820 | db = self.env.get_db_cnx() |
---|
| 821 | handle_ta = True |
---|
| 822 | else: |
---|
| 823 | handle_ta = False |
---|
| 824 | |
---|
| 825 | assert self.build and self.step |
---|
| 826 | |
---|
| 827 | cursor = db.cursor() |
---|
| 828 | cursor.execute("INSERT INTO bitten_log (build,step,generator,orderno) " |
---|
| 829 | "VALUES (%s,%s,%s,%s)", (self.build, self.step, |
---|
| 830 | self.generator, self.orderno)) |
---|
| 831 | id = db.get_last_id(cursor, 'bitten_log') |
---|
| 832 | log_file = "%s.log" % (id,) |
---|
| 833 | cursor.execute("UPDATE bitten_log SET filename=%s WHERE id=%s", (log_file, id)) |
---|
| 834 | if self.messages: |
---|
| 835 | log_file_name = self.get_log_file(log_file) |
---|
| 836 | level_file_name = log_file_name + self.LEVELS_SUFFIX |
---|
| 837 | codecs.open(log_file_name, "wb", "UTF-8").writelines([to_unicode(msg[1]+"\n") for msg in self.messages]) |
---|
| 838 | codecs.open(level_file_name, "wb", "UTF-8").writelines([to_unicode(msg[0]+"\n") for msg in self.messages]) |
---|
| 839 | |
---|
| 840 | if handle_ta: |
---|
| 841 | db.commit() |
---|
| 842 | self.id = id |
---|
| 843 | |
---|
| 844 | def fetch(cls, env, id, db=None): |
---|
| 845 | """Retrieve an existing build from the database by ID.""" |
---|
| 846 | if not db: |
---|
| 847 | db = env.get_db_cnx() |
---|
| 848 | |
---|
| 849 | cursor = db.cursor() |
---|
| 850 | cursor.execute("SELECT build,step,generator,orderno,filename FROM bitten_log " |
---|
| 851 | "WHERE id=%s", (id,)) |
---|
| 852 | row = cursor.fetchone() |
---|
| 853 | if not row: |
---|
| 854 | return None |
---|
| 855 | log = BuildLog(env, int(row[0]), row[1], row[2], row[3], row[4]) |
---|
| 856 | log.id = id |
---|
| 857 | if log.filename: |
---|
| 858 | log_filename = log.get_log_file(log.filename) |
---|
| 859 | if os.path.exists(log_filename): |
---|
| 860 | log_lines = codecs.open(log_filename, "rb", "UTF-8").readlines() |
---|
| 861 | else: |
---|
| 862 | log_lines = [] |
---|
| 863 | level_filename = log.get_log_file(log.filename + cls.LEVELS_SUFFIX) |
---|
| 864 | if os.path.exists(level_filename): |
---|
| 865 | log_levels = dict(enumerate(codecs.open(level_filename, "rb", "UTF-8").readlines())) |
---|
| 866 | else: |
---|
| 867 | log_levels = {} |
---|
| 868 | log.messages = [(log_levels.get(line_num, BuildLog.UNKNOWN).rstrip("\n"), line.rstrip("\n")) for line_num, line in enumerate(log_lines)] |
---|
| 869 | else: |
---|
| 870 | log.messages = [] |
---|
| 871 | |
---|
| 872 | return log |
---|
| 873 | |
---|
| 874 | fetch = classmethod(fetch) |
---|
| 875 | |
---|
| 876 | def select(cls, env, build=None, step=None, generator=None, db=None): |
---|
| 877 | """Retrieve existing build logs from the database that match the |
---|
| 878 | specified criteria. |
---|
| 879 | """ |
---|
| 880 | if not db: |
---|
| 881 | db = env.get_db_cnx() |
---|
| 882 | |
---|
| 883 | where_clauses = [] |
---|
| 884 | if build is not None: |
---|
| 885 | where_clauses.append(("build=%s", build)) |
---|
| 886 | if step is not None: |
---|
| 887 | where_clauses.append(("step=%s", step)) |
---|
| 888 | if generator is not None: |
---|
| 889 | where_clauses.append(("generator=%s", generator)) |
---|
| 890 | if where_clauses: |
---|
| 891 | where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) |
---|
| 892 | else: |
---|
| 893 | where = "" |
---|
| 894 | |
---|
| 895 | cursor = db.cursor() |
---|
| 896 | cursor.execute("SELECT id FROM bitten_log %s ORDER BY orderno" |
---|
| 897 | % where, [wc[1] for wc in where_clauses]) |
---|
| 898 | for (id, ) in cursor: |
---|
| 899 | yield BuildLog.fetch(env, id, db=db) |
---|
| 900 | |
---|
| 901 | select = classmethod(select) |
---|
| 902 | |
---|
| 903 | |
---|
| 904 | class Report(object): |
---|
| 905 | """Represents a generated report.""" |
---|
| 906 | |
---|
| 907 | _schema = [ |
---|
| 908 | Table('bitten_report', key='id')[ |
---|
| 909 | Column('id', auto_increment=True), Column('build', type='int'), |
---|
| 910 | Column('step'), Column('category'), Column('generator'), |
---|
| 911 | Index(['build', 'step', 'category']) |
---|
| 912 | ], |
---|
| 913 | Table('bitten_report_item', key=('report', 'item', 'name'))[ |
---|
| 914 | Column('report', type='int'), Column('item', type='int'), |
---|
| 915 | Column('name'), Column('value') |
---|
| 916 | ] |
---|
| 917 | ] |
---|
| 918 | |
---|
| 919 | def __init__(self, env, build=None, step=None, category=None, |
---|
| 920 | generator=None): |
---|
| 921 | """Initialize a new report with the specified attributes. |
---|
| 922 | |
---|
| 923 | To actually create this build log in the database, the `insert` method |
---|
| 924 | needs to be called. |
---|
| 925 | """ |
---|
| 926 | self.env = env |
---|
| 927 | self.id = None |
---|
| 928 | self.build = build |
---|
| 929 | self.step = step |
---|
| 930 | self.category = category |
---|
| 931 | self.generator = generator or '' |
---|
| 932 | self.items = [] |
---|
| 933 | |
---|
| 934 | exists = property(fget=lambda self: self.id is not None, |
---|
| 935 | doc='Whether this report exists in the database') |
---|
| 936 | |
---|
| 937 | def delete(self, db=None): |
---|
| 938 | """Remove the report from the database.""" |
---|
| 939 | assert self.exists, 'Cannot delete a non-existing report' |
---|
| 940 | if not db: |
---|
| 941 | db = self.env.get_db_cnx() |
---|
| 942 | handle_ta = True |
---|
| 943 | else: |
---|
| 944 | handle_ta = False |
---|
| 945 | |
---|
| 946 | cursor = db.cursor() |
---|
| 947 | cursor.execute("DELETE FROM bitten_report_item WHERE report=%s", |
---|
| 948 | (self.id,)) |
---|
| 949 | cursor.execute("DELETE FROM bitten_report WHERE id=%s", (self.id,)) |
---|
| 950 | |
---|
| 951 | if handle_ta: |
---|
| 952 | db.commit() |
---|
| 953 | self.id = None |
---|
| 954 | |
---|
| 955 | def insert(self, db=None): |
---|
| 956 | """Insert a new build log into the database.""" |
---|
| 957 | if not db: |
---|
| 958 | db = self.env.get_db_cnx() |
---|
| 959 | handle_ta = True |
---|
| 960 | else: |
---|
| 961 | handle_ta = False |
---|
| 962 | |
---|
| 963 | assert self.build and self.step and self.category |
---|
| 964 | |
---|
| 965 | # Enforce uniqueness of build-step-category. |
---|
| 966 | # This should be done by the database, but the DB schema helpers in Trac |
---|
| 967 | # currently don't support UNIQUE() constraints |
---|
| 968 | assert not list(Report.select(self.env, build=self.build, |
---|
| 969 | step=self.step, category=self.category, |
---|
| 970 | db=db)), 'Report already exists' |
---|
| 971 | |
---|
| 972 | cursor = db.cursor() |
---|
| 973 | cursor.execute("INSERT INTO bitten_report " |
---|
| 974 | "(build,step,category,generator) VALUES (%s,%s,%s,%s)", |
---|
| 975 | (self.build, self.step, self.category, self.generator)) |
---|
| 976 | id = db.get_last_id(cursor, 'bitten_report') |
---|
| 977 | for idx, item in enumerate([item for item in self.items if item]): |
---|
| 978 | cursor.executemany("INSERT INTO bitten_report_item " |
---|
| 979 | "(report,item,name,value) VALUES (%s,%s,%s,%s)", |
---|
| 980 | [(id, idx, key, value) for key, value |
---|
| 981 | in item.items()]) |
---|
| 982 | |
---|
| 983 | if handle_ta: |
---|
| 984 | db.commit() |
---|
| 985 | self.id = id |
---|
| 986 | |
---|
| 987 | def fetch(cls, env, id, db=None): |
---|
| 988 | """Retrieve an existing build from the database by ID.""" |
---|
| 989 | if not db: |
---|
| 990 | db = env.get_db_cnx() |
---|
| 991 | |
---|
| 992 | cursor = db.cursor() |
---|
| 993 | cursor.execute("SELECT build,step,category,generator " |
---|
| 994 | "FROM bitten_report WHERE id=%s", (id,)) |
---|
| 995 | row = cursor.fetchone() |
---|
| 996 | if not row: |
---|
| 997 | return None |
---|
| 998 | report = Report(env, int(row[0]), row[1], row[2] or '', row[3] or '') |
---|
| 999 | report.id = id |
---|
| 1000 | |
---|
| 1001 | cursor.execute("SELECT item,name,value FROM bitten_report_item " |
---|
| 1002 | "WHERE report=%s ORDER BY item", (id,)) |
---|
| 1003 | items = {} |
---|
| 1004 | for item, name, value in cursor: |
---|
| 1005 | items.setdefault(item, {})[name] = value |
---|
| 1006 | report.items = items.values() |
---|
| 1007 | |
---|
| 1008 | return report |
---|
| 1009 | |
---|
| 1010 | fetch = classmethod(fetch) |
---|
| 1011 | |
---|
| 1012 | def select(cls, env, config=None, build=None, step=None, category=None, |
---|
| 1013 | db=None): |
---|
| 1014 | """Retrieve existing reports from the database that match the specified |
---|
| 1015 | criteria. |
---|
| 1016 | """ |
---|
| 1017 | where_clauses = [] |
---|
| 1018 | joins = [] |
---|
| 1019 | if config is not None: |
---|
| 1020 | where_clauses.append(("config=%s", config)) |
---|
| 1021 | joins.append("INNER JOIN bitten_build ON (bitten_build.id=build)") |
---|
| 1022 | if build is not None: |
---|
| 1023 | where_clauses.append(("build=%s", build)) |
---|
| 1024 | if step is not None: |
---|
| 1025 | where_clauses.append(("step=%s", step)) |
---|
| 1026 | if category is not None: |
---|
| 1027 | where_clauses.append(("category=%s", category)) |
---|
| 1028 | |
---|
| 1029 | if where_clauses: |
---|
| 1030 | where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) |
---|
| 1031 | else: |
---|
| 1032 | where = "" |
---|
| 1033 | |
---|
| 1034 | if not db: |
---|
| 1035 | db = env.get_db_cnx() |
---|
| 1036 | cursor = db.cursor() |
---|
| 1037 | cursor.execute("SELECT bitten_report.id FROM bitten_report %s %s " |
---|
| 1038 | "ORDER BY category" % (' '.join(joins), where), |
---|
| 1039 | [wc[1] for wc in where_clauses]) |
---|
| 1040 | for (id, ) in cursor: |
---|
| 1041 | yield Report.fetch(env, id, db=db) |
---|
| 1042 | |
---|
| 1043 | select = classmethod(select) |
---|
| 1044 | |
---|
| 1045 | |
---|
| 1046 | schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \ |
---|
| 1047 | BuildStep._schema + BuildLog._schema + Report._schema |
---|
| 1048 | schema_version = 12 |
---|