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.
32 class BuildStatus(object):
34 def __init__(self, stages, other_failures):
36 self.other_failures = other_failures
39 return repr((self.stages, self.other_failures))
42 def check_dir_exists(kind, path):
43 if not os.path.isdir(path):
44 raise Exception("%s directory %s does not exist" % (kind, path))
47 def build_status_from_logs(log, err):
48 """get status of build"""
49 m = re.search("TEST STATUS:(\s*\d+)", log)
51 tstatus = int(m.group(1).strip())
53 m = re.search("ACTION (PASSED|FAILED): test", log)
55 test_failures = len(re.findall("testsuite-(failure|error): ", log))
56 test_successes = len(re.findall("testsuite-success: ", log))
57 if test_successes > 0:
58 tstatus = test_failures
61 if m.group(1) == "FAILED" and tstatus == 0:
66 m = re.search("INSTALL STATUS:(\s*\d+)", log)
68 istatus = int(m.group(1).strip())
72 m = re.search("BUILD STATUS:(\s*\d+)", log)
74 bstatus = int(m.group(1).strip())
78 m = re.search("CONFIGURE STATUS:(\s*\d+)", log)
80 cstatus = int(m.group(1).strip())
84 other_failures = set()
85 m = re.search("(PANIC|INTERNAL ERROR):.*", log)
87 other_failures.add("panic")
89 if "No space left on device" in err or "No space left on device" in log:
90 other_failures.add("disk full")
92 if "maximum runtime exceeded" in log:
93 other_failures.add("timeout")
95 m = re.search("CC_CHECKER STATUS:(\s*\d+)", log)
97 sstatus = int(m.group(1).strip())
101 return BuildStatus((cstatus, bstatus, istatus, tstatus, sstatus), other_failures)
104 def lcov_extract_percentage(text):
105 m = re.search('\<td class="headerItem".*?\>Code\ \;covered\:\<\/td\>.*?\n.*?\<td class="headerValue".*?\>([0-9.]+) \%', text)
112 class NoSuchBuildError(Exception):
113 """The build with the specified name does not exist."""
115 def __init__(self, tree, host, compiler, rev=None):
118 self.compiler = compiler
123 """A tree to build."""
125 def __init__(self, name, scm, repo, branch, subdir="", srcdir=""):
135 return "<%s %r>" % (self.__class__.__name__, self.name)
139 """A single build of a tree on a particular host using a particular compiler.
142 def __init__(self, store, tree, host, compiler, rev=None):
146 self.compiler = compiler
150 # the mtime age is used to determine if builds are still happening
152 # the ctime age is used to determine when the last real build happened
155 """get the age of build from mtime"""
156 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
158 st = os.stat("%s.log" % file)
159 return time.time() - st.st_mtime
162 """get the age of build from ctime"""
163 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
165 st = os.stat("%s.log" % file)
166 return time.time() - st.st_ctime
169 """read full log file"""
170 f = open(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".log", "r")
177 """read full err file"""
178 return util.FileLoad(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".err")
180 def revision_details(self):
181 """get the revision of build
183 :return: Tuple with revision id and timestamp (if available)
185 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
189 f = open("%s.log" % file, 'r')
191 for l in f.readlines():
192 if l.startswith("BUILD COMMIT REVISION: "):
193 revid = l.split(":", 1)[1].strip()
194 elif l.startswith("BUILD REVISION: "):
195 revid = l.split(":", 1)[1].strip()
196 elif l.startswith("BUILD COMMIT TIME"):
197 timestamp = l.split(":", 1)[1].strip()
201 return (revid, timestamp)
204 """get status of build
206 :return: tuple with build status
209 log = self.read_log()
210 err = self.read_err()
212 return build_status_from_logs(log, err)
215 """get status of build"""
216 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
219 err = util.FileLoad("%s.err" % file)
221 # File does not exist
224 return util.count_lines(err)
227 class CachingBuild(Build):
228 """Build subclass that caches some of the results that are expensive
231 def revision_details(self):
232 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
233 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
234 st1 = os.stat("%s.log" % file)
237 st2 = os.stat("%s.revision" % cachef)
239 # File does not exist
242 # the ctime/mtime asymmetry is needed so we don't get fooled by
243 # the mtime update from rsync
244 if st2 and st1.st_ctime <= st2.st_mtime:
245 (revid, timestamp) = util.FileLoad("%s.revision" % cachef).split(":", 1)
249 return (revid, timestamp)
250 (revid, timestamp) = super(CachingBuild, self).revision_details()
251 if not self._store.readonly:
252 util.FileSave("%s.revision" % cachef, "%s:%s" % (revid, timestamp or ""))
253 return (revid, timestamp)
256 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
257 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
258 st1 = os.stat("%s.err" % file)
261 st2 = os.stat("%s.errcount" % cachef)
263 # File does not exist
266 if st2 and st1.st_ctime <= st2.st_mtime:
267 return util.FileLoad("%s.errcount" % cachef)
269 ret = super(CachingBuild, self).err_count()
271 if not self._store.readonly:
272 util.FileSave("%s.errcount" % cachef, str(ret))
277 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
278 cachefile = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)+".status"
280 st1 = os.stat("%s.log" % file)
283 st2 = os.stat(cachefile)
288 if st2 and st1.st_ctime <= st2.st_mtime:
289 return BuildStatus(*eval(util.FileLoad(cachefile)))
291 ret = super(CachingBuild, self).status()
293 if not self._store.readonly:
294 util.FileSave(cachefile, str(ret))
300 def read_trees_from_conf(path):
301 """Read trees from a configuration file."""
303 cfp = ConfigParser.ConfigParser()
304 cfp.readfp(open(path))
305 for s in cfp.sections():
306 ret[s] = Tree(name=s, **dict(cfp.items(s)))
310 class BuildResultStore(object):
311 """The build farm build result database."""
317 def __init__(self, basedir, readonly=False):
318 """Open the database.
320 :param basedir: Build result base directory
321 :param readonly: Whether to avoid saving cache files
323 self.basedir = basedir
324 check_dir_exists("base", self.basedir)
325 self.readonly = readonly
327 self.webdir = os.path.join(basedir, "web")
328 check_dir_exists("web", self.webdir)
330 self.datadir = os.path.join(basedir, "data")
331 check_dir_exists("data", self.datadir)
333 self.cachedir = os.path.join(basedir, "cache")
334 check_dir_exists("cache", self.cachedir)
336 self.lcovdir = os.path.join(basedir, "lcov/data")
337 check_dir_exists("lcov", self.lcovdir)
339 self.compilers = util.load_list(os.path.join(self.webdir, "compilers.list"))
341 self.trees = read_trees_from_conf(os.path.join(self.webdir, "trees.conf"))
343 def get_build(self, tree, host, compiler, rev=None):
344 logf = self.build_fname(tree, host, compiler, rev) + ".log"
345 if not os.path.exists(logf):
346 raise NoSuchBuildError(tree, host, compiler, rev)
347 return CachingBuild(self, tree, host, compiler, rev)
349 def cache_fname(self, tree, host, compiler, rev=None):
351 return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
353 return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
355 def build_fname(self, tree, host, compiler, rev=None):
356 """get the name of the build file"""
358 return os.path.join(self.datadir, "oldrevs/build.%s.%s.%s-%s" % (tree, host, compiler, rev))
359 return os.path.join(self.datadir, "upload/build.%s.%s.%s" % (tree, host, compiler))
361 def lcov_status(self, tree):
362 """get status of build"""
363 cachefile = os.path.join(self.cachedir, "lcov.%s.%s.status" % (
364 self.LCOVHOST, tree))
365 file = os.path.join(self.lcovdir, self.LCOVHOST, tree, "index.html")
369 # File does not exist
370 raise NoSuchBuildError(tree, self.LCOVHOST, "lcov")
372 st2 = os.stat(cachefile)
374 # file does not exist
377 if st2 and st1.st_ctime <= st2.st_mtime:
378 ret = util.FileLoad(cachefile)
383 lcov_html = util.FileLoad(file)
384 perc = lcov_extract_percentage(lcov_html)
390 util.FileSave(cachefile, ret)
393 def get_old_revs(self, tree, host, compiler):
394 """get a list of old builds and their status."""
396 directory = os.path.join(self.datadir, "oldrevs")
397 logfiles = [d for d in os.listdir(directory) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
399 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
402 stat = os.stat(os.path.join(directory, l))
403 # skip the current build
404 if stat.st_nlink == 2:
406 build = self.get_build(tree, host, compiler, rev)
408 "STATUS": build.status(),
410 "TIMESTAMP": build.age_ctime(),
414 ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
418 def has_host(self, host):
419 for name in os.listdir(os.path.join(self.datadir, "upload")):
421 if name.split(".")[2] == host:
427 def host_age(self, host):
428 """get the overall age of a host"""
430 for compiler in self.compilers:
431 for tree in self.trees:
433 build = self.get_build(tree, host, compiler)
434 except NoSuchBuildError:
437 ret = min(ret, build.age_mtime())