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).
38 from optparse import OptionParser
44 signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing
47 def __init__(q,ec,m): q.ec = ec; q.m = m
49 def bomb(m): raise Quit(20, "unexpected error: %s" % m)
50 def badpkg(m): raise Quit(12, "erroneous package: %s" % m)
51 def report(tname, result): print '%-20s %s' % (tname, result)
54 def __init__(u, lno, m):
55 if lno >= 0: u.m = '%s (control line %d)' % (m, lno)
60 report(tname, 'SKIP %s' % u.m)
64 if not opts.debug: return
65 print >>sys.stderr, 'atd-run: debug:', m
68 return reduce((lambda a,b: a + b), l, [])
71 # p.path[tb] None or path not None => path known
72 # p.file[tb] None or path not None => file exists
77 def ensure_path(p, tb=False):
78 if tb and not p.spec_tb:
79 if not testbed.scratch:
80 error "called ensure_path for `%s' when testbed closed"
82 if not p.tb_scratch or p.tb_scratch is not testbed.scratch:
85 if p.path[tb] is not None: return
86 if tb: p.path[tb] = p.tb_tmpdir
87 else: p.path[tb] = tmpdir
88 p.path[tb] += '/'+p.what
90 def ensure_file(p, tb=False):
91 if p.file[tb] is not None: return
96 def write(p, tb=False):
99 def read(p, tb=False):
106 def __init__(p, path, spec_tb, what, dir=False):
109 bomb("path %s specified as being in testbed but"
110 " not absolute: `%s'" % (what, p.p))
111 p.path[spec_tb] = p.file[spec_tb] = path
122 4 def __init__(p, tb, path, what, dir=False, tbscratch=None, xfmap=None
128 p.tbscratch = tbscratch
132 bomb("path %s specified as being in testbed but"
133 " not absolute: `%s'" % (what, p.p))
139 if p.dir: p.dirsfx = '/'
142 return p.p + p.dirsfx
143 def append(p, suffix, what, dir=False):
144 return Path(p.tb, p.path() + suffix, what=what, dir=dir,
145 tbscratch=p.tbscratch)
147 if p.tb: pfx = '/VIRT'
148 elif p.p[:1] == '/': pfx = '/HOST'
152 def xfmapcopy(p, cud, dstdir):
153 if p.xfmap is None: return
154 srcdir = os.path.dirname(p.path()+'/')
155 dstdir = p.xfmapdstdir+'/'
156 for f in p.xfmap(file(p.local)):
157 if '/' in f: bomb("control file %s mentions other filename"
158 "containing slash" % p.what)
159 testbed.command(cud, (srcdir+f, dstdir+f))
161 def onhost(p, lpath = None):
162 if lpath is not None:
163 if p.lpath is not None: assert(p.lpath == lpath)
165 if p.local is not None:
166 if p.lpath is not None: assert(p.local == p.lpath)
172 if p.local is None: p.local = tmpdir + '/tb-' + p.what
174 assert(p.lpath is None)
176 p.xfmapdstdir = tmpdir + '/tbd-' + p.what
177 os.mkdir(p.xfmapdstdir)
178 p.local = p.xfmapdstdir + '/' + os.path.basename(p.down)
180 testbed.command('copyup', (p.path(), p.local + p.dirsfx))
181 p.xfmapcopy('copyup')
186 if p.lpath is None: return None
192 if p.tbscratch is not None:
193 if p.tbscratch != testbed.scratch:
195 if p.down is not None: return p.down
197 bomb("testbed scratch path " + str(p) + " survived testbed")
200 p.down = testbed.scratch.p + '/host-' + p.what
203 p.xfmapdstdir = testbed.scratch.p + '/hostd-' + p.what
204 testbed.command('mkdir '+p.xfmapdstdir)
205 p.down = p.xfmapdstdir + '/' + os.path.basename(p.local)
207 p.tbscratch = testbed.scratch
208 testbed.command('copydown', (p.path(), p.down + p.dirsfx))
209 p.xfmapcopy('copydown')
213 def __init__(a, kind, path, arghandling, ix):
217 a.what = '%s%s' % (kind,ix); ix++
221 usage = "%prog <options> -- <virt-server>..."
222 parser = OptionParser(usage=usage)
223 pa = parser.add_option
224 pe = parser.add_option
229 'deb_forbuilds': 'auto',
230 'deb_fortests': 'auto',
232 'override_control': None
234 initial_arghandling = arghandling.copy()
238 # actions (ie, test sets to run, sources to build, binaries to use):
240 def cb_action(op,optstr,value,parser, long,kindpath,is_act):
241 parser.largs.append((value,kindpath))
244 def pa_action(long, metavar, kindpath, help, is_act=True):
245 pa('','--'+long, action='callback', callback=cb_action,
246 nargs=1, type='string',
247 callback_args=(long,kindpath,is_act), help=help)
249 pa_action('build-tree', 'TREE', '@/',
250 help='run tests from build tree TREE')
252 pa_action('source', 'DSC', '@.dsc',
253 help='build DSC and use its tests and/or'
254 ' generated binary packages')
256 pa_action('binary', 'DEB', '@.deb',
257 help='use binary package DEB according'
258 ' to most recent --binaries-* settings')
260 pa_action('override-control', 'CONTROL', ('control',), is_act=0,
261 help='run tests from control file CONTROL instead,
262 ' (applies to next test suite only)')
265 # argument handling settings (what ways to use action
266 # arguments, and pathname processing):
268 def cb_setah(option, opt_str, value, parser, toset,setval):
269 if type(setval) == list:
270 if not value in setval:
271 parser.error('value for %s option (%s) is not '
272 'one of the permitted values (%s)' %
273 (value, opt_str, setval.join(' ')))
274 elif setval is not None:
277 arghandling[v] = value
278 parser.largs.append(arghandling.copy())
280 def pa_setah(long, affected,effect, **kwargs):
281 type = metavar; if type: type = 'string'
282 pa('',long, action='callback', callback=cb_setah,
283 callback_args=(affected,effect), **kwargs)
284 ' according to most recent --binaries-* settings')
286 #---- paths: host or testbed:
288 pa_setah('--paths-testbed', ['tb'],True,
289 help='subsequent path specifications refer to the testbed')
290 pa_setah('--paths-host', ['tb'],False,
291 help='subsequent path specifications refer to the host')
293 #---- source processing settings:
295 pa_setah('--sources-tests', ['dsc_tests'],True,
296 help='run tests from builds of subsequent sources')
297 pa_setah('--sources-no-tests', ['dsc_tests'],False,
298 help='do not run tests from builds of subsequent sources')
300 pa_setah('--built-binaries-filter', ['dsc_filter'],None,
301 type=string, metavar='PATTERN-LIST',
302 help='from subsequent sources, use binaries matching'
303 ' PATTERN-LIST (comma-separated glob patterns)'
304 ' according to most recent --binaries-* settings')
305 pa_setah('--no-built-binaries', ['dsc_filter'], '_',
306 help='from subsequent sources, do not use any binaries')
308 #---- binary package processing settings:
310 def pa_setahbins(long,toset,how):
311 pa_setah(long, toset,['ignore','auto','install'],
312 type=string, metavar='IGNORE|AUTO|INSTALL', default='auto',
313 help=how+' ignore binaries, install them as needed'
314 ' for dependencies, or unconditionally install'
315 ' them, respectively')
316 pa_setahbins('--binaries', ['deb_forbuilds','deb_fortests'], '')
317 pa_setahbins('--binaries-forbuilds', ['deb_forbuilds'], 'for builds, ')
318 pa_setahbins('--binaries-fortests', ['deb_fortests'], 'for tests, ')
323 def cb_vserv(op,optstr,value,parser):
324 parser.values.vserver = list(parser.rargs)
327 def cb_path(op,optstr,value,parser, long,dir,xfmap):
328 name = long.replace('-','_')
329 path = Path(arghandling['tb'], value, long, dir, xfmap=xfmap)
330 setattr(parser.values, name, path)
332 def pa_path(long, help, dir=False, xfmap=None):
333 pa('','--'+long, action='callback', callback=cb_path,
334 nargs=1, type='string', callback_args=(long,dir,xfmap),
335 help=, metavar='PATH')
337 pa_path('output-dir', 'write stderr/out files in PATH', dir=True)
339 pa('','--user', type='string', dest='user',
340 help='run tests as USER (needs root on testbed)')
341 pa('','--fakeroot', type='string', dest='fakeroot',
342 help='prefix debian/rules build with FAKEROOT')
343 pa('-d', '--debug', action='store_true', dest='debug');
348 class SpecialOption(optparse.Option): pass
349 vs_op = SpecialOption('','--VSERVER-DUMMY')
350 vs_op.action = 'callback'
354 vs_op.callback = cb_vserv
355 vs_op.callback_args = ( )
356 vs_op.callback_kwargs = { }
357 vs_op.help = 'introduces virtualisation server and args'
358 vs_op._short_opts = []
359 vs_op._long_opts = ['---']
363 (opts,args) = parser.parse_args()
364 if not hasattr(opts,'vserver'):
365 parser.error('you must specifiy --- <virt-server>...')
367 parser.error('nothing to do specified')
369 arghandling = initial_arghandling
373 if type(act) == dict:
376 elif type(act) == tuple:
378 elif type(act) == string:
381 error "unknown action in list `%s' having"
382 "type `%s' % (act, type(act))
383 (path, kindpath) = act
385 if type(kindpath) is tuple: kind = kindpath[0]
386 elif kindpath.endswith('/'): kind = 'tree'
387 elif kindpath.endswith('.deb'): kind = 'deb'
388 elif kindpath.endswith('.dsc'): kind = 'dsc'
389 else: parser.error("do not know how to handle filename \`%s';"
390 " specify --source --binary or --build-tree")
392 opts.actions.append(Action(kind, path, arghandling, ix))
395 def finalise_options():
398 if opts.user is None and 'root-on-testbed' not in caps:
401 if opts.user is None:
402 su = 'suggested-normal-user='
409 print >>sys.stderr, "warning: virtualisation"
410 " system offers several suggested-normal-user"
411 " values: "+('/'.join(ul))+", using "+ul[0]
418 if 'root-on-testbed' not in caps:
419 print >>sys.stderr, "warning: virtualisation"
420 " system does not offer root on testbed,"
421 " but --user option specified: failure likely"
422 opts.user_wrap = lambda x: 'su %s -c "%s"' % (opts.user, x)
424 opts.user_wrap = lambda x: x
426 if opts.fakeroot is None:
429 'root-on-testbed' not in testbed.caps:
430 opts.fakeroot = 'fakeroot'
432 logpath_counters = {}
435 # if idstr ends with `-' then a counter is appended
436 if idstr.endswith('-'):
437 if not logpath_counters.has_key(idstr):
438 logpath_counters[idstr] = 1
440 logpath_counters[idstr] += 1
441 idstr.append(`logpath_counters[idstr]`)
442 idstr = 'log-' + idstr
443 if opts.output_dir is None:
444 return testbed.scratch.append(idstr, idstr)
445 elif opts.output_dir.tb:
446 return opts.output_dir.append(idstr, idstr)
448 return Path(True, testbed.scratch.p, idstr,
449 lpath=opts.output_dir.p+'/'+idstr)
458 tb.sp = subprocess.Popen(opts.vserver,
459 stdin=p, stdout=p, stderr=None)
461 tb.caps = tb.command('capabilities')
464 if tb.sp is None: return
465 ec = tb.sp.returncode
472 tb.bomb('testbed gave exit status %d after quit' % ec)
474 if tb.scratch is not None: return
475 p = tb.commandr1('open')
476 tb.scratch = Path(True, p, 'tb-scratch', dir=True)
477 tb.scratch.tbscratch = tb.scratch
479 if tb.scratch is None: return
481 if tb.sp is None: return
484 if tb.sp is not None:
488 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
489 ' exit status %d' % ec)
491 raise Quit(16, 'testbed failed: %s' % m)
492 def send(tb, string):
496 print >>tb.sp.stdin, string
500 (type, value, dummy) = sys.exc_info()
501 tb.bomb('cannot send to testbed: %s' % traceback.
502 format_exception_only(type, value))
503 def expect(tb, keyword, nresults=-1):
504 l = tb.sp.stdout.readline()
505 if not l: tb.bomb('unexpected eof from the testbed')
506 if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
510 if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
512 if tb.lastsend is None:
513 tb.bomb("got banner `%s', expected `%s...'" %
516 tb.bomb("sent `%s', got `%s', expected `%s...'" %
517 (tb.lastsend, l, keyword))
519 if nresults >= 0 and len(ll) != nresults:
520 tb.bomb("sent `%s', got `%s' (%d result parameters),"
521 " expected %d result parameters" %
522 (string, l, len(ll), nresults))
524 def commandr(tb, cmd, nresults, args=()):
525 if type(cmd) is str: cmd = [cmd]
526 al = cmd + map(urllib.quote, args)
527 tb.send(string.join(al))
529 rl = map(urllib.unquote, ll)
531 def command(tb, cmd, args=()):
532 tb.commandr(cmd, 0, args)
533 def commandr1(tb, cmd, args=()):
534 rl = tb.commandr(cmd, 1, args)
538 def __init__(f, fname, stz, base, tnames, vl):
548 r = map((lambda w: (lno, w)), r)
550 return flatten(map(distribute, f.vl))
555 raise Unsupported(f.vl[1][0],
556 'only one %s field allowed' % fn)
560 def acquire_built_source():
563 if opts.build_source:
564 assert(opts.tests_tree is None)
565 bss = build_some_source('t', opts.build_source)
566 opts.tests_tree = bss[0]
568 class FieldIgnore(FieldBase):
572 def __init__(r,rname,base): pass
574 class Restriction_rw_tests_tree(Restriction): pass
576 class Field_Restrictions(FieldBase):
578 for wle in f.words():
580 rname = rname.replace('-','_')
581 try: rclass = globals()['Restriction_'+rname]
582 except KeyError: raise Unsupported(lno,
583 'unknown restriction %s' % rname)
584 r = rclass(rname, f.base)
585 f.base['restrictions'].append(r)
587 class Field_Tests(FieldIgnore): pass
589 class Field_Tests_directory(FieldBase):
592 if td.startswith('/'): raise Unspported(f.lno,
593 'Tests-Directory may not be absolute')
594 base['testsdir'] = td
601 report('*', 'SKIP no tests in this package')
605 def __init__(t, tname, base):
606 if '/' in tname: raise Unsupported(base[' lno'],
607 'test name may not contain / character')
608 for k in base: setattr(t,k,base[k])
610 if len(base['testsdir']): tpath = base['testsdir'] + '/' + tname
612 t.p = opts.tests_tree.append(tpath, 'test-'+tname)
615 def reportfail(t, m):
618 report(t.tname, 'FAIL ' + m)
622 idstr = oe + '-' + t.tname
623 if opts.output_dir is not None and opts.output_dir.tb:
624 return opts.output_dir.append(idstr)
626 return testbed.scratch.append(idstr, idstr)
627 def stdouterrh(p, oe):
628 idstr = oe + '-' + t.tname
629 if opts.output_dir is None or opts.output_dir.tb:
632 return p.onhost(opts.output_dir.onhost() + '/' + idstr)
633 so = stdouterr('stdout')
634 se = stdouterr('stderr')
635 rc = testbed.commandr1('execute',(t.p.ontb(),
636 '/dev/null', so.ontb(), se.ontb(), opts.tests_tree.ontb()))
637 soh = stdouterrh(so, 'stdout')
638 seh = stdouterrh(se, 'stderr')
641 if stab.st_size != 0:
642 l = file(seh).readline()
643 l = l.rstrip('\n \t\r')
644 if len(l) > 40: l = l[:40] + '...'
645 t.reportfail('stderr: %s' % l)
647 t.reportfail('non-zero exit status %d' % rc)
654 control = file(opts.control.onhost(), 'r')
656 if oe[0] != errno.ENOENT: raise
660 def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
661 stz = None # stz[field_name][index] = (lno, value)
667 if stz is None: return
673 initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
675 l = control.readline()
678 if not l.endswith('\n'): badctrl('unterminated line')
679 if regexp.compile('\s*\#').match(l): continue
680 if not regexp.compile('\S').match(l): end_stanza(stz); continue
681 initmat = initre.match(l)
683 (fname, l) = initmat.groups()
684 fname = string.capwords(fname)
686 stz = { ' lno': lno }
687 if not stz.has_key(fname): stz[fname] = [ ]
688 hcurrent = stz[fname]
689 elif regexp.compile('\s').match(l):
690 if not hcurrent: badctrl('unexpected continuation')
692 badctrl('syntax error')
693 hcurrent.append((lno, l))
696 def testbadctrl(stz, lno, m):
697 report_badctrl(lno, m)
702 try: tnames = stz['Tests']
705 raise Unsupported(stz[' lno'],
707 tnames = map((lambda lt: lt[1]), tnames)
708 tnames = string.join(tnames).split()
711 'testsdir': 'debian/tests'
713 for fname in stz.keys():
714 if fname.startswith(' '): continue
716 try: fclass = globals()['Field_'+
717 fname.replace('-','_')]
718 except KeyError: raise Unsupported(vl[0][0],
719 'unknown metadata field %s' % fname)
720 f = fclass(stz, fname, base, tnames, vl)
724 t = Test(tname, base)
726 except Unsupported, u:
727 for tname in tnames: u.report(tname)
730 def print_exception(ei, msgprefix=''):
731 if msgprefix: print >>sys.stderr, msgprefix
734 print >>sys.stderr, 'adt-run:', q.m
737 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
738 traceback.print_exc()
744 if tmpdir is not None:
745 rm_ec = subprocess.call(['rm','-rf','--',tmpdir])
746 if testbed is not None:
748 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
750 print_exception(sys.exc_info(),
751 '\nadt-run: error cleaning up:\n')
754 def source_rules_command(act,script,which,work,results_lines=0):
755 script = "exec 3>&1 >&2\n" + '\n'.join(script)
756 so = TemporaryPath('%s-%s-results' % (what,which))
757 se = TemporaryPath('%s-%s-log' & (what,which))
758 rc = testbed.commandr1(['execute',
759 ','.join(map(urllib.quote, ['sh','-xec',script]))],
760 '/dev/null', so.write(True), se.write(True), work.write(True))
761 results = file(so.read()).read().split("\n")
763 act.bomb("%s failed with exit code %d" % (which,rc), se)
764 if results_lines is not None and len(results) != results_lines:
765 act.bomb("got %d lines of results from %s where %d expected"
766 % (len(results), which, results_lines), se)
767 if results_lines==1: return results[0]
770 def build_source(act,ah):
771 prepare_testbed_for_action()
773 what = act.ah['what']
774 dsc_what = what+'/'+os.path.basename(act.path)
775 dsc = InputPath(dsc_what, act.path, arghandling['tb'])
778 dsc_file = open(dsc.read())
780 re = regexp.compile('^\s+[0-9a-f]+\s+\d+\s+([^/.][^/]*)$')
782 if l.startswith('Files:'): in_files = True
783 elif l.startswith('#'): pass
784 elif not l.startswith(' '): in_files = False
785 elif not in_files: pass
788 if not m: act.bomb(".dsc contains unparseable line"
789 " in Files: `%s'" % (`dsc`,l))
791 subfile = dsc.enclosingdir().append('/'+m.groups(0))
792 subfile.ensure_file(True)
793 dsc.ensure_file(True)
795 work = AccumulationPath(what+'/build', dir=True)
798 'cd '+work.write(True),
799 'gdebi '+dsc.read(True),
800 'dpkg-source -x '+dsc.read(True),
803 opts.user_wrap('debian/rules build'),
805 result_pwd = source_rules_command(act,script,what,'build',work,1)
807 if os.path.dirname(result_pwd) != work.read(True):
808 act.bomb("results dir `%s' is not in expected parent dir `%s'"
809 % (results[0], work.read(True)), se)
811 ah.tests_tree = work.append('/'+os.path.basename(results[0]))
813 ah.tests_tree.preserve_now()
816 if ah['dsc_filter'] != '_':
818 'cd '+work.write(True)+'/*/.',
819 opts.user_wrap(opts.fakeroot+' debian/rules binary'),
823 result_debs = source_rules_command(act,script,what,
824 'debian/rules binary',work,1)
825 if result_debs == '*': debs = []
826 else: debs = debs.split(' ')
827 re = regexp.compile('^([-+.0-9a-z]+)_[^_/]+(?:_[^_/]+)\.deb$')
830 if not m: act.bomb("badly-named binary `%s'" % deb, se)
831 package = m.groups(deb)
832 for pat in ah['dsc_filter'].split(','):
833 if fnmatch.fnmatchcase(package,pat):
834 deb_path = work.read()+'/'+deb
835 deb_what = package+'_'+what+'.deb'
836 bin = InputPath(deb_what,deb_path,True)
838 record_binary(bin,'builds')
839 ah.binaries.append(bin)
844 def process_actions():
849 for act opts.actions:
850 if act.kind == 'deb':
851 record_binary(act,'builds')
852 if act.kind == 'dsc':
854 # build_source records tree location in ah
857 control_override = None
858 for (kind,path,ah) in opts.actions:
859 if kind == 'control':
860 control_override = act
862 record_binary(act,'tests')
864 for bin in act.binaries: record_binary(bin,'tests')
865 if not act.ah['dsc_tests']: continue
866 run_tests(act,control_override)
867 control_override = None
869 run_tests(act,control_override)
870 control_override = None
877 except SystemExit, se:
880 tmpdir = tempfile.mkdtemp()
887 ec = print_exception(sys.exc_info(), '')