chiark / gitweb /
work on adt-run (still wip)
[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 def debug(m):
40         global opts
41         if not opts.debug: return
42         print >>sys.stderr, 'atd-run: debug:', m
43
44 class Path:
45  def __init__(p, tb, path, what, dir=False):
46         p.tb = tb
47         p.p = path
48         p.what = what
49         p.dir = dir
50         if p.tb:
51                 if p.p[:1] != '/':
52                         bomb("path %s specified as being in testbed but"
53                                 " not absolute: `%s'" % (what, p.p))
54                 p.local = None
55         else:
56                 p.local = p.p
57         if p.dir: p.dirsfx = '/'
58         else: p.dirsfx = ''
59  def path(p):
60         return p.p + p.dirsfx
61  def append(p, suffix, what, dir=False):
62         return Path(p.tb, p.path() + suffix, what=what, dir=dir)
63  def __str__(p):
64         if p.tb: pfx = '/VIRT'
65         elif p.p[:1] == '/': pfx = '/HOST'
66         else: pfx = './'
67         return pfx + p.p
68  def onhost(p):
69         if not p.tb: return p.p
70         if p.local is not None: return p.local
71         testbed.open()
72         p.local = tmpdir + '/tb.' + p.what
73         testbed.command('copyup', (p.path(), p.local + p.dirsfx))
74         return p.local
75
76 def parse_args():
77         global opts
78         usage = "%prog <options> -- <virt-server>..."
79         parser = OptionParser(usage=usage)
80         pa = parser.add_option
81         pe = parser.add_option
82
83         def cb_vserv(op,optstr,value,parser):
84                 parser.values.vserver = list(parser.rargs)
85                 del parser.rargs[:]
86
87         def cb_path(op,optstr,value,parser, long,tb,dir):
88                 name = long.replace('-','_')
89                 parser.values.__dict__[name] = Path(tb, value, long, dir)
90
91         def pa_path(long, dir, help):
92                 def papa_tb(long, ca, pahelp):
93                         pa('', long, action='callback', callback=cb_path,
94                                 nargs=1, type='string', callback_args=ca,
95                                 help=(help % pahelp), metavar='PATH')
96                 papa_tb('--'+long,      (long, False, dir), 'host')
97                 papa_tb('--'+long+'-tb',(long, True, dir), 'testbed')
98
99         pa_path('build-tree',   True, 'use build tree from PATH on %s')
100         pa_path('control',      False, 'read control file PATH on %s')
101
102         pa('-d', '--debug', action='store_true', dest='debug');
103         pa('','--user', type='string',
104                 help='run tests as USER (needs root on testbed)')
105
106         class SpecialOption(optparse.Option): pass
107         vs_op = SpecialOption('','--VSERVER-DUMMY')
108         vs_op.action = 'callback'
109         vs_op.type = None
110         vs_op.default = None
111         vs_op.nargs = 0
112         vs_op.callback = cb_vserv
113         vs_op.callback_args = ( )
114         vs_op.callback_kwargs = { }
115         vs_op.help = 'introduces virtualisation server and args'
116         vs_op._short_opts = []
117         #vs_op._long_opts = ['--DUMMY']
118         vs_op._long_opts = ['---']
119
120         pa(vs_op)
121
122         (opts,args) = parser.parse_args()
123         if not hasattr(opts,'vserver'):
124                 parser.error('you must specifiy --- <virt-server>...')
125
126         if opts.build_tree is None:
127                 opts.build_tree = Path(False, '.', 'build-tree', dir=True)
128         if opts.control is None:
129                 opts.control = opts.build_tree.append(
130                         'debian/tests/control', 'control')
131
132 class Testbed:
133  def __init__(tb):
134         tb.sp = None
135         tb.lastsend = None
136         tb.scratch = None
137  def start(tb):
138         p = subprocess.PIPE
139         tb.sp = subprocess.Popen(opts.vserver,
140                 stdin=p, stdout=p, stderr=None)
141         tb.expect('ok')
142  def stop(tb):
143         tb.close()
144         if tb.sp is None: return
145         ec = tb.sp.returncode
146         if ec is None:
147                 tb.sp.stdout.close()
148                 tb.send('quit')
149                 tb.sp.stdin.close()
150                 ec = tb.sp.wait()
151         if ec:
152                 tb.bomb('testbed gave exit status %d after quit' % ec)
153  def open(tb):
154         if tb.scratch is not None: return
155         p = tb.commandr1('open')
156         tb.scratch = Path(True, p, 'tb-scratch', dir=True)
157  def close(tb):
158         if tb.scratch is None: return
159         tb.scratch = None
160         tb.command('close')
161  def bomb(tb, m):
162         if tb.sp is not None:
163                 tb.sp.stdout.close()
164                 tb.sp.stdin.close()
165                 ec = tb.sp.wait()
166                 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
167                         ' exit status %d' % ec)
168         tb.sp = None
169         raise Quit(16, 'testbed failed: %s' % m)
170  def send(tb, string):
171         try:
172                 debug('>> '+string)
173                 print >>tb.sp.stdin, string
174                 tb.sp.stdin.flush()
175                 tb.lastsend = string
176         except:
177                 tb.bomb('cannot send to testbed: %s' %
178                         formatexception_only(sys.last_type, sys.last_value))
179  def expect(tb, keyword, nresults=-1):
180         l = tb.sp.stdout.readline()
181         if not l: tb.bomb('unexpected eof from the testbed')
182         if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
183         l = l.rstrip('\n')
184         debug('<< '+l)
185         ll = l.split()
186         if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
187         if ll[0] != keyword:
188                 if tb.lastsend is None:
189                         tb.bomb("got banner `%s', expected `%s...'" %
190                                 (l, keyword))
191                 else:
192                         tb.bomb("sent `%s', got `%s', expected `%s...'" %
193                                 (tb.lastsend, l, keyword))
194         ll = ll[1:]
195         if nresults >= 0 and len(ll) != nresults:
196                 tb.bomb("sent `%s', got `%s' (%d result parameters),"
197                         " expected %d result parameters" %
198                         (string, l, len(ll), nresults))
199         return ll
200  def commandr(tb, cmd, nresults, args=()):
201         al = [cmd] + map(urllib.quote, args)
202         tb.send(string.join(al))
203         ll = tb.expect('ok')
204         rl = map(urllib.unquote, ll)
205         return rl
206  def command(tb, cmd, args=()):
207         tb.commandr(cmd, 0, args)
208  def commandr1(tb, cmd, args=()):
209         rl = tb.commandr(cmd, 1, args)
210         return rl[0]
211
212 def read_control():
213         global tests
214         control = file(opts.control.onhost(), 'r')
215         lno = 0
216         def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
217         hmap = None # hmap[header_name][index] = (lno, value)
218
219         stanzas = [ ]
220
221         def end_stanza():
222                 if hmap is None: continue
223                 stanzas.append(hmap)
224                 hmap = None
225                 hcurrent = None
226         initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
227         while 1:
228                 l = control.readline()
229                 if not l: break
230                 lno++
231                 if not l.endswith('\n'): badctrl('unterminated line')
232                 if regexp.compile('\s*\#').match(l): continue
233                 if not regexp.compile('\S').match(l): end_stanza(); continue
234                 initmat = initre.match(l)
235                 if initmat:
236                         (hname, l) = initmat.groups()
237                         hname = capwords(hname)
238                         if hmap is None:
239                                 hmap = { ' lno' => lno }
240                         if not haskey(hmap, hname): hmap[hname] = [ ]
241                         hcurrent = hmap[hname]
242                 elif regexp.compile('\s').match(l):
243                         if not hcurrent: badctrl('unexpected continuation')
244                 else:
245                         badctrl('syntax error')
246                 hcurrent.append((lno, l))
247         end_stanza()
248
249         def mergesplit(v): return string.join(v).split()
250         for stz in stanzas:
251                 try: tests = stz['Tests']
252                 except KeyError:
253                         report_unsupported_test('*',
254                                 'no Tests field (near control file line %d)'
255                                 % stz[lno])
256                         continue
257                 tests = mergesplit(tests)
258                 base = { }
259                 restrictions = mergesplit(stz.get('Restrictions',[]))
260                 for rname in restrictions:
261                         try: rr = globals()['Restriction_'+rname]
262                         except KeyError:
263                                 for t in tests:
264                                         report_unsupported_test(t,
265                                                 'unsupported restriction %s'
266                                                 % rname)
267                                 continue
268                         
269                         if rstr in ['needs-root
270                 base['restrictions'] = restrictions
271                 base.testsdir = oneonly(Tests-directory:
272
273                 try: 
274
275                         hcurrent
276                         hmap[hname].append(l)
277                 if : pass
278                 elif
279                         
280                         tb.badpkg('unterminated line in control')
281         testbed.close()
282
283 def print_exception(ei, msgprefix=''):
284         if msgprefix: print >>sys.stderr, msgprefix
285         (et, q, tb) = ei
286         if et is Quit:
287                 print >>sys.stderr, 'adt-run:', q.m
288                 return q.ec
289         else:
290                 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
291                 traceback.print_exc()
292                 return 20
293
294 def cleanup():
295         try:
296                 rm_ec = 0
297                 if tmpdir is not None:
298                         rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
299                 if testbed is not None:
300                         testbed.stop()
301                 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
302         except:
303                 print_exception(sys.exc_info(),
304                         '\nadt-run: error cleaning up:\n')
305                 sys.exit(20)
306
307 def main():
308         global testbed
309         global tmpdir
310         try:
311                 parse_args()
312                 tmpdir = tempfile.mkdtemp()
313                 testbed = Testbed()
314                 testbed.start()
315                 read_control()
316         except:
317                 ec = print_exception(sys.exc_info(), '')
318                 cleanup()
319                 sys.exit(ec)
320         cleanup()
321
322 main()