Make the fix script update revision details.
[amitay/build-farm.git] / buildfarm / data.py
index 2ef40b0a347d64ad156fa26f55162df8022c6995..90345f87fb8e781b99f74c1fcaac1d756cc14d12 100644 (file)
@@ -45,10 +45,14 @@ 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):
@@ -148,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:"):
@@ -195,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."""
 
@@ -214,10 +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 (self.log_checksum() == other.log_checksum())
+        return (isinstance(other, Build) and
+                self.log_checksum() == other.log_checksum())
 
     def __repr__(self):
         if self.revision is not None:
@@ -226,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"""
@@ -277,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
 
@@ -376,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):
@@ -387,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):
@@ -412,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")