2 # run tests on all Samba subprojects and push to a git tree on success
3 # Copyright Andrew Tridgell 2010
4 # Copyright Jelmer Vernooij 2010
5 # released under GNU GPL v3 or later
7 from cStringIO
import StringIO
9 from subprocess
import call
, check_call
, Popen
, PIPE
10 import os
, tarfile
, sys
, time
11 from optparse
import OptionParser
13 sys
.path
.insert(0, os
.path
.join(os
.path
.dirname(__file__
), "../selftest"))
14 sys
.path
.insert(0, os
.path
.join(os
.path
.dirname(__file__
), "../lib/testtools"))
15 sys
.path
.insert(0, os
.path
.join(os
.path
.dirname(__file__
), "../lib/subunit/python"))
20 from email
.mime
.application
import MIMEApplication
21 from email
.mime
.text
import MIMEText
22 from email
.mime
.multipart
import MIMEMultipart
24 samba_master
= os
.getenv('SAMBA_MASTER', 'git://git.samba.org/samba.git')
25 samba_master_ssh
= os
.getenv('SAMBA_MASTER_SSH', 'git+ssh://git.samba.org/data/git/samba.git')
29 os
.environ
['CC'] = "ccache gcc"
32 "source3" : [ ("autogen", "./autogen.sh", "text/plain"),
33 ("configure", "./configure.developer ${PREFIX}", "text/plain"),
34 ("make basics", "make basics", "text/plain"),
35 ("make", "make -j 4 everything", "text/plain"), # don't use too many processes
36 ("install", "make install", "text/plain"),
37 ("test", "TDB_NO_FSYNC=1 make subunit-test", "text/x-subunit") ],
39 "source4" : [ ("configure", "./configure.developer ${PREFIX}", "text/plain"),
40 ("make", "make -j", "text/plain"),
41 ("install", "make install", "text/plain"),
42 ("test", "TDB_NO_FSYNC=1 make subunit-test", "text/x-subunit") ],
44 "source4/lib/ldb" : [ ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
45 ("make", "make -j", "text/plain"),
46 ("install", "make install", "text/plain"),
47 ("test", "make test", "text/plain") ],
49 "lib/tdb" : [ ("autogen", "./autogen-waf.sh", "text/plain"),
50 ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
51 ("make", "make -j", "text/plain"),
52 ("install", "make install", "text/plain"),
53 ("test", "make test", "text/plain") ],
55 "lib/talloc" : [ ("autogen", "./autogen-waf.sh", "text/plain"),
56 ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
57 ("make", "make -j", "text/plain"),
58 ("install", "make install", "text/plain"),
59 ("test", "make test", "text/x-subunit"), ],
61 "lib/replace" : [ ("autogen", "./autogen-waf.sh", "text/plain"),
62 ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
63 ("make", "make -j", "text/plain"),
64 ("install", "make install", "text/plain"),
65 ("test", "make test", "text/plain"), ],
67 "lib/tevent" : [ ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
68 ("make", "make -j", "text/plain"),
69 ("install", "make install", "text/plain"),
70 ("test", "make test", "text/plain"), ],
74 def run_cmd(cmd
, dir=None, show
=None, output
=False, checkfail
=True, shell
=False):
76 show
= options
.verbose
78 print("Running: '%s' in '%s'" % (cmd
, dir))
80 return Popen(cmd
, stdout
=PIPE
, cwd
=dir, shell
=shell
).communicate()[0]
82 return check_call(cmd
, cwd
=dir, shell
=shell
)
84 return call(cmd
, cwd
=dir, shell
=shell
)
87 def clone_gitroot(test_master
, revision
="HEAD"):
88 run_cmd(["git", "clone", "--shared", gitroot
, test_master
])
89 if revision
!= "HEAD":
90 run_cmd(["git", "checkout", revision
])
93 class RetryChecker(object):
94 """Check whether it is necessary to retry."""
96 def __init__(self
, dir):
97 run_cmd(["git", "remote", "add", "-t", "master", "master", samba_master
])
98 run_cmd(["git", "fetch", "master"])
102 git describe master/master > old_master.desc
104 git describe master/master > master.desc
105 diff old_master.desc master.desc
108 self
.proc
= Popen(cmd
, shell
=True, cwd
=self
.dir)
111 return self
.proc
.poll()
114 self
.proc
.terminate()
116 self
.retry
.proc
= None
119 class TreeStageBuilder(object):
120 """Handle building of a particular stage for a tree.
123 def __init__(self
, tree
, name
, command
, fail_quickly
=False):
126 self
.command
= command
127 self
.fail_quickly
= fail_quickly
129 self
.stdin
= open(os
.devnull
, 'r')
132 raise NotImplementedError(self
.start
)
135 self
.exitcode
= self
.proc
.poll()
139 if self
.proc
is not None:
141 run_cmd(["killbysubdir", self
.tree
.sdir
], checkfail
=False)
143 # killbysubdir doesn't exist ?
145 self
.proc
.terminate()
150 def failure_reason(self
):
151 raise NotImplementedError(self
.failure_reason
)
155 return (self
.exitcode
!= 0)
158 class PlainTreeStageBuilder(TreeStageBuilder
):
161 print '%s: [%s] Running %s' % (self
.name
, self
.name
, self
.command
)
162 self
.proc
= Popen(self
.command
, shell
=True, cwd
=self
.tree
.dir,
163 stdout
=self
.tree
.stdout
, stderr
=self
.tree
.stderr
,
167 def failure_reason(self
):
168 return "failed '%s' with exit code %d" % (self
.command
, self
.exitcode
)
171 class AbortingTestResult(subunithelper
.TestsuiteEnabledTestResult
):
173 def __init__(self
, stage
):
174 super(AbortingTestResult
, self
).__init
__()
177 def addError(self
, test
, details
=None):
178 self
.stage
.proc
.terminate()
180 def addFailure(self
, test
, details
=None):
181 self
.stage
.proc
.terminate()
184 class FailureTrackingTestResult(subunithelper
.TestsuiteEnabledTestResult
):
186 def __init__(self
, stage
):
187 super(FailureTrackingTestResult
, self
).__init
__()
190 def addError(self
, test
, details
=None):
191 if self
.stage
.failed_test
is None:
192 self
.stage
.failed_test
= ("error", test
)
194 def addFailure(self
, test
, details
=None):
195 if self
.stage
.failed_test
is None:
196 self
.stage
.failed_test
= ("failure", test
)
199 class SubunitTreeStageBuilder(TreeStageBuilder
):
201 def __init__(self
, tree
, name
, command
, fail_quickly
=False):
202 super(SubunitTreeStageBuilder
, self
).__init
__(tree
, name
, command
,
204 self
.failed_test
= None
205 self
.subunit_path
= os
.path
.join(gitroot
,
206 "%s.%s.subunit" % (self
.tree
.tag
, self
.name
))
207 self
.tree
.logfiles
.append(
208 (self
.subunit_path
, os
.path
.basename(self
.subunit_path
),
210 self
.subunit
= open(self
.subunit_path
, 'w')
212 formatter
= subunithelper
.PlainFormatter(False, True, {})
213 clients
= [formatter
, subunit
.TestProtocolClient(self
.subunit
),
214 FailureTrackingTestResult(self
)]
216 clients
.append(AbortingTestResult(self
))
217 self
.subunit_server
= subunit
.TestProtocolServer(
218 testtools
.MultiTestResult(*clients
),
223 print '%s: [%s] Running' % (self
.tree
.name
, self
.name
)
224 self
.proc
= Popen(self
.command
, shell
=True, cwd
=self
.tree
.dir,
225 stdout
=PIPE
, stderr
=self
.tree
.stderr
, stdin
=self
.stdin
)
226 fd
= self
.proc
.stdout
.fileno()
227 fl
= fcntl
.fcntl(fd
, fcntl
.F_GETFL
)
228 fcntl
.fcntl(fd
, fcntl
.F_SETFL
, fl | os
.O_NONBLOCK
)
232 data
= self
.proc
.stdout
.read()
236 self
.buffered
+= data
238 for l
in self
.buffered
.splitlines(True):
240 self
.subunit_server
.lineReceived(l
)
243 self
.buffered
= buffered
244 self
.exitcode
= self
.proc
.poll()
245 if self
.exitcode
is not None:
250 def failure_reason(self
):
252 return "failed '%s' with %s in test %s" (self
.command
, self
.failed_test
[0], self
.failed_test
[1])
254 return "failed '%s' with exit code %d in unknown test" % (self
.command
, self
.exitcode
)
257 class TreeBuilder(object):
258 '''handle build of one directory'''
260 def __init__(self
, name
, sequence
, fail_quickly
=False):
262 self
.fail_quickly
= fail_quickly
264 self
.tag
= self
.name
.replace('/', '_')
265 self
.sequence
= sequence
268 self
.stdout_path
= os
.path
.join(gitroot
, "%s.stdout" % (self
.tag
, ))
269 self
.stderr_path
= os
.path
.join(gitroot
, "%s.stderr" % (self
.tag
, ))
271 (self
.stdout_path
, os
.path
.basename(self
.stdout_path
), "text/plain"),
272 (self
.stderr_path
, os
.path
.basename(self
.stderr_path
), "text/plain"),
275 print("stdout for %s in %s" % (self
.name
, self
.stdout_path
))
276 print("stderr for %s in %s" % (self
.name
, self
.stderr_path
))
277 if os
.path
.exists(self
.stdout_path
):
278 os
.unlink(self
.stdout_path
)
279 if os
.path
.exists(self
.stderr_path
):
280 os
.unlink(self
.stderr_path
)
281 self
.stdout
= open(self
.stdout_path
, 'w')
282 self
.stderr
= open(self
.stderr_path
, 'w')
283 self
.sdir
= os
.path
.join(testbase
, self
.tag
)
284 if name
in ['pass', 'fail', 'retry']:
287 self
.dir = os
.path
.join(self
.sdir
, self
.name
)
288 self
.prefix
= os
.path
.join(testbase
, "prefix", self
.tag
)
289 run_cmd(["rm", "-rf", self
.sdir
])
290 cleanup_list
.append(self
.sdir
)
291 cleanup_list
.append(self
.prefix
)
292 os
.makedirs(self
.sdir
)
293 run_cmd(["rm", "-rf", self
.sdir
])
294 clone_gitroot(self
.sdir
, revision
)
298 def start_next(self
):
299 if self
.next
== len(self
.sequence
):
300 print '%s: Completed OK' % self
.name
305 (stage_name
, cmd
, output_mime_type
) = self
.sequence
[self
.next
]
306 cmd
= cmd
.replace("${PREFIX}", "--prefix=%s" % self
.prefix
)
307 if output_mime_type
== "text/plain":
308 self
.current_stage
= PlainTreeStageBuilder(self
, stage_name
, cmd
,
310 elif output_mime_type
== "text/x-subunit":
311 self
.current_stage
= SubunitTreeStageBuilder(self
, stage_name
, cmd
,
314 raise Exception("Unknown output mime type %s" % output_mime_type
)
315 self
.stages
.append(self
.current_stage
)
316 self
.current_stage
.start()
319 def remove_logs(self
):
320 for path
, name
, mime_type
in self
.logfiles
:
324 self
.exitcode
= self
.current_stage
.poll()
325 if self
.exitcode
is not None:
326 self
.current_stage
= None
330 if self
.current_stage
is not None:
331 self
.current_stage
.kill()
332 self
.current_stage
= None
336 return any([s
.failed
for s
in self
.stages
])
339 def failed_stage(self
):
340 for s
in self
.stages
:
346 def failure_reason(self
):
347 return "%s: [%s] %s" % (self
.name
, self
.failed_stage
.name
,
348 self
.failed_stage
.failure_reason
)
351 class BuildList(object):
352 '''handle build of multiple directories'''
354 def __init__(self
, tasklist
, tasknames
):
357 self
.tail_proc
= None
359 if tasknames
== ['pass']:
360 tasks
= { 'pass' : [ ("pass", '/bin/true', "text/plain") ]}
361 if tasknames
== ['fail']:
362 tasks
= { 'fail' : [ ("fail", '/bin/false', "text/plain") ]}
366 b
= TreeBuilder(n
, tasks
[n
], not options
.fail_slowly
)
369 self
.retry
= RetryChecker(self
.sdir
)
370 self
.need_retry
= False
373 if self
.tail_proc
is not None:
374 self
.tail_proc
.terminate()
375 self
.tail_proc
.wait()
376 self
.tail_proc
= None
377 if self
.retry
is not None:
386 if b
.current_stage
is None:
393 ret
= self
.retry
.poll()
395 self
.need_retry
= True
405 if options
.retry
and self
.need_retry
:
407 print("retry needed")
408 return (0, None, None, None, "retry")
413 return (b
.exitcode
, b
.name
, b
.failed_stage
, b
.tag
, b
.failure_reason
)
416 return (0, None, None, None, "All OK")
418 def tarlogs(self
, name
=None, fileobj
=None):
419 tar
= tarfile
.open(name
=name
, fileobj
=fileobj
, mode
="w:gz")
421 for (path
, name
, mime_type
) in b
.logfiles
:
422 tar
.add(path
, arcname
=name
)
423 if os
.path
.exists("autobuild.log"):
424 tar
.add("autobuild.log")
427 def attach_logs(self
, outer
):
429 self
.tarlogs(fileobj
=f
)
430 msg
= MIMEApplication(f
.getvalue(), "x-gzip")
431 msg
.add_header('Content-Disposition', 'attachment',
432 filename
="logs.tar.gz")
435 def remove_logs(self
):
439 def start_tail(self
):
440 cmd
= "tail -f *.stdout *.stderr"
441 self
.tail_proc
= Popen(cmd
, shell
=True, cwd
=gitroot
)
445 if options
.nocleanup
:
447 print("Cleaning up ....")
448 for d
in cleanup_list
:
449 run_cmd(["rm", "-rf", d
])
452 def find_git_root(p
):
453 '''get to the top of the git repo'''
455 if os
.path
.isdir(os
.path
.join(p
, ".git")):
457 p
= os
.path
.abspath(os
.path
.join(p
, '..'))
461 def daemonize(logfile
):
463 if pid
== 0: # Parent
466 if pid
!= 0: # Actual daemon
471 import resource
# Resource usage information.
472 maxfd
= resource
.getrlimit(resource
.RLIMIT_NOFILE
)[1]
473 if maxfd
== resource
.RLIM_INFINITY
:
474 maxfd
= 1024 # Rough guess at maximum number of open file descriptors.
475 for fd
in range(0, maxfd
):
480 os
.open(logfile
, os
.O_RDWR | os
.O_CREAT
)
485 def rebase_tree(url
):
486 print("Rebasing on %s" % url
)
487 run_cmd(["git", "remote", "add", "-t", "master", "master", url
], show
=True,
489 run_cmd(["git", "fetch", "master"], show
=True, dir=test_master
)
490 if options
.fix_whitespace
:
491 run_cmd(["git", "rebase", "--whitespace=fix", "master/master"],
492 show
=True, dir=test_master
)
494 run_cmd(["git", "rebase", "master/master"], show
=True, dir=test_master
)
495 diff
= run_cmd(["git", "--no-pager", "diff", "HEAD", "master/master"],
496 dir=test_master
, output
=True)
498 print("No differences between HEAD and master/master - exiting")
502 print("Pushing to %s" % url
)
504 run_cmd("EDITOR=script/commit_mark.sh git commit --amend -c HEAD",
505 dir=test_master
, shell
=True)
506 # the notes method doesn't work yet, as metze hasn't allowed
507 # refs/notes/* in master
508 # run_cmd("EDITOR=script/commit_mark.sh git notes edit HEAD",
510 run_cmd(["git", "remote", "add", "-t", "master", "pushto", url
], show
=True,
512 run_cmd(["git", "push", "pushto", "+HEAD:master"], show
=True,
515 def_testbase
= os
.getenv("AUTOBUILD_TESTBASE")
516 if def_testbase
is None:
517 if os
.path
.exists("/memdisk"):
518 def_testbase
= "/memdisk/%s" % os
.getenv('USER')
520 def_testbase
= os
.path
.join(tempfile
.gettempdir(), "autobuild-%s" % os
.getenv("USER"))
522 parser
= OptionParser()
523 parser
.add_option("--repository", help="repository to run tests for", default
=None, type=str)
524 parser
.add_option("--revision", help="revision to compile if not HEAD", default
=None, type=str)
525 parser
.add_option("--tail", help="show output while running", default
=False, action
="store_true")
526 parser
.add_option("--keeplogs", help="keep logs", default
=False, action
="store_true")
527 parser
.add_option("--nocleanup", help="don't remove test tree", default
=False, action
="store_true")
528 parser
.add_option("--testbase", help="base directory to run tests in (default %s)" % def_testbase
,
529 default
=def_testbase
)
530 parser
.add_option("--passcmd", help="command to run on success", default
=None)
531 parser
.add_option("--verbose", help="show all commands as they are run",
532 default
=False, action
="store_true")
533 parser
.add_option("--rebase", help="rebase on the given tree before testing",
534 default
=None, type='str')
535 parser
.add_option("--rebase-master", help="rebase on %s before testing" % samba_master
,
536 default
=False, action
='store_true')
537 parser
.add_option("--pushto", help="push to a git url on success",
538 default
=None, type='str')
539 parser
.add_option("--push-master", help="push to %s on success" % samba_master_ssh
,
540 default
=False, action
='store_true')
541 parser
.add_option("--mark", help="add a Tested-By signoff before pushing",
542 default
=False, action
="store_true")
543 parser
.add_option("--fix-whitespace", help="fix whitespace on rebase",
544 default
=False, action
="store_true")
545 parser
.add_option("--retry", help="automatically retry if master changes",
546 default
=False, action
="store_true")
547 parser
.add_option("--email", help="send email to the given address on failure",
548 type='str', default
=None)
549 parser
.add_option("--always-email", help="always send email, even on success",
551 parser
.add_option("--daemon", help="daemonize after initial setup",
553 parser
.add_option("--fail-slowly", help="continue running tests even after one has already failed",
557 def email_failure(blist
, exitcode
, failed_task
, failed_stage
, failed_tag
, errstr
):
558 '''send an email to options.email about the failure'''
559 user
= os
.getenv("USER")
563 Your autobuild failed when trying to test %s with the following error:
566 the autobuild has been abandoned. Please fix the error and resubmit.
568 You can see logs of the failed task here:
570 http://git.samba.org/%s/samba-autobuild/%s.stdout
571 http://git.samba.org/%s/samba-autobuild/%s.stderr
573 A summary of the autobuild process is here:
575 http://git.samba.org/%s/samba-autobuild/autobuild.log
577 or you can get full logs of all tasks in this job here:
579 http://git.samba.org/%s/samba-autobuild/logs.tar.gz
581 The top commit for the tree that was built was:
585 ''' % (failed_task
, errstr
, user
, failed_tag
, user
, failed_tag
, user
, user
,
586 get_top_commit_msg(test_master
))
588 msg
= MIMEMultipart()
589 msg
['Subject'] = 'autobuild failure for task %s during %s' % (
590 failed_task
, failed_stage
.name
)
591 msg
['From'] = 'autobuild@samba.org'
592 msg
['To'] = options
.email
594 main
= MIMEText(text
)
597 blist
.attach_logs(msg
)
601 s
.sendmail(msg
['From'], [msg
['To']], msg
.as_string())
604 def email_success(blist
):
605 '''send an email to options.email about a successful build'''
606 user
= os
.getenv("USER")
610 Your autobuild has succeeded.
617 you can get full logs of all tasks in this job here:
619 http://git.samba.org/%s/samba-autobuild/logs.tar.gz
624 The top commit for the tree that was built was:
627 ''' % (get_top_commit_msg(test_master
),)
629 msg
= MIMEMultipart()
630 msg
['Subject'] = 'autobuild success'
631 msg
['From'] = 'autobuild@samba.org'
632 msg
['To'] = options
.email
634 main
= MIMEText(text
, 'plain')
637 blist
.attach_logs(msg
)
641 s
.sendmail(msg
['From'], [msg
['To']], msg
.as_string())
645 (options
, args
) = parser
.parse_args()
648 if not options
.rebase_master
and options
.rebase
is None:
649 raise Exception('You can only use --retry if you also rebase')
651 testbase
= os
.path
.join(options
.testbase
, "b%u" % (os
.getpid(),))
652 test_master
= os
.path
.join(testbase
, "master")
654 if options
.repository
is not None:
655 repository
= options
.repository
657 repository
= os
.getcwd()
659 gitroot
= find_git_root(repository
)
661 raise Exception("Failed to find git root under %s" % repository
)
663 # get the top commit message, for emails
664 if options
.revision
is not None:
665 revision
= options
.revision
669 def get_top_commit_msg(reporoot
):
670 return run_cmd(["git", "log", "-1"], dir=reporoot
, output
=True)
673 os
.makedirs(testbase
)
674 except Exception, reason
:
675 raise Exception("Unable to create %s : %s" % (testbase
, reason
))
676 cleanup_list
.append(testbase
)
679 logfile
= os
.path
.join(testbase
, "log")
680 print "Forking into the background, writing progress to %s" % logfile
685 run_cmd(["rm", "-rf", test_master
])
686 cleanup_list
.append(test_master
)
687 clone_gitroot(test_master
, revision
)
693 if options
.rebase
is not None:
694 rebase_tree(options
.rebase
)
695 elif options
.rebase_master
:
696 rebase_tree(samba_master
)
697 blist
= BuildList(tasks
, args
)
700 (exitcode
, failed_task
, failed_stage
, failed_tag
, errstr
) = blist
.run()
701 if exitcode
!= 0 or errstr
!= "retry":
710 print("waiting for tail to flush")
715 if options
.passcmd
is not None:
716 print("Running passcmd: %s" % options
.passcmd
)
717 run_cmd(options
.passcmd
, dir=test_master
, shell
=True)
718 if options
.pushto
is not None:
719 push_to(options
.pushto
)
720 elif options
.push_master
:
721 push_to(samba_master_ssh
)
723 blist
.tarlogs("logs.tar.gz")
724 print("Logs in logs.tar.gz")
725 if options
.always_email
:
731 # something failed, gather a tar of the logs
732 blist
.tarlogs("logs.tar.gz")
734 if options
.email
is not None:
735 email_failure(blist
, exitcode
, failed_task
, failed_stage
, failed_tag
,
740 print("Logs in logs.tar.gz")