3 # adt-run is part of autopkgtest
4 # autopkgtest is a tool for testing Debian binary packages
6 # autopkgtest is Copyright (C) 2006 Canonical Ltd.
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.
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.
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.
22 # See the file CREDITS for a full list of credits information (often
23 # installed as /usr/share/doc/autopkgtest/CREDITS).
37 from optparse import OptionParser
43 signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing
46 def __init__(q,ec,m): q.ec = ec; q.m = m
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)
53 def __init__(u, lno, m):
54 if lno >= 0: u.m = '%s (control line %d)' % (m, lno)
59 report(tname, 'SKIP %s' % u.m)
63 if not opts.debug: return
64 print >>sys.stderr, 'atd-run: debug:', m
67 return reduce((lambda a,b: a + b), l, [])
70 def __init__(p, tb, path, what, dir=False, tbscratch=None, xfmap=None
76 p.tbscratch = tbscratch
80 bomb("path %s specified as being in testbed but"
81 " not absolute: `%s'" % (what, p.p))
87 if p.dir: p.dirsfx = '/'
91 def append(p, suffix, what, dir=False):
92 return Path(p.tb, p.path() + suffix, what=what, dir=dir,
93 tbscratch=p.tbscratch)
95 if p.tb: pfx = '/VIRT'
96 elif p.p[:1] == '/': pfx = '/HOST'
100 def xfmapcopy(p, cud, dstdir):
101 if p.xfmap is None: return
102 srcdir = os.path.dirname(p.path()+'/')
103 dstdir = p.xfmapdstdir+'/'
104 for f in p.xfmap(file(p.local)):
105 if '/' in f: bomb("control file %s mentions other filename"
106 "containing slash" % p.what)
107 testbed.command(cud, (srcdir+f, dstdir+f))
109 def onhost(p, lpath = None):
110 if lpath is not None:
111 if p.lpath is not None: assert(p.lpath == lpath)
113 if p.local is not None:
114 if p.lpath is not None: assert(p.local == p.lpath)
120 if p.local is None: p.local = tmpdir + '/tb-' + p.what
122 assert(p.lpath is None)
124 p.xfmapdstdir = tmpdir + '/tbd-' + p.what
125 os.mkdir(p.xfmapdstdir)
126 p.local = p.xfmapdstdir + '/' + os.path.basename(p.down)
128 testbed.command('copyup', (p.path(), p.local + p.dirsfx))
129 p.xfmapcopy('copyup')
134 if p.lpath is None: return None
140 if p.tbscratch is not None:
141 if p.tbscratch != testbed.scratch:
143 if p.down is not None: return p.down
145 bomb("testbed scratch path " + str(p) + " survived testbed")
148 p.down = testbed.scratch.p + '/host-' + p.what
151 p.xfmapdstdir = testbed.scratch.p + '/hostd-' + p.what
152 testbed.command('mkdir '+p.xfmapdstdir)
153 p.down = p.xfmapdstdir + '/' + os.path.basename(p.local)
155 p.tbscratch = testbed.scratch
156 testbed.command('copydown', (p.path(), p.down + p.dirsfx))
157 p.xfmapcopy('copydown')
162 usage = "%prog <options> -- <virt-server>..."
163 parser = OptionParser(usage=usage)
164 pa = parser.add_option
165 pe = parser.add_option
167 def cb_vserv(op,optstr,value,parser):
168 parser.values.vserver = list(parser.rargs)
171 def cb_path(op,optstr,value,parser, long,tb,dir,xfmap):
172 name = long.replace('-','_')
173 path = Path(tb, value, long, dir, xfmap=xfmap)
174 setattr(parser.values, name, path)
176 def pa_path(long, help, dir=False, xfmap=None):
177 def papa_tb(long, ca, pahelp):
178 pa('', long, action='callback', callback=cb_path,
179 nargs=1, type='string', callback_args=ca,
180 help=(help % pahelp), metavar='PATH')
181 papa_tb('--'+long, (long, False, dir, xfmap), 'host')
182 papa_tb('--'+long+'-tb',(long, True, dir, xfmap), 'testbed')
184 pa_path('build-tree', 'use build tree from PATH on %s', dir=True)
185 pa_path('build-source', 'use tests in DSC on %s (building it)', xfmap=xfmap_dsc)
186 #nyi pa_path('install-binary', 'install package found in PATH on %s')
187 #nyi pa_path('install-from-source', 'build and install package found'+
188 # ' in PATH on %s', xfmap=xfmap_dsc)
189 #nyi these install-* options need cb_path to be able to make a list
190 # nyi: without-depends,with-depends-only,with-depends,with-recommends
191 # nyi: package-filter-dependency
192 # nyi: package-filter-from-source
193 pa_path('install-binary', 'build source package PATH on %s')
194 pa_path('control', 'read control file PATH on %s')
195 pa_path('output-dir', 'write stderr/out files in PATH on %s', dir=True)
197 # nyi: on testbed gain root command
198 pa('-d', '--debug', action='store_true', dest='debug');
199 pa('','--user', type='string', dest='user', metavar='USER',
200 help='run tests as USER (needs root on testbed)')
201 pa('','--fakeroot', type='string', dest='fakeroot', metavar='FAKEROOT',
202 help='prefix debian/rules build with FAKEROOT')
204 class SpecialOption(optparse.Option): pass
205 vs_op = SpecialOption('','--VSERVER-DUMMY')
206 vs_op.action = 'callback'
210 vs_op.callback = cb_vserv
211 vs_op.callback_args = ( )
212 vs_op.callback_kwargs = { }
213 vs_op.help = 'introduces virtualisation server and args'
214 vs_op._short_opts = []
215 #vs_op._long_opts = ['--DUMMY']
216 vs_op._long_opts = ['---']
220 (opts,args) = parser.parse_args()
221 if not hasattr(opts,'vserver'):
222 parser.error('you must specifiy --- <virt-server>...')
224 if opts.build_tree is not None and opts.build_source is not None:
225 parser.error('do not specify both --build-tree and'
228 if opts.control is None:
229 opts.control = opts.build_tree.append(
230 'debian/tests/control', 'control')
232 def finalise_options():
235 if opts.build_tree is None and opts.build_source is None:
236 opts.build_tree = Path(False, '.', 'build-tree', dir=True)
238 if opts.user is None and 'root-on-testbed' not in caps:
241 if opts.user is None:
242 su = 'suggested-normal-user='
249 print >>sys.stderr, "warning: virtualisation"
250 " system offers several suggested-normal-user"
251 " values: "+('/'.join(ul))+", using "+ul[0]
258 if 'root-on-testbed' not in caps:
259 print >>sys.stderr, "warning: virtualisation"
260 " system does not offer root on testbed,"
261 " but --user option specified: failure likely"
262 opts.user_wrap = lambda x: 'su %s -c "%s"' % (opts.user, x)
264 opts.user_wrap = lambda x: x
266 if opts.fakeroot is None:
269 'root-on-testbed' not in testbed.caps:
270 opts.fakeroot = 'fakeroot'
272 logpath_counters = {}
275 # if idstr ends with `-' then a counter is appended
276 if idstr.endswith('-'):
277 if not logpath_counters.has_key(idstr):
278 logpath_counters[idstr] = 1
280 logpath_counters[idstr] += 1
281 idstr.append(`logpath_counters[idstr]`)
282 idstr = 'log-' + idstr
283 if opts.output_dir is None:
284 return testbed.scratch.append(idstr, idstr)
285 elif opts.output_dir.tb:
286 return opts.output_dir.append(idstr, idstr)
288 return Path(True, testbed.scratch.p, idstr,
289 lpath=opts.output_dir.p+'/'+idstr)
298 tb.sp = subprocess.Popen(opts.vserver,
299 stdin=p, stdout=p, stderr=None)
301 tb.caps = tb.command('capabilities')
304 if tb.sp is None: return
305 ec = tb.sp.returncode
312 tb.bomb('testbed gave exit status %d after quit' % ec)
314 if tb.scratch is not None: return
315 p = tb.commandr1('open')
316 tb.scratch = Path(True, p, 'tb-scratch', dir=True)
317 tb.scratch.tbscratch = tb.scratch
319 if tb.scratch is None: return
321 if tb.sp is None: return
324 if tb.sp is not None:
328 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
329 ' exit status %d' % ec)
331 raise Quit(16, 'testbed failed: %s' % m)
332 def send(tb, string):
336 print >>tb.sp.stdin, string
340 (type, value, dummy) = sys.exc_info()
341 tb.bomb('cannot send to testbed: %s' % traceback.
342 format_exception_only(type, value))
343 def expect(tb, keyword, nresults=-1):
344 l = tb.sp.stdout.readline()
345 if not l: tb.bomb('unexpected eof from the testbed')
346 if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
350 if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
352 if tb.lastsend is None:
353 tb.bomb("got banner `%s', expected `%s...'" %
356 tb.bomb("sent `%s', got `%s', expected `%s...'" %
357 (tb.lastsend, l, keyword))
359 if nresults >= 0 and len(ll) != nresults:
360 tb.bomb("sent `%s', got `%s' (%d result parameters),"
361 " expected %d result parameters" %
362 (string, l, len(ll), nresults))
364 def commandr(tb, cmd, nresults, args=()):
365 if type(cmd) is str: cmd = [cmd]
366 al = cmd + map(urllib.quote, args)
367 tb.send(string.join(al))
369 rl = map(urllib.unquote, ll)
371 def command(tb, cmd, args=()):
372 tb.commandr(cmd, 0, args)
373 def commandr1(tb, cmd, args=()):
374 rl = tb.commandr(cmd, 1, args)
378 def __init__(f, fname, stz, base, tnames, vl):
388 r = map((lambda w: (lno, w)), r)
390 return flatten(map(distribute, f.vl))
395 raise Unsupported(f.vl[1][0],
396 'only one %s field allowed' % fn)
399 def build_some_source(keyletter, dsc, binaries=False):
400 idstr = 'build'+keyletter
402 bd = testbed.scratch.append(idstr, idstr)
407 'dpkg-source -x '+dsc.ontb()+' >&2',
410 opts.user_wrap('debian/rules build'),
414 opts.user_wrap(opts.fakeroot+' debian/rules binary'),
419 script = '\n'.join(script)
420 so = testbed.scratch.append(idstr+'-tree-path')
421 se = logpath('log-'+idstr)
422 rc = testbed.commandr1(['execute',
423 ','.join(map(urllib.quote, ['sh','-xec',script]))],
424 '/dev/null',so.ontb(),se.ontb(), testbed.scratch.ontb())
425 sod = file(so.onhost()).read().split("\n")
426 build_tree = Path(True, sod[0], idstr+'-tree', dir=True)
430 def acquire_built_source():
433 if opts.build_source:
434 assert(opts.build_tree is None)
435 bss = build_some_source('t', opts.build_source)
436 opts.build_tree = bss[0]
438 class FieldIgnore(FieldBase):
442 def __init__(r,rname,base): pass
444 class Restriction_rw_build_tree(Restriction): pass
446 class Field_Restrictions(FieldBase):
448 for wle in f.words():
450 rname = rname.replace('-','_')
451 try: rclass = globals()['Restriction_'+rname]
452 except KeyError: raise Unsupported(lno,
453 'unknown restriction %s' % rname)
454 r = rclass(rname, f.base)
455 f.base['restrictions'].append(r)
457 class Field_Tests(FieldIgnore): pass
459 class Field_Tests_directory(FieldBase):
462 if td.startswith('/'): raise Unspported(f.lno,
463 'Tests-Directory may not be absolute')
464 base['testsdir'] = td
471 report('*', 'SKIP no tests in this package')
475 def __init__(t, tname, base):
476 if '/' in tname: raise Unsupported(base[' lno'],
477 'test name may not contain / character')
478 for k in base: setattr(t,k,base[k])
480 if len(base['testsdir']): tpath = base['testsdir'] + '/' + tname
482 t.p = opts.build_tree.append(tpath, 'test-'+tname)
485 def reportfail(t, m):
488 report(t.tname, 'FAIL ' + m)
492 idstr = oe + '-' + t.tname
493 if opts.output_dir is not None and opts.output_dir.tb:
494 return opts.output_dir.append(idstr)
496 return testbed.scratch.append(idstr, idstr)
497 def stdouterrh(p, oe):
498 idstr = oe + '-' + t.tname
499 if opts.output_dir is None or opts.output_dir.tb:
502 return p.onhost(opts.output_dir.onhost() + '/' + idstr)
503 so = stdouterr('stdout')
504 se = stdouterr('stderr')
505 rc = testbed.commandr1('execute',(t.p.ontb(),
506 '/dev/null', so.ontb(), se.ontb(), opts.build_tree.ontb()))
507 soh = stdouterrh(so, 'stdout')
508 seh = stdouterrh(se, 'stderr')
511 if stab.st_size != 0:
512 l = file(seh).readline()
513 l = l.rstrip('\n \t\r')
514 if len(l) > 40: l = l[:40] + '...'
515 t.reportfail('stderr: %s' % l)
517 t.reportfail('non-zero exit status %d' % rc)
524 control = file(opts.control.onhost(), 'r')
526 if oe[0] != errno.ENOENT: raise
530 def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
531 stz = None # stz[field_name][index] = (lno, value)
537 if stz is None: return
543 initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
545 l = control.readline()
548 if not l.endswith('\n'): badctrl('unterminated line')
549 if regexp.compile('\s*\#').match(l): continue
550 if not regexp.compile('\S').match(l): end_stanza(stz); continue
551 initmat = initre.match(l)
553 (fname, l) = initmat.groups()
554 fname = string.capwords(fname)
556 stz = { ' lno': lno }
557 if not stz.has_key(fname): stz[fname] = [ ]
558 hcurrent = stz[fname]
559 elif regexp.compile('\s').match(l):
560 if not hcurrent: badctrl('unexpected continuation')
562 badctrl('syntax error')
563 hcurrent.append((lno, l))
566 def testbadctrl(stz, lno, m):
567 report_badctrl(lno, m)
572 try: tnames = stz['Tests']
575 raise Unsupported(stz[' lno'],
577 tnames = map((lambda lt: lt[1]), tnames)
578 tnames = string.join(tnames).split()
581 'testsdir': 'debian/tests'
583 for fname in stz.keys():
584 if fname.startswith(' '): continue
586 try: fclass = globals()['Field_'+
587 fname.replace('-','_')]
588 except KeyError: raise Unsupported(vl[0][0],
589 'unknown metadata field %s' % fname)
590 f = fclass(stz, fname, base, tnames, vl)
594 t = Test(tname, base)
596 except Unsupported, u:
597 for tname in tnames: u.report(tname)
600 def print_exception(ei, msgprefix=''):
601 if msgprefix: print >>sys.stderr, msgprefix
604 print >>sys.stderr, 'adt-run:', q.m
607 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
608 traceback.print_exc()
614 if tmpdir is not None:
615 rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
616 if testbed is not None:
618 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
620 print_exception(sys.exc_info(),
621 '\nadt-run: error cleaning up:\n')
629 except SystemExit, se:
632 tmpdir = tempfile.mkdtemp()
641 ec = print_exception(sys.exc_info(), '')