# 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
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):
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):
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):
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.
"""
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()
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__()
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)
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,
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]))