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.
33 class BuildStatus(object):
35 def __init__(self, stages, other_failures):
37 self.other_failures = other_failures
40 return repr((self.stages, self.other_failures))
42 def setcheckerstage(self, val):
45 def getcheckerstage(self):
48 def check_dir_exists(kind, path):
49 if not os.path.isdir(path):
50 raise Exception("%s directory %s does not exist" % (kind, path))
53 def build_status_from_logs(log, err):
54 """get status of build"""
55 m = re.search("TEST STATUS:(\s*\d+)", log)
57 tstatus = int(m.group(1).strip())
59 m = re.search("ACTION (PASSED|FAILED): test", log)
61 test_failures = len(re.findall("testsuite-(failure|error): ", log))
62 test_successes = len(re.findall("testsuite-success: ", log))
63 if test_successes > 0:
64 tstatus = test_failures
67 if m.group(1) == "FAILED" and tstatus == 0:
72 m = re.search("INSTALL STATUS:(\s*\d+)", log)
74 istatus = int(m.group(1).strip())
78 m = re.search("BUILD STATUS:(\s*\d+)", log)
80 bstatus = int(m.group(1).strip())
84 m = re.search("CONFIGURE STATUS:(\s*\d+)", log)
86 cstatus = int(m.group(1).strip())
90 other_failures = set()
91 m = re.search("(PANIC|INTERNAL ERROR):.*", log)
93 other_failures.add("panic")
95 if "No space left on device" in err or "No space left on device" in log:
96 other_failures.add("disk full")
98 if "maximum runtime exceeded" in log:
99 other_failures.add("timeout")
101 m = re.search("CC_CHECKER STATUS:(\s*\d+)", log)
103 sstatus = int(m.group(1).strip())
107 return BuildStatus([cstatus, bstatus, istatus, tstatus, sstatus], other_failures)
110 def lcov_extract_percentage(text):
111 m = re.search('\<td class="headerItem".*?\>Code\ \;covered\:\<\/td\>.*?\n.*?\<td class="headerValue".*?\>([0-9.]+) \%', text)
118 class NoSuchBuildError(Exception):
119 """The build with the specified name does not exist."""
121 def __init__(self, tree, host, compiler, rev=None):
124 self.compiler = compiler
129 """A tree to build."""
131 def __init__(self, name, scm, repo, branch, subdir="", srcdir=""):
141 return "<%s %r>" % (self.__class__.__name__, self.name)
145 """A single build of a tree on a particular host using a particular compiler.
148 def __init__(self, store, tree, host, compiler, rev=None):
152 self.compiler = compiler
156 # the mtime age is used to determine if builds are still happening
158 # the ctime age is used to determine when the last real build happened
161 """get the age of build from mtime"""
162 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
164 st = os.stat("%s.log" % file)
165 return time.time() - st.st_mtime
168 """get the age of build from ctime"""
169 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
171 st = os.stat("%s.log" % file)
172 return time.time() - st.st_ctime
175 """read full log file"""
176 f = open(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".log", "r")
183 """read full err file"""
184 return util.FileLoad(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".err")
186 def log_checksum(self):
187 return hashlib.sha1(self.read_log()).hexdigest()
189 def revision_details(self):
190 """get the revision of build
192 :return: Tuple with revision id and timestamp (if available)
194 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
198 f = open("%s.log" % file, 'r')
200 for l in f.readlines():
201 if l.startswith("BUILD COMMIT REVISION: "):
202 revid = l.split(":", 1)[1].strip()
203 elif l.startswith("BUILD REVISION: ") and not revid:
204 revid = l.split(":", 1)[1].strip()
205 elif l.startswith("BUILD COMMIT TIME"):
206 timestamp = l.split(":", 1)[1].strip()
210 return (revid, timestamp)
213 """get status of build
215 :return: tuple with build status
218 log = self.read_log()
219 err = self.read_err()
221 return build_status_from_logs(log, err)
224 """get status of build"""
225 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
228 err = util.FileLoad("%s.err" % file)
230 # File does not exist
233 return util.count_lines(err)
236 class CachingBuild(Build):
237 """Build subclass that caches some of the results that are expensive
240 def revision_details(self):
241 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
242 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
243 st1 = os.stat("%s.log" % file)
246 st2 = os.stat("%s.revision" % cachef)
248 # File does not exist
251 # the ctime/mtime asymmetry is needed so we don't get fooled by
252 # the mtime update from rsync
253 if st2 and st1.st_ctime <= st2.st_mtime:
254 (revid, timestamp) = util.FileLoad("%s.revision" % cachef).split(":", 1)
258 return (revid, timestamp)
259 (revid, timestamp) = super(CachingBuild, self).revision_details()
260 if not self._store.readonly:
261 util.FileSave("%s.revision" % cachef, "%s:%s" % (revid, timestamp or ""))
262 return (revid, timestamp)
265 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
266 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
267 st1 = os.stat("%s.err" % file)
270 st2 = os.stat("%s.errcount" % cachef)
272 # File does not exist
275 if st2 and st1.st_ctime <= st2.st_mtime:
276 return util.FileLoad("%s.errcount" % cachef)
278 ret = super(CachingBuild, self).err_count()
280 if not self._store.readonly:
281 util.FileSave("%s.errcount" % cachef, str(ret))
286 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
287 cachefile = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)+".status"
289 st1 = os.stat("%s.log" % file)
292 st2 = os.stat(cachefile)
297 if st2 and st1.st_ctime <= st2.st_mtime:
298 return BuildStatus(*eval(util.FileLoad(cachefile)))
300 ret = super(CachingBuild, self).status()
302 if not self._store.readonly:
303 util.FileSave(cachefile, str(ret))
309 def read_trees_from_conf(path):
310 """Read trees from a configuration file."""
312 cfp = ConfigParser.ConfigParser()
313 cfp.readfp(open(path))
314 for s in cfp.sections():
315 ret[s] = Tree(name=s, **dict(cfp.items(s)))
319 class BuildResultStore(object):
320 """The build farm build result database."""
326 def __init__(self, basedir, readonly=False):
327 """Open the database.
329 :param basedir: Build result base directory
330 :param readonly: Whether to avoid saving cache files
332 self.basedir = basedir
333 check_dir_exists("base", self.basedir)
334 self.readonly = readonly
336 self.webdir = os.path.join(basedir, "web")
337 check_dir_exists("web", self.webdir)
339 self.datadir = os.path.join(basedir, "data")
340 check_dir_exists("data", self.datadir)
342 self.cachedir = os.path.join(basedir, "cache")
343 check_dir_exists("cache", self.cachedir)
345 self.lcovdir = os.path.join(basedir, "lcov/data")
346 check_dir_exists("lcov", self.lcovdir)
348 self.compilers = util.load_list(os.path.join(self.webdir, "compilers.list"))
350 self.trees = read_trees_from_conf(os.path.join(self.webdir, "trees.conf"))
352 def get_build(self, tree, host, compiler, rev=None):
353 logf = self.build_fname(tree, host, compiler, rev) + ".log"
354 if not os.path.exists(logf):
355 raise NoSuchBuildError(tree, host, compiler, rev)
356 return CachingBuild(self, tree, host, compiler, rev)
358 def cache_fname(self, tree, host, compiler, rev=None):
360 return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
362 return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
364 def build_fname(self, tree, host, compiler, rev=None):
365 """get the name of the build file"""
367 return os.path.join(self.datadir, "oldrevs/build.%s.%s.%s-%s" % (tree, host, compiler, rev))
368 return os.path.join(self.datadir, "upload/build.%s.%s.%s" % (tree, host, compiler))
370 def lcov_status(self, tree):
371 """get status of build"""
372 cachefile = os.path.join(self.cachedir, "lcov.%s.%s.status" % (
373 self.LCOVHOST, tree))
374 file = os.path.join(self.lcovdir, self.LCOVHOST, tree, "index.html")
378 # File does not exist
379 raise NoSuchBuildError(tree, self.LCOVHOST, "lcov")
381 st2 = os.stat(cachefile)
383 # file does not exist
386 if st2 and st1.st_ctime <= st2.st_mtime:
387 ret = util.FileLoad(cachefile)
392 lcov_html = util.FileLoad(file)
393 perc = lcov_extract_percentage(lcov_html)
399 util.FileSave(cachefile, ret)
402 def get_old_revs(self, tree, host, compiler):
403 """get a list of old builds and their status."""
405 directory = os.path.join(self.datadir, "oldrevs")
406 logfiles = [d for d in os.listdir(directory) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
408 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
411 stat = os.stat(os.path.join(directory, l))
412 # skip the current build
413 if stat.st_nlink == 2:
415 build = self.get_build(tree, host, compiler, rev)
417 "STATUS": build.status(),
419 "TIMESTAMP": build.age_ctime(),
423 ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
427 def has_host(self, host):
428 for name in os.listdir(os.path.join(self.datadir, "upload")):
430 if name.split(".")[2] == host:
436 def host_age(self, host):
437 """get the overall age of a host"""
439 for compiler in self.compilers:
440 for tree in self.trees:
442 build = self.get_build(tree, host, compiler)
443 except NoSuchBuildError:
446 ret = min(ret, build.age_mtime())