2 # Simple database query script for the buildfarm
4 # Copyright (C) Andrew Tridgell <tridge@samba.org> 2001-2005
5 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2001
6 # Copyright (C) Vance Lankhaar <vance@samba.org> 2002-2005
7 # Copyright (C) Martin Pool <mbp@samba.org> 2001
8 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
25 from cStringIO import StringIO
33 class BuildSummary(object):
35 def __init__(self, host, tree, compiler, rev, status):
38 self.compiler = compiler
43 class BuildStatus(object):
45 def __init__(self, stages=None, other_failures=None):
46 if stages is not None:
50 if other_failures is not None:
51 self.other_failures = other_failures
53 self.other_failures = set()
55 def broken_host(self):
56 if "disk full" in self.other_failures:
60 def _status_tuple(self):
61 return [v for (k, v) in self.stages]
63 def regressed_since(self, other):
64 """Check if this build has regressed since another build."""
65 if "disk full" in self.other_failures:
67 return cmp(self._status_tuple(), other._status_tuple())
69 def __cmp__(self, other):
70 other_extra = other.other_failures - self.other_failures
71 self_extra = self.other_failures - other.other_failures
72 # Give more importance to other failures
79 lb = len(other.stages)
85 return cmp(other.stages, self.stages)
88 return repr((self.stages, self.other_failures))
91 def check_dir_exists(kind, path):
92 if not os.path.isdir(path):
93 raise Exception("%s directory %s does not exist" % (kind, path))
96 def build_status_from_logs(log, err):
97 """get status of build"""
106 m = re.match("^([A-Z_]+) STATUS:(\s*\d+)$", l)
108 stages.append((m.group(1), int(m.group(2).strip())))
109 if m.group(1) == "TEST":
112 m = re.match("^ACTION (PASSED|FAILED):\s+test$", l)
113 if m and not test_seen:
114 if m.group(1) == "PASSED":
115 stages.append(("TEST", 0))
117 stages.append(("TEST", 1))
120 if l.startswith("No space left on device"):
121 ret.other_failures.add("disk full")
123 if l.startswith("maximum runtime exceeded"):
124 ret.other_failures.add("timeout")
126 m = re.match("^(PANIC|INTERNAL ERROR):.*$", l)
128 ret.other_failures.add("panic")
130 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
133 if l.startswith("testsuite-success: "):
137 # Scan err file for specific errors
139 if "No space left on device" in l:
140 ret.other_failures.add("disk full")
142 def map_stage(name, result):
144 return (name, result)
146 if test_successes + test_failures == 0:
147 # No granular test output
148 return ("TEST", result)
149 if result == 1 and test_failures == 0:
150 ret.other_failures.add("inconsistent test result")
152 return ("TEST", test_failures)
154 ret.stages = [map_stage(name, result) for (name, result) in stages]
158 class NoSuchBuildError(Exception):
159 """The build with the specified name does not exist."""
161 def __init__(self, tree, host, compiler, rev=None):
164 self.compiler = compiler
169 """A single build of a tree on a particular host using a particular compiler.
172 def __init__(self, store, tree, host, compiler, rev=None):
176 self.compiler = compiler
179 self.basename = self._store.build_fname(self.tree, self.host, self.compiler)
181 self.basename = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
184 # the mtime age is used to determine if builds are still happening
186 # the ctime age is used to determine when the last real build happened
189 """get the age of build from mtime"""
190 st = os.stat("%s.log" % self.basename)
191 return time.time() - st.st_mtime
194 """get the age of build from ctime"""
195 st = os.stat("%s.log" % self.basename)
196 return time.time() - st.st_ctime
199 """read full log file"""
200 return open(self.basename+".log", "r")
203 """read full err file"""
205 return open(self.basename+".err", 'r')
210 def log_checksum(self):
213 return hashlib.sha1(f.read()).hexdigest()
218 (revid, commit_revid, timestamp) = self.revision_details()
221 status = self.status()
222 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
224 def revision_details(self):
225 """get the revision of build
227 :return: Tuple with revision id and timestamp (if available)
236 if l.startswith("BUILD COMMIT REVISION: "):
237 commit_revid = l.split(":", 1)[1].strip()
238 elif l.startswith("BUILD REVISION: "):
239 revid = l.split(":", 1)[1].strip()
240 elif l.startswith("BUILD COMMIT TIME"):
241 timestamp = l.split(":", 1)[1].strip()
245 return (revid, commit_revid, timestamp)
248 """get status of build
250 :return: tuple with build status
252 log = self.read_log()
254 err = self.read_err()
256 return build_status_from_logs(log, err)
263 """get status of build"""
264 file = self.read_err()
265 return len(file.readlines())
268 class CachingBuild(Build):
269 """Build subclass that caches some of the results that are expensive
272 def revision_details(self):
273 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
274 st1 = os.stat("%s.log" % self.basename)
277 st2 = os.stat("%s.revision" % cachef)
279 # File does not exist
282 # the ctime/mtime asymmetry is needed so we don't get fooled by
283 # the mtime update from rsync
284 if st2 and st1.st_ctime <= st2.st_mtime:
285 (revid, commit_revid, timestamp) = util.FileLoad("%s.revision" % cachef).split(":", 2)
290 if commit_revid == "":
292 return (revid, commit_revid, timestamp)
293 (revid, commit_revid, timestamp) = super(CachingBuild, self).revision_details()
294 if not self._store.readonly:
295 util.FileSave("%s.revision" % cachef, "%s:%s:%s" % (revid, commit_revid or "", timestamp or ""))
296 return (revid, commit_revid, timestamp)
299 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
300 st1 = os.stat("%s.err" % self.basename)
303 st2 = os.stat("%s.errcount" % cachef)
305 # File does not exist
308 if st2 and st1.st_ctime <= st2.st_mtime:
309 return util.FileLoad("%s.errcount" % cachef)
311 ret = super(CachingBuild, self).err_count()
313 if not self._store.readonly:
314 util.FileSave("%s.errcount" % cachef, str(ret))
319 cachefile = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)+".status"
321 st1 = os.stat("%s.log" % self.basename)
324 st2 = os.stat(cachefile)
329 if st2 and st1.st_ctime <= st2.st_mtime:
330 return BuildStatus(*eval(util.FileLoad(cachefile)))
332 ret = super(CachingBuild, self).status()
334 if not self._store.readonly:
335 util.FileSave(cachefile, str(ret))
340 class UploadBuildResultStore(object):
342 def __init__(self, path):
343 """Open the database.
345 :param path: Build result base directory
349 def build_fname(self, tree, host, compiler):
350 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
352 def has_host(self, host):
353 for name in os.listdir(self.path):
355 if name.split(".")[2] == host:
361 def get_build(self, tree, host, compiler):
362 logf = self.build_fname(tree, host, compiler) + ".log"
363 if not os.path.exists(logf):
364 raise NoSuchBuildError(tree, host, compiler)
365 return Build(self, tree, host, compiler)
368 class CachingUploadBuildResultStore(UploadBuildResultStore):
370 def __init__(self, basedir, cachedir, readonly=False):
371 """Open the database.
373 :param readonly: Whether to avoid saving cache files
375 super(CachingUploadBuildResultStore, self).__init__(basedir)
376 self.cachedir = cachedir
377 self.readonly = readonly
379 def cache_fname(self, tree, host, compiler):
380 return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
382 def get_build(self, tree, host, compiler):
383 logf = self.build_fname(tree, host, compiler) + ".log"
384 if not os.path.exists(logf):
385 raise NoSuchBuildError(tree, host, compiler)
386 return CachingBuild(self, tree, host, compiler)
389 class BuildResultStore(object):
390 """The build farm build result database."""
392 def __init__(self, path):
393 """Open the database.
395 :param path: Build result base directory
399 def get_lcov_cached_status(self, host, tree):
402 def get_build(self, tree, host, compiler, rev):
403 logf = self.build_fname(tree, host, compiler, rev) + ".log"
404 if not os.path.exists(logf):
405 raise NoSuchBuildError(tree, host, compiler, rev)
406 return Build(self, tree, host, compiler, rev)
408 def build_fname(self, tree, host, compiler, rev):
409 """get the name of the build file"""
410 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
412 def get_old_revs(self, tree, host, compiler):
413 """get a list of old builds and their status."""
415 logfiles = [d for d in os.listdir(self.path) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
417 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
420 stat = os.stat(os.path.join(self.path, l))
421 # skip the current build
422 if stat.st_nlink == 2:
424 build = self.get_build(tree, host, compiler, rev)
426 "STATUS": build.status(),
428 "TIMESTAMP": build.age_ctime(),
432 ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
436 def upload_build(self, build):
437 (rev, commit_rev, rev_timestamp) = build.revision_details()
439 if commit_rev is not None:
442 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
443 os.rename(build.basename+".log", new_basename+".log")
444 if os.path.exists(build.basename+".err"):
445 os.rename(build.basename+".err", new_basename+".err")
448 # $st = $dbh->prepare("INSERT INTO build (tree, revision, commit_revision, host, compiler, checksum, age, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
449 # $st->execute($tree, $rev, $commit, $host, $compiler, $checksum, $stat->ctime, $status_html)
452 def get_previous_revision(self, tree, host, compiler, revision):
453 # Look up the database to find the previous status
454 $st = $dbh->prepare("SELECT status, revision, commit_revision FROM build WHERE tree = ? AND host = ? AND compiler = ? AND revision != ? AND commit_revision != ? ORDER BY id DESC LIMIT 1")
455 $st->execute( $tree, $host, $compiler, $rev, $commit)
457 while ( my @row = $st->fetchrow_array ) {
458 $old_status_html = @row[0]
460 $old_commit = @row[2]
464 class CachingBuildResultStore(BuildResultStore):
466 def __init__(self, basedir, cachedir, readonly=False):
467 super(CachingBuildResultStore, self).__init__(basedir)
469 self.cachedir = cachedir
470 check_dir_exists("cache", self.cachedir)
472 self.readonly = readonly
474 def get_build(self, tree, host, compiler, rev):
475 logf = self.build_fname(tree, host, compiler, rev) + ".log"
476 if not os.path.exists(logf):
477 raise NoSuchBuildError(tree, host, compiler, rev)
478 return CachingBuild(self, tree, host, compiler, rev)
480 def cache_fname(self, tree, host, compiler, rev):
481 return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
483 def get_lcov_cached_status(self, host, tree):
484 return os.path.join(self.cachedir, "lcov.%s.%s.status" % (host, tree))