build: a more portable way of finding waf in makefiles
[sfrench/samba-autobuild/.git] / script / land.py
index 28769486e21de807c51d40e614e66de908246209..4b7d1cacba324c4c9a7867e95cea4ac846fe5a23 100755 (executable)
@@ -1,9 +1,10 @@
-#!/usr/bin/env python -u
+#!/usr/bin/env python
 # run tests on all Samba subprojects and push to a git tree on success
 # Copyright Andrew Tridgell 2010
 # Copyright Jelmer Vernooij 2010
 # released under GNU GPL v3 or later
 
+from cStringIO import StringIO
 import fcntl
 from subprocess import call, check_call, Popen, PIPE
 import os, tarfile, sys, time
@@ -15,7 +16,10 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib/subunit/pytho
 import subunit
 import testtools
 import subunithelper
+import tempfile
+from email.mime.application import MIMEApplication
 from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
 
 samba_master = os.getenv('SAMBA_MASTER', 'git://git.samba.org/samba.git')
 samba_master_ssh = os.getenv('SAMBA_MASTER_SSH', 'git+ssh://git.samba.org/data/git/samba.git')
@@ -66,19 +70,6 @@ tasks = {
                      ("test", "make test", "text/plain"), ],
 }
 
-retry_task = [ ( "retry",
-                 '''set -e
-                git remote add -t master master %s
-                git fetch master
-                while :; do
-                  sleep 60
-                  git describe master/master > old_master.desc
-                  git fetch master
-                  git describe master/master > master.desc
-                  diff old_master.desc master.desc
-                done
-               ''' % samba_master, "test/plain" ) ]
-
 
 def run_cmd(cmd, dir=None, show=None, output=False, checkfail=True, shell=False):
     if show is None:
@@ -93,6 +84,38 @@ def run_cmd(cmd, dir=None, show=None, output=False, checkfail=True, shell=False)
         return call(cmd, cwd=dir, shell=shell)
 
 
+def clone_gitroot(test_master, revision="HEAD"):
+    run_cmd(["git", "clone", "--shared", gitroot, test_master])
+    if revision != "HEAD":
+        run_cmd(["git", "checkout", revision])
+
+
+class RetryChecker(object):
+    """Check whether it is necessary to retry."""
+
+    def __init__(self, dir):
+        run_cmd(["git", "remote", "add", "-t", "master", "master", samba_master])
+        run_cmd(["git", "fetch", "master"])
+        cmd = '''set -e
+                while :; do
+                  sleep 60
+                  git describe master/master > old_master.desc
+                  git fetch master
+                  git describe master/master > master.desc
+                  diff old_master.desc master.desc
+                done
+               '''
+        self.proc = Popen(cmd, shell=True, cwd=self.dir)
+
+    def poll(self):
+        return self.proc.poll()
+
+    def kill(self):
+        self.proc.terminate()
+        self.proc.wait()
+        self.retry.proc = None
+
+
 class TreeStageBuilder(object):
     """Handle building of a particular stage for a tree.
     """
@@ -102,15 +125,15 @@ class TreeStageBuilder(object):
         self.name = name
         self.command = command
         self.fail_quickly = fail_quickly
-        self.status = None
+        self.exitcode = None
         self.stdin = open(os.devnull, 'r')
 
     def start(self):
         raise NotImplementedError(self.start)
 
     def poll(self):
-        self.status = self.proc.poll()
-        return self.status
+        self.exitcode = self.proc.poll()
+        return self.exitcode
 
     def kill(self):
         if self.proc is not None:
@@ -125,11 +148,11 @@ class TreeStageBuilder(object):
 
     @property
     def failure_reason(self):
-        return "failed '%s' with status %d" % (self.cmd, self.status)
+        raise NotImplementedError(self.failure_reason)
 
     @property
     def failed(self):
-        return (os.WIFSIGNALED(self.status) or os.WEXITSTATUS(self.status) != 0)
+        return (self.exitcode != 0)
 
 
 class PlainTreeStageBuilder(TreeStageBuilder):
