autobuild: added --retry option
[sfrench/samba-autobuild/.git] / script / autobuild.py
1 #!/usr/bin/env python
2 # run tests on all Samba subprojects and push to a git tree on success
3 # Copyright Andrew Tridgell 2010
4 # released under GNU GPL v3 or later
5
6 from subprocess import Popen, PIPE
7 import os, signal, tarfile, sys, time
8 from optparse import OptionParser
9
10
11 samba_master = os.getenv('SAMBA_MASTER', 'git://git.samba.org/samba.git')
12 samba_master_ssh = os.getenv('SAMBA_MASTER_SSH', 'git+ssh://git.samba.org/data/git/samba.git')
13
14 cleanup_list = []
15
16 os.putenv('CC', "ccache gcc")
17
18 tasks = {
19     "source3" : [ "./autogen.sh",
20                   "./configure.developer ${PREFIX}",
21                   "make basics",
22                   "make -j 4 everything", # don't use too many processes
23                   "make install",
24                   "TDB_NO_FSYNC=1 make test" ],
25
26     "source4" : [ "./autogen.sh",
27                   "./configure.developer ${PREFIX}",
28                   "make -j",
29                   "make install",
30                   "TDB_NO_FSYNC=1 make test" ],
31
32     "source4/lib/ldb" : [ "./autogen-waf.sh",
33                           "./configure --enable-developer -C ${PREFIX}",
34                           "make -j",
35                           "make install",
36                           "make test" ],
37
38     "lib/tdb" : [ "./autogen-waf.sh",
39                   "./configure --enable-developer -C ${PREFIX}",
40                   "make -j",
41                   "make install",
42                   "make test" ],
43
44     "lib/talloc" : [ "./autogen-waf.sh",
45                      "./configure --enable-developer -C ${PREFIX}",
46                      "make -j",
47                      "make install",
48                      "make test" ],
49
50     "lib/replace" : [ "./autogen-waf.sh",
51                       "./configure --enable-developer -C ${PREFIX}",
52                       "make -j",
53                       "make install",
54                       "make test" ],
55
56     "lib/tevent" : [ "./autogen-waf.sh",
57                      "./configure --enable-developer -C ${PREFIX}",
58                      "make -j",
59                      "make install",
60                      "make test" ],
61 }
62
63 retry_task = [ '''set -e
64                 git remote add -t master master %s
65                 while :; do
66                   sleep 60
67                   git fetch master
68                   git describe > HEAD.desc
69                   git describe > master.desc
70                   diff HEAD.desc master.desc
71                 done
72                ''' % samba_master]
73
74 def run_cmd(cmd, dir=".", show=None):
75     cwd = os.getcwd()
76     os.chdir(dir)
77     if show is None:
78         show = options.verbose
79     if show:
80         print("Running: '%s' in '%s'" % (cmd, dir))
81     ret = os.system(cmd)
82     os.chdir(cwd)
83     if ret != 0:
84         raise Exception("FAILED %s: %d" % (cmd, ret))
85
86 class builder:
87     '''handle build of one directory'''
88     def __init__(self, name, sequence):
89         self.name = name
90
91         if name in ['pass', 'fail', 'retry']:
92             self.dir = "."
93         else:
94             self.dir = self.name
95
96         self.tag = self.name.replace('/', '_')
97         self.sequence = sequence
98         self.next = 0
99         self.stdout_path = "%s/%s.stdout" % (testbase, self.tag)
100         self.stderr_path = "%s/%s.stderr" % (testbase, self.tag)
101         cleanup_list.append(self.stdout_path)
102         cleanup_list.append(self.stderr_path)
103         run_cmd("rm -f %s %s" % (self.stdout_path, self.stderr_path))
104         self.stdout = open(self.stdout_path, 'w')
105         self.stderr = open(self.stderr_path, 'w')
106         self.stdin  = open("/dev/null", 'r')
107         self.sdir = "%s/%s" % (testbase, self.tag)
108         self.prefix = "%s/prefix/%s" % (testbase, self.tag)
109         run_cmd("rm -rf %s" % self.sdir)
110         cleanup_list.append(self.sdir)
111         cleanup_list.append(self.prefix)
112         os.makedirs(self.sdir)
113         run_cmd("rm -rf %s" % self.sdir)
114         run_cmd("git clone --shared %s %s" % (gitroot, self.sdir))
115         self.start_next()
116
117     def start_next(self):
118         if self.next == len(self.sequence):
119             print '%s: Completed OK' % self.name
120             self.done = True
121             return
122         self.cmd = self.sequence[self.next].replace("${PREFIX}", "--prefix=%s" % self.prefix)
123         print '%s: Running %s' % (self.name, self.cmd)
124         cwd = os.getcwd()
125         os.chdir("%s/%s" % (self.sdir, self.dir))
126         self.proc = Popen(self.cmd, shell=True,
127                           stdout=self.stdout, stderr=self.stderr, stdin=self.stdin)
128         os.chdir(cwd)
129         self.next += 1
130
131
132 class buildlist:
133     '''handle build of multiple directories'''
134     def __init__(self, tasklist, tasknames):
135         global tasks
136         self.tlist = []
137         self.tail_proc = None
138         self.retry = None
139         if tasknames == ['pass']:
140             tasks = { 'pass' : [ '/bin/true' ]}
141         if tasknames == ['fail']:
142             tasks = { 'fail' : [ '/bin/false' ]}
143         if tasknames == []:
144             tasknames = tasklist
145         for n in tasknames:
146             b = builder(n, tasks[n])
147             self.tlist.append(b)
148         if options.retry:
149             self.retry = builder('retry', retry_task)
150             self.need_retry = False
151
152     def kill_kids(self):
153         if self.tail_proc is not None:
154             self.tail_proc.terminate()
155             self.tail_proc.wait()
156             self.tail_proc = None
157         if self.retry is not None:
158             self.retry.proc.terminate()
159             self.retry.proc.wait()
160             self.retry = None
161         for b in self.tlist:
162             if b.proc is not None:
163                 b.proc.terminate()
164                 b.proc.wait()
165                 b.proc = None
166
167     def wait_one(self):
168         while True:
169             none_running = True
170             for b in self.tlist:
171                 if b.proc is None:
172                     continue
173                 none_running = False
174                 b.status = b.proc.poll()
175                 if b.status is None:
176                     continue
177                 b.proc = None
178                 return b
179             if options.retry:
180                 ret = self.retry.proc.poll()
181                 if ret is not None:
182                     self.need_retry = True
183                     self.retry = None
184                     return None
185             if none_running:
186                 return None
187             time.sleep(0.1)
188
189     def run(self):
190         while True:
191             b = self.wait_one()
192             if options.retry and self.need_retry:
193                 self.kill_kids()
194                 print("retry needed")
195                 return (0, "retry")
196             if b is None:
197                 break
198             if os.WIFSIGNALED(b.status) or os.WEXITSTATUS(b.status) != 0:
199                 self.kill_kids()
200                 return (b.status, "%s: failed '%s' with status %d" % (b.name, b.cmd, b.status))
201             b.start_next()
202         self.kill_kids()
203         return (0, "All OK")
204
205     def tarlogs(self, fname):
206         tar = tarfile.open(fname, "w:gz")
207         for b in self.tlist:
208             tar.add(b.stdout_path, arcname="%s.stdout" % b.tag)
209             tar.add(b.stderr_path, arcname="%s.stderr" % b.tag)
210         tar.close()
211
212     def remove_logs(self):
213         for b in self.tlist:
214             os.unlink(b.stdout_path)
215             os.unlink(b.stderr_path)
216
217     def start_tail(self):
218         cwd = os.getcwd()
219         cmd = "tail -f *.stdout *.stderr"
220         os.chdir(testbase)
221         self.tail_proc = Popen(cmd, shell=True)
222         os.chdir(cwd)
223
224
225 def cleanup():
226     if options.nocleanup:
227         return
228     print("Cleaning up ....")
229     for d in cleanup_list:
230         run_cmd("rm -rf %s" % d)
231
232
233 def find_git_root():
234     '''get to the top of the git repo'''
235     cwd=os.getcwd()
236     while os.getcwd() != '/':
237         try:
238             os.stat(".git")
239             ret = os.getcwd()
240             os.chdir(cwd)
241             return ret
242         except:
243             os.chdir("..")
244             pass
245     os.chdir(cwd)
246     return None
247
248 def rebase_tree(url):
249     print("Rebasing on %s" % url)
250     run_cmd("git remote add -t master master %s" % url, show=True, dir=test_master)
251     run_cmd("git fetch master", show=True, dir=test_master)
252     if options.fix_whitespace:
253         run_cmd("git rebase --whitespace=fix master/master", show=True, dir=test_master)
254     else:
255         run_cmd("git rebase master/master", show=True, dir=test_master)
256
257 def push_to(url):
258     print("Pushing to %s" % url)
259     if options.mark:
260         run_cmd("EDITOR=script/commit_mark.sh git commit --amend -c HEAD", dir=test_master)
261     run_cmd("git remote add -t master pushto %s" % url, show=True, dir=test_master)
262     run_cmd("git push pushto +HEAD:master", show=True, dir=test_master)
263
264 def_testbase = os.getenv("AUTOBUILD_TESTBASE", "/memdisk/%s" % os.getenv('USER'))
265
266 parser = OptionParser()
267 parser.add_option("", "--tail", help="show output while running", default=False, action="store_true")
268 parser.add_option("", "--keeplogs", help="keep logs", default=False, action="store_true")
269 parser.add_option("", "--nocleanup", help="don't remove test tree", default=False, action="store_true")
270 parser.add_option("", "--testbase", help="base directory to run tests in (default %s)" % def_testbase,
271                   default=def_testbase)
272 parser.add_option("", "--passcmd", help="command to run on success", default=None)
273 parser.add_option("", "--verbose", help="show all commands as they are run",
274                   default=False, action="store_true")
275 parser.add_option("", "--rebase", help="rebase on the given tree before testing",
276                   default=None, type='str')
277 parser.add_option("", "--rebase-master", help="rebase on %s before testing" % samba_master,
278                   default=False, action='store_true')
279 parser.add_option("", "--pushto", help="push to a git url on success",
280                   default=None, type='str')
281 parser.add_option("", "--push-master", help="push to %s on success" % samba_master_ssh,
282                   default=False, action='store_true')
283 parser.add_option("", "--mark", help="add a Tested-By signoff before pushing",
284                   default=False, action="store_true")
285 parser.add_option("", "--fix-whitespace", help="fix whitespace on rebase",
286                   default=False, action="store_true")
287 parser.add_option("", "--retry", help="automatically retry if master changes",
288                   default=False, action="store_true")
289
290
291 (options, args) = parser.parse_args()
292
293 if options.retry:
294     if not options.rebase_master and options.rebase is None:
295         raise Exception('You can only use --retry if you also rebase')
296
297 testbase = "%s/build.%u" % (options.testbase, os.getpid())
298 test_master = "%s/master" % testbase
299
300 gitroot = find_git_root()
301 if gitroot is None:
302     raise Exception("Failed to find git root")
303
304 try:
305     os.makedirs(testbase)
306 except Exception, reason:
307     raise Exception("Unable to create %s : %s" % (testbase, reason))
308 cleanup_list.append(testbase)
309
310 while True:
311     try:
312         run_cmd("rm -rf %s" % test_master)
313         cleanup_list.append(test_master)
314         run_cmd("git clone --shared %s %s" % (gitroot, test_master))
315     except:
316         cleanup()
317         raise
318
319     try:
320         if options.rebase is not None:
321             rebase_tree(options.rebase)
322         elif options.rebase_master:
323             rebase_tree(samba_master)
324         blist = buildlist(tasks, args)
325         if options.tail:
326             blist.start_tail()
327         (status, errstr) = blist.run()
328         if status != 0 or errstr != "retry":
329             break
330         cleanup()
331     except:
332         cleanup()
333         raise
334
335 blist.kill_kids()
336 if options.tail:
337     print("waiting for tail to flush")
338     time.sleep(1)
339
340 if status == 0:
341     print errstr
342     if options.passcmd is not None:
343         print("Running passcmd: %s" % options.passcmd)
344         run_cmd(options.passcmd, dir=test_master)
345     if options.pushto is not None:
346         push_to(options.pushto)
347     elif options.push_master:
348         push_to(samba_master_ssh)
349     if options.keeplogs:
350         blist.tarlogs("logs.tar.gz")
351         print("Logs in logs.tar.gz")
352     blist.remove_logs()
353     cleanup()
354     print(errstr)
355     sys.exit(0)
356
357 # something failed, gather a tar of the logs
358 blist.tarlogs("logs.tar.gz")
359 blist.remove_logs()
360 cleanup()
361 print(errstr)
362 print("Logs in logs.tar.gz")
363 sys.exit(os.WEXITSTATUS(status))