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