chiark / gitweb /
fixes to adt-virt-chroot; 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
25 from optparse import OptionParser
26
27 tmpdir = None
28 testbed = None
29
30 signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing
31
32 class Quit:
33         def __init__(q,ec,m): q.ec = ec; q.m = m
34
35 def bomb(m): raise Quit(20, "unexpected error: %s" % m)
36 def badpkg(m): raise Quit(12, "erroneous package: %s" % m)
37
38 def debug(m):
39         global opts
40         if not opts.debug: return
41         print >>sys.stderr, 'atd-run: debug:', m
42
43 class Path:
44  def __init__(p, tb, path, what, dir=False):
45         p.tb = tb
46         p.p = path
47         p.what = what
48         p.dir = dir
49         if p.tb:
50                 if p.p[:1] != '/':
51                         bomb("path %s specified as being in testbed but"
52                                 " not absolute: `%s'" % (what, p.p))
53                 p.local = None
54         else:
55                 p.local = p.p
56         if p.dir: p.dirsfx = '/'
57         else: p.dirsfx = ''
58  def path(p):
59         return p.p + p.dirsfx
60  def append(p, suffix, what, dir=False):
61         return Path(p.tb, p.path() + suffix, what=what, dir=dir)
62  def __str__(p):
63         if p.tb: pfx = '/VIRT'
64         elif p.p[:1] == '/': pfx = '/HOST'
65         else: pfx = './'
66         return pfx + p.p
67  def onhost(p):
68         if not p.tb: return p.p
69         if p.local is not None: return p.local
70         testbed.open()
71         p.local = tmpdir + '/tb.' + p.what
72         testbed.command('copyup', (p.path(), p.local + p.dirsfx))
73         return p.local
74
75 def parse_args():
76         global opts
77         usage = "%prog <options> -- <virt-server>..."
78         parser = OptionParser(usage=usage)
79         pa = parser.add_option
80         pe = parser.add_option
81
82         def cb_vserv(op,optstr,value,parser):
83                 parser.values.vserver = list(parser.rargs)
84                 del parser.rargs[:]
85
86         def cb_path(op,optstr,value,parser, long,tb,dir):
87                 name = long.replace('-','_')
88                 parser.values.__dict__[name] = Path(tb, value, long, dir)
89
90         def pa_path(long, dir, help):
91                 def papa_tb(long, ca, pahelp):
92                         pa('', long, action='callback', callback=cb_path,
93                                 nargs=1, type='string', callback_args=ca,
94                                 help=(help % pahelp), metavar='PATH')
95                 papa_tb('--'+long,      (long, False, dir), 'host')
96                 papa_tb('--'+long+'-tb',(long, True, dir), 'testbed')
97
98         pa_path('build-tree',   True, 'use build tree from PATH on %s')
99         pa_path('control',      False, 'read control file PATH on %s')
100
101         pa('-d', '--debug', action='store_true', dest='debug');
102         pa('','--user', type='string',
103                 help='run tests as USER (needs root on testbed)')
104
105         class SpecialOption(optparse.Option): pass
106         vs_op = SpecialOption('','--VSERVER-DUMMY')
107         vs_op.action = 'callback'
108         vs_op.type = None
109         vs_op.default = None
110         vs_op.nargs = 0
111         vs_op.callback = cb_vserv
112         vs_op.callback_args = ( )
113         vs_op.callback_kwargs = { }
114         vs_op.help = 'introduces virtualisation server and args'
115         vs_op._short_opts = []
116         #vs_op._long_opts = ['--DUMMY']
117         vs_op._long_opts = ['---']
118
119         pa(vs_op)
120
121         (opts,args) = parser.parse_args()
122         if not hasattr(opts,'vserver'):
123                 parser.error('you must specifiy --- <virt-server>...')
124
125         if opts.build_tree is None:
126                 opts.build_tree = Path(False, '.', 'build-tree', dir=True)
127         if opts.control is None:
128                 opts.control = opts.build_tree.append(
129                         'debian/tests/control', 'control')
130
131 class Testbed:
132  def __init__(tb):
133         tb.sp = None
134         tb.lastsend = None
135         tb.scratch = None
136  def start(tb):
137         p = subprocess.PIPE
138         tb.sp = subprocess.Popen(opts.vserver,
139                 stdin=p, stdout=p, stderr=None)
140         tb.expect('ok')
141  def stop(tb):
142         tb.close()
143         if tb.sp is None: return
144         ec = tb.sp.returncode
145         if ec is None:
146                 tb.sp.stdout.close()
147                 tb.send('quit')
148                 tb.sp.stdin.close()
149                 ec = tb.sp.wait()
150         if ec:
151                 tb.bomb('testbed gave exit status %d after quit' % ec)
152  def open(tb):
153         if tb.scratch is not None: return
154         p = tb.commandr1('open')
155         tb.scratch = Path(True, p, 'tb-scratch', dir=True)
156  def close(tb):
157         if tb.scratch is None: return
158         tb.scratch = None
159         tb.command('close')
160  def bomb(tb, m):
161         if tb.sp is not None:
162                 tb.sp.stdout.close()
163                 tb.sp.stdin.close()
164                 ec = tb.sp.wait()
165                 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
166                         ' exit status %d' % ec)
167         tb.sp = None
168         raise Quit(16, 'testbed failed: %s' % m)
169  def send(tb, string):
170         try:
171                 debug('>> '+string)
172                 print >>tb.sp.stdin, string
173                 tb.sp.stdin.flush()
174                 tb.lastsend = string
175         except:
176                 tb.bomb('cannot send to testbed: %s' %
177                         formatexception_only(sys.last_type, sys.last_value))
178  def expect(tb, keyword, nresults=-1):
179         l = tb.sp.stdout.readline()
180         if not l: tb.bomb('unexpected eof from the testbed')
181         if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
182         l = l.rstrip('\n')
183         debug('<< '+l)
184         ll = l.split()
185         if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
186         if ll[0] != keyword:
187                 if tb.lastsend is None:
188                         tb.bomb("got banner `%s', expected `%s...'" %
189                                 (l, keyword))
190                 else:
191                         tb.bomb("sent `%s', got `%s', expected `%s...'" %
192                                 (tb.lastsend, l, keyword))
193         ll = ll[1:]
194         if nresults >= 0 and len(ll) != nresults:
195                 tb.bomb("sent `%s', got `%s' (%d result parameters),"
196                         " expected %d result parameters" %
197                         (string, l, len(ll), nresults))
198         return ll
199  def commandr(tb, cmd, nresults, args=()):
200         al = [cmd] + map(urllib.quote, args)
201         tb.send(string.join(al))
202         ll = tb.expect('ok')
203         rl = map(urllib.unquote, ll)
204         return rl
205  def command(tb, cmd, args=()):
206         tb.commandr(cmd, 0, args)
207  def commandr1(tb, cmd, args=()):
208         rl = tb.commandr(cmd, 1, args)
209         return rl[0]
210
211 def read_control():
212         control = file(opts.control.onhost(), 'r')
213         testbed.close()
214
215 def print_exception(ei, msgprefix=''):
216         if msgprefix: print >>sys.stderr, msgprefix
217         (et, q, tb) = ei
218         if et is Quit:
219                 print >>sys.stderr, 'adt-run:', q.m
220                 return q.ec
221         else:
222                 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
223                 traceback.print_exc()
224                 return 20
225
226 def cleanup():
227         try:
228                 rm_ec = 0
229                 if tmpdir is not None:
230                         rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
231                 if testbed is not None:
232                         testbed.stop()
233                 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
234         except:
235                 print_exception(sys.exc_info(),
236                         '\nadt-run: error cleaning up:\n')
237                 sys.exit(20)
238
239 def main():
240         global testbed
241         global tmpdir
242         try:
243                 parse_args()
244                 tmpdir = tempfile.mkdtemp()
245                 testbed = Testbed()
246                 testbed.start()
247                 read_control()
248         except:
249                 ec = print_exception(sys.exc_info(), '')
250                 cleanup()
251                 sys.exit(ec)
252         cleanup()
253
254 main()