Edgewall Software
close Warning: No coverage annotation found for /trunk/bitten/model.py for revision range [1001:1001].

source: trunk/bitten/model.py @ 1001

Last change on this file since 1001 was 910, checked in by osimons, 13 years ago

Updated copyright to 2010.

  • Property svn:eol-style set to native
File size: 38.1 KB
CovLine 
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
13from trac.attachment import Attachment
14from trac.db import Table, Column, Index
15from trac.resource import Resource
16from trac.util.text import to_unicode
17from trac.util.datefmt import to_timestamp, utcmin, utcmax
18from datetime import datetime
19import codecs
20import os
21
22__docformat__ = 'restructuredtext en'
23
24
25class 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
226class 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
371class 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
585class 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
734class 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
904class 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
1046schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \
1047         BuildStep._schema + BuildLog._schema + Report._schema
1048schema_version = 12
Note: See TracBrowser for help on using the repository browser.