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.
24 from cStringIO import StringIO
33 class BuildSummary(object):
35 def __init__(self, host, tree, compiler, revision, status):
38 self.compiler = compiler
39 self.revision = revision
43 BuildStageResult = collections.namedtuple("BuildStageResult", "name result")
46 class BuildStatus(object):
48 def __init__(self, stages=None, other_failures=None):
49 if stages is not None:
53 if other_failures is not None:
54 self.other_failures = other_failures
56 self.other_failures = set()
59 if self.other_failures:
60 return ",".join(self.other_failures)
61 return "/".join(map(str, self._status_tuple()))
63 def broken_host(self):
64 if "disk full" in self.other_failures:
68 def _status_tuple(self):
69 return [sr.result for sr in self.stages]
71 def regressed_since(self, other):
72 """Check if this build has regressed since another build."""
73 if "disk full" in self.other_failures:
75 return cmp(self._status_tuple(), other._status_tuple())
77 def __cmp__(self, other):
78 other_extra = other.other_failures - self.other_failures
79 self_extra = self.other_failures - other.other_failures
80 # Give more importance to other failures
87 lb = len(other.stages)
93 return cmp(other.stages, self.stages)
96 return "%s(%r, %r)" % (self.__class__.__name__, self.stages, self.other_failures)
99 def check_dir_exists(kind, path):
100 if not os.path.isdir(path):
101 raise Exception("%s directory %s does not exist" % (kind, path))
104 def build_status_from_logs(log, err):
105 """get status of build"""
114 m = re.match("^([A-Z_]+) STATUS:(\s*\d+)$", l)
116 stages.append(BuildStageResult(m.group(1), int(m.group(2).strip())))
117 if m.group(1) == "TEST":
120 m = re.match("^ACTION (PASSED|FAILED):\s+test$", l)
121 if m and not test_seen:
122 if m.group(1) == "PASSED":
123 stages.append(BuildStageResult("TEST", 0))
125 stages.append(BuildStageResult("TEST", 1))
128 if l.startswith("No space left on device"):
129 ret.other_failures.add("disk full")
131 if l.startswith("maximum runtime exceeded"):
132 ret.other_failures.add("timeout")
134 m = re.match("^(PANIC|INTERNAL ERROR):.*$", l)
136 ret.other_failures.add("panic")
138 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
141 if l.startswith("testsuite-success: "):
145 # Scan err file for specific errors
147 if "No space left on device" in l:
148 ret.other_failures.add("disk full")
151 if sr.name != "TEST":
154 if test_successes + test_failures == 0:
155 # No granular test output
156 return BuildStageResult("TEST", sr.result)
157 if sr.result == 1 and test_failures == 0:
158 ret.other_failures.add("inconsistent test result")
159 return BuildStageResult("TEST", -1)
160 return BuildStageResult("TEST", test_failures)
162 ret.stages = map(map_stage, stages)
166 class NoSuchBuildError(Exception):
167 """The build with the specified name does not exist."""
169 def __init__(self, tree, host, compiler, rev=None):
172 self.compiler = compiler
177 """A single build of a tree on a particular host using a particular compiler.
180 def __init__(self, basename, tree, host, compiler, rev=None):
181 self.basename = basename
184 self.compiler = compiler
185 self.commit_revision = self.revision = rev
188 if self.revision is not None:
189 return "<%s: revision %s of %s on %s using %s>" % (self.__class__.__name__, self.revision, self.tree, self.host, self.compiler)
191 return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
193 def remove_logs(self):
194 os.unlink(self.basename + ".log")
195 if os.path.exists(self.basename+".err"):
196 os.unlink(self.basename+".err")
202 # the mtime age is used to determine if builds are still happening
204 # the ctime age is used to determine when the last real build happened
207 """get the age of build from mtime"""
208 st = os.stat("%s.log" % self.basename)
209 return time.time() - st.st_mtime
212 """get the age of build from ctime"""
213 st = os.stat("%s.log" % self.basename)
214 return time.time() - st.st_ctime
217 """read full log file"""
218 return open(self.basename+".log", "r")
221 """read full err file"""
223 return open(self.basename+".err", 'r')
228 def log_checksum(self):
231 return hashlib.sha1(f.read()).hexdigest()
236 (revid, timestamp) = self.revision_details()
237 status = self.status()
238 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
240 def revision_details(self):
241 """get the revision of build
243 :return: Tuple with revision id and timestamp (if available)
250 if l.startswith("BUILD COMMIT REVISION: "):
251 revid = l.split(":", 1)[1].strip()
252 elif l.startswith("BUILD COMMIT TIME"):
253 timestamp = l.split(":", 1)[1].strip()
257 return (revid, timestamp)
260 """get status of build
262 :return: tuple with build status
264 log = self.read_log()
266 err = self.read_err()
268 return build_status_from_logs(log, err)
275 """get status of build"""
276 file = self.read_err()
277 return len(file.readlines())
280 class CachingBuild(Build):
281 """Build subclass that caches some of the results that are expensive
284 def __init__(self, store, *args, **kwargs):
286 super(CachingBuild, self).__init__(*args, **kwargs)
288 self.cache_basename = self._store.cache_fname(self.tree, self.host, self.compiler, self.revision)
290 self.cache_basename = self._store.cache_fname(self.tree, self.host, self.compiler)
292 def revision_details(self):
293 st1 = os.stat("%s.log" % self.basename)
296 st2 = os.stat("%s.revision" % self.cache_basename)
298 # File does not exist
301 # the ctime/mtime asymmetry is needed so we don't get fooled by
302 # the mtime update from rsync
303 if st2 and st1.st_ctime <= st2.st_mtime:
304 (revid, timestamp) = util.FileLoad("%s.revision" % self.cache_basename).split(":", 2)
309 return (revid, timestamp)
310 (revid, timestamp) = super(CachingBuild, self).revision_details()
311 if not self._store.readonly:
312 util.FileSave("%s.revision" % self.cache_basename, "%s:%s" % (revid, timestamp or ""))
313 return (revid, timestamp)
316 st1 = os.stat("%s.err" % self.basename)
319 st2 = os.stat("%s.errcount" % self.cache_basename)
321 # File does not exist
324 if st2 and st1.st_ctime <= st2.st_mtime:
325 return util.FileLoad("%s.errcount" % self.cache_basename)
327 ret = super(CachingBuild, self).err_count()
329 if not self._store.readonly:
330 util.FileSave("%s.errcount" % self.cache_basename, str(ret))
335 cachefile = self.cache_basename + ".status"
337 st1 = os.stat("%s.log" % self.basename)
340 st2 = os.stat(cachefile)
345 if st2 and st1.st_ctime <= st2.st_mtime:
346 return eval(util.FileLoad(cachefile))
348 ret = super(CachingBuild, self).status()
350 if not self._store.readonly:
351 util.FileSave(cachefile, repr(ret))
356 class UploadBuildResultStore(object):
358 def __init__(self, path):
359 """Open the database.
361 :param path: Build result base directory
365 def get_new_builds(self):
366 for name in os.listdir(self.path):
368 (build, tree, host, compiler, extension) = name.split(".")
371 if build != "build" or extension != "log":
373 yield self.get_build(tree, host, compiler)
375 def build_fname(self, tree, host, compiler):
376 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
378 def has_host(self, host):
379 for name in os.listdir(self.path):
381 if name.split(".")[2] == host:
387 def get_build(self, tree, host, compiler):
388 basename = self.build_fname(tree, host, compiler)
389 logf = "%s.log" % basename
390 if not os.path.exists(logf):
391 raise NoSuchBuildError(tree, host, compiler)
392 return Build(basename, tree, host, compiler)
395 class CachingUploadBuildResultStore(UploadBuildResultStore):
397 def __init__(self, basedir, cachedir, readonly=False):
398 """Open the database.
400 :param readonly: Whether to avoid saving cache files
402 super(CachingUploadBuildResultStore, self).__init__(basedir)
403 self.cachedir = cachedir
404 self.readonly = readonly
406 def cache_fname(self, tree, host, compiler):
407 return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
409 def get_build(self, tree, host, compiler):
410 basename = self.build_fname(tree, host, compiler)
411 logf = "%s.log" % basename
412 if not os.path.exists(logf):
413 raise NoSuchBuildError(tree, host, compiler)
414 return CachingBuild(self, basename, tree, host, compiler)
417 class BuildResultStore(object):
418 """The build farm build result database."""
420 def __init__(self, path):
421 """Open the database.
423 :param path: Build result base directory
427 def get_build(self, tree, host, compiler, rev):
428 basename = self.build_fname(tree, host, compiler, rev)
429 logf = "%s.log" % basename
430 if not os.path.exists(logf):
431 raise NoSuchBuildError(tree, host, compiler, rev)
432 return Build(basename, tree, host, compiler, rev)
434 def build_fname(self, tree, host, compiler, rev):
435 """get the name of the build file"""
436 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
438 def get_old_revs(self, tree, host, compiler):
439 """get a list of old builds and their status."""
441 logfiles = [d for d in os.listdir(self.path) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
443 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
446 stat = os.stat(os.path.join(self.path, l))
447 # skip the current build
448 if stat.st_nlink == 2:
450 ret.append(self.get_build(tree, host, compiler, rev))
452 ret.sort(lambda a, b: cmp(a.age_mtime(), b.age_mtime()))
456 def upload_build(self, build):
457 (rev, rev_timestamp) = build.revision_details()
460 raise Exception("Unable to find revision in %r log" % build)
462 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
464 existing_build = self.get_build(build.tree, build.host, build.compiler, rev)
465 except NoSuchBuildError:
468 existing_build.remove_logs()
469 os.link(build.basename+".log", new_basename+".log")
470 if os.path.exists(build.basename+".err"):
471 os.link(build.basename+".err", new_basename+".err")
473 def get_previous_revision(self, tree, host, compiler, revision):
474 raise NoSuchBuildError(tree, host, compiler, revision)
476 def get_latest_revision(self, tree, host, compiler):
477 raise NoSuchBuildError(tree, host, compiler)
480 class CachingBuildResultStore(BuildResultStore):
482 def __init__(self, basedir, cachedir, readonly=False):
483 super(CachingBuildResultStore, self).__init__(basedir)
485 self.cachedir = cachedir
486 check_dir_exists("cache", self.cachedir)
488 self.readonly = readonly
490 def get_build(self, tree, host, compiler, rev):
491 basename = self.build_fname(tree, host, compiler, rev)
492 logf = "%s.log" % basename
493 if not os.path.exists(logf):
494 raise NoSuchBuildError(tree, host, compiler, rev)
495 return CachingBuild(self, basename, tree, host, compiler, rev)
497 def cache_fname(self, tree, host, compiler, rev):
498 return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))