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.
26 from cStringIO import StringIO
34 class BuildStatus(object):
36 def __init__(self, stages=None, other_failures=None):
37 if stages is not None:
41 if other_failures is not None:
42 self.other_failures = other_failures
44 self.other_failures = set()
46 def broken_host(self):
47 if "disk full" in self.other_failures:
51 def _status_tuple(self):
52 return [v for (k, v) in self.stages]
54 def regressed_since(self, other):
55 """Check if this build has regressed since another build."""
56 if "disk full" in self.other_failures:
58 return cmp(self._status_tuple(), other._status_tuple())
62 #Give more importance to other failures
63 if len(b.other_failures):
65 if len(a.other_failures):
81 return cmp(sb[1], sa[1])
84 return repr((self.stages, self.other_failures))
87 def check_dir_exists(kind, path):
88 if not os.path.isdir(path):
89 raise Exception("%s directory %s does not exist" % (kind, path))
92 def build_status_from_logs(log, err):
93 """get status of build"""
102 m = re.match("^([A-Z_]+) STATUS:(\s*\d+)$", l)
104 stages.append((m.group(1), int(m.group(2).strip())))
105 if m.group(1) == "TEST":
108 m = re.match("^ACTION (PASSED|FAILED):\s+test$", l)
109 if m and not test_seen:
110 if m.group(1) == "PASSED":
111 stages.append(("TEST", 0))
113 stages.append(("TEST", 1))
116 if l.startswith("No space left on device"):
117 ret.other_failures.add("disk full")
119 if l.startswith("maximum runtime exceeded"):
120 ret.other_failures.add("timeout")
122 m = re.match("^(PANIC|INTERNAL ERROR):.*$", l)
124 ret.other_failures.add("panic")
126 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
129 if l.startswith("testsuite-success: "):
133 # Scan err file for specific errors
135 if "No space left on device" in l:
136 ret.other_failures.add("disk full")
138 stage_results = dict(stages)
139 def map_stage(name, result):
141 return (name, result)
143 if test_successes + test_failures == 0:
144 # No granular test output
145 return ("TEST", result)
146 if result == 1 and test_failures == 0:
147 ret.other_failures.add("inconsistent test result")
149 return ("TEST", test_failures)
151 ret.stages = [map_stage(name, result) for (name, result) in stages]
155 def lcov_extract_percentage(text):
156 m = re.search('\<td class="headerItem".*?\>Code\ \;covered\:\<\/td\>.*?\n.*?\<td class="headerValue".*?\>([0-9.]+) \%', text)
163 class NoSuchBuildError(Exception):
164 """The build with the specified name does not exist."""
166 def __init__(self, tree, host, compiler, rev=None):
169 self.compiler = compiler
174 """A tree to build."""
176 def __init__(self, name, scm, repo, branch, subdir="", srcdir=""):
186 return "<%s %r>" % (self.__class__.__name__, self.name)
190 """A single build of a tree on a particular host using a particular compiler.
193 def __init__(self, store, tree, host, compiler, rev=None):
197 self.compiler = compiler
201 # the mtime age is used to determine if builds are still happening
203 # the ctime age is used to determine when the last real build happened
206 """get the age of build from mtime"""
207 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
209 st = os.stat("%s.log" % file)
210 return time.time() - st.st_mtime
213 """get the age of build from ctime"""
214 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
216 st = os.stat("%s.log" % file)
217 return time.time() - st.st_ctime
220 """read full log file"""
221 return open(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".log", "r")
224 """read full err file"""
226 return open(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".err", 'r')
232 def log_checksum(self):
235 return hashlib.sha1(f.read()).hexdigest()
239 def revision_details(self):
240 """get the revision of build
242 :return: Tuple with revision id and timestamp (if available)
244 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
249 f = open("%s.log" % file, 'r')
251 for l in f.readlines():
252 if l.startswith("BUILD COMMIT REVISION: "):
253 commit_revid = l.split(":", 1)[1].strip()
254 elif l.startswith("BUILD REVISION: "):
255 revid = l.split(":", 1)[1].strip()
256 elif l.startswith("BUILD COMMIT TIME"):
257 timestamp = l.split(":", 1)[1].strip()
261 return (revid, commit_revid, timestamp)
264 """get status of build
266 :return: tuple with build status
268 log = self.read_log()
270 err = self.read_err()
272 return build_status_from_logs(log, err)
279 """get status of build"""
280 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
283 err = util.FileLoad("%s.err" % file)
285 # File does not exist
288 return util.count_lines(err)
291 class CachingBuild(Build):
292 """Build subclass that caches some of the results that are expensive
295 def revision_details(self):
296 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
297 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
298 st1 = os.stat("%s.log" % file)
301 st2 = os.stat("%s.revision" % cachef)
303 # File does not exist
306 # the ctime/mtime asymmetry is needed so we don't get fooled by
307 # the mtime update from rsync
308 if st2 and st1.st_ctime <= st2.st_mtime:
309 (revid, commit_revid, timestamp) = util.FileLoad("%s.revision" % cachef).split(":", 2)
314 if commit_revid == "":
316 return (revid, commit_revid, timestamp)
317 (revid, commit_revid, timestamp) = super(CachingBuild, self).revision_details()
318 if not self._store.readonly:
319 util.FileSave("%s.revision" % cachef, "%s:%s:%s" % (revid, commit_revid or "", timestamp or ""))
320 return (revid, commit_revid, timestamp)
323 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
324 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
325 st1 = os.stat("%s.err" % file)
328 st2 = os.stat("%s.errcount" % cachef)
330 # File does not exist
333 if st2 and st1.st_ctime <= st2.st_mtime:
334 return util.FileLoad("%s.errcount" % cachef)
336 ret = super(CachingBuild, self).err_count()
338 if not self._store.readonly:
339 util.FileSave("%s.errcount" % cachef, str(ret))
344 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
345 cachefile = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)+".status"
347 st1 = os.stat("%s.log" % file)
350 st2 = os.stat(cachefile)
355 if st2 and st1.st_ctime <= st2.st_mtime:
356 return BuildStatus(*eval(util.FileLoad(cachefile)))
358 ret = super(CachingBuild, self).status()
360 if not self._store.readonly:
361 util.FileSave(cachefile, str(ret))
366 def read_trees_from_conf(path):
367 """Read trees from a configuration file."""
369 cfp = ConfigParser.ConfigParser()
370 cfp.readfp(open(path))
371 for s in cfp.sections():
372 ret[s] = Tree(name=s, **dict(cfp.items(s)))
376 class BuildResultStore(object):
377 """The build farm build result database."""
383 def __init__(self, basedir, readonly=False):
384 """Open the database.
386 :param basedir: Build result base directory
387 :param readonly: Whether to avoid saving cache files
389 self.basedir = basedir
390 check_dir_exists("base", self.basedir)
391 self.readonly = readonly
393 self.webdir = os.path.join(basedir, "web")
394 check_dir_exists("web", self.webdir)
396 self.datadir = os.path.join(basedir, "data")
397 check_dir_exists("data", self.datadir)
399 self.cachedir = os.path.join(basedir, "cache")
400 check_dir_exists("cache", self.cachedir)
402 self.lcovdir = os.path.join(basedir, "lcov/data")
403 check_dir_exists("lcov", self.lcovdir)
405 self.compilers = util.load_list(os.path.join(self.webdir, "compilers.list"))
407 self.trees = read_trees_from_conf(os.path.join(self.webdir, "trees.conf"))
409 def get_build(self, tree, host, compiler, rev=None):
410 logf = self.build_fname(tree, host, compiler, rev) + ".log"
411 if not os.path.exists(logf):
412 raise NoSuchBuildError(tree, host, compiler, rev)
413 return CachingBuild(self, tree, host, compiler, rev)
415 def cache_fname(self, tree, host, compiler, rev=None):
417 return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
419 return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
421 def build_fname(self, tree, host, compiler, rev=None):
422 """get the name of the build file"""
424 return os.path.join(self.datadir, "oldrevs/build.%s.%s.%s-%s" % (tree, host, compiler, rev))
425 return os.path.join(self.datadir, "upload/build.%s.%s.%s" % (tree, host, compiler))
427 def lcov_status(self, tree):
428 """get status of build"""
429 cachefile = os.path.join(self.cachedir, "lcov.%s.%s.status" % (
430 self.LCOVHOST, tree))
431 file = os.path.join(self.lcovdir, self.LCOVHOST, tree, "index.html")
435 # File does not exist
436 raise NoSuchBuildError(tree, self.LCOVHOST, "lcov")
438 st2 = os.stat(cachefile)
440 # file does not exist
443 if st2 and st1.st_ctime <= st2.st_mtime:
444 ret = util.FileLoad(cachefile)
449 lcov_html = util.FileLoad(file)
450 perc = lcov_extract_percentage(lcov_html)
456 util.FileSave(cachefile, ret)
459 def get_old_revs(self, tree, host, compiler):
460 """get a list of old builds and their status."""
462 directory = os.path.join(self.datadir, "oldrevs")
463 logfiles = [d for d in os.listdir(directory) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
465 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
468 stat = os.stat(os.path.join(directory, l))
469 # skip the current build
470 if stat.st_nlink == 2:
472 build = self.get_build(tree, host, compiler, rev)
474 "STATUS": build.status(),
476 "TIMESTAMP": build.age_ctime(),
480 ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
484 def has_host(self, host):
485 for name in os.listdir(os.path.join(self.datadir, "upload")):
487 if name.split(".")[2] == host:
493 def host_age(self, host):
494 """get the overall age of a host"""
495 # FIXME: Turn this into a simple SQL query, or use something in hostdb ?
497 for compiler in self.compilers:
498 for tree in self.trees:
500 build = self.get_build(tree, host, compiler)
501 except NoSuchBuildError:
504 ret = min(ret, build.age_mtime())