@@ -140,6 +163,10 @@ class PlainTreeStageBuilder(TreeStageBuilder):
                           stdout=self.tree.stdout, stderr=self.tree.stderr,
                           stdin=self.stdin)
 
+    @property
+    def failure_reason(self):
+        return "failed '%s' with exit code %d" % (self.command, self.exitcode)
+
 
 class AbortingTestResult(subunithelper.TestsuiteEnabledTestResult):
 
@@ -154,20 +181,37 @@ class AbortingTestResult(subunithelper.TestsuiteEnabledTestResult):
         self.stage.proc.terminate()
 
 
+class FailureTrackingTestResult(subunithelper.TestsuiteEnabledTestResult):
+
+    def __init__(self, stage):
+        super(FailureTrackingTestResult, self).__init__()
+        self.stage = stage
+
+    def addError(self, test, details=None):
+        if self.stage.failed_test is None:
+            self.stage.failed_test = ("error", test)
+
+    def addFailure(self, test, details=None):
+        if self.stage.failed_test is None:
+            self.stage.failed_test = ("failure", test)
+
+
 class SubunitTreeStageBuilder(TreeStageBuilder):
 
     def __init__(self, tree, name, command, fail_quickly=False):
         super(SubunitTreeStageBuilder, self).__init__(tree, name, command,
                 fail_quickly)
-        self.failed_tests = []
+        self.failed_test = None
         self.subunit_path = os.path.join(gitroot,
             "%s.%s.subunit" % (self.tree.tag, self.name))
         self.tree.logfiles.append(
-            (self.subunit_path, os.path.basename(self.subunit_path)))
+            (self.subunit_path, os.path.basename(self.subunit_path),
+             "text/x-subunit"))
         self.subunit = open(self.subunit_path, 'w')
 
         formatter = subunithelper.PlainFormatter(False, True, {})
