chiark / gitweb /
rename package and finalise changelog
[autopkgtest.git] / runner / adt-run
1 #!/usr/bin/python2.4
2 #
3 # adt-run is part of autopkgtest
4 # autopkgtest is a tool for testing Debian binary packages
5 #
6 # autopkgtest 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/autopkgtest/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=None):
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         testbed.open()
108         if p.tbscratch is not None:
109                 if p.tbscratch != testbed.scratch:
110                         p.down = None
111         if p.down is not None: return p.down
112         if p.tb:
113                 bomb("testbed scratch path " + str(p) + " survived testbed")
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         testbed.open()
337         def stdouterr(oe):
338                 idstr = oe + '-' + t.tname
339                 if opts.output_dir is not None and opts.output_dir.tb:
340                         return opts.output_dir.append(idstr)
341                 else:
342                         return testbed.scratch.append(idstr, idstr)
343         def stdouterrh(p, oe):
344                 idstr = oe + '-' + t.tname
345                 if opts.output_dir is None or opts.output_dir.tb:
346                         return p.onhost()
347                 else:
348                         return p.onhost(opts.output_dir.onhost() + '/' + idstr)
349         so = stdouterr('stdout')
350         se = stdouterr('stderr')
351         rc = testbed.commandr1('execute',(t.p.ontb(),
352                 '/dev/null', so.ontb(), se.ontb(), opts.build_tree.ontb()))
353         soh = stdouterrh(so, 'stdout')
354         seh = stdouterrh(se, 'stderr')
355         testbed.close()
356         rc = int(rc)
357         stab = os.stat(seh)
358         if stab.st_size != 0:
359                 l = file(seh).readline()
360                 l = l.rstrip('\n \t\r')
361                 if len(l) > 40: l = l[:40] + '...'
362                 t.reportfail('stderr: %s' % l)
363         elif rc != 0:
364                 t.reportfail('non-zero exit status %d' % rc)
365         else:
366                 t.report('PASS')
367
368 def read_control():
369         global tests
370         try:
371                 control = file(opts.control.onhost(), 'r')
372         except IOError, oe:
373                 if oe[0] != errno.ENOENT: raise
374                 tests = []
375                 return
376         lno = 0
377         def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
378         stz = None # stz[field_name][index] = (lno, value)
379
380         stanzas = [ ]
381         stz = None
382
383         def end_stanza(stz):
384                 if stz is None: return
385                 stz[' errs'] = 0
386                 stanzas.append(stz)
387                 stz = None
388                 hcurrent = None
389
390         initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
391         while 1:
392                 l = control.readline()
393                 if not l: break
394                 lno += 1
395                 if not l.endswith('\n'): badctrl('unterminated line')
396                 if regexp.compile('\s*\#').match(l): continue
397                 if not regexp.compile('\S').match(l): end_stanza(stz); continue
398                 initmat = initre.match(l)
399                 if initmat:
400                         (fname, l) = initmat.groups()
401                         fname = string.capwords(fname)
402                         if stz is None:
403                                 stz = { ' lno': lno }
404                         if not stz.has_key(fname): stz[fname] = [ ]
405                         hcurrent = stz[fname]
406                 elif regexp.compile('\s').match(l):
407                         if not hcurrent: badctrl('unexpected continuation')
408                 else:
409                         badctrl('syntax error')
410                 hcurrent.append((lno, l))
411         end_stanza(stz)
412
413         def testbadctrl(stz, lno, m):
414                 report_badctrl(lno, m)
415                 stz[' errs'] += 1
416
417         for stz in stanzas:
418                 try:
419                         try: tnames = stz['Tests']
420                         except KeyError:
421                                 tnames = ['*']
422                                 raise Unsupported(stz[' lno'],
423                                         'no Tests field')
424                         tnames = map((lambda lt: lt[1]), tnames)
425                         tnames = string.join(tnames).split()
426                         base = {
427                                 'restrictions': [],
428                                 'testsdir': 'debian/tests'
429                         }
430                         for fname in stz.keys():
431                                 if fname.startswith(' '): continue
432                                 vl = stz[fname]
433                                 try: fclass = globals()['Field_'+
434                                         fname.replace('-','_')]
435                                 except KeyError: raise Unsupported(vl[0][0],
436                                         'unknown metadata field %s' % fname)
437                                 f = fclass(stz, fname, base, tnames, vl)
438                                 f.parse()
439                         tests = []
440                         for tname in tnames:
441                                 t = Test(tname, base)
442                                 tests.append(t)
443                 except Unsupported, u:
444                         for tname in tnames: u.report(tname)
445                         continue
446
447 def print_exception(ei, msgprefix=''):
448         if msgprefix: print >>sys.stderr, msgprefix
449         (et, q, tb) = ei
450         if et is Quit:
451                 print >>sys.stderr, 'adt-run:', q.m
452                 return q.ec
453         else:
454                 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
455                 traceback.print_exc()
456                 return 20
457
458 def cleanup():
459         try:
460                 rm_ec = 0
461                 if tmpdir is not None:
462                         rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
463                 if testbed is not None:
464                         testbed.stop()
465                 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
466         except:
467                 print_exception(sys.exc_info(),
468                         '\nadt-run: error cleaning up:\n')
469                 os._exit(20)
470
471 def main():
472         global testbed
473         global tmpdir
474         try:
475                 parse_args()
476         except SystemExit, se:
477                 os._exit(20)
478         try:
479                 tmpdir = tempfile.mkdtemp()
480                 testbed = Testbed()
481                 testbed.start()
482                 testbed.open()
483                 testbed.close()
484                 read_control()
485                 run_tests()
486         except:
487                 ec = print_exception(sys.exc_info(), '')
488                 cleanup()
489                 os._exit(ec)
490         cleanup()
491         os._exit(errorcode)
492
493 main()