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