-        clients = [formatter, subunit.TestProtocolClient(self.subunit)]
+        clients = [formatter, subunit.TestProtocolClient(self.subunit),
+                   FailureTrackingTestResult(self)]
         if fail_quickly:
             clients.append(AbortingTestResult(self))
         self.subunit_server = subunit.TestProtocolServer(
@@ -189,7 +233,6 @@ class SubunitTreeStageBuilder(TreeStageBuilder):
         except IOError:
             return None
         else:
-            self.tree.stdout.write(data)
             self.buffered += data
             buffered = ""
             for l in self.buffered.splitlines(True):
@@ -198,10 +241,17 @@ class SubunitTreeStageBuilder(TreeStageBuilder):
                 else:
                     buffered += l
             self.buffered = buffered
-            self.status = self.proc.poll()
-            if self.status is not None:
+            self.exitcode = self.proc.poll()
+            if self.exitcode is not None:
                 self.subunit.close()
-            return self.status
+            return self.exitcode
+
+    @property
+    def failure_reason(self):
+        if self.failed_test:
+            return "failed '%s' with %s in test %s" (self.command, self.failed_test[0], self.failed_test[1])
+        else:
+            return "failed '%s' with exit code %d in unknown test" % (self.command, self.exitcode)
 
 
 class TreeBuilder(object):
@@ -214,10 +264,13 @@ class TreeBuilder(object):
         self.tag = self.name.replace('/', '_')
         self.sequence = sequence
         self.next = 0
+        self.stages = []
         self.stdout_path = os.path.join(gitroot, "%s.stdout" % (self.tag, ))
         self.stderr_path = os.path.join(gitroot, "%s.stderr" % (self.tag, ))
-        self.logfiles = [(self.stdout_path, os.path.basename(self.stdout_path)),
-                         (self.stderr_path, os.path.basename(self.stderr_path))]
+        self.logfiles = [
+            (self.stdout_path, os.path.basename(self.stdout_path), "text/plain"),
+            (self.stderr_path, os.path.basename(self.stderr_path), "text/plain"),
+            ]
         if options.verbose:
             print("stdout for %s in %s" % (self.name, self.stdout_path))
             print("stderr for %s in %s" % (self.name, self.stderr_path))
@@ -238,8 +291,9 @@ class TreeBuilder(object):
         cleanup_list.append(self.prefix)
         os.makedirs(self.sdir)
         run_cmd(["rm",  "-rf", self.sdir])
-        run_cmd(["git", "clone", "--shared", gitroot, self.sdir])
+        clone_gitroot(self.sdir, revision)
         self.start_next()
+        self.exitcode = None
 
     def start_next(self):
         if self.next == len(self.sequence):
@@ -251,42 +305,47 @@ class TreeBuilder(object):
         (stage_name, cmd, output_mime_type) = self.sequence[self.next]
         cmd = cmd.replace("${PREFIX}", "--prefix=%s" % self.prefix)
         if output_mime_type == "text/plain":
-            self.stage = PlainTreeStageBuilder(self, stage_name, cmd,
+            self.current_stage = PlainTreeStageBuilder(self, stage_name, cmd,
                 self.fail_quickly)
         elif output_mime_type == "text/x-subunit":
-            self.stage = SubunitTreeStageBuilder(self, stage_name, cmd,
+            self.current_stage = SubunitTreeStageBuilder(self, stage_name, cmd,
                 self.fail_quickly)
         else:
             raise Exception("Unknown output mime type %s" % output_mime_type)
-        self.stage.start()
+        self.stages.append(self.current_stage)
+        self.current_stage.start()
         self.next += 1
 
     def remove_logs(self):
-        for path, name in self.logfiles:
+        for path, name, mime_type in self.logfiles:
             os.unlink(path)
 
-    @property
-    def status(self):
-        return self.stage.status
-
     def poll(self):
-        return self.stage.poll()
+        self.exitcode = self.current_stage.poll()
+        if self.exitcode is not None:
+            self.current_stage = None
+        return self.exitcode
 
     def kill(self):
-        if self.stage is not None:
-            self.stage.kill()
-            self.stage = None
+        if self.current_stage is not None:
+            self.current_stage.kill()
+            self.current_stage = None
 
     @property
     def failed(self):
-        if self.stage is None:
-            return False
-        return self.stage.failed
+        return any([s.failed for s in self.stages])
+
+    @property
+    def failed_stage(self):
+        for s in self.stages:
+            if s.failed:
+                return s
+        return s
 
     @property
     def failure_reason(self):
-        return "%s: [%s] %s" % (self.name, self.stage.name,
-            self.stage.failure_reason)
+        return "%s: [%s] %s" % (self.name, self.failed_stage.name,
+            self.failed_stage.failure_reason)
 
 
 class BuildList(object):
@@ -307,8 +366,7 @@ class BuildList(object):
             b = TreeBuilder(n, tasks[n], not options.fail_slowly)
             self.tlist.append(b)
         if options.retry:
-            self.retry = TreeBuilder('retry', retry_task,
-                not options.fail_slowly)
+            self.retry = RetryChecker(self.sdir)
             self.need_retry = False
 
     def kill_kids(self):
@@ -317,9 +375,7 @@ class BuildList(object):
             self.tail_proc.wait()
             self.tail_proc = None
         if self.retry is not None:
-            self.retry.proc.terminate()
-            self.retry.proc.wait()
-            self.retry = None
+            self.retry.kill()
         for b in self.tlist:
             b.kill()
 
@@ -327,16 +383,15 @@ class BuildList(object):
         while True:
             none_running = True
             for b in self.tlist:
-                if b.stage is None:
+                if b.current_stage is None:
                     continue
                 none_running = False
                 if b.poll() is None:
                     continue
-                b.stage = None
                 return b
             if options.retry:
-                ret = self.retry.proc.poll()
-                if ret is not None:
+                ret = self.retry.poll()
+                if ret:
                     self.need_retry = True
                     self.retry = None
                     return None
@@ -355,20 +410,28 @@ class BuildList(object):
                 break
             if b.failed:
                 self.kill_kids()
-                return (b.status, b.name, b.stage, b.tag, b.failure_reason)
+                return (b.exitcode, b.name, b.failed_stage, b.tag, b.failure_reason)
             b.start_next()
         self.kill_kids()
         return (0, None, None, None, "All OK")
 
-    def tarlogs(self, fname):
-        tar = tarfile.open(fname, "w:gz")
+    def tarlogs(self, name=None, fileobj=None):
+        tar = tarfile.open(name=name, fileobj=fileobj, mode="w:gz")
         for b in self.tlist:
-            for (path, name) in b.logfiles:
+            for (path, name, mime_type) in b.logfiles:
                 tar.add(path, arcname=name)
         if os.path.exists("autobuild.log"):
             tar.add("autobuild.log")
         tar.close()
 
+    def attach_logs(self, outer):
+        f = StringIO()
+        self.tarlogs(fileobj=f)
+        msg = MIMEApplication(f.getvalue(), "x-gzip")
+        msg.add_header('Content-Disposition', 'attachment',
+                       filename="logs.tar.gz")
+        outer.attach(msg)
+
     def remove_logs(self):
         for b in self.tlist:
             b.remove_logs()
@@ -421,13 +484,16 @@ def daemonize(logfile):
 
 def rebase_tree(url):
     print("Rebasing on %s" % url)
-    run_cmd(["git", "remote", "add", "-t", "master", "master", url], show=True, dir=test_master)
+    run_cmd(["git", "remote", "add", "-t", "master", "master", url], show=True,
+            dir=test_master)
     run_cmd(["git", "fetch", "master"], show=True, dir=test_master)
     if options.fix_whitespace:
-        run_cmd(["git", "rebase", "--whitespace=fix", "master/master"], show=True, dir=test_master)
+        run_cmd(["git", "rebase", "--whitespace=fix", "master/master"],
+                show=True, dir=test_master)
     else:
         run_cmd(["git", "rebase", "master/master"], show=True, dir=test_master)
-    diff = run_cmd(["git", "--no-pager", "diff", "HEAD", "master/master"], dir=test_master, output=True)
+    diff = run_cmd(["git", "--no-pager", "diff", "HEAD", "master/master"],
+        dir=test_master, output=True)
     if diff == '':
         print("No differences between HEAD and master/master - exiting")
         sys.exit(0)
@@ -435,49 +501,60 @@ def rebase_tree(url):
 def push_to(url):
     print("Pushing to %s" % url)
     if options.mark:
-        run_cmd("EDITOR=script/commit_mark.sh git commit --amend -c HEAD", dir=test_master, shell=True)
-        # the notes method doesn't work yet, as metze hasn't allowed refs/notes/* in master
-        # run_cmd("EDITOR=script/commit_mark.sh git notes edit HEAD", dir=test_master)
-    run_cmd(["git", "remote", "add", "-t", "master", "pushto", url], show=True, dir=test_master)
-    run_cmd(["git", "push", "pushto", "+HEAD:master"], show=True, dir=test_master)
-
-def_testbase = os.getenv("AUTOBUILD_TESTBASE", "/memdisk/%s" % os.getenv('USER'))
+        run_cmd("EDITOR=script/commit_mark.sh git commit --amend -c HEAD",
+            dir=test_master, shell=True)
+        # the notes method doesn't work yet, as metze hasn't allowed
+        # refs/notes/* in master
+        # run_cmd("EDITOR=script/commit_mark.sh git notes edit HEAD",
+        #     dir=test_master)
+    run_cmd(["git", "remote", "add", "-t", "master", "pushto", url], show=True,
+        dir=test_master)
+    run_cmd(["git", "push", "pushto", "+HEAD:master"], show=True,
+        dir=test_master)
+
+def_testbase = os.getenv("AUTOBUILD_TESTBASE")
+if def_testbase is None:
+    if os.path.exists("/memdisk"):
+        def_testbase = "/memdisk/%s" % os.getenv('USER')
+    else:
+        def_testbase = os.path.join(tempfile.gettempdir(), "autobuild-%s" % os.getenv("USER"))
 
 parser = OptionParser()
-parser.add_option("", "--repository", help="repository to run tests for", default=None, type=str)
-parser.add_option("", "--tail", help="show output while running", default=False, action="store_true")
-parser.add_option("", "--keeplogs", help="keep logs", default=False, action="store_true")
-parser.add_option("", "--nocleanup", help="don't remove test tree", default=False, action="store_true")
-parser.add_option("", "--testbase", help="base directory to run tests in (default %s)" % def_testbase,
+parser.add_option("--repository", help="repository to run tests for", default=None, type=str)
+parser.add_option("--revision", help="revision to compile if not HEAD", default=None, type=str)
+parser.add_option("--tail", help="show output while running", default=False, action="store_true")
+parser.add_option("--keeplogs", help="keep logs", default=False, action="store_true")
+parser.add_option("--nocleanup", help="don't remove test tree", default=False, action="store_true")
+parser.add_option("--testbase", help="base directory to run tests in (default %s)" % def_testbase,
                   default=def_testbase)
-parser.add_option("", "--passcmd", help="command to run on success", default=None)
-parser.add_option("", "--verbose", help="show all commands as they are run",
+parser.add_option("--passcmd", help="command to run on success", default=None)
+parser.add_option("--verbose", help="show all commands as they are run",
                   default=False, action="store_true")
-parser.add_option("", "--rebase", help="rebase on the given tree before testing",
+parser.add_option("--rebase", help="rebase on the given tree before testing",
                   default=None, type='str')
-parser.add_option("", "--rebase-master", help="rebase on %s before testing" % samba_master,
+parser.add_option("--rebase-master", help="rebase on %s before testing" % samba_master,
                   default=False, action='store_true')
-parser.add_option("", "--pushto", help="push to a git url on success",
+parser.add_option("--pushto", help="push to a git url on success",
                   default=None, type='str')
-parser.add_option("", "--push-master", help="push to %s on success" % samba_master_ssh,
+parser.add_option("--push-master", help="push to %s on success" % samba_master_ssh,
                   default=False, action='store_true')
-parser.add_option("", "--mark", help="add a Tested-By signoff before pushing",
+parser.add_option("--mark", help="add a Tested-By signoff before pushing",
                   default=False, action="store_true")
-parser.add_option("", "--fix-whitespace", help="fix whitespace on rebase",
+parser.add_option("--fix-whitespace", help="fix whitespace on rebase",
                   default=False, action="store_true")
-parser.add_option("", "--retry", help="automatically retry if master changes",
+parser.add_option("--retry", help="automatically retry if master changes",
                   default=False, action="store_true")
-parser.add_option("", "--email", help="send email to the given address on failure",
+parser.add_option("--email", help="send email to the given address on failure",
                   type='str', default=None)
-parser.add_option("", "--always-email", help="always send email, even on success",
+parser.add_option("--always-email", help="always send email, even on success",
                   action="store_true")
-parser.add_option("", "--daemon", help="daemonize after initial setup",
+parser.add_option("--daemon", help="daemonize after initial setup",
                   action="store_true")
-parser.add_option("", "--fail-slowly", help="continue running tests even after one has already failed",
+parser.add_option("--fail-slowly", help="continue running tests even after one has already failed",
                   action="store_true")
 
 
-def email_failure(status, failed_task, failed_stage, failed_tag, errstr):
+def email_failure(blist, exitcode, failed_task, failed_stage, failed_tag, errstr):
     '''send an email to options.email about the failure'''
     user = os.getenv("USER")
     text = '''
@@ -505,18 +582,26 @@ The top commit for the tree that was built was:
 
 %s
 
-''' % (failed_task, errstr, user, failed_tag, user, failed_tag, user, user, top_commit_msg)
-    msg = MIMEText(text)
-    msg['Subject'] = 'autobuild failure for task %s during %s' % (failed_task, failed_stage)
+''' % (failed_task, errstr, user, failed_tag, user, failed_tag, user, user,
+       get_top_commit_msg(test_master))
+
+    msg = MIMEMultipart()
+    msg['Subject'] = 'autobuild failure for task %s during %s' % (
+        failed_task, failed_stage.name)
     msg['From'] = 'autobuild@samba.org'
     msg['To'] = options.email
 
+    main = MIMEText(text)
+    msg.attach(main)
+
+    blist.attach_logs(msg)
+
     s = smtplib.SMTP()
     s.connect()
     s.sendmail(msg['From'], [msg['To']], msg.as_string())
     s.quit()
 
-def email_success():
+def email_success(blist):
     '''send an email to options.email about a successful build'''
     user = os.getenv("USER")
     text = '''
@@ -539,13 +624,18 @@ you can get full logs of all tasks in this job here:
 The top commit for the tree that was built was:
 
 %s
-''' % top_commit_msg
+''' % (get_top_commit_msg(test_master),)
 
-    msg = MIMEText(text)
+    msg = MIMEMultipart()
     msg['Subject'] = 'autobuild success'
     msg['From'] = 'autobuild@samba.org'
     msg['To'] = options.email
 
+    main = MIMEText(text, 'plain')
+    msg.attach(main)
+
+    blist.attach_logs(msg)
+
     s = smtplib.SMTP()
     s.connect()
     s.sendmail(msg['From'], [msg['To']], msg.as_string())
@@ -571,7 +661,13 @@ if gitroot is None:
     raise Exception("Failed to find git root under %s" % repository)
 
 # get the top commit message, for emails
-top_commit_msg = run_cmd(["git", "log", "-1"], dir=gitroot, output=True)
+if options.revision is not None:
+    revision = options.revision
+else:
+    revision = "HEAD"
+
+def get_top_commit_msg(reporoot):
+    return run_cmd(["git", "log", "-1"], dir=reporoot, output=True)
 
 try:
     os.makedirs(testbase)
@@ -588,7 +684,7 @@ while True:
     try:
         run_cmd(["rm", "-rf", test_master])
         cleanup_list.append(test_master)
-        run_cmd(["git", "clone", "--shared", gitroot, test_master])
+        clone_gitroot(test_master, revision)
     except:
         cleanup()
         raise
@@ -601,8 +697,8 @@ while True:
         blist = BuildList(tasks, args)
         if options.tail:
             blist.start_tail()
-        (status, failed_task, failed_stage, failed_tag, errstr) = blist.run()
-        if status != 0 or errstr != "retry":
+        (exitcode, failed_task, failed_stage, failed_tag, errstr) = blist.run()
+        if exitcode != 0 or errstr != "retry":
             break
         cleanup()
     except:
@@ -614,7 +710,7 @@ if options.tail:
     print("waiting for tail to flush")
     time.sleep(1)
 
-if status == 0:
+if exitcode == 0:
     print errstr
     if options.passcmd is not None:
         print("Running passcmd: %s" % options.passcmd)
@@ -627,7 +723,7 @@ if status == 0:
         blist.tarlogs("logs.tar.gz")
         print("Logs in logs.tar.gz")
     if options.always_email:
-        email_success()
+        email_success(blist)
     blist.remove_logs()
     cleanup()
     print(errstr)
@@ -636,9 +732,10 @@ else:
     blist.tarlogs("logs.tar.gz")
 
     if options.email is not None:
-        email_failure(status, failed_task, failed_stage, failed_tag, errstr)
+        email_failure(blist, exitcode, failed_task, failed_stage, failed_tag,
+            errstr)
 
     cleanup()
     print(errstr)
     print("Logs in logs.tar.gz")
-sys.exit(status)
+sys.exit(exitcode)