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):
129 if len(self.stages) < len(older.stages):
130 # Less stages completed
132 for ((old_name, old_result), (new_name, new_result)) in zip(
133 older.stages, self.stages):
134 assert old_name == new_name
135 if new_result > old_result:
139 def __cmp__(self, other):
140 other_extra = other.other_failures - self.other_failures
141 self_extra = self.other_failures - other.other_failures
142 # Give more importance to other failures
148 la = len(self.stages)
149 lb = len(other.stages)
155 return cmp(other.stages, self.stages)
158 return "%s(%r, %r)" % (self.__class__.__name__, self.stages, self.other_failures)
161 def check_dir_exists(kind, path):
162 if not os.path.isdir(path):
163 raise Exception("%s directory %s does not exist" % (kind, path))
166 def extract_phase_output(f):
169 re_action = re.compile("^ACTION (PASSED|FAILED):\s+(.*)$")
171 if l.startswith("Running action "):
172 name = l[len("Running action "):].strip()
175 m = re_action.match(l)
177 assert name == m.group(2).strip(), "%r != %r" % (name, m.group(2))
181 elif output is not None:
185 def extract_test_output(f):
186 for name, output in extract_phase_output(f):
192 def build_status_from_logs(log, err):
193 """get status of build"""
194 # FIXME: Perhaps also extract revision here?
202 re_status = re.compile("^([A-Z_]+) STATUS:(\s*\d+)$")
203 re_action = re.compile("^ACTION (PASSED|FAILED):\s+test$")
206 if l.startswith("No space left on device"):
207 ret.other_failures.add("disk full")
209 if "Maximum time expired in timelimit" in l: # Ugh.
210 ret.other_failures.add("timeout")
212 if "maximum runtime exceeded" in l: # Ugh.
213 ret.other_failures.add("timeout")
215 if l.startswith("PANIC:") or l.startswith("INTERNAL ERROR:"):
216 ret.other_failures.add("panic")
218 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
221 if l.startswith("testsuite-success: "):
224 m = re_status.match(l)
226 stages.append(BuildStageResult(m.group(1), int(m.group(2).strip())))
227 if m.group(1) == "TEST":
230 m = re_action.match(l)
231 if m and not test_seen:
232 if m.group(1) == "PASSED":
233 stages.append(BuildStageResult("TEST", 0))
235 stages.append(BuildStageResult("TEST", 1))
238 # Scan err file for specific errors
240 if "No space left on device" in l:
241 ret.other_failures.add("disk full")
244 if sr.name != "TEST":
247 if test_successes + test_failures == 0:
248 # No granular test output
249 return BuildStageResult("TEST", sr.result)
250 if sr.result == 1 and test_failures == 0:
251 ret.other_failures.add("inconsistent test result")
252 return BuildStageResult("TEST", -1)
253 return BuildStageResult("TEST", test_failures)
255 ret.stages = map(map_stage, stages)
259 def revision_from_log(log):
262 if l.startswith("BUILD COMMIT REVISION: "):
263 revid = l.split(":", 1)[1].strip()
265 raise MissingRevisionInfo()
269 class NoSuchBuildError(Exception):
270 """The build with the specified name does not exist."""
272 def __init__(self, tree, host, compiler, rev=None):
275 self.compiler = compiler
279 class NoTestOutput(Exception):
280 """The build did not have any associated test output."""
284 """A single build of a tree on a particular host using a particular compiler.
287 def __init__(self, basename, tree, host, compiler, rev=None):
288 self.basename = basename
291 self.compiler = compiler
294 def __cmp__(self, other):
296 (self.upload_time, self.revision, self.host, self.tree, self.compiler),
297 (other.upload_time, other.revision, other.host, other.tree, other.compiler))
299 def __eq__(self, other):
300 return (isinstance(other, Build) and
301 self.log_checksum() == other.log_checksum())
304 if self.revision is not None:
305 return "<%s: revision %s of %s on %s using %s>" % (self.__class__.__name__, self.revision, self.tree, self.host, self.compiler)
307 return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
309 def remove_logs(self):
310 # In general, basename.log should *always* exist.
311 if os.path.exists(self.basename+".log"):
312 os.unlink(self.basename + ".log")
313 if os.path.exists(self.basename+".err"):
314 os.unlink(self.basename+".err")
320 def upload_time(self):
321 """get timestamp of build"""
322 st = os.stat("%s.log" % self.basename)
327 """get the age of build"""
328 return time.time() - self.upload_time
330 def read_subunit(self):
331 """read the test output as subunit"""
332 return StringIO("".join(extract_test_output(self.read_log())))
335 """read full log file"""
337 return open_opt_compressed_file(self.basename+".log")
339 raise LogFileMissing()
342 """read full err file"""
344 return open_opt_compressed_file(self.basename+".err")
349 def log_checksum(self):
352 return hashlib.sha1(f.read()).hexdigest()
357 revid = self.revision_details()
358 status = self.status()
359 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
361 def revision_details(self):
362 """get the revision of build
368 return revision_from_log(f)
373 """get status of build
375 :return: tuple with build status
377 log = self.read_log()
379 err = self.read_err()
381 return build_status_from_logs(log, err)
388 """get status of build"""
389 file = self.read_err()
390 return len(file.readlines())
393 class UploadBuildResultStore(object):
395 def __init__(self, path):
396 """Open the database.
398 :param path: Build result base directory
402 def get_all_builds(self):
403 for name in os.listdir(self.path):
405 (build, tree, host, compiler, extension) = name.split(".")
408 if build != "build" or extension != "log":
410 yield self.get_build(tree, host, compiler)
412 def build_fname(self, tree, host, compiler):
413 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
415 def has_host(self, host):
416 for name in os.listdir(self.path):
418 if name.split(".")[2] == host:
424 def get_build(self, tree, host, compiler):
425 basename = self.build_fname(tree, host, compiler)
426 logf = "%s.log" % basename
427 if not os.path.exists(logf):
428 raise NoSuchBuildError(tree, host, compiler)
429 return Build(basename, tree, host, compiler)
432 class StormBuild(Build):
433 __storm_table__ = "build"
435 id = Int(primary=True)
441 upload_time = Int(name="age")
442 status_str = RawStr(name="status")
449 return BuildStatus.__deserialize__(self.status_str)
451 def revision_details(self):
454 def log_checksum(self):
458 super(StormBuild, self).remove()
459 Store.of(self).remove(self)
461 def remove_logs(self):
462 super(StormBuild, self).remove_logs()
466 class BuildResultStore(object):
467 """The build farm build result database."""
469 def __init__(self, basedir, store=None):
470 from buildfarm.sqldb import memory_store
472 store = memory_store()
477 def __contains__(self, build):
479 self.get_by_checksum(build.log_checksum())
481 except NoSuchBuildError:
484 def get_build(self, tree, host, compiler, revision=None, checksum=None):
485 from buildfarm.sqldb import Cast
487 Cast(StormBuild.tree, "TEXT") == Cast(tree, "TEXT"),
488 Cast(StormBuild.host, "TEXT") == Cast(host, "TEXT"),
489 Cast(StormBuild.compiler, "TEXT") == Cast(compiler, "TEXT"),
491 if revision is not None:
492 expr.append(Cast(StormBuild.revision, "TEXT") == Cast(revision, "TEXT"))
493 if checksum is not None:
494 expr.append(Cast(StormBuild.checksum, "TEXT") == Cast(checksum, "TEXT"))
495 result = self.store.find(StormBuild, *expr).order_by(Desc(StormBuild.upload_time))
498 raise NoSuchBuildError(tree, host, compiler, revision)
501 def build_fname(self, tree, host, compiler, rev):
502 """get the name of the build file"""
503 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
505 def get_all_builds(self):
506 for l in os.listdir(self.path):
507 m = re.match("^build\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)-([0-9A-Fa-f]+).log$", l)
512 compiler = m.group(3)
514 stat = os.stat(os.path.join(self.path, l))
515 # skip the current build
516 if stat.st_nlink == 2:
518 yield self.get_build(tree, host, compiler, rev)
520 def get_old_builds(self, tree, host, compiler):
521 result = self.store.find(StormBuild,
522 StormBuild.tree == tree,
523 StormBuild.host == host,
524 StormBuild.compiler == compiler)
525 return result.order_by(Desc(StormBuild.upload_time))
527 def upload_build(self, build):
528 from buildfarm.sqldb import Cast, StormHost
530 existing_build = self.get_by_checksum(build.log_checksum())
531 except NoSuchBuildError:
535 assert build.tree == existing_build.tree
536 assert build.host == existing_build.host
537 assert build.compiler == existing_build.compiler
538 return existing_build
539 rev = build.revision_details()
541 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
542 for name in os.listdir(self.path):
543 p = os.path.join(self.path, name)
544 if p.startswith(new_basename+"."):
546 os.link(build.basename+".log", new_basename+".log")
547 if os.path.exists(build.basename+".err"):
548 os.link(build.basename+".err", new_basename+".err")
549 new_build = StormBuild(new_basename, build.tree, build.host,
551 new_build.checksum = build.log_checksum()
552 new_build.upload_time = build.upload_time
553 new_build.status_str = build.status().__serialize__()
554 new_build.basename = new_basename
555 host = self.store.find(StormHost,
556 Cast(StormHost.name, "TEXT") == Cast(build.host, "TEXT")).one()
557 assert host is not None, "Unable to find host %r" % build.host
558 new_build.host_id = host.id
559 self.store.add(new_build)
562 def get_by_checksum(self, checksum):
563 from buildfarm.sqldb import Cast
564 result = self.store.find(StormBuild,
565 Cast(StormBuild.checksum, "TEXT") == Cast(checksum, "TEXT")).order_by(Desc(StormBuild.upload_time))
568 raise NoSuchBuildError(None, None, None, None)
571 def get_previous_build(self, tree, host, compiler, revision):
572 from buildfarm.sqldb import Cast
573 cur_build = self.get_build(tree, host, compiler, revision)
575 result = self.store.find(StormBuild,
576 Cast(StormBuild.tree, "TEXT") == Cast(tree, "TEXT"),
577 Cast(StormBuild.host, "TEXT") == Cast(host, "TEXT"),
578 Cast(StormBuild.compiler, "TEXT") == Cast(compiler, "TEXT"),
579 Cast(StormBuild.revision, "TEXT") != Cast(revision, "TEXT"),
580 StormBuild.id < cur_build.id)
581 result = result.order_by(Desc(StormBuild.id))
582 prev_build = result.first()
583 if prev_build is None:
584 raise NoSuchBuildError(tree, host, compiler, revision)
587 def get_latest_build(self, tree, host, compiler):
588 result = self.store.find(StormBuild,
589 StormBuild.tree == tree,
590 StormBuild.host == host,
591 StormBuild.compiler == compiler)
592 result = result.order_by(Desc(StormBuild.id))
593 build = result.first()
595 raise NoSuchBuildError(tree, host, compiler)