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