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
29 from storm.locals import Int, RawStr
30 from storm.store import Store
31 from storm.expr import Desc
37 def __init__(self, name):
42 class TestResult(object):
44 def __init__(self, build, test, result):
50 class BuildSummary(object):
52 def __init__(self, host, tree, compiler, revision, status):
55 self.compiler = compiler
56 self.revision = revision
60 BuildStageResult = collections.namedtuple("BuildStageResult", "name result")
63 class MissingRevisionInfo(Exception):
64 """Revision info could not be found in the build log."""
66 def __init__(self, build=None):
70 class LogFileMissing(Exception):
71 """Log file missing."""
74 class BuildStatus(object):
76 def __init__(self, stages=None, other_failures=None):
77 if stages is not None:
78 self.stages = [BuildStageResult(n, r) for (n, r) in stages]
81 if other_failures is not None:
82 self.other_failures = other_failures
84 self.other_failures = set()
88 if self.other_failures:
90 return not all([x.result == 0 for x in self.stages])
92 def __serialize__(self):
96 def __deserialize__(cls, text):
100 if self.other_failures:
101 return ",".join(self.other_failures)
102 return "/".join([str(x.result) for x in self.stages])
104 def broken_host(self):
105 if "disk full" in self.other_failures:
109 def regressed_since(self, older):
110 """Check if this build has regressed since another build."""
111 if "disk full" in self.other_failures:
113 if ("timeout" in self.other_failures and
114 "timeout" in older.other_failures):
115 # When the timeout happens exactly can differ slightly, so it's
116 # okay if the numbers are a bit different..
118 if ("panic" in self.other_failures and
119 not "panic" in older.other_failures):
121 if len(self.stages) < len(older.stages):
122 # Less stages completed
124 for ((old_name, old_result), (new_name, new_result)) in zip(
125 older.stages, self.stages):
126 assert old_name == new_name
127 if new_result > old_result:
131 def __cmp__(self, other):
132 other_extra = other.other_failures - self.other_failures
133 self_extra = self.other_failures - other.other_failures
134 # Give more importance to other failures
140 la = len(self.stages)
141 lb = len(other.stages)
147 return cmp(other.stages, self.stages)
150 return "%s(%r, %r)" % (self.__class__.__name__, self.stages, self.other_failures)
153 def check_dir_exists(kind, path):
154 if not os.path.isdir(path):
155 raise Exception("%s directory %s does not exist" % (kind, path))
158 def extract_phase_output(f):
164 def extract_test_output(f):
165 raise NotImplementedError
168 def build_status_from_logs(log, err):
169 """get status of build"""
170 # FIXME: Perhaps also extract revision here?
178 re_status = re.compile("^([A-Z_]+) STATUS:(\s*\d+)$")
179 re_action = re.compile("^ACTION (PASSED|FAILED):\s+test$")
182 if l.startswith("No space left on device"):
183 ret.other_failures.add("disk full")
185 if "Maximum time expired in timelimit" in l: # Ugh.
186 ret.other_failures.add("timeout")
188 if "maximum runtime exceeded" in l: # Ugh.
189 ret.other_failures.add("timeout")
191 if l.startswith("PANIC:") or l.startswith("INTERNAL ERROR:"):
192 ret.other_failures.add("panic")
194 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
197 if l.startswith("testsuite-success: "):
200 m = re_status.match(l)
202 stages.append(BuildStageResult(m.group(1), int(m.group(2).strip())))
203 if m.group(1) == "TEST":
206 m = re_action.match(l)
207 if m and not test_seen:
208 if m.group(1) == "PASSED":
209 stages.append(BuildStageResult("TEST", 0))
211 stages.append(BuildStageResult("TEST", 1))
214 # Scan err file for specific errors
216 if "No space left on device" in l:
217 ret.other_failures.add("disk full")
220 if sr.name != "TEST":
223 if test_successes + test_failures == 0:
224 # No granular test output
225 return BuildStageResult("TEST", sr.result)
226 if sr.result == 1 and test_failures == 0:
227 ret.other_failures.add("inconsistent test result")
228 return BuildStageResult("TEST", -1)
229 return BuildStageResult("TEST", test_failures)
231 ret.stages = map(map_stage, stages)
235 def revision_from_log(log):
238 if l.startswith("BUILD COMMIT REVISION: "):
239 revid = l.split(":", 1)[1].strip()
241 raise MissingRevisionInfo()
245 class NoSuchBuildError(Exception):
246 """The build with the specified name does not exist."""
248 def __init__(self, tree, host, compiler, rev=None):
251 self.compiler = compiler
256 """A single build of a tree on a particular host using a particular compiler.
259 def __init__(self, basename, tree, host, compiler, rev=None):
260 self.basename = basename
263 self.compiler = compiler
266 def __cmp__(self, other):
268 (self.upload_time, self.revision, self.host, self.tree, self.compiler),
269 (other.upload_time, other.revision, other.host, other.tree, other.compiler))
271 def __eq__(self, other):
272 return (isinstance(other, Build) and
273 self.log_checksum() == other.log_checksum())
276 if self.revision is not None:
277 return "<%s: revision %s of %s on %s using %s>" % (self.__class__.__name__, self.revision, self.tree, self.host, self.compiler)
279 return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
281 def remove_logs(self):
282 # In general, basename.log should *always* exist.
283 if os.path.exists(self.basename+".log"):
284 os.unlink(self.basename + ".log")
285 if os.path.exists(self.basename+".err"):
286 os.unlink(self.basename+".err")
292 def upload_time(self):
293 """get timestamp of build"""
294 st = os.stat("%s.log" % self.basename)
299 """get the age of build"""
300 return time.time() - self.upload_time
302 def read_subunit(self):
303 """read the test output as subunit"""
304 return StringIO("".join(extract_test_output(self.read_log())))
307 """read full log file"""
309 return open(self.basename+".log", "r")
311 raise LogFileMissing()
314 """read full err file"""
316 return open(self.basename+".err", 'r')
321 def log_checksum(self):
324 return hashlib.sha1(f.read()).hexdigest()
329 revid = self.revision_details()
330 status = self.status()
331 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
333 def revision_details(self):
334 """get the revision of build
340 return revision_from_log(f)
345 """get status of build
347 :return: tuple with build status
349 log = self.read_log()
351 err = self.read_err()
353 return build_status_from_logs(log, err)
360 """get status of build"""
361 file = self.read_err()
362 return len(file.readlines())
365 class UploadBuildResultStore(object):
367 def __init__(self, path):
368 """Open the database.
370 :param path: Build result base directory
374 def get_all_builds(self):
375 for name in os.listdir(self.path):
377 (build, tree, host, compiler, extension) = name.split(".")
380 if build != "build" or extension != "log":
382 yield self.get_build(tree, host, compiler)
384 def build_fname(self, tree, host, compiler):
385 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
387 def has_host(self, host):
388 for name in os.listdir(self.path):
390 if name.split(".")[2] == host:
396 def get_build(self, tree, host, compiler):
397 basename = self.build_fname(tree, host, compiler)
398 logf = "%s.log" % basename
399 if not os.path.exists(logf):
400 raise NoSuchBuildError(tree, host, compiler)
401 return Build(basename, tree, host, compiler)
404 class StormBuild(Build):
405 __storm_table__ = "build"
407 id = Int(primary=True)
413 upload_time = Int(name="age")
414 status_str = RawStr(name="status")
421 return BuildStatus.__deserialize__(self.status_str)
423 def revision_details(self):
426 def log_checksum(self):
430 super(StormBuild, self).remove()
431 Store.of(self).remove(self)
433 def remove_logs(self):
434 super(StormBuild, self).remove_logs()
438 class BuildResultStore(object):
439 """The build farm build result database."""
441 def __init__(self, basedir, store=None):
442 from buildfarm.sqldb import memory_store
444 store = memory_store()
449 def __contains__(self, build):
451 self.get_by_checksum(build.log_checksum())
453 except NoSuchBuildError:
456 def get_build(self, tree, host, compiler, revision=None, checksum=None):
457 from buildfarm.sqldb import Cast
459 Cast(StormBuild.tree, "TEXT") == Cast(tree, "TEXT"),
460 Cast(StormBuild.host, "TEXT") == Cast(host, "TEXT"),
461 Cast(StormBuild.compiler, "TEXT") == Cast(compiler, "TEXT"),
463 if revision is not None:
464 expr.append(Cast(StormBuild.revision, "TEXT") == Cast(revision, "TEXT"))
465 if checksum is not None:
466 expr.append(Cast(StormBuild.checksum, "TEXT") == Cast(checksum, "TEXT"))
467 result = self.store.find(StormBuild, *expr).order_by(Desc(StormBuild.upload_time))
470 raise NoSuchBuildError(tree, host, compiler, revision)
473 def build_fname(self, tree, host, compiler, rev):
474 """get the name of the build file"""
475 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
477 def get_all_builds(self):
478 for l in os.listdir(self.path):
479 m = re.match("^build\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)-([0-9A-Fa-f]+).log$", l)
484 compiler = m.group(3)
486 stat = os.stat(os.path.join(self.path, l))
487 # skip the current build
488 if stat.st_nlink == 2:
490 yield self.get_build(tree, host, compiler, rev)
492 def get_old_builds(self, tree, host, compiler):
493 result = self.store.find(StormBuild,
494 StormBuild.tree == tree,
495 StormBuild.host == host,
496 StormBuild.compiler == compiler)
497 return result.order_by(Desc(StormBuild.upload_time))
499 def upload_build(self, build):
500 from buildfarm.sqldb import Cast, StormHost
502 existing_build = self.get_by_checksum(build.log_checksum())
503 except NoSuchBuildError:
507 assert build.tree == existing_build.tree
508 assert build.host == existing_build.host
509 assert build.compiler == existing_build.compiler
510 return existing_build
511 rev = build.revision_details()
513 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
515 existing_build = self.get_build(build.tree, build.host, build.compiler, rev)
516 except NoSuchBuildError:
517 if os.path.exists(new_basename+".log"):
518 os.remove(new_basename+".log")
519 if os.path.exists(new_basename+".err"):
520 os.remove(new_basename+".err")
522 existing_build.remove_logs()
523 os.link(build.basename+".log", new_basename+".log")
524 if os.path.exists(build.basename+".err"):
525 os.link(build.basename+".err", new_basename+".err")
526 new_basename = self.build_fname(build.tree, build.host, build.compiler,
528 new_build = StormBuild(new_basename, build.tree, build.host,
530 new_build.checksum = build.log_checksum()
531 new_build.upload_time = build.upload_time
532 new_build.status_str = build.status().__serialize__()
533 new_build.basename = new_basename
534 host = self.store.find(StormHost,
535 Cast(StormHost.name, "TEXT") == Cast(build.host, "TEXT")).one()
536 assert host is not None, "Unable to find host %r" % build.host
537 new_build.host_id = host.id
538 self.store.add(new_build)
541 def get_by_checksum(self, checksum):
542 from buildfarm.sqldb import Cast
543 result = self.store.find(StormBuild,
544 Cast(StormBuild.checksum, "TEXT") == checksum)
547 raise NoSuchBuildError(None, None, None, None)
550 def get_previous_revision(self, tree, host, compiler, revision):
551 from buildfarm.sqldb import Cast
552 cur_build = self.get_build(tree, host, compiler, revision)
554 result = self.store.find(StormBuild,
555 Cast(StormBuild.tree, "TEXT") == Cast(tree, "TEXT"),
556 Cast(StormBuild.host, "TEXT") == Cast(host, "TEXT"),
557 Cast(StormBuild.compiler, "TEXT") == Cast(compiler, "TEXT"),
558 Cast(StormBuild.revision, "TEXT") != Cast(revision, "TEXT"),
559 StormBuild.id < cur_build.id)
560 result = result.order_by(Desc(StormBuild.id))
561 prev_build = result.first()
562 if prev_build is None:
563 raise NoSuchBuildError(tree, host, compiler, revision)
564 return prev_build.revision
566 def get_latest_revision(self, tree, host, compiler):
567 result = self.store.find(StormBuild,
568 StormBuild.tree == tree,
569 StormBuild.host == host,
570 StormBuild.compiler == compiler)
571 result = result.order_by(Desc(StormBuild.id))
572 build = result.first()
574 raise NoSuchBuildError(tree, host, compiler)
575 return build.revision