chiark / gitweb /
0a50a0ac09a239404cf24868e10ed3a07e497601
[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, lpath = None):
96         if p.local is not None:
97                 if lpath is not none: assert(p.local == lpath)
98                 return p.local
99         testbed.open()
100         p.local = lpath
101         if p.local is None: p.local = tmpdir + '/tb-' + p.what
102         testbed.command('copyup', (p.path(), p.local + p.dirsfx))
103         return p.local
104  def ontb(p):
105         if p.down is not None: return p.down
106         testbed.open()
107         p.down = testbed.scratch.p + '/host-' + p.what
108         testbed.command('copydown', (p.path(), p.down + p.dirsfx))
109         return p.down
110
111 def parse_args():
112         global opts
113         usage = "%prog <options> -- <virt-server>..."
114         parser = OptionParser(usage=usage)
115         pa = parser.add_option
116         pe = parser.add_option
117
118         def cb_vserv(op,optstr,value,parser):
119                 parser.values.vserver = list(parser.rargs)
120                 del parser.rargs[:]
121
122         def cb_path(op,optstr,value,parser, long,tb,dir):
123                 name = long.replace('-','_')
124                 setattr(parser.values, name, Path(tb, value, long, dir))
125
126         def pa_path(long, dir, help):
127                 def papa_tb(long, ca, pahelp):
128                         pa('', long, action='callback', callback=cb_path,
129                                 nargs=1, type='string', callback_args=ca,
130                                 help=(help % pahelp), metavar='PATH')
131                 papa_tb('--'+long,      (long, False, dir), 'host')
132                 papa_tb('--'+long+'-tb',(long, True, dir), 'testbed')
133
134         pa_path('build-tree',   True, 'use build tree from PATH on %s')
135         pa_path('control',      False, 'read control file PATH on %s')
136         pa_path('output-dir',   False, 'write stderr/out files in PATH on %s')
137
138         pa('-d', '--debug', action='store_true', dest='debug');
139         # pa('','--user', type='string',
140         #       help='run tests as USER (needs root on testbed)')
141         # nyi
142
143         class SpecialOption(optparse.Option): pass
144         vs_op = SpecialOption('','--VSERVER-DUMMY')
145         vs_op.action = 'callback'
146         vs_op.type = None
147         vs_op.default = None
148         vs_op.nargs = 0
149         vs_op.callback = cb_vserv
150         vs_op.callback_args = ( )
151         vs_op.callback_kwargs = { }
152         vs_op.help = 'introduces virtualisation server and args'
153         vs_op._short_opts = []
154         #vs_op._long_opts = ['--DUMMY']
155         vs_op._long_opts = ['---']
156
157         pa(vs_op)
158
159         (opts,args) = parser.parse_args()
160         if not hasattr(opts,'vserver'):
161                 parser.error('you must specifiy --- <virt-server>...')
162
163         if opts.build_tree is None:
164                 opts.build_tree = Path(False, '.', 'build-tree', dir=True)
165         if opts.control is None:
166                 opts.control = opts.build_tree.append(
167                         'debian/tests/control', 'control')
168
169 class Testbed:
170  def __init__(tb):
171         tb.sp = None
172         tb.lastsend = None
173         tb.scratch = None
174  def start(tb):
175         p = subprocess.PIPE
176         tb.sp = subprocess.Popen(opts.vserver,
177                 stdin=p, stdout=p, stderr=None)
178         tb.expect('ok')
179  def stop(tb):
180         tb.close()
181         if tb.sp is None: return
182         ec = tb.sp.returncode
183         if ec is None:
184                 tb.sp.stdout.close()
185                 tb.send('quit')
186                 tb.sp.stdin.close()
187                 ec = tb.sp.wait()
188         if ec:
189                 tb.bomb('testbed gave exit status %d after quit' % ec)
190  def open(tb):
191         if tb.scratch is not None: return
192         p = tb.commandr1('open')
193         tb.scratch = Path(True, p, 'tb-scratch', dir=True)
194  def close(tb):
195         if tb.scratch is None: return
196         tb.scratch = None
197         if tb.sp is None: return
198         tb.command('close')
199  def bomb(tb, m):
200         if tb.sp is not None:
201                 tb.sp.stdout.close()
202                 tb.sp.stdin.close()
203                 ec = tb.sp.wait()
204                 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
205                         ' exit status %d' % ec)
206         tb.sp = None
207         raise Quit(16, 'testbed failed: %s' % m)
208  def send(tb, string):
209         tb.sp.stdin
210         try:
211                 debug('>> '+string)
212                 print >>tb.sp.stdin, string
213                 tb.sp.stdin.flush()
214                 tb.lastsend = string
215         except:
216                 (type, value, dummy) = sys.exc_info()
217                 tb.bomb('cannot send to testbed: %s' % traceback.
218                         format_exception_only(type, value))
219  def expect(tb, keyword, nresults=-1):
220         l = tb.sp.stdout.readline()
221         if not l: tb.bomb('unexpected eof from the testbed')
222         if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
223         l = l.rstrip('\n')
224         debug('<< '+l)
225         ll = l.split()
226         if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
227         if ll[0] != keyword:
228                 if tb.lastsend is None:
229                         tb.bomb("got banner `%s', expected `%s...'" %
230                                 (l, keyword))
231                 else:
232                         tb.bomb("sent `%s', got `%s', expected `%s...'" %
233                                 (tb.lastsend, l, keyword))
234         ll = ll[1:]
235         if nresults >= 0 and len(ll) != nresults:
236                 tb.bomb("sent `%s', got `%s' (%d result parameters),"
237                         " expected %d result parameters" %
238                         (string, l, len(ll), nresults))
239         return ll
240  def commandr(tb, cmd, nresults, args=()):
241         al = [cmd] + map(urllib.quote, args)
242         tb.send(string.join(al))
243         ll = tb.expect('ok')
244         rl = map(urllib.unquote, ll)
245         return rl
246  def command(tb, cmd, args=()):
247         tb.commandr(cmd, 0, args)
248  def commandr1(tb, cmd, args=()):
249         rl = tb.commandr(cmd, 1, args)
250         return rl[0]
251
252 class FieldBase:
253  def __init__(f, fname, stz, base, tnames, vl):
254         assert(vl)
255         f.stz = stz
256         f.base = base
257         f.tnames = tnames
258         f.vl = vl
259  def words(f):
260         def distribute(vle):
261                 (lno, v) = vle
262                 r = v.split()
263                 r = map((lambda w: (lno, w)), r)
264                 return r
265         return flatten(map(distribute, f.vl))
266  def atmostone(f):
267         if len(vl) == 1:
268                 (f.lno, f.v) = vl[0]
269         else:
270                 raise Unsupported(f.vl[1][0],
271                         'only one %s field allowed' % fn)
272         return f.v
273
274 class FieldIgnore(FieldBase):
275  def parse(f): pass
276
277 class Restriction:
278  def __init__(r,rname,base): pass
279
280 class Restriction_rw_build_tree(Restriction): pass
281
282 class Field_Restrictions(FieldBase):
283  def parse(f):
284         for wle in f.words():
285                 (lno, rname) = wle
286                 rname = rname.replace('-','_')
287                 try: rclass = globals()['Restriction_'+rname]
288                 except KeyError: raise Unsupported(lno,
289                         'unknown restriction %s' % rname)
290                 r = rclass(rname, f.base)
291                 f.base['restrictions'].append(r)
292
293 class Field_Tests(FieldIgnore): pass
294
295 class Field_Tests_directory(FieldBase):
296  def parse(f):
297         td = atmostone(f)
298         if td.startswith('/'): raise Unspported(f.lno,
299                 'Tests-Directory may not be absolute')
300         base['testsdir'] = td
301
302 def run_tests():
303         testbed.close()
304         for t in tests:
305                 t.run()
306         if not tests:
307                 global errorcode
308                 report('*', 'SKIP no tests in this package')
309                 errorcode |= 8
310
311 class Test:
312  def __init__(t, tname, base):
313         if '/' in tname: raise Unsupported(base[' lno'],
314                 'test name may not contain / character')
315         for k in base: setattr(t,k,base[k])
316         t.tname = tname
317         if len(base['testsdir']): tpath = base['testsdir'] + '/' + tname
318         else: tpath = tname
319         t.p = opts.build_tree.append(tpath, 'test-'+tname)
320         t.p.ontb()
321  def report(t, m):
322         report(t.tname, m)
323  def reportfail(t, m):
324         global errorcode
325         errorcode |= 4
326         report(t.tname, 'FAIL ' + m)
327  def run(t):
328         testbed.open()
329         so = testbed.scratch.append('stdout-' + t.tname, 'stdout-' + t.tname)
330         se = testbed.scratch.append('stdout-' + t.tname, 'stdout-' + t.tname)
331         rc = testbed.commandr1('execute',(t.p.ontb(),
332                 '/dev/null', so.ontb(), se.ontb()))
333         soh = so.onhost()
334         seh = se.onhost()
335         testbed.close()
336         rc = int(rc)
337         stab = os.stat(soh)
338         if stab.st_size != 0:
339                 l = file(seh).readline()
340                 l = l.rstrip('\n \t\r')
341                 if len(l) > 40: l = l[:40] + '...'
342                 t.reportfail('stderr: %s' % l)
343         elif rc != 0:
344                 t.reportfail('non-zero exit status %d' % rc)
345         else:
346                 t.report('PASS')
347
348 def read_control():
349         global tests
350         try:
351                 control = file(opts.control.onhost(), 'r')
352         except IOError, oe:
353                 if oe[0] != errno.ENOENT: raise
354                 tests = []
355                 return
356         lno = 0
357         def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
358         stz = None # stz[field_name][index] = (lno, value)
359
360         stanzas = [ ]
361         stz = None
362
363         def end_stanza(stz):
364                 if stz is None: return
365                 stz[' errs'] = 0
366                 stanzas.append(stz)
367                 stz = None
368                 hcurrent = None
369
370         initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
371         while 1:
372                 l = control.readline()
373                 if not l: break
374                 lno += 1
375                 if not l.endswith('\n'): badctrl('unterminated line')
376                 if regexp.compile('\s*\#').match(l): continue
377                 if not regexp.compile('\S').match(l): end_stanza(stz); continue
378                 initmat = initre.match(l)
379                 if initmat:
380                         (fname, l) = initmat.groups()
381                         fname = string.capwords(fname)
382                         if stz is None:
383                                 stz = { ' lno': lno }
384                         if not stz.has_key(fname): stz[fname] = [ ]
385                         hcurrent = stz[fname]
386                 elif regexp.compile('\s').match(l):
387                         if not hcurrent: badctrl('unexpected continuation')
388                 else:
389                         badctrl('syntax error')
390                 hcurrent.append((lno, l))
391         end_stanza(stz)
392
393         def testbadctrl(stz, lno, m):
394                 report_badctrl(lno, m)
395                 stz[' errs'] += 1
396
397         for stz in stanzas:
398                 try:
399                         try: tnames = stz['Tests']
400                         except KeyError:
401                                 tnames = ['*']
402                                 raise Unsupported(stz[' lno'],
403                                         'no Tests field')
404                         tnames = map((lambda lt: lt[1]), tnames)
405                         tnames = string.join(tnames).split()
406                         base = {
407                                 'restrictions': [],
408                                 'testsdir': 'debian/tests'
409                         }
410                         for fname in stz.keys():
411                                 if fname.startswith(' '): continue
412                                 vl = stz[fname]
413                                 try: fclass = globals()['Field_'+
414                                         fname.replace('-','_')]
415                                 except KeyError: raise Unsupported(vl[0][0],
416                                         'unknown metadata field %s' % fname)
417                                 f = fclass(stz, fname, base, tnames, vl)
418                                 f.parse()
419                         tests = []
420                         for tname in tnames:
421                                 t = Test(tname, base)
422                                 tests.append(t)
423                 except Unsupported, u:
424                         for tname in tnames: u.report(tname)
425                         continue
426
427 def print_exception(ei, msgprefix=''):
428         if msgprefix: print >>sys.stderr, msgprefix
429         (et, q, tb) = ei
430         if et is Quit:
431                 print >>sys.stderr, 'adt-run:', q.m
432                 return q.ec
433         else:
434                 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
435                 traceback.print_exc()
436                 return 20
437
438 def cleanup():
439         try:
440                 rm_ec = 0
441                 if tmpdir is not None:
442                         rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
443                 if testbed is not None:
444                         testbed.stop()
445                 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
446         except:
447                 print_exception(sys.exc_info(),
448                         '\nadt-run: error cleaning up:\n')
449                 os._exit(20)
450
451 def main():
452         global testbed
453         global tmpdir
454         try:
455                 parse_args()
456         except SystemExit, se:
457                 os._exit(20)
458         try:
459                 tmpdir = tempfile.mkdtemp()
460                 testbed = Testbed()
461                 testbed.start()
462                 read_control()
463                 run_tests()
464         except:
465                 ec = print_exception(sys.exc_info(), '')
466                 cleanup()
467                 os._exit(ec)
468         cleanup()
469         os._exit(errorcode)
470
471 main()