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
34 def __init__(self, name):
39 class TestResult(object):
41 def __init__(self, build, test, result):
47 class BuildSummary(object):
49 def __init__(self, host, tree, compiler, revision, status):
52 self.compiler = compiler
53 self.revision = revision
57 BuildStageResult = collections.namedtuple("BuildStageResult", "name result")
60 class MissingRevisionInfo(Exception):
61 """Revision info could not be found in the build log."""
63 def __init__(self, build=None):
67 class LogFileMissing(Exception):
68 """Log file missing."""
71 class BuildStatus(object):
73 def __init__(self, stages=None, other_failures=None):
74 if stages is not None:
75 self.stages = [BuildStageResult(n, r) for (n, r) in stages]
78 if other_failures is not None:
79 self.other_failures = other_failures
81 self.other_failures = set()
85 if self.other_failures:
87 return not all([x.result == 0 for x in self.stages])
89 def __serialize__(self):
93 def __deserialize__(cls, text):
97 if self.other_failures:
98 return ",".join(self.other_failures)
99 return "/".join([str(x.result) for x in self.stages])
101 def broken_host(self):
102 if "disk full" in self.other_failures:
106 def regressed_since(self, older):
107 """Check if this build has regressed since another build."""
108 if "disk full" in self.other_failures:
110 if ("timeout" in self.other_failures and
111 "timeout" in older.other_failures):
112 # When the timeout happens exactly can differ slightly, so it's
113 # okay if the numbers are a bit different..
115 if ("panic" in self.other_failures and
116 not "panic" in older.other_failures):
118 if len(self.stages) < len(older.stages):
119 # Less stages completed
121 for ((old_name, old_result), (new_name, new_result)) in zip(
122 older.stages, self.stages):
123 assert old_name == new_name
124 if new_result > old_result:
128 def __cmp__(self, other):
129 other_extra = other.other_failures - self.other_failures
130 self_extra = self.other_failures - other.other_failures
131 # Give more importance to other failures
137 la = len(self.stages)
138 lb = len(other.stages)
144 return cmp(other.stages, self.stages)
147 return "%s(%r, %r)" % (self.__class__.__name__, self.stages, self.other_failures)
150 def check_dir_exists(kind, path):
151 if not os.path.isdir(path):
152 raise Exception("%s directory %s does not exist" % (kind, path))
155 def build_status_from_logs(log, err):
156 """get status of build"""
157 # FIXME: Perhaps also extract revision here?
165 re_status = re.compile("^([A-Z_]+) STATUS:(\s*\d+)$")
166 re_action = re.compile("^ACTION (PASSED|FAILED):\s+test$")
169 if l.startswith("No space left on device"):
170 ret.other_failures.add("disk full")
172 if "maximum runtime exceeded" in l: # Ugh.
173 ret.other_failures.add("timeout")
175 if l.startswith("PANIC:") or l.startswith("INTERNAL ERROR:"):
176 ret.other_failures.add("panic")
178 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
181 if l.startswith("testsuite-success: "):
184 m = re_status.match(l)
186 stages.append(BuildStageResult(m.group(1), int(m.group(2).strip())))
187 if m.group(1) == "TEST":
190 m = re_action.match(l)
191 if m and not test_seen:
192 if m.group(1) == "PASSED":
193 stages.append(BuildStageResult("TEST", 0))
195 stages.append(BuildStageResult("TEST", 1))
198 # Scan err file for specific errors
200 if "No space left on device" in l:
201 ret.other_failures.add("disk full")
204 if sr.name != "TEST":
207 if test_successes + test_failures == 0:
208 # No granular test output
209 return BuildStageResult("TEST", sr.result)
210 if sr.result == 1 and test_failures == 0:
211 ret.other_failures.add("inconsistent test result")
212 return BuildStageResult("TEST", -1)
213 return BuildStageResult("TEST", test_failures)
215 ret.stages = map(map_stage, stages)
219 def revision_from_log(log):
222 if l.startswith("BUILD COMMIT REVISION: "):
223 revid = l.split(":", 1)[1].strip()
225 raise MissingRevisionInfo()
229 class NoSuchBuildError(Exception):
230 """The build with the specified name does not exist."""
232 def __init__(self, tree, host, compiler, rev=None):
235 self.compiler = compiler
240 """A single build of a tree on a particular host using a particular compiler.
243 def __init__(self, basename, tree, host, compiler, rev=None):
244 self.basename = basename
247 self.compiler = compiler
250 def __cmp__(self, other):
252 (self.upload_time, self.revision, self.host, self.tree, self.compiler),
253 (other.upload_time, other.revision, other.host, other.tree, other.compiler))
255 def __eq__(self, other):
256 return (isinstance(other, Build) and
257 self.log_checksum() == other.log_checksum())
260 if self.revision is not None:
261 return "<%s: revision %s of %s on %s using %s>" % (self.__class__.__name__, self.revision, self.tree, self.host, self.compiler)
263 return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
265 def remove_logs(self):
266 # In general, basename.log should *always* exist.
267 if os.path.exists(self.basename+".log"):
268 os.unlink(self.basename + ".log")
269 if os.path.exists(self.basename+".err"):
270 os.unlink(self.basename+".err")
276 def upload_time(self):
277 """get timestamp of build"""
278 st = os.stat("%s.log" % self.basename)
283 """get the age of build"""
284 return time.time() - self.upload_time
287 """read full log file"""
289 return open(self.basename+".log", "r")
291 raise LogFileMissing()
294 """read full err file"""
296 return open(self.basename+".err", 'r')
301 def log_checksum(self):
304 return hashlib.sha1(f.read()).hexdigest()
309 revid = self.revision_details()
310 status = self.status()
311 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
313 def revision_details(self):
314 """get the revision of build
320 return revision_from_log(f)
325 """get status of build
327 :return: tuple with build status
329 log = self.read_log()
331 err = self.read_err()
333 return build_status_from_logs(log, err)
340 """get status of build"""
341 file = self.read_err()
342 return len(file.readlines())
345 class UploadBuildResultStore(object):
347 def __init__(self, path):
348 """Open the database.
350 :param path: Build result base directory
354 def get_all_builds(self):
355 for name in os.listdir(self.path):
357 (build, tree, host, compiler, extension) = name.split(".")
360 if build != "build" or extension != "log":
362 yield self.get_build(tree, host, compiler)
364 def build_fname(self, tree, host, compiler):
365 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
367 def has_host(self, host):
368 for name in os.listdir(self.path):
370 if name.split(".")[2] == host:
376 def get_build(self, tree, host, compiler):
377 basename = self.build_fname(tree, host, compiler)
378 logf = "%s.log" % basename
379 if not os.path.exists(logf):
380 raise NoSuchBuildError(tree, host, compiler)
381 return Build(basename, tree, host, compiler)
384 class BuildResultStore(object):
385 """The build farm build result database."""
387 def __init__(self, path):
388 """Open the database.
390 :param path: Build result base directory
394 def __contains__(self, build):
399 rev = build.revision_details()
400 self.get_build(build.tree, build.host, build.compiler, rev)
401 except NoSuchBuildError:
406 def get_build(self, tree, host, compiler, rev, checksum=None):
407 basename = self.build_fname(tree, host, compiler, rev)
408 logf = "%s.log" % basename
409 if not os.path.exists(logf):
410 raise NoSuchBuildError(tree, host, compiler, rev)
411 return Build(basename, tree, host, compiler, rev)
413 def build_fname(self, tree, host, compiler, rev):
414 """get the name of the build file"""
415 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
417 def get_all_builds(self):
418 for l in os.listdir(self.path):
419 m = re.match("^build\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)-([0-9A-Fa-f]+).log$", l)
424 compiler = m.group(3)
426 stat = os.stat(os.path.join(self.path, l))
427 # skip the current build
428 if stat.st_nlink == 2:
430 yield self.get_build(tree, host, compiler, rev)
432 def get_old_builds(self, tree, host, compiler):
433 """get a list of old builds and their status."""
435 for build in self.get_all_builds():
436 if build.tree == tree and build.host == host and build.compiler == compiler:
438 ret.sort(lambda a, b: cmp(a.upload_time, b.upload_time))
441 def upload_build(self, build):
442 rev = build.revision_details()
444 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
446 existing_build = self.get_build(build.tree, build.host, build.compiler, rev)
447 except NoSuchBuildError:
448 if os.path.exists(new_basename+".log"):
449 os.remove(new_basename+".log")
450 if os.path.exists(new_basename+".err"):
451 os.remove(new_basename+".err")
453 existing_build.remove_logs()
454 os.link(build.basename+".log", new_basename+".log")
455 if os.path.exists(build.basename+".err"):
456 os.link(build.basename+".err", new_basename+".err")
457 return Build(new_basename, build.tree, build.host, build.compiler, rev)
459 def get_previous_revision(self, tree, host, compiler, revision):
460 raise NoSuchBuildError(tree, host, compiler, revision)
462 def get_latest_revision(self, tree, host, compiler):
463 raise NoSuchBuildError(tree, host, compiler)