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 def check_dir_exists(kind, path):
33 if not os.path.isdir(path):
34 raise Exception("%s directory %s does not exist" % (kind, path))
37 def build_status_from_logs(log, err):
38 """get status of build"""
39 m = re.search("TEST STATUS:(.*)", log)
43 m = re.search("ACTION (PASSED|FAILED): test", log)
45 test_failures = len(re.findall("testsuite-(failure|error): ", log))
46 test_successes = len(re.findall("testsuite-success: ", log))
47 if test_successes > 0:
48 tstatus = test_failures
54 m = re.search("INSTALL STATUS:(.*)", log)
60 m = re.search("BUILD STATUS:(.*)", log)
66 m = re.search("CONFIGURE STATUS:(.*)", log)
72 other_failures = set()
73 m = re.search("(PANIC|INTERNAL ERROR):.*", log)
75 other_failures.add("panic")
77 if "No space left on device" in err or "No space left on device" in log:
78 other_failures.add("disk full")
80 if "maximum runtime exceeded" in log:
81 other_failures.add("timeout")
83 m = re.search("CC_CHECKER STATUS: (.*)", log)
89 return {"config": cstatus, "build": bstatus, "install": istatus,\
90 "test": tstatus, "checker": sstatus, "other": other_failures}
93 def lcov_extract_percentage(text):
94 m = re.search('\<td class="headerItem".*?\>Code\ \;covered\:\<\/td\>.*?\n.*?\<td class="headerValue".*?\>([0-9.]+) \%', text)
101 class NoSuchBuildError(Exception):
102 """The build with the specified name does not exist."""
104 def __init__(self, tree, host, compiler, rev=None):
107 self.compiler = compiler
112 """A tree to build."""
114 def __init__(self, name, scm, repo, branch, subdir="", srcdir=""):
124 return "<%s %r>" % (self.__class__.__name__, self.name)
128 """A single build of a tree on a particular host using a particular compiler.
131 def __init__(self, store, tree, host, compiler, rev=None):
135 self.compiler = compiler
139 # the mtime age is used to determine if builds are still happening
141 # the ctime age is used to determine when the last real build happened
144 """get the age of build from mtime"""
145 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
147 st = os.stat("%s.log" % file)
148 return time.time() - st.st_mtime
151 """get the age of build from ctime"""
152 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
154 st = os.stat("%s.log" % file)
155 return time.time() - st.st_ctime
158 """read full log file"""
159 f = open(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".log", "r")
166 """read full err file"""
167 return util.FileLoad(self._store.build_fname(self.tree, self.host, self.compiler, self.rev)+".err")
169 def revision_details(self):
170 """get the revision of build
172 :return: Tuple with revision id and timestamp (if available)
174 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
178 f = open("%s.log" % file, 'r')
180 for l in f.readlines():
181 if l.startswith("BUILD COMMIT REVISION: "):
182 revid = l.split(":", 1)[1].strip()
183 elif l.startswith("BUILD REVISION: "):
184 revid = l.split(":", 1)[1].strip()
185 elif l.startswith("BUILD COMMIT TIME"):
186 timestamp = l.split(":", 1)[1].strip()
190 return (revid, timestamp)
193 """get status of build
195 :return: tuple with build status
198 log = self.read_log()
199 err = self.read_err()
201 return build_status_from_logs(log, err)
204 """get status of build"""
205 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
208 err = util.FileLoad("%s.err" % file)
210 # File does not exist
213 return util.count_lines(err)
216 class CachingBuild(Build):
217 """Build subclass that caches some of the results that are expensive
220 def revision_details(self):
221 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
222 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
223 st1 = os.stat("%s.log" % file)
226 st2 = os.stat("%s.revision" % cachef)
228 # File does not exist
231 # the ctime/mtime asymmetry is needed so we don't get fooled by
232 # the mtime update from rsync
233 if st2 and st1.st_ctime <= st2.st_mtime:
234 (revid, timestamp) = util.FileLoad("%s.revision" % cachef).split(":", 1)
238 return (revid, timestamp)
239 (revid, timestamp) = super(CachingBuild, self).revision_details()
240 if not self._store.readonly:
241 util.FileSave("%s.revision" % cachef, "%s:%s" % (revid, timestamp or ""))
242 return (revid, timestamp)
245 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
246 cachef = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)
247 st1 = os.stat("%s.err" % file)
250 st2 = os.stat("%s.errcount" % cachef)
252 # File does not exist
255 if st2 and st1.st_ctime <= st2.st_mtime:
256 return util.FileLoad("%s.errcount" % cachef)
258 ret = super(CachingBuild, self).err_count()
260 if not self._store.readonly:
261 util.FileSave("%s.errcount" % cachef, str(ret))
265 def unmarshall_status(self, cnt):
266 tab = cnt.split('\n')
269 tab2 = l.split(':', 1)
270 if tab2[0] == "other":
273 tab3 = tab2[1].split('%')
276 if tab2[1] == "None":
277 hash[tab2[0]] = tab2[1]
283 def marshall_status(self, val):
287 tab.append("%s:%s" % (k, val[k]))
289 tab.append("%s:%s" % (k, "%".join(val[k])))
291 return "\n".join(tab)
294 file = self._store.build_fname(self.tree, self.host, self.compiler, self.rev)
295 cachefile = self._store.cache_fname(self.tree, self.host, self.compiler, self.rev)+".status"
297 st1 = os.stat("%s.log" % file)
300 st2 = os.stat(cachefile)
305 if st2 and st1.st_ctime <= st2.st_mtime:
306 cnt = util.FileLoad(cachefile)
307 return self.unmarshall_status(cnt)
309 ret = super(CachingBuild, self).status()
311 if not self._store.readonly:
312 cnt = self.marshall_status(ret)
313 util.FileSave(cachefile, cnt)
319 def read_trees_from_conf(path):
320 """Read trees from a configuration file."""
322 cfp = ConfigParser.ConfigParser()
323 cfp.readfp(open(path))
324 for s in cfp.sections():
325 ret[s] = Tree(name=s, **dict(cfp.items(s)))
329 class BuildResultStore(object):
330 """The build farm build result database."""
336 def __init__(self, basedir, readonly=False):
337 """Open the database.
339 :param basedir: Build result base directory
340 :param readonly: Whether to avoid saving cache files
342 self.basedir = basedir
343 check_dir_exists("base", self.basedir)
344 self.readonly = readonly
346 self.webdir = os.path.join(basedir, "web")
347 check_dir_exists("web", self.webdir)
349 self.datadir = os.path.join(basedir, "data")
350 check_dir_exists("data", self.datadir)
352 self.cachedir = os.path.join(basedir, "cache")
353 check_dir_exists("cache", self.cachedir)
355 self.lcovdir = os.path.join(basedir, "lcov/data")
356 check_dir_exists("lcov", self.lcovdir)
358 self.compilers = util.load_list(os.path.join(self.webdir, "compilers.list"))
359 self.hosts = util.load_hash(os.path.join(self.webdir, "hosts.list"))
361 self.trees = read_trees_from_conf(os.path.join(self.webdir, "trees.conf"))
363 def get_build(self, tree, host, compiler, rev=None):
364 logf = self.build_fname(tree, host, compiler, rev) + ".log"
365 if not os.path.exists(logf):
366 raise NoSuchBuildError(tree, host, compiler, rev)
367 return CachingBuild(self, tree, host, compiler, rev)
369 def cache_fname(self, tree, host, compiler, rev=None):
371 return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
373 return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree, host, compiler))
375 def build_fname(self, tree, host, compiler, rev=None):
376 """get the name of the build file"""
378 return os.path.join(self.datadir, "oldrevs/build.%s.%s.%s-%s" % (tree, host, compiler, rev))
379 return os.path.join(self.datadir, "upload/build.%s.%s.%s" % (tree, host, compiler))
381 def lcov_status(self, tree):
382 """get status of build"""
383 cachefile = os.path.join(self.cachedir, "lcov.%s.%s.status" % (
384 self.LCOVHOST, tree))
385 file = os.path.join(self.lcovdir, self.LCOVHOST, tree, "index.html")
389 # File does not exist
390 raise NoSuchBuildError(tree, self.LCOVHOST, "lcov")
392 st2 = os.stat(cachefile)
394 # file does not exist
397 if st2 and st1.st_ctime <= st2.st_mtime:
398 ret = util.FileLoad(cachefile)
403 lcov_html = util.FileLoad(file)
404 perc = lcov_extract_percentage(lcov_html)
410 util.FileSave(cachefile, ret)
413 def get_old_revs(self, tree, host, compiler):
414 """get a list of old builds and their status."""
416 directory = os.path.join(self.datadir, "oldrevs")
417 logfiles = [d for d in os.listdir(directory) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
419 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
422 stat = os.stat(os.path.join(directory, l))
423 # skip the current build
424 if stat.st_nlink == 2:
426 build = self.get_build(tree, host, compiler, rev)
428 "STATUS": build.status(),
430 "TIMESTAMP": build.age_ctime(),
434 ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
438 def has_host(self, host):
439 for name in os.listdir(os.path.join(self.datadir, "upload")):
441 if name.split(".")[2] == host:
447 def host_age(self, host):
448 """get the overall age of a host"""
450 for compiler in self.compilers:
451 for tree in self.trees:
453 build = self.get_build(tree, host, compiler)
454 except NoSuchBuildError:
457 ret = min(ret, build.age_mtime())