class MissingRevisionInfo(Exception):
"""Revision info could not be found in the build log."""
- def __init__(self, build):
+ def __init__(self, build=None):
self.build = build
+class LogFileMissing(Exception):
+ """Log file missing."""
+
+
class BuildStatus(object):
def __init__(self, stages=None, other_failures=None):
if stages is not None:
- self.stages = stages
+ self.stages = [BuildStageResult(n, r) for (n, r) in stages]
else:
self.stages = []
if other_failures is not None:
def failed(self):
if self.other_failures:
return True
- return not all([x == 0 for x in self._status_tuple()])
+ return not all([x.result == 0 for x in self.stages])
def __serialize__(self):
return repr(self)
def __str__(self):
if self.other_failures:
return ",".join(self.other_failures)
- return "/".join(map(str, self._status_tuple()))
+ return "/".join([str(x.result) for x in self.stages])
def broken_host(self):
if "disk full" in self.other_failures:
return True
return False
- def _status_tuple(self):
- return [sr.result for sr in self.stages]
-
- def regressed_since(self, other):
+ def regressed_since(self, older):
"""Check if this build has regressed since another build."""
if "disk full" in self.other_failures:
return False
- if "timeout" in self.other_failures and "timeout" in other.other_failures:
+ if "timeout" in self.other_failures and "timeout" in older.other_failures:
# When the timeout happens exactly can differ slightly, so it's okay
# if the numbers are a bit different..
return False
- if "panic" in self.other_failures and not "panic" in other.other_failures:
+ if "panic" in self.other_failures and not "panic" in older.other_failures:
return True
- return cmp(self._status_tuple(), other._status_tuple())
+ if len(self.stages) < len(older.stages):
+ # Less stages completed
+ return True
+ for ((old_name, old_result), (new_name, new_result)) in zip(
+ older.stages, self.stages):
+ assert old_name == new_name
+ if new_result > old_result:
+ return True
+ return False
def __cmp__(self, other):
other_extra = other.other_failures - self.other_failures
def build_status_from_logs(log, err):
"""get status of build"""
+ # FIXME: Perhaps also extract revision here?
+
test_failures = 0
test_successes = 0
test_seen = 0
if l.startswith("No space left on device"):
ret.other_failures.add("disk full")
continue
- if l.startswith("maximum runtime exceeded"):
+ if "maximum runtime exceeded" in l: # Ugh.
ret.other_failures.add("timeout")
continue
if l.startswith("PANIC:") or l.startswith("INTERNAL ERROR:"):
return ret
+def revision_from_log(log):
+ revid = None
+ timestamp = None
+ for l in log:
+ if l.startswith("BUILD COMMIT REVISION: "):
+ revid = l.split(":", 1)[1].strip()
+ elif l.startswith("BUILD COMMIT TIME"):
+ timestamp = l.split(":", 1)[1].strip()
+ if revid is None:
+ raise MissingRevisionInfo()
+ return (revid, timestamp)
+
+
class NoSuchBuildError(Exception):
"""The build with the specified name does not exist."""
self.tree = tree
self.host = host
self.compiler = compiler
- self.commit_revision = self.revision = rev
+ self.revision = rev
+
+ def __cmp__(self, other):
+ return cmp(
+ (self.upload_time, self.revision, self.host, self.tree, self.compiler),
+ (other.upload_time, other.revision, other.host, other.tree, other.compiler))
+
+ def __eq__(self, other):
+ return (isinstance(other, Build) and
+ self.log_checksum() == other.log_checksum())
def __repr__(self):
if self.revision is not None:
return "<%s: %s on %s using %s>" % (self.__class__.__name__, self.tree, self.host, self.compiler)
def remove_logs(self):
- os.unlink(self.basename + ".log")
+ # In general, basename.log should *always* exist.
+ if os.path.exists(self.basename+".log"):
+ os.unlink(self.basename + ".log")
if os.path.exists(self.basename+".err"):
os.unlink(self.basename+".err")
def remove(self):
self.remove_logs()
- ###################
- # the mtime age is used to determine if builds are still happening
- # on a host.
- # the ctime age is used to determine when the last real build happened
-
- def age_mtime(self):
- """get the age of build from mtime"""
+ @property
+ def upload_time(self):
+ """get timestamp of build"""
st = os.stat("%s.log" % self.basename)
- return time.time() - st.st_mtime
+ return st.st_mtime
- def age_ctime(self):
- """get the age of build from ctime"""
- st = os.stat("%s.log" % self.basename)
- return time.time() - st.st_ctime
+ @property
+ def age(self):
+ """get the age of build"""
+ return time.time() - self.upload_time
def read_log(self):
"""read full log file"""
- return open(self.basename+".log", "r")
+ try:
+ return open(self.basename+".log", "r")
+ except IOError:
+ raise LogFileMissing()
def read_err(self):
"""read full err file"""
:return: Tuple with revision id and timestamp (if available)
"""
- revid = None
- timestamp = None
f = self.read_log()
try:
- for l in f:
- if l.startswith("BUILD COMMIT REVISION: "):
- revid = l.split(":", 1)[1].strip()
- elif l.startswith("BUILD COMMIT TIME"):
- timestamp = l.split(":", 1)[1].strip()
+ return revision_from_log(f)
finally:
f.close()
- if revid is None:
- raise MissingRevisionInfo(self)
-
- return (revid, timestamp)
-
def status(self):
"""get status of build
else:
return True
- def get_build(self, tree, host, compiler, rev):
+ def get_build(self, tree, host, compiler, rev, checksum=None):
basename = self.build_fname(tree, host, compiler, rev)
logf = "%s.log" % basename
if not os.path.exists(logf):
"""get the name of the build file"""
return os.path.join(self.path, "build.%s.%s.%s-%s" % (tree, host, compiler, rev))
- def get_old_revs(self, tree, host, compiler):
+ def get_all_builds(self):
+ for l in os.listdir(self.path):
+ m = re.match("^build\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)\.([0-9A-Za-z]+)-([0-9A-Fa-f]+).log$", l)
+ if not m:
+ continue
+ tree = m.group(1)
+ host = m.group(2)
+ compiler = m.group(3)
+ rev = m.group(4)
+ stat = os.stat(os.path.join(self.path, l))
+ # skip the current build
+ if stat.st_nlink == 2:
+ continue
+ yield self.get_build(tree, host, compiler, rev)
+
+ def get_old_builds(self, tree, host, compiler):
"""get a list of old builds and their status."""
ret = []
- logfiles = [d for d in os.listdir(self.path) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
- for l in logfiles:
- m = re.match(".*-([0-9A-Fa-f]+).log$", l)
- if m:
- rev = m.group(1)
- stat = os.stat(os.path.join(self.path, l))
- # skip the current build
- if stat.st_nlink == 2:
- continue
- ret.append(self.get_build(tree, host, compiler, rev))
-
- ret.sort(lambda a, b: cmp(a.age_mtime(), b.age_mtime()))
-
+ for build in self.get_all_builds():
+ if build.tree == tree and build.host == host and build.compiler == compiler:
+ ret.append(build)
+ ret.sort(lambda a, b: cmp(a.upload_time, b.upload_time))
return ret
def upload_build(self, build):
try:
existing_build = self.get_build(build.tree, build.host, build.compiler, rev)
except NoSuchBuildError:
- pass
+ if os.path.exists(new_basename+".log"):
+ os.remove(new_basename+".log")
+ if os.path.exists(new_basename+".err"):
+ os.remove(new_basename+".err")
else:
existing_build.remove_logs()
os.link(build.basename+".log", new_basename+".log")