Make the fix script update revision details.
[amitay/build-farm.git] / buildfarm / data.py
index 750deac94f644d6f2d83eaa14eb5e42be94eb6c6..90345f87fb8e781b99f74c1fcaac1d756cc14d12 100644 (file)
@@ -45,15 +45,19 @@ BuildStageResult = collections.namedtuple("BuildStageResult", "name result")
 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:
@@ -65,7 +69,7 @@ class BuildStatus(object):
     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)
@@ -77,27 +81,32 @@ class BuildStatus(object):
     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
@@ -128,6 +137,8 @@ def check_dir_exists(kind, path):
 
 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
@@ -141,7 +152,7 @@ def build_status_from_logs(log, err):
         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:"):
@@ -188,6 +199,19 @@ def build_status_from_logs(log, err):
     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."""
 
@@ -207,7 +231,16 @@ class Build(object):
         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:
@@ -216,31 +249,32 @@ class Build(object):
             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"""
@@ -267,23 +301,12 @@ class Build(object):
 
         :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
 
@@ -366,7 +389,7 @@ class BuildResultStore(object):
         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):
@@ -377,22 +400,28 @@ class BuildResultStore(object):
         """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):
@@ -402,7 +431,10 @@ class BuildResultStore(object):
         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")