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