chiark / gitweb /
manpages written; some bugs in error handling fixed
[autopkgtest.git] / runner / adt-run
1 #!/usr/bin/python2.4
2 #
3 # adt-run is part of autodebtest
4 # autodebtest is a tool for testing Debian binary packages
5 #
6 # autodebtest is Copyright (C) 2006 Canonical Ltd.
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21 #
22 # See the file CREDITS for a full list of credits information (often
23 # installed as /usr/share/doc/autodebtest/CREDITS).
24
25 import signal
26 import optparse
27 import tempfile
28 import sys
29 import subprocess
30 import traceback
31 import urllib
32 import string
33 import re as regexp
34 import os
35 import errno
36
37 from optparse import OptionParser
38
39 tmpdir = None
40 testbed = None
41 errorcode = 0
42
43 signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing
44
45 class Quit:
46         def __init__(q,ec,m): q.ec = ec; q.m = m
47
48 def bomb(m): raise Quit(20, "unexpected error: %s" % m)
49 def badpkg(m): raise Quit(12, "erroneous package: %s" % m)
50 def report(tname, result): print '%-20s %s' % (tname, result)
51
52 class Unsupported:
53  def __init__(u, lno, m):
54         if lno >= 0: u.m = '%s (control line %d)' % (m, lno)
55         else: u.m = m
56  def report(u, tname):
57         global errorcode
58         errorcode != 2
59         report(tname, 'SKIP %s' % u.m)
60
61 def debug(m):
62         global opts
63         if not opts.debug: return
64         print >>sys.stderr, 'atd-run: debug:', m
65
66 def flatten(l):
67         return reduce((lambda a,b: a + b), l, []) 
68
69 class Path:
70  def __init__(p, tb, path, what, dir=False):
71         p.tb = tb
72         p.p = path
73         p.what = what
74         p.dir = dir
75         if p.tb:
76                 if p.p[:1] != '/':
77                         bomb("path %s specified as being in testbed but"
78                                 " not absolute: `%s'" % (what, p.p))
79                 p.local = None
80                 p.down = p.p
81         else:
82                 p.local = p.p
83                 p.down = None
84         if p.dir: p.dirsfx = '/'
85         else: p.dirsfx = ''
86  def path(p):
87         return p.p + p.dirsfx
88  def append(p, suffix, what, dir=False):
89         return Path(p.tb, p.path() + suffix, what=what, dir=dir)
90  def __str__(p):
91         if p.tb: pfx = '/VIRT'
92         elif p.p[:1] == '/': pfx = '/HOST'
93         else: pfx = './'
94         return pfx + p.p
95  def onhost(p):
96         if p.local is not None: return p.local
97         testbed.open()
98         p.local = tmpdir + '/tb-' + p.what
99         testbed.command('copyup', (p.path(), p.local + p.dirsfx))
100         return p.local
101  def ontb(p):
102         if p.down is not None: return p.down
103         p.down = testbed.scratch.p + '/host-' + p.what
104         testbed.command('copydown', (p.path(), p.down + p.dirsfx))
105         return p.down
106
107 def parse_args():
108         global opts
109         usage = "%prog <options> -- <virt-server>..."
110         parser = OptionParser(usage=usage)
111         pa = parser.add_option
112         pe = parser.add_option
113
114         def cb_vserv(op,optstr,value,parser):
115                 parser.values.vserver = list(parser.rargs)
116                 del parser.rargs[:]
117
118         def cb_path(op,optstr,value,parser, long,tb,dir):
119                 name = long.replace('-','_')
120                 setattr(parser.values, name, Path(tb, value, long, dir))
121
122         def pa_path(long, dir, help):
123                 def papa_tb(long, ca, pahelp):
124                         pa('', long, action='callback', callback=cb_path,
125                                 nargs=1, type='string', callback_args=ca,
126                                 help=(help % pahelp), metavar='PATH')
127                 papa_tb('--'+long,      (long, False, dir), 'host')
128                 papa_tb('--'+long+'-tb',(long, True, dir), 'testbed')
129
130         pa_path('build-tree',   True, 'use build tree from PATH on %s')
131         pa_path('control',      False, 'read control file PATH on %s')
132
133         pa('-d', '--debug', action='store_true', dest='debug');
134         # pa('','--user', type='string',
135         #       help='run tests as USER (needs root on testbed)')
136         # nyi
137
138         class SpecialOption(optparse.Option): pass
139         vs_op = SpecialOption('','--VSERVER-DUMMY')
140         vs_op.action = 'callback'
141         vs_op.type = None
142         vs_op.default = None
143         vs_op.nargs = 0
144         vs_op.callback = cb_vserv
145         vs_op.callback_args = ( )
146         vs_op.callback_kwargs = { }
147         vs_op.help = 'introduces virtualisation server and args'
148         vs_op._short_opts = []
149         #vs_op._long_opts = ['--DUMMY']
150         vs_op._long_opts = ['---']
151
152         pa(vs_op)
153
154         (opts,args) = parser.parse_args()
155         if not hasattr(opts,'vserver'):
156                 parser.error('you must specifiy --- <virt-server>...')
157
158         if opts.build_tree is None:
159                 opts.build_tree = Path(False, '.', 'build-tree', dir=True)
160         if opts.control is None:
161                 opts.control = opts.build_tree.append(
162                         'debian/tests/control', 'control')
163
164 class Testbed:
165  def __init__(tb):
166         tb.sp = None
167         tb.lastsend = None
168         tb.scratch = None
169  def start(tb):
170         p = subprocess.PIPE
171         tb.sp = subprocess.Popen(opts.vserver,
172                 stdin=p, stdout=p, stderr=None)
173         tb.expect('ok')
174  def stop(tb):
175         tb.close()
176         if tb.sp is None: return
177         ec = tb.sp.returncode
178         if ec is None:
179                 tb.sp.stdout.close()
180                 tb.send('quit')
181                 tb.sp.stdin.close()
182                 ec = tb.sp.wait()
183         if ec:
184                 tb.bomb('testbed gave exit status %d after quit' % ec)
185  def open(tb):
186         if tb.scratch is not None: return
187         p = tb.commandr1('open')
188         tb.scratch = Path(True, p, 'tb-scratch', dir=True)
189  def close(tb):
190         if tb.scratch is None: return
191         tb.scratch = None
192         tb.command('close')
193  def bomb(tb, m):
194         if tb.sp is not None:
195                 tb.sp.stdout.close()
196                 tb.sp.stdin.close()
197                 ec = tb.sp.wait()
198                 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
199                         ' exit status %d' % ec)
200         tb.sp = None
201         raise Quit(16, 'testbed failed: %s' % m)
202  def send(tb, string):
203         try:
204                 debug('>> '+string)
205                 print >>tb.sp.stdin, string
206                 tb.sp.stdin.flush()
207                 tb.lastsend = string
208         except:
209                 tb.bomb('cannot send to testbed: %s' %
210                         formatexception_only(sys.last_type, sys.last_value))
211  def expect(tb, keyword, nresults=-1):
212         l = tb.sp.stdout.readline()
213         if not l: tb.bomb('unexpected eof from the testbed')
214         if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
215         l = l.rstrip('\n')
216         debug('<< '+l)
217         ll = l.split()
218         if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
219         if ll[0] != keyword:
220                 if tb.lastsend is None:
221                         tb.bomb("got banner `%s', expected `%s...'" %
222                                 (l, keyword))
223                 else:
224                         tb.bomb("sent `%s', got `%s', expected `%s...'" %
225                                 (tb.lastsend, l, keyword))
226         ll = ll[1:]
227         if nresults >= 0 and len(ll) != nresults:
228                 tb.bomb("sent `%s', got `%s' (%d result parameters),"
229                         " expected %d result parameters" %
230                         (string, l, len(ll), nresults))
231         return ll
232  def commandr(tb, cmd, nresults, args=()):
233         al = [cmd] + map(urllib.quote, args)
234         tb.send(string.join(al))
235         ll = tb.expect('ok')
236         rl = map(urllib.unquote, ll)
237         return rl
238  def command(tb, cmd, args=()):
239         tb.commandr(cmd, 0, args)
240  def commandr1(tb, cmd, args=()):
241         rl = tb.commandr(cmd, 1, args)
242         return rl[0]
243
244 class FieldBase:
245  def __init__(f, fname, stz, base, tnames, vl):
246         assert(vl)
247         f.stz = stz
248         f.base = base
249         f.tnames = tnames
250         f.vl = vl
251  def words(f):
252         def distribute(vle):
253                 (lno, v) = vle
254                 r = v.split()
255                 r = map((lambda w: (lno, w)), r)
256                 return r
257         return flatten(map(distribute, f.vl))
258  def atmostone(f):
259         if len(vl) == 1:
260                 (f.lno, f.v) = vl[0]
261         else:
262                 raise Unsupported(f.vl[1][0],
263                         'only one %s field allowed' % fn)
264         return f.v
265
266 class FieldIgnore(FieldBase):
267  def parse(f): pass
268
269 class Restriction:
270  def __init__(r,rname,base): pass
271
272 class Restriction_rw_build_tree(Restriction): pass
273
274 class Field_Restrictions(FieldBase):
275  def parse(f):
276         for wle in f.words():
277                 (lno, rname) = wle
278                 rname = rname.replace('-','_')
279                 try: rclass = globals()['Restriction_'+rname]
280                 except KeyError: raise Unsupported(lno,
281                         'unknown restriction %s' % rname)
282                 r = rclass(rname, f.base)
283                 f.base['restrictions'].append(r)
284
285 class Field_Tests(FieldIgnore): pass
286
287 class Field_Tests_directory(FieldBase):
288  def parse(f):
289         td = atmostone(f)
290         if td.startswith('/'): raise Unspported(f.lno,
291                 'Tests-Directory may not be absolute')
292         base['testsdir'] = td
293
294 def run_tests():
295         testbed.close()
296         for t in tests:
297                 t.run()
298         if not tests:
299                 global errorcode
300                 report('*', 'SKIP no tests in this package')
301                 errorcode |= 8
302
303 class Test:
304  def __init__(t, tname, base):
305         if '/' in tname: raise Unsupported(base[' lno'],
306                 'test name may not contain / character')
307         for k in base: setattr(t,k,base[k])
308         t.tname = tname
309         t.p = opts.build_tree.append(tname, 'test-'+tname)
310         t.p.ontb()
311  def report(t, m):
312         report(t.tname, m)
313  def reportfail(t, m):
314         global errorcode
315         errorcode |= 4
316         report(t.tname, 'FAIL ' + m)
317  def run(t):
318         testbed.open()
319         so = testbed.scratch.append('stdout-' + t.tname, 'stdout-' + t.tname)
320         se = testbed.scratch.append('stdout-' + t.tname, 'stdout-' + t.tname)
321         rc = testbed.commandr1('execute',(t.p.ontb(),
322                 '/dev/null', so.ontb(), se.ontb()))
323         soh = so.onhost()
324         seh = se.onhost()
325         testbed.close()
326         rc = int(rc)
327         stab = os.stat(soh)
328         if stab.st_size != 0:
329                 l = file(seh).readline()
330                 l = l.rstrip('\n \t\r')
331                 if len(l) > 40: l = l[:40] + '...'
332                 t.reportfail('stderr: %s' % l)
333         elif rc != 0:
334                 t.reportfail('non-zero exit status %d' % rc)
335         else:
336                 t.report('PASS')
337
338 def read_control():
339         global tests
340         try:
341                 control = file(opts.control.onhost(), 'r')
342         except IOError, oe:
343                 if oe[0] != errno.ENOENT: raise
344                 tests = []
345                 return
346         lno = 0
347         def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
348         stz = None # stz[field_name][index] = (lno, value)
349
350         stanzas = [ ]
351         stz = None
352
353         def end_stanza(stz):
354                 if stz is None: return
355                 stz[' errs'] = 0
356                 stanzas.append(stz)
357                 stz = None
358                 hcurrent = None
359
360         initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
361         while 1:
362                 l = control.readline()
363                 if not l: break
364                 lno += 1
365                 if not l.endswith('\n'): badctrl('unterminated line')
366                 if regexp.compile('\s*\#').match(l): continue
367                 if not regexp.compile('\S').match(l): end_stanza(stz); continue
368                 initmat = initre.match(l)
369                 if initmat:
370                         (fname, l) = initmat.groups()
371                         fname = string.capwords(fname)
372                         if stz is None:
373                                 stz = { ' lno': lno }
374                         if not stz.has_key(fname): stz[fname] = [ ]
375                         hcurrent = stz[fname]
376                 elif regexp.compile('\s').match(l):
377                         if not hcurrent: badctrl('unexpected continuation')
378                 else:
379                         badctrl('syntax error')
380                 hcurrent.append((lno, l))
381         end_stanza(stz)
382
383         def testbadctrl(stz, lno, m):
384                 report_badctrl(lno, m)
385                 stz[' errs'] += 1
386
387         for stz in stanzas:
388                 try:
389                         try: tnames = stz['Tests']
390                         except KeyError:
391                                 tnames = ['*']
392                                 raise Unsupported(stz[' lno'],
393                                         'no Tests field')
394                         tnames = map((lambda lt: lt[1]), tnames)
395                         tnames = string.join(tnames).split()
396                         base = {
397                                 'restrictions': [],
398                                 'testsdir': 'debian/tests'
399                         }
400                         for fname in stz.keys():
401                                 if fname.startswith(' '): continue
402                                 vl = stz[fname]
403                                 try: fclass = globals()['Field_'+
404                                         fname.replace('-','_')]
405                                 except KeyError: raise Unsupported(vl[0][0],
406                                         'unknown metadata field %s' % fname)
407                                 f = fclass(stz, fname, base, tnames, vl)
408                                 f.parse()
409                         tests = []
410                         for tname in tnames:
411                                 t = Test(tname, base)
412                                 tests.append(t)
413                 except Unsupported, u:
414                         for tname in tnames: u.report(tname)
415                         continue
416
417 def print_exception(ei, msgprefix=''):
418         if msgprefix: print >>sys.stderr, msgprefix
419         (et, q, tb) = ei
420         if et is Quit:
421                 print >>sys.stderr, 'adt-run:', q.m
422                 return q.ec
423         else:
424                 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
425                 traceback.print_exc()
426                 return 20
427
428 def cleanup():
429         try:
430                 rm_ec = 0
431                 if tmpdir is not None:
432                         rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
433                 if testbed is not None:
434                         testbed.stop()
435                 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
436         except:
437                 print_exception(sys.exc_info(),
438                         '\nadt-run: error cleaning up:\n')
439                 os._exit(20)
440
441 def main():
442         global testbed
443         global tmpdir
444         try:
445                 parse_args()
446         except SystemExit, se:
447                 os._exit(20)
448         try:
449                 tmpdir = tempfile.mkdtemp()
450                 testbed = Testbed()
451                 testbed.start()
452                 read_control()
453                 run_tests()
454         except:
455                 ec = print_exception(sys.exc_info(), '')
456                 cleanup()
457                 os._exit(ec)
458         cleanup()
459         os._exit(errorcode)
460
461 main()