changes to reviews
[build-farm.git] / buildfarm / build.py
index c708f576dc37cacb71274cbec973aa05619e703c..a064bfaf6984eeb600087bc889e371547dc3a231 100644 (file)
@@ -21,6 +21,7 @@
 #   along with this program; if not, write to the Free Software
 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
+import bz2
 from cStringIO import StringIO
 import collections
 import hashlib
@@ -32,6 +33,13 @@ from storm.expr import Desc
 import time
 
 
+def open_opt_compressed_file(path):
+    try:
+        return bz2.BZ2File(path+".bz2", 'r')
+    except IOError:
+        return open(path, 'r')
+
+
 class Test(object):
 
     def __init__(self, name):
@@ -117,15 +125,26 @@ class BuildStatus(object):
             return False
         if ("panic" in self.other_failures and
             not "panic" in older.other_failures):
+            # If this build introduced panics, then that's always worse.
             return True
         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:
+        old_stages = dict(older.stages)
+        new_stages = dict(self.stages)
+        for name, new_result in new_stages.iteritems():
+            try:
+                old_result = old_stages[name]
+            except KeyError:
+                continue
+            if new_result == old_result:
+                continue
+            if new_result < 0 and old_result >= 0:
                 return True
+            elif new_result >= 0 and old_result < 0:
+                return False
+            else:
+                return (abs(new_result) > abs(old_result))
         return False
 
     def __cmp__(self, other):
@@ -158,11 +177,27 @@ def check_dir_exists(kind, path):
 def extract_phase_output(f):
     name = None
     output = None
+    re_action = re.compile("^ACTION (PASSED|FAILED):\s+(.*)$")
     for l in f:
+        if l.startswith("Running action "):
+            name = l[len("Running action "):].strip()
+            output = []
+            continue
+        m = re_action.match(l)
+        if m:
+            assert name == m.group(2).strip(), "%r != %r" % (name, m.group(2))
+            yield name, output
+            name = None
+            output = []
+        elif output is not None:
+            output.append(l)
 
 
 def extract_test_output(f):
-    raise NotImplementedError
+    for name, output in extract_phase_output(f):
+        if name == "test":
+            return output
+    raise NoTestOutput()
 
 
 def build_status_from_logs(log, err):
@@ -252,6 +287,10 @@ class NoSuchBuildError(Exception):
         self.rev = rev
 
 
+class NoTestOutput(Exception):
+    """The build did not have any associated test output."""
+
+
 class Build(object):
     """A single build of a tree on a particular host using a particular compiler.
     """
@@ -306,14 +345,23 @@ class Build(object):
     def read_log(self):
         """read full log file"""
         try:
-            return open(self.basename+".log", "r")
+            return open_opt_compressed_file(self.basename+".log")
         except IOError:
             raise LogFileMissing()
 
+    def has_log(self):
+        try:
+            f = self.read_log()
+        except LogFileMissing:
+            return False
+        else:
+            f.close()
+            return True
+
     def read_err(self):
         """read full err file"""
         try:
-            return open(self.basename+".err", 'r')
+            return open_opt_compressed_file(self.basename+".err")
         except IOError:
             # No such file
             return StringIO()
@@ -511,22 +559,16 @@ class BuildResultStore(object):
         rev = build.revision_details()
 
         new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
-        try:
-            existing_build = self.get_build(build.tree, build.host, build.compiler, rev)
-        except NoSuchBuildError:
-            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()
+        for name in os.listdir(self.path):
+            p = os.path.join(self.path, name)
+            if p.startswith(new_basename+"."):
+                os.remove(p)
         os.link(build.basename+".log", new_basename+".log")
         if os.path.exists(build.basename+".err"):
             os.link(build.basename+".err", new_basename+".err")
-        new_basename = self.build_fname(build.tree, build.host, build.compiler,
-                rev)
-        new_build = StormBuild(new_basename, build.tree, build.host,
-            build.compiler, rev)
+        # They are supposed to be in unicode only but since comparision for sumary page depends on them 
+        # the unicode conversion is done to avoid duplicates when running query in summary_builds
+        new_build = StormBuild(new_basename, unicode(build.tree), unicode(build.host), unicode(build.compiler), rev)
         new_build.checksum = build.log_checksum()
         new_build.upload_time = build.upload_time
         new_build.status_str = build.status().__serialize__()
@@ -541,13 +583,13 @@ class BuildResultStore(object):
     def get_by_checksum(self, checksum):
         from buildfarm.sqldb import Cast
         result = self.store.find(StormBuild,
-            Cast(StormBuild.checksum, "TEXT") == checksum)
-        ret = result.one()
+            Cast(StormBuild.checksum, "TEXT") == Cast(checksum, "TEXT")).order_by(Desc(StormBuild.upload_time))
+        ret = result.first()
         if ret is None:
             raise NoSuchBuildError(None, None, None, None)
         return ret
 
-    def get_previous_revision(self, tree, host, compiler, revision):
+    def get_previous_build(self, tree, host, compiler, revision):
         from buildfarm.sqldb import Cast
         cur_build = self.get_build(tree, host, compiler, revision)
 
@@ -561,9 +603,9 @@ class BuildResultStore(object):
         prev_build = result.first()
         if prev_build is None:
             raise NoSuchBuildError(tree, host, compiler, revision)
-        return prev_build.revision
+        return prev_build
 
-    def get_latest_revision(self, tree, host, compiler):
+    def get_latest_build(self, tree, host, compiler):
         result = self.store.find(StormBuild,
             StormBuild.tree == tree,
             StormBuild.host == host,
@@ -572,4 +614,27 @@ class BuildResultStore(object):
         build = result.first()
         if build is None:
             raise NoSuchBuildError(tree, host, compiler)
-        return build.revision
+        return build
+
+
+class BuildDiff(object):
+    """Represents the difference between two builds."""
+
+    def __init__(self, tree, old, new):
+        self.tree = tree
+        self.old = old
+        self.new = new
+        self.new_rev = new.revision_details()
+        self.new_status = new.status()
+
+        self.old_rev = old.revision_details()
+        self.old_status = old.status()
+
+    def is_regression(self):
+        """Is there a regression in new build since old build?"""
+        return self.new_status.regressed_since(self.old_status)
+
+    def revisions(self):
+        """Returns the revisions introduced since old in new."""
+        branch = self.tree.get_branch()
+        return branch.log(from_rev=self.new.revision, exclude_revs=set([self.old.revision]))