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
32 class BuildSummary(object):
34 def __init__(self, host, tree, compiler, revision, status):
37 self.compiler = compiler
38 self.revision = revision
42 BuildStageResult = collections.namedtuple("BuildStageResult", "name result")
45 class MissingRevisionInfo(Exception):
46 """Revision info could not be found in the build log."""
48 def __init__(self, build):
52 class BuildStatus(object):
54 def __init__(self, stages=None, other_failures=None):
55 if stages is not None:
56 self.stages = [BuildStageResult(n, r) for (n, r) in stages]
59 if other_failures is not None:
60 self.other_failures = other_failures
62 self.other_failures = set()
66 if self.other_failures:
68 return not all([x.result == 0 for x in self.stages])
70 def __serialize__(self):
74 def __deserialize__(cls, text):
78 if self.other_failures:
79 return ",".join(self.other_failures)
80 return "/".join([str(x.result) for x in self.stages])
82 def broken_host(self):
83 if "disk full" in self.other_failures:
87 def regressed_since(self, older):
88 """Check if this build has regressed since another build."""
89 if "disk full" in self.other_failures:
91 if "timeout" in self.other_failures and "timeout" in older.other_failures:
92 # When the timeout happens exactly can differ slightly, so it's okay
93 # if the numbers are a bit different..
95 if "panic" in self.other_failures and not "panic" in older.other_failures:
97 if len(self.stages) < len(older.stages):
98 # Less stages completed
100 for ((old_name, old_result), (new_name, new_result)) in zip(
101 older.stages, self.stages):
102 assert old_name == new_name
103 if new_result > old_result:
107 def __cmp__(self, other):
108 other_extra = other.other_failures - self.other_failures
109 self_extra = self.other_failures - other.other_failures
110 # Give more importance to other failures
116 la = len(self.stages)
117 lb = len(other.stages)
123 return cmp(other.stages, self.stages)
126 return "%s(%r, %r)" % (self.__class__.__name__, self.stages, self.other_failures)
129 def check_dir_exists(kind, path):
130 if not os.path.isdir(path):
131 raise Exception("%s directory %s does not exist" % (kind, path))
134 def build_status_from_logs(log, err):
135 """get status of build"""
136 # FIXME: Perhaps also extract revision here?
144 re_status = re.compile("^([A-Z_]+) STATUS:(\s*\d+)$")
145 re_action = re.compile("^ACTION (PASSED|FAILED):\s+test$")
148 if l.startswith("No space left on device"):
149 ret.other_failures.add("disk full")
151 if l.startswith("maximum runtime exceeded"):
152 ret.other_failures.add("timeout")
154 if l.startswith("PANIC:") or l.startswith("INTERNAL ERROR:"):
155 ret.other_failures.add("panic")
157 if l.startswith("testsuite-failure: ") or l.startswith("testsuite-error: "):
160 if l.startswith("testsuite-success: "):
163 m = re_status.match(l)
165 stages.append(BuildStageResult(m.group(1), int(m.group(2).strip())))
166 if m.group(1) == "TEST":
169 m = re_action.match(l)
170 if m and not test_seen:
171 if m.group(1) == "PASSED":
172 stages.append(BuildStageResult("TEST", 0))
174 stages.append(BuildStageResult("TEST", 1))
177 # Scan err file for specific errors
179 if "No space left on device" in l:
180 ret.other_failures.add("disk full")
183 if sr.name != "TEST":
186 if test_successes + test_failures == 0:
187 # No granular test output
188 return BuildStageResult("TEST", sr.result)
189 if sr.result == 1 and test_failures == 0:
190 ret.other_failures.add("inconsistent test result")
191 return BuildStageResult("TEST", -1)
192 return BuildStageResult("TEST", test_failures)
194 ret.stages = map(map_stage, stages)
198 class NoSuchBuildError(Exception):
199 """The build with the specified name does not exist."""
201 def __init__(self, tree, host, compiler, rev=None):
204 self.compiler = compiler
209 """A single build of a tree on a particular host using a particular compiler.
212 def __init__(self, basename, tree, host, compiler, rev=None):
213 self.basename = basename
216 self.compiler = compiler
217 self.commit_revision = self.revision = rev
219 def __eq__(self, other):
220 return (self.log_checksum() == other.log_checksum())
223 if self.revision is not None:
224 return "<%s: revision %s of %s on %s using %s>" % (self.__class__.__name__, self.revision, self.tree, self.host, self.compiler)
226 return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
228 def remove_logs(self):
229 os.unlink(self.basename + ".log")
230 if os.path.exists(self.basename+".err"):
231 os.unlink(self.basename+".err")
238 """get the age of build"""
239 st = os.stat("%s.log" % self.basename)
240 return time.time() - st.st_ctime
243 """read full log file"""
244 return open(self.basename+".log", "r")
247 """read full err file"""
249 return open(self.basename+".err", 'r')
254 def log_checksum(self):
257 return hashlib.sha1(f.read()).hexdigest()
262 (revid, timestamp) = self.revision_details()
263 status = self.status()
264 return BuildSummary(self.host, self.tree, self.compiler, revid, status)
266 def revision_details(self):
267 """get the revision of build
269 :return: Tuple with revision id and timestamp (if available)
276 if l.startswith("BUILD COMMIT REVISION: "):
277 revid = l.split(":", 1)[1].strip()
278 elif l.startswith("BUILD COMMIT TIME"):
279 timestamp = l.split(":", 1)[1].strip()
284 raise MissingRevisionInfo(self)
286 return (revid, timestamp)
289 """get status of build
291 :return: tuple with build status
293 log = self.read_log()
295 err = self.read_err()
297 return build_status_from_logs(log, err)
304 """get status of build"""
305 file = self.read_err()
306 return len(file.readlines())
309 class UploadBuildResultStore(object):
311 def __init__(self, path):
312 """Open the database.
314 :param path: Build result base directory
318 def get_new_builds(self):
319 for name in os.listdir(self.path):
321 (build, tree, host, compiler, extension) = name.split(".")
324 if build != "build" or extension != "log":
326 yield self.get_build(tree, host, compiler)
328 def build_fname(self, tree, host, compiler):
329 return os.path.join(self.path, "build.%s.%s.%s" % (tree, host, compiler))
331 def has_host(self, host):
332 for name in os.listdir(self.path):
334 if name.split(".")[2] == host:
340 def get_build(self, tree, host, compiler):
341 basename = self.build_fname(tree, host, compiler)
342 logf = "%s.log" % basename
343 if not os.path.exists(logf):
344 raise NoSuchBuildError(tree, host, compiler)
345 return Build(basename, tree, host, compiler)
348 class BuildResultStore(object):
349 """The build farm build result database."""
351 def __init__(self, path):
352 """Open the database.
354 :param path: Build result base directory
358 def __contains__(self, build):
363 rev, timestamp = build.revision_details()
364 self.get_build(build.tree, build.host, build.compiler, rev)
365 except NoSuchBuildError:
370 def get_build(self, tree, host, compiler, rev):
371 basename = self.build_fname(tree, host, compiler, rev)
372 logf = "%s.log" % basename
373 if not os.path.exists(logf):
374 raise NoSuchBuildError(tree, host, compiler, rev)
375 return Build(basename, tree, host, compiler, rev)
377 def build_fname(self, tree, host, compiler, rev):
378 """get the name of the build file"""
379 return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
381 def get_old_revs(self, tree, host, compiler):
382 """get a list of old builds and their status."""
384 logfiles = [d for d in os.listdir(self.path) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
386 m = re.match(".*-([0-9A-Fa-f]+).log$", l)
389 stat = os.stat(os.path.join(self.path, l))
390 # skip the current build
391 if stat.st_nlink == 2:
393 ret.append(self.get_build(tree, host, compiler, rev))
395 ret.sort(lambda a, b: cmp(a.age, b.age))
399 def upload_build(self, build):
400 (rev, rev_timestamp) = build.revision_details()
402 new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
404 existing_build = self.get_build(build.tree, build.host, build.compiler, rev)
405 except NoSuchBuildError:
408 existing_build.remove_logs()
409 os.link(build.basename+".log", new_basename+".log")
410 if os.path.exists(build.basename+".err"):
411 os.link(build.basename+".err", new_basename+".err")
412 return Build(new_basename, build.tree, build.host, build.compiler, rev)
414 def get_previous_revision(self, tree, host, compiler, revision):
415 raise NoSuchBuildError(tree, host, compiler, revision)
417 def get_latest_revision(self, tree, host, compiler):
418 raise NoSuchBuildError(tree, host, compiler)