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.
25 from cStringIO import StringIO
30 from storm.locals import Int, RawStr
31 from storm.store import Store
32 from storm.expr import Desc
36 def open_opt_compressed_file(path):
38 return bz2.BZ2File(path+".bz2", 'r')
40 return open(path, 'r')
45 def __init__(self, name):
50 class TestResult(object):
52 def __init__(self, build, test, result):
58 class BuildSummary(object):
60 def __init__(self, host, tree, compiler, revision, status):
63 self.compiler = compiler
64 self.revision = revision
68 BuildStageResult = collections.namedtuple("BuildStageResult", "name result")
71 class MissingRevisionInfo(Exception):
72 """Revision info could not be found in the build log."""
74 def __init__(self, build=None):
78 class LogFileMissing(Exception):
79 """Log file missing."""
82 class BuildStatus(object):
84 def __init__(self, stages=None, other_failures=None):
85 if stages is not None:
86 self.stages = [BuildStageResult(n, r) for (n, r) in stages]
89 if other_failures is not None:
90 self.other_failures = other_failures
92 self.other_failures = set()
96 if self.other_failures:
98 return not all([x.result == 0 for x in self.stages])
100 def __serialize__(self):
104 def __deserialize__(cls, text):
108 if self.other_failures:
109 return ",".join(self.other_failures)
110 return "/".join([str(x.result) for x in self.stages])
112 def broken_host(self):
113 if "disk full" in self.other_failures:
117 def regressed_since(self, older):
118 """Check if this build has regressed since another build."""
119 if "disk full" in self.other_failures:
121 if ("timeout" in self.other_failures and
122 "timeout" in older.other_failures):
123 # When the timeout happens exactly can differ slightly, so it's
124 # okay if the numbers are a bit different..
126 if ("panic" in self.other_failures and
127 not "panic" in older.other_failures):
128 # If this build introduced panics, then that's always worse.
130 if len(self.stages) < len(older.stages):
131 # Less stages completed
133 old_stages = dict(older.stages)
134 new_stages = dict(self.stages)
135 for name, new_result in new_stages.iteritems():
137 old_result = old_stages[name]
140 if new_result == old_result:
142 if new_result < 0 and old_result >= 0:
144 elif new_result >= 0 and old_result < 0:
147 return (abs(new_result) > abs(old_result))
150 def __cmp__(self, other):
151 other_extra = other.other_failures - self.other_failures
152 self_extra = self.other_failures - other.other_failures
153 # Give more importance to other failures
159 la = len(self.stages)
160 lb = len(other.stages)
166 return cmp(other.stages, self.stages)
169 return "%s(%r, %r)" % (self.__class__.__name__, self.stages, self.other_failures)
172 def check_dir_exists(kind, path):
173 if not os.path.isdir(path):
174 raise Exception("%s directory %s does not exist" % (kind, path))
177 def extract_phase_output(f):
180 re_action = re.compile("^ACTION (PASSED|FAILED):\s+(.*)$")
182 if l.startswith("Running action "):
183 name = l[len("Running action "):].strip()
186 m = re_action.match(l)
188 assert name == m.group(2).strip(), "%r != %r" % (name, m.group(2))
192 elif output is not None:
196 def extract_test_output(f):
197 for name, output in extract_phase_output(f):
203 def build_status_from_logs(log, err):
204 """get status of build"""
205 # FIXME: Perhaps also extract revision here?
213 re_status = re.compile("^([A-Z_]+) STATUS:(\s*\d+)$")
214 re_action = re.compile("^ACTION (PASSED|FAILED):\s+test$")
217 if l.startswith("No space left on device"):
218 ret.other_failures.add("disk full")
220 if "Maximum time expired in timelimit" in l: # Ugh.
221 ret.other_failures.add("timeout")
223 if "maximum runtime exceeded" in l: # Ugh.
224 ret.other_failures.add("timeout")
226 if l.startswith("PANIC:") or l.startswith("INTERNAL ERROR:"):
227 ret.other_failures.add("panic")
229 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
232 if l.startswith("testsuite-success: "):
235 m = re_status.match(l)
237 stages.append(BuildStageResult(m.group(1), int(m.group(2).strip())))
238 if m.group(1) == "TEST":
241 m = re_action.match(l)
242 if m and not test_seen:
243 if m.group(1) == "PASSED":
244 stages.append(BuildStageResult("TEST", 0))
246 stages.append(BuildStageResult("TEST", 1))
249 # Scan err file for specific errors
251 if "No space left on device" in l:
252 ret.other_failures.add("disk full")
255 if sr.name != "TEST":
258 if test_successes + test_failures == 0:
259 # No granular test output
260 return BuildStageResult("TEST", sr.result)
261 if sr.result == 1 and test_failures == 0:
262 ret.other_failures.add("inconsistent test result")
263 return BuildStageResult("TEST", -1)
264 return BuildStageResult("TEST", test_failures)
266 ret.stages = map(map_stage, stages)
270 def revision_from_log(log):
273 if l.startswith("BUILD COMMIT REVISION: "):
274 revid = l.split(":", 1)[1].strip()
276 raise MissingRevisionInfo()
280 class NoSuchBuildError(Exception):
281 """The build with the specified name does not exist."""
283 def __init__(self, tree, host, compiler, rev=None):
286 self.compiler = compiler
290 class NoTestOutput(Exception):
291 """The build did not have any associated test output."""
295 """A single build of a tree on a particular host using a particular compiler.
298 def __init__(self, basename, tree, host, compiler, rev=None):
299 self.basename = basename
302 self.compiler = compiler
305 def __cmp__(self, other):
307 (self.upload_time, self.revision, self.host, self.tree, self.compiler),
308 (other.upload_time, other.revision, other.host, other.tree, other.compiler))
310 def __eq__(self, other):
311 return (isinstance(other, Build) and
312 self.log_checksum() == other.log_checksum())
315 if self.revision is not None:
316 return "<%s: revision %s of %s on %s using %s>" % (self.__class__.__name__, self.revision, self.tree, self.host, self.compiler)
318 return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
320 def remove_logs(self):
321 # In general, basename.log should *always* exist.
322 if os.path.exists(self.basename+".log"):
323 os.unlink(self.basename + ".log")
324 if os.path.exists(self.basename+".err"):
325 os.unlink(self.basename+".err")
331 def upload_time(self):
332 """get timestamp of build"""
333 st = os.stat("%s.log" % self.basename)
338 """get the age of build"""
339 return time.time() - self.upload_time
341 def read_subunit(self):
342 """read the test output as subunit"""
343 return StringIO("".join(extract_test_output(self.read_log())))
346 """read full log file"""
348 return open_opt_compressed_file(self.basename+".log")
350 raise LogFileMissing()
353 """read full err file"""
355 return open_opt_compressed_file(self.basename+".err")
360 def log_checksum(self):
363 return hashlib.sha1(f.read()).hexdigest()
368 revid = self.revision_details()
369 status = self.status()
370 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
372 def revision_details(self):
373 """get the revision of build
379 return revision_from_log(f)
384 """get status of build
386 :return: tuple with build status
388 log = self.read_log()
390 err = self.read_err()
392 return build_status_from_logs(log, err)
399 """get status of build"""
400 file = self.read_err()
401 return len(file.readlines())
404 class UploadBuildResultStore(object):
406 def __init__(self, path):
407 """Open the database.
409 :param path: Build result base directory
413 def get_all_builds(self):
414 for name in os.listdir(self.path):
416 (build, tree, host, compiler, extension) = name.split(".")
419 if build != "build" or extension != "log":
421 yield self.get_build(tree, host, compiler)
423 def build_fname(self, tree, host, compiler):
424 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
426 def has_host(self, host):
427 for name in os.listdir(self.path):
429 if name.split(".")[2] == host:
435 def get_build(self, tree, host, compiler):
436 basename = self.build_fname(tree, host, compiler)
437 logf = "%s.log" % basename
438 if not os.path.exists(logf):
439 raise NoSuchBuildError(tree, host, compiler)
440 return Build(basename, tree, host, compiler)
443 class StormBuild(Build):
444 __storm_table__ = "build"
446 id = Int(primary=True)
452 upload_time = Int(name="age")
453 status_str = RawStr(name="status")
460 return BuildStatus.__deserialize__(self.status_str)
462 def revision_details(self):
465 def log_checksum(self):
469 super(StormBuild, self).remove()
470 Store.of(self).remove(self)
472 def remove_logs(self):
473 super(StormBuild, self).remove_logs()
477 class BuildResultStore(object):
478 """The build farm build result database."""
480 def __init__(self, basedir, store=None):
481 from buildfarm.sqldb import memory_store
483 store = memory_store()
488 def __contains__(self, build):
490 self.get_by_checksum(build.log_checksum())
492 except NoSuchBuildError:
495 def get_build(self, tree, host, compiler, revision=None, checksum=None):
496 from buildfarm.sqldb import Cast
498 Cast(StormBuild.tree, "TEXT") == Cast(tree, "TEXT"),
499 Cast(StormBuild.host, "TEXT") == Cast(host, "TEXT"),
500 Cast(StormBuild.compiler, "TEXT") == Cast(compiler, "TEXT"),
502 if revision is not None:
503 expr.append(Cast(StormBuild.revision, "TEXT") == Cast(revision, "TEXT"))
504 if checksum is not None:
505 expr.append(Cast(StormBuild.checksum, "TEXT") == Cast(checksum, "TEXT"))
506 result = self.store.find(StormBuild, *expr).order_by(Desc(StormBuild.upload_time))
509 raise NoSuchBuildError(tree, host, compiler, revision)
512 def build_fname(self, tree, host, compiler, rev):
513 """get the name of the build file"""
514 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
516 def get_all_builds(self):
517 for l in os.listdir(self.path):
518 m = re.match("^build\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)-([0-9A-Fa-f]+).log$", l)
523 compiler = m.group(3)
525 stat = os.stat(os.path.join(self.path, l))
526 # skip the current build
527 if stat.st_nlink == 2:
529 yield self.get_build(tree, host, compiler, rev)
531 def get_old_builds(self, tree, host, compiler):
532 result = self.store.find(StormBuild,
533 StormBuild.tree == tree,
534 StormBuild.host == host,
535 StormBuild.compiler == compiler)
536 return result.order_by(Desc(StormBuild.upload_time))
538 def upload_build(self, build):
539 from buildfarm.sqldb import Cast, StormHost
541 existing_build = self.get_by_checksum(build.log_checksum())
542 except NoSuchBuildError:
546 assert build.tree == existing_build.tree
547 assert build.host == existing_build.host
548 assert build.compiler == existing_build.compiler
549 return existing_build
550 rev = build.revision_details()
552 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
553 for name in os.listdir(self.path):
554 p = os.path.join(self.path, name)
555 if p.startswith(new_basename+"."):
557 os.link(build.basename+".log", new_basename+".log")
558 if os.path.exists(build.basename+".err"):
559 os.link(build.basename+".err", new_basename+".err")
560 new_build = StormBuild(new_basename, build.tree, build.host,
562 new_build.checksum = build.log_checksum()
563 new_build.upload_time = build.upload_time
564 new_build.status_str = build.status().__serialize__()
565 new_build.basename = new_basename
566 host = self.store.find(StormHost,
567 Cast(StormHost.name, "TEXT") == Cast(build.host, "TEXT")).one()
568 assert host is not None, "Unable to find host %r" % build.host
569 new_build.host_id = host.id
570 self.store.add(new_build)
573 def get_by_checksum(self, checksum):
574 from buildfarm.sqldb import Cast
575 result = self.store.find(StormBuild,
576 Cast(StormBuild.checksum, "TEXT") == Cast(checksum, "TEXT")).order_by(Desc(StormBuild.upload_time))
579 raise NoSuchBuildError(None, None, None, None)
582 def get_previous_build(self, tree, host, compiler, revision):
583 from buildfarm.sqldb import Cast
584 cur_build = self.get_build(tree, host, compiler, revision)
586 result = self.store.find(StormBuild,
587 Cast(StormBuild.tree, "TEXT") == Cast(tree, "TEXT"),
588 Cast(StormBuild.host, "TEXT") == Cast(host, "TEXT"),
589 Cast(StormBuild.compiler, "TEXT") == Cast(compiler, "TEXT"),
590 Cast(StormBuild.revision, "TEXT") != Cast(revision, "TEXT"),
591 StormBuild.id < cur_build.id)
592 result = result.order_by(Desc(StormBuild.id))
593 prev_build = result.first()
594 if prev_build is None:
595 raise NoSuchBuildError(tree, host, compiler, revision)
598 def get_latest_build(self, tree, host, compiler):
599 result = self.store.find(StormBuild,
600 StormBuild.tree == tree,
601 StormBuild.host == host,
602 StormBuild.compiler == compiler)
603 result = result.order_by(Desc(StormBuild.id))
604 build = result.first()
606 raise NoSuchBuildError(tree, host, compiler)