chiark / gitweb /
sorted out Path invocations, now called File and Dir; just need to make Temporary...
[autopkgtest.git] / runner / adt-run
1 #!/usr/bin/python2.4
2 #
3 # adt-run is part of autopkgtest
4 # autopkgtest is a tool for testing Debian binary packages
5 #
6 # autopkgtest is Copyright (C) 2006 Canonical Ltd.
7 #
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.
12 #
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.
17 #
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.
21 #
22 # See the file CREDITS for a full list of credits information (often
23 # installed as /usr/share/doc/autopkgtest/CREDITS).
24
25 import signal
26 import optparse
27 import tempfile
28 import sys
29 import subprocess
30 import traceback
31 import urllib
32 import string
33 import re as regexp
34 import os
35 import errno
36 import fnmatch
37 import shutil
38
39 from optparse import OptionParser
40 signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing
41
42 #---------- global variables
43
44 tmpdir = None           # pathstring on host
45 testbed = None          # Testbed
46 errorcode = 0           # exit status that we are going to use
47 binaries = None         # Binaries (.debs we have registered)
48
49 #---------- errors we define
50
51 class Quit:
52         def __init__(q,ec,m): q.ec = ec; q.m = m
53
54 def bomb(m): raise Quit(20, "unexpected error: %s" % m)
55 def badpkg(m):
56         print 'blame: ', ' '.join(testbed.blamed)
57         raise Quit(12, "erroneous package: %s" % m)
58
59 def report(tname, result): print '%-20s %s' % (tname, result)
60
61 class Unsupported:
62  def __init__(u, lno, m):
63         if lno >= 0: u.m = '%s (control line %d)' % (m, lno)
64         else: u.m = m
65  def report(u, tname):
66         global errorcode
67         errorcode != 2
68         report(tname, 'SKIP %s' % u.m)
69
70 def debug(m):
71         global opts
72         if not opts.debug: return
73         print >>sys.stderr, 'atd-run: debug:', m
74
75 def flatten(l):
76         return reduce((lambda a,b: a + b), l, []) 
77
78 #---------- fancy automatic file-copying class
79
80 class AutoFile:
81         # p.what
82         # p.path[tb]    None or path    not None => path known
83         # p.file[tb]    None or path    not None => file exists
84         # p.spec        string
85         # p.spec_tb     True or False, None iff p.spec is None
86         # p.tb_scratch
87         # p.dir         '' or '/'
88  def __init__(p, what):
89         p.what = what
90         p.path = [None,None]
91         p.file = [None,None]
92         p.dir = ''
93  def subpath(p, what, leaf, constructor):
94         if not p.dir: error "creating subpath of non-directory"
95         return constructor(what, p.spec+'/'+leaf, p.spec_tb)
96  def invalidate(p, tb=False):
97         p.file[tb] = None
98
99 class InputFile(Path):
100  def __init__(p, what, spec, spec_tb=False):
101         AutoFile.__init__(p, what)
102         p.spec = spec
103         p.spec_tb = spec_tb
104         p.path[spec_tb] = p.file[spec_tb] = spec
105
106 class InputDir(Path):
107  def __init__(p, what, spec, spec_tb=False):
108         InputFile.__init__(p,what,spec,spec_tb)
109         p.dir = '/'
110
111 class OutputFile(Path):
112  def __init__(p, what, spec, spec_tb=False):
113         AutoFile.__init__(p, what)
114         p.spec = spec
115         p.spec_tb = spec_tb
116         p.path[spec_tb] = spec
117
118 class OutputDir(Path):
119  def __init__(p, what, spec, spec_tb=False):
120         OutputFile.__init__(p,what,spec,spec_tb)
121         p.dir = '/'
122
123 class TemporaryFile(Path):
124  def __init__(p, what):
125         p.path = 
126         OutputFile.__init__(p,what, testbed.scratch
127
128  def ensure_path(p, tb=False):
129         if tb and not p.spec_tb:
130                 if not testbed.scratch:
131                         error "called ensure_path for `%s' when testbed closed"
132                                 % what
133                 if not p.tb_scratch or p.tb_scratch is not testbed.scratch:
134                         
135
136         if p.path[tb] is not None: return
137         if tb: p.path[tb] = p.tb_tmpdir
138         else: p.path[tb] = tmpdir
139         p.path[tb] += '/'+p.what
140
141  def ensure_file(p, tb=False):
142         if p.file[tb] is not None: return
143         p.ensure_path(tb)
144         testbed.open()
145         
146
147  def write(p, tb=False):
148         p.ensure_path(tb)
149         return p.path[tb]
150  def read(p, tb=False):
151         p.ensure_file(tb)
152
153
154
155
156 class InputPath:
157 class OutputPath:
158
159  def __init__(p, path, spec_tb, what, dir=False):
160         if p.tb:
161                 if p.p[:1] != '/':
162                         bomb("path %s specified as being in testbed but"
163                                 " not absolute: `%s'" % (what, p.p))
164         p.path[spec_tb] = p.file[spec_tb] = path
165         p.what = what
166
167
168         if spec_tb:
169                 p.
170 tb_path = path
171                 p.tb_onpath = path
172                 p.tb_onhost = None
173         
174
175 4 def __init__(p, tb, path, what, dir=False, tbscratch=None, xfmap=None
176                 lpath=None):
177         p.tb = tb
178         p.p = path
179         p.what = what
180         p.dir = dir
181         p.tbscratch = tbscratch
182         p.lpath = None
183         if p.tb:
184                 if p.p[:1] != '/':
185                         bomb("path %s specified as being in testbed but"
186                                 " not absolute: `%s'" % (what, p.p))
187                 p.local = None
188                 p.down = p.p
189         else:
190                 p.local = p.p
191                 p.down = None
192         if p.dir: p.dirsfx = '/'
193         else: p.dirsfx = ''
194  def path(p):
195         return p.p + p.dirsfx
196  def append(p, suffix, what, dir=False):
197         return Path(p.tb, p.path() + suffix, what=what, dir=dir,
198                         tbscratch=p.tbscratch)
199  def __str__(p):
200         if p.tb: pfx = '/VIRT'
201         elif p.p[:1] == '/': pfx = '/HOST'
202         else: pfx = './'
203         return pfx + p.p
204
205  def xfmapcopy(p, cud, dstdir):
206         if p.xfmap is None: return
207         srcdir = os.path.dirname(p.path()+'/')
208         dstdir = p.xfmapdstdir+'/'
209         for f in p.xfmap(file(p.local)):
210                 if '/' in f: bomb("control file %s mentions other filename"
211                                 "containing slash" % p.what)
212                 testbed.command(cud, (srcdir+f, dstdir+f))
213
214  def onhost(p, lpath = None):
215         if lpath is not None:
216                 if p.lpath is not None: assert(p.lpath == lpath)
217                 p.lpath = lpath
218         if p.local is not None:
219                 if p.lpath is not None: assert(p.local == p.lpath)
220                 return p.local
221
222         if p.xfmap is None:
223                 p.local = p.lpath
224                 if p.local is None: p.local = tmpdir + '/tb-' + p.what
225         else:
226                 assert(p.lpath is None)
227                 assert(not p.dir)
228                 p.xfmapdstdir = tmpdir + '/tbd-' + p.what
229                 os.mkdir(p.xfmapdstdir)
230                 p.local = p.xfmapdstdir + '/' + os.path.basename(p.down)
231
232         testbed.command('copyup', (p.path(), p.local + p.dirsfx))
233         p.xfmapcopy('copyup')
234
235         return p.local
236
237  def maybe_onhost(p):
238         if p.lpath is None: return None
239         return p.onhost()
240
241  def ontb(p):
242
243         if p.tbscratch is not None:
244                 if p.tbscratch != testbed.scratch:
245                         p.down = None
246         if p.down is not None: return p.down
247         if p.tb:
248                 bomb("testbed scratch path " + str(p) + " survived testbed")
249
250         if p.xfmap is None:
251                 p.down = testbed.scratch.p + '/host-' + p.what          
252         else:
253                 assert(not p.dir)
254                 p.xfmapdstdir = testbed.scratch.p + '/hostd-' + p.what
255                 testbed.command('mkdir '+p.xfmapdstdir)
256                 p.down = p.xfmapdstdir + '/' + os.path.basename(p.local)
257
258         p.tbscratch = testbed.scratch
259         testbed.command('copydown', (p.path(), p.down + p.dirsfx))
260         p.xfmapcopy('copydown')
261         return p.down
262
263 #---------- parsing and representation of the arguments
264
265 class Action:
266  def __init__(a, kind, af, arghandling, what):
267         # extra attributes get added during processing
268         a.kind = kind
269         a.af = af
270         a.ah = arghandling
271         a.what = what
272
273 def parse_args():
274         global opts
275         usage = "%prog <options> -- <virt-server>..."
276         parser = OptionParser(usage=usage)
277         pa = parser.add_option
278         pe = parser.add_option
279
280         arghandling = {
281                 'dsc_tests': True,
282                 'dsc_filter': '*',
283                 'deb_forbuilds': 'auto',
284                 'deb_fortests': 'auto',
285                 'tb': False,
286                 'override_control': None
287         }
288         initial_arghandling = arghandling.copy()
289         n_actions = 0
290
291         #----------
292         # actions (ie, test sets to run, sources to build, binaries to use):
293
294         def cb_action(op,optstr,value,parser, long,kindpath,is_act):
295                 parser.largs.append((value,kindpath))
296                 n_actions += is_act
297
298         def pa_action(long, metavar, kindpath, help, is_act=True):
299                 pa('','--'+long, action='callback', callback=cb_action,
300                         nargs=1, type='string',
301                         callback_args=(long,kindpath,is_act), help=help)
302
303         pa_action('build-tree',         'TREE', '@/',
304                 help='run tests from build tree TREE')
305
306         pa_action('source',             'DSC', '@.dsc',
307                 help='build DSC and use its tests and/or'
308                     ' generated binary packages')
309
310         pa_action('binary',             'DEB', '@.deb',
311                help='use binary package DEB according'
312                     ' to most recent --binaries-* settings')
313
314         pa_action('override-control',   'CONTROL', ('control',), is_act=0,
315                help='run tests from control file CONTROL instead,
316                     ' (applies to next test suite only)')
317
318         #----------
319         # argument handling settings (what ways to use action
320         #  arguments, and pathname processing):
321
322         def cb_setah(option, opt_str, value, parser, toset,setval):
323                 if type(setval) == list:
324                         if not value in setval:
325                                 parser.error('value for %s option (%s) is not '
326                                  'one of the permitted values (%s)' %
327                                  (value, opt_str, setval.join(' ')))
328                 elif setval is not None:
329                         value = setval
330                 for v in toset:
331                         arghandling[v] = value
332                 parser.largs.append(arghandling.copy())
333
334         def pa_setah(long, affected,effect, **kwargs):
335                 type = metavar; if type: type = 'string'
336                 pa('',long, action='callback', callback=cb_setah,
337                    callback_args=(affected,effect), **kwargs)
338                      ' according to most recent --binaries-* settings')
339
340         #---- paths: host or testbed:
341         #
342         pa_setah('--paths-testbed', ['tb'],True,
343                 help='subsequent path specifications refer to the testbed')
344         pa_setah('--paths-host', ['tb'],False,
345                 help='subsequent path specifications refer to the host')
346
347         #---- source processing settings:
348
349         pa_setah('--sources-tests', ['dsc_tests'],True,
350                 help='run tests from builds of subsequent sources')
351         pa_setah('--sources-no-tests', ['dsc_tests'],False,
352                 help='do not run tests from builds of subsequent sources')
353
354         pa_setah('--built-binaries-filter', ['dsc_filter'],None,
355                 type=string, metavar='PATTERN-LIST',
356                 help='from subsequent sources, use binaries matching'
357                      ' PATTERN-LIST (comma-separated glob patterns)'
358                      ' according to most recent --binaries-* settings')
359         pa_setah('--no-built-binaries', ['dsc_filter'], '_',
360                 help='from subsequent sources, do not use any binaries')
361
362         #---- binary package processing settings:
363
364         def pa_setahbins(long,toset,how):
365          pa_setah(long, toset,['ignore','auto','install'],
366                 type=string, metavar='IGNORE|AUTO|INSTALL', default='auto',
367                 help=how+' ignore binaries, install them as needed'
368                         ' for dependencies, or unconditionally install'
369                         ' them, respectively')
370         pa_setahbins('--binaries', ['deb_forbuilds','deb_fortests'], '')
371         pa_setahbins('--binaries-forbuilds', ['deb_forbuilds'], 'for builds, ')
372         pa_setahbins('--binaries-fortests', ['deb_fortests'], 'for tests, ')
373
374         #----------
375         # general options:
376
377         def cb_vserv(op,optstr,value,parser):
378                 parser.values.vserver = list(parser.rargs)
379                 del parser.rargs[:]
380
381         def cb_path(op,optstr,value,parser, constructor,long,dir):
382                 name = long.replace('-','_')
383                 af = constructor(arghandling['tb'], value, long, dir)
384                 setattr(parser.values, name, af)
385
386         def pa_path(long, constructor, help, dir=False):
387                 pa('','--'+long, action='callback', callback=cb_path,
388                         callback_args=(constructor,long,dir),
389                         nargs=1, type='string',
390                         help=help, metavar='PATH')
391
392         pa_path('output-dir', OutputDir, dir=True,
393                 help='write stderr/out files in PATH')
394         pa_path('tmp-dir', OutputDir, dir=True,
395                 help='write temporary files to PATH, emptying PATH'
396                      ' beforehand and leaving it behind at the end')
397
398         pa('','--user',                 type='string', dest='user',
399                 help='run tests as USER (needs root on testbed)')
400         pa('','--fakeroot',             type='string', dest='fakeroot',
401                 help='prefix debian/rules build with FAKEROOT')
402         pa('-d', '--debug', action='store_true', dest='debug');
403         pa('','--gnupg-home',           type='string', dest='gnupghome',
404                 default='~/.autopkgtest/gpg',
405                 help='use GNUPGHOME rather than ~/.autopkgtest (for
406                         " signing private apt archive);"
407                         " `fresh' means generate new key each time.")
408
409         #----------
410         # actual meat:
411
412         class SpecialOption(optparse.Option): pass
413         vs_op = SpecialOption('','--VSERVER-DUMMY')
414         vs_op.action = 'callback'
415         vs_op.type = None
416         vs_op.default = None
417         vs_op.nargs = 0
418         vs_op.callback = cb_vserv
419         vs_op.callback_args = ( )
420         vs_op.callback_kwargs = { }
421         vs_op.help = 'introduces virtualisation server and args'
422         vs_op._short_opts = []
423         vs_op._long_opts = ['---']
424
425         pa(vs_op)
426
427         (opts,args) = parser.parse_args()
428         if not hasattr(opts,'vserver'):
429                 parser.error('you must specifiy --- <virt-server>...')
430         if not n_actions:
431                 parser.error('nothing to do specified')
432
433         arghandling = initial_arghandling
434         opts.actions = []
435         ix = 0
436         for act in args:
437                 if type(act) == dict:
438                         arghandling = act
439                         continue
440                 elif type(act) == tuple:
441                         pass
442                 elif type(act) == string:
443                         act = (act,act)
444                 else:
445                         error "unknown action in list `%s' having"
446                               "type `%s' % (act, type(act))
447                 (pathstr, kindpath) = act
448
449                 constructor = InputPath
450                 if type(kindpath) is tuple:             kind = kindpath[0]
451                 elif kindpath.endswith('.deb'):         kind = 'deb'
452                 elif kindpath.endswith('.dsc'):         kind = 'dsc'
453                 elif kindpath.endswith('/'):
454                         kind = 'tree'
455                         constructor = InputPathDir
456                 else: parser.error("do not know how to handle filename \`%s';"
457                         " specify --source --binary or --build-tree")
458
459                 what = '%s%s' % (kind,ix); ix++
460
461                 af = constructor(what+'-'+kind, pathstr, arghandling['tb'])
462                 opts.actions.append(Action(kind, af, arghandling, ix))
463
464 def finalise_options():
465         global opts, testbed
466
467         if opts.user is None and 'root-on-testbed' not in caps:
468                 opts.user = ''
469
470         if opts.user is None:
471                 su = 'suggested-normal-user='
472                 ul = [
473                         e[length(su):]
474                         for e in caps
475                         if e.startswith(su)
476                         ]
477                 if len(ul) > 1:
478                         print >>sys.stderr, "warning: virtualisation"
479                                 " system offers several suggested-normal-user"
480                                 " values: "+('/'.join(ul))+", using "+ul[0]
481                 if ul:
482                         opts.user = ul[0]
483                 else:
484                         opts.user = ''
485
486         if opts.user:
487                 if 'root-on-testbed' not in caps:
488                         print >>sys.stderr, "warning: virtualisation"
489                                 " system does not offer root on testbed,"
490                                 " but --user option specified: failure likely"
491                 opts.user_wrap = lambda x: 'su %s -c "%s"' % (opts.user, x)
492         else:
493                 opts.user_wrap = lambda x: x
494
495         if opts.fakeroot is None:
496                 opts.fakeroot = ''
497                 if opts.user or
498                    'root-on-testbed' not in testbed.caps:
499                         opts.fakeroot = 'fakeroot'
500
501         if opts.gnupghome.startswith('~/'):
502                 try: home = os.environ['HOME']
503                 except KeyError:
504                         parser.error("HOME environment variable"
505                                 " not set, needed for --gnupghome=`%s"
506                                 % opts.gnupghome)
507                 opts.gnupghome = home + opts.gnupghome[1:]
508         elif opts.gnupghome == 'fresh':
509                 opts.gnupghome = None
510
511 #---------- testbed management - the Testbed class
512
513 class Testbed:
514  def __init__(tb):
515         tb.sp = None
516         tb.lastsend = None
517         tb.scratch = None
518         tb.modified = False
519         tb.blamed = []
520  def start(tb):
521         p = subprocess.PIPE
522         tb.sp = subprocess.Popen(opts.vserver,
523                 stdin=p, stdout=p, stderr=None)
524         tb.expect('ok')
525         tb.caps = tb.command('capabilities')
526  def stop(tb):
527         tb.close()
528         if tb.sp is None: return
529         ec = tb.sp.returncode
530         if ec is None:
531                 tb.sp.stdout.close()
532                 tb.send('quit')
533                 tb.sp.stdin.close()
534                 ec = tb.sp.wait()
535         if ec:
536                 tb.bomb('testbed gave exit status %d after quit' % ec)
537  def open(tb):
538         if tb.scratch is not None: return
539         p = tb.commandr1('open')
540         tb.scratch = OutputDir('tb-scratch', p, True)
541  def close(tb):
542         if tb.scratch is None: return
543         tb.scratch = None
544         if tb.sp is None: return
545         tb.command('close')
546  def prepare(tb):
547         if tb.modified and 'reset' in caps:
548                 tb.command('reset')
549                 tb.blamed = []
550         tb.modified = False
551         binaries.publish(act)
552  def needs_reset(tb):
553         tb.modified = True
554  def blame(tb, m):
555         tb.blamed.append(m)
556  def bomb(tb, m):
557         if tb.sp is not None:
558                 tb.sp.stdout.close()
559                 tb.sp.stdin.close()
560                 ec = tb.sp.wait()
561                 if ec: print >>sys.stderr, ('adt-run: testbed failing,'
562                         ' exit status %d' % ec)
563         tb.sp = None
564         raise Quit(16, 'testbed failed: %s' % m)
565  def send(tb, string):
566         tb.sp.stdin
567         try:
568                 debug('>> '+string)
569                 print >>tb.sp.stdin, string
570                 tb.sp.stdin.flush()
571                 tb.lastsend = string
572         except:
573                 (type, value, dummy) = sys.exc_info()
574                 tb.bomb('cannot send to testbed: %s' % traceback.
575                         format_exception_only(type, value))
576  def expect(tb, keyword, nresults=-1):
577         l = tb.sp.stdout.readline()
578         if not l: tb.bomb('unexpected eof from the testbed')
579         if not l.endswith('\n'): tb.bomb('unterminated line from the testbed')
580         l = l.rstrip('\n')
581         debug('<< '+l)
582         ll = l.split()
583         if not ll: tb.bomb('unexpected whitespace-only line from the testbed')
584         if ll[0] != keyword:
585                 if tb.lastsend is None:
586                         tb.bomb("got banner `%s', expected `%s...'" %
587                                 (l, keyword))
588                 else:
589                         tb.bomb("sent `%s', got `%s', expected `%s...'" %
590                                 (tb.lastsend, l, keyword))
591         ll = ll[1:]
592         if nresults >= 0 and len(ll) != nresults:
593                 tb.bomb("sent `%s', got `%s' (%d result parameters),"
594                         " expected %d result parameters" %
595                         (string, l, len(ll), nresults))
596         return ll
597  def commandr(tb, cmd, nresults, args=()):
598         if type(cmd) is str: cmd = [cmd]
599         al = cmd + map(urllib.quote, args)
600         tb.send(string.join(al))
601         ll = tb.expect('ok')
602         rl = map(urllib.unquote, ll)
603         return rl
604  def command(tb, cmd, args=()):
605         tb.commandr(cmd, 0, args)
606  def commandr1(tb, cmd, args=()):
607         rl = tb.commandr(cmd, 1, args)
608         return rl[0]
609
610 #---------- representation of test control files: Field*, Test, etc.
611
612 class FieldBase:
613  def __init__(f, fname, stz, base, tnames, vl):
614         assert(vl)
615         f.stz = stz
616         f.base = base
617         f.tnames = tnames
618         f.vl = vl
619  def words(f):
620         def distribute(vle):
621                 (lno, v) = vle
622                 r = v.split()
623                 r = map((lambda w: (lno, w)), r)
624                 return r
625         return flatten(map(distribute, f.vl))
626  def atmostone(f):
627         if len(vl) == 1:
628                 (f.lno, f.v) = vl[0]
629         else:
630                 raise Unsupported(f.vl[1][0],
631                         'only one %s field allowed' % fn)
632         return f.v
633
634 class FieldIgnore(FieldBase):
635  def parse(f): pass
636
637 class Restriction:
638  def __init__(r,rname,base): pass
639
640 class Restriction_rw_tests_tree(Restriction): pass
641 class Restriction_breaks_testbed(Restriction):
642         if 'reset' not in caps:
643                 raise Unsupported(f.lno,
644                         'Test breaks testbed but testbed cannot reset')
645
646 class Field_Restrictions(FieldBase):
647  def parse(f):
648         for wle in f.words():
649                 (lno, rname) = wle
650                 rname = rname.replace('-','_')
651                 try: rclass = globals()['Restriction_'+rname]
652                 except KeyError: raise Unsupported(lno,
653                         'unknown restriction %s' % rname)
654                 r = rclass(rname, f.base)
655                 f.base['restrictions'].append(r)
656
657 class Field_Tests(FieldIgnore): pass
658
659 class Field_Tests_directory(FieldBase):
660  def parse(f):
661         td = atmostone(f)
662         if td.startswith('/'): raise Unspported(f.lno,
663                 'Tests-Directory may not be absolute')
664         base['testsdir'] = td
665
666 def run_tests(stanzas):
667         global errorcode
668         for stanza in stanzas:
669                 tests = stanza[' tests']
670                 if not tests:
671                         report('*', 'SKIP no tests in this package')
672                         errorcode |= 8
673                 for t in tests:
674                         testbed.prepare()
675                         t.run()
676                         if 'breaks-testbed' in t.restrictions:
677                                 testbed.needs_reset()
678                 testbed.needs_reset()
679
680 class Test:
681  def __init__(t, tname, base):
682         if '/' in tname: raise Unsupported(base[' lno'],
683                 'test name may not contain / character')
684         for k in base: setattr(t,k,base[k])
685         t.tname = tname
686         if len(base['testsdir']): tpath = base['testsdir'] + '/' + tname
687         else: tpath = tname
688         t.af = opts.tests_tree.subpath('test-'+tname, tpath, InputFile)
689  def report(t, m):
690         report(t.tname, m)
691  def reportfail(t, m):
692         global errorcode
693         errorcode |= 4
694         report(t.tname, 'FAIL ' + m)
695  def run(t):
696         def stdouterr(oe):
697                 idstr = oe + '-' + t.tname
698                 if opts.output_dir is not None and opts.output_dir.tb:
699                         use_dir = opts.output_dir
700                 else:
701                         use_dir = testbed.scratch
702                 return use_dir.subpath(idstr, idstr, OutputFile)
703         def stdouterrh(p, oe):
704                 idstr = oe + '-' + t.tname
705                 if opts.output_dir is None or opts.output_dir.tb:
706                         return p.onhost()
707                 else:
708                         return p.onhost(opts.output_dir.onhost() + '/' + idstr)
709         so = stdouterr('stdout')
710         se = stdouterr('stderr')
711         rc = testbed.commandr1('execute',(t.af.ontb(),
712                 '/dev/null', so.ontb(), se.ontb(), opts.tests_tree.ontb()))
713         soh = stdouterrh(so, 'stdout')
714         seh = stdouterrh(se, 'stderr')
715         rc = int(rc)
716         stab = os.stat(seh)
717         if stab.st_size != 0:
718                 l = file(seh).readline()
719                 l = l.rstrip('\n \t\r')
720                 if len(l) > 40: l = l[:40] + '...'
721                 t.reportfail('stderr: %s' % l)
722         elif rc != 0:
723                 t.reportfail('non-zero exit status %d' % rc)
724         else:
725                 t.report('PASS')
726
727 def read_control(act, tree, control_override):
728         stanzas = [ ]
729
730         if control_override is not None:
731                 control_af = control_override
732                 testbed.blame('arg:'+control_override.spec)
733         else:
734                 control_af = tree.subpath(act.what+'-testcontrol',
735                         'debian/tests/control', InputFile)
736                 testbed.blame('arg:'+tree.spec)
737
738         try:
739                 control = file(control_af.read(), 'r')
740         except IOError, oe:
741                 if oe[0] != errno.ENOENT: raise
742                 return []
743
744         lno = 0
745         def badctrl(m): act.bomb('tests/control line %d: %s' % (lno, m))
746         stz = None      # stz[field_name][index] = (lno, value)
747                         # special field names:
748                         # stz[' lno'] = number
749                         # stz[' tests'] = list of Test objects
750         def end_stanza(stz):
751                 if stz is None: return
752                 stz[' errs'] = 0
753                 stanzas.append(stz)
754                 stz = None
755                 hcurrent = None
756
757         initre = regexp.compile('([A-Z][-0-9a-z]*)\s*\:\s*(.*)$')
758         while 1:
759                 l = control.readline()
760                 if not l: break
761                 lno += 1
762                 if not l.endswith('\n'): badctrl('unterminated line')
763                 if regexp.compile('\s*\#').match(l): continue
764                 if not regexp.compile('\S').match(l): end_stanza(stz); continue
765                 initmat = initre.match(l)
766                 if initmat:
767                         (fname, l) = initmat.groups()
768                         fname = string.capwords(fname)
769                         if stz is None:
770                                 stz = { ' lno': lno, ' tests': [] }
771                         if not stz.has_key(fname): stz[fname] = [ ]
772                         hcurrent = stz[fname]
773                 elif regexp.compile('\s').match(l):
774                         if not hcurrent: badctrl('unexpected continuation')
775                 else:
776                         badctrl('syntax error')
777                 hcurrent.append((lno, l))
778         end_stanza(stz)
779
780         def testbadctrl(stz, lno, m):
781                 report_badctrl(lno, m)
782                 stz[' errs'] += 1
783
784         for stz in stanzas:
785                 try:
786                         try: tnames = stz['Tests']
787                         except KeyError:
788                                 tnames = ['*']
789                                 raise Unsupported(stz[' lno'],
790                                         'no Tests field')
791                         tnames = map((lambda lt: lt[1]), tnames)
792                         tnames = string.join(tnames).split()
793                         base = {
794                                 'restrictions': [],
795                                 'testsdir': 'debian/tests'
796                         }
797                         for fname in stz.keys():
798                                 if fname.startswith(' '): continue
799                                 vl = stz[fname]
800                                 try: fclass = globals()['Field_'+
801                                         fname.replace('-','_')]
802                                 except KeyError: raise Unsupported(vl[0][0],
803                                         'unknown metadata field %s' % fname)
804                                 f = fclass(stz, fname, base, tnames, vl)
805                                 f.parse()
806                         for tname in tnames:
807                                 t = Test(tname, base)
808                                 stz[' tests'].append(t)
809                 except Unsupported, u:
810                         for tname in tnames: u.report(tname)
811                         continue
812
813         return stanzas
814
815 def print_exception(ei, msgprefix=''):
816         if msgprefix: print >>sys.stderr, msgprefix
817         (et, q, tb) = ei
818         if et is Quit:
819                 print >>sys.stderr, 'adt-run:', q.m
820                 return q.ec
821         else:
822                 print >>sys.stderr, "adt-run: unexpected, exceptional, error:"
823                 traceback.print_exc()
824                 return 20
825
826 def cleanup():
827         try:
828                 rm_ec = 0
829                 if tmpdir is not None:
830                         shutil.rmtree(tmpdir)
831                 if testbed is not None:
832                         testbed.stop()
833                 if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec))
834         except:
835                 print_exception(sys.exc_info(),
836                         '\nadt-run: error cleaning up:\n')
837                 os._exit(20)
838
839 #---------- registration, installation etc. of .deb's: Binaries
840
841 def determine_package(act):
842         cmd = 'dpkg-deb --info --'.split(' ')+[act.af.read(),'control']
843         running = Popen(cmd, stdout=PIPE)
844         output = running.communicate()[0]
845         rc = running.wait()
846         if rc: badpkg('failed to parse binary package, code %d' % rc)
847         re = regexp.compile('^\s*Package\s*:\s*([0-9a-z][-+.0-9a-z]*)\s*$')
848         act.pkg = None
849         for l in '\n'.split(output):
850                 m = re.match(output)
851                 if not m: continue
852                 if act.pkg: badpkg('two Package: lines in control file')
853                 act.pkg = m.groups
854         if not act.pkg: badpkg('no good Package: line in control file')
855
856 class Binaries:
857  def __init__(b):
858         b.dir = TemporaryDir('binaries')
859
860         if opts.gnupghome is None:
861                 opts.gnupghome = tmpdir+'/gnupg'
862
863         try:
864                 for x in ['pubring','secring']:
865                         os.stat(opts.gnupghome + '/' + x + '.gpg')
866         except IOError, oe:
867                 if oe.errno != errno.ENOENT: raise
868
869         try: os.mkdir(opts.gnupghome, 0700)
870         except IOError, oe: if oe.errno != errno.EEXIST: raise
871                 script = '
872   cd "$1"
873   exec >key-gen-log 2>&1
874   cat <<"END" >key-gen-params
875 Key-Type: DSA
876 Key-Length: 1024
877 Key-Usage: sign
878 Name-Real: autopkgtest per-run key
879 Name-Comment: do not trust this key
880 Name-Email: autopkgtest@example.com
881 END
882   set -x
883   gpg --homedir="$1" --batch --gen-key key-gen-params
884                         '
885                 cmdl = ['sh','-ec',script,'x',opts.gnupghome]
886                 rc = subprocess.call(cmdl)
887                 if rc:
888                         try:
889                                 f = open(opts.gnupghome+'/key-gen-log')
890                                 tp = file.read()
891                         except IOError, e: tp = e
892                         print >>sys.stderr, tp
893                         bomb('key generation failed, code %d' % rc)
894
895  def reset(b):
896         shutil.rmtree(b.dir.read())
897         b.dir.write()
898         b.install = []
899         b.blamed = []
900
901  def register(b, act, pkg, af, forwhat, blamed):
902         if act.ah['deb_'+forwhat] == 'ignore': return
903
904         b.blamed += testbed.blamed
905
906         leafname = pkg+'.deb'
907         dest = b.dir.subpath('binaries--'+leafname, leafname, OutputFile)
908
909         try: os.remove(dest.write())
910         except IOError, oe:
911                 if oe.errno != errno.ENOENT: raise e
912
913         try: os.link(af.read(), dest.write())
914         except IOError, oe:
915                 if oe.errno != errno.EXDEV: raise e
916                 shutil.copy(af.read(), dest)
917
918         if act.ah['deb_'+forwhat] == 'install':
919                 b.install.append(pkg)
920
921  def publish(b):
922         script = '
923   cd "$1"
924   apt-ftparchive packages . >Packages
925   gzip -f Packages
926   apt-ftparchive release . >Release
927   gpg --homedir="$2" --batch --detach-sign --armour -o Release.gpg Release
928   gpg --homedir="$2" --batch --export >archive-key.pgp
929         '
930         cmdl = ['sh','-ec',script,'x',b.dir.write(),opts.gnupghome]
931         rc = subprocess.call(cmd)
932         if rc: bomb('apt-ftparchive or signature failed, code %d' % rc)
933
934         b.dir.invalidate(True)
935         apt_source = b.dir.read(True)
936
937         se = TemporaryFile('%s-aptkey-stderr' % act.what)
938         script = '
939   apt-key add archive-key.pgp
940   echo "deb file:///'+apt_source+'/ /" >/etc/apt/sources.list.d/autopkgtest
941         '
942         rc = testbed.commandr1(['execute',
943                         ','.join(map(urllib.quote, ['sh','-ec','script']))],
944                         '/dev/null', '/dev/null', se.write(True), tbp)
945         if rc: bomb('apt setup failed with exit code %d' % rc, se)
946
947         testbed.blamed += b.blamed
948
949         for pkg in b.install:
950                 testbed.blame(pkg)
951                 se = TemporaryFile('%s-install-%s-stderr' % (act.what,pkg))
952                 rc = testbed.commandr1('execute','apt-get,-qy,install,'+pkg,
953                                 '/dev/null','/dev/null',se.ontb(),
954                                 testbed.scratch.read(True))
955                 if rc:
956                         badpkg("installation of %s failed, exit code %d"
957                                 % (pkg, rc), se)
958
959 #---------- processing of sources (building)
960
961 def source_rules_command(act,script,which,work,results_lines=0):
962         script = "exec 3>&1 >&2\n" + '\n'.join(script)
963         so = TemporaryFile('%s-%s-results' % (what,which))
964         se = TemporaryFile('%s-%s-log' & (what,which))
965         rc = testbed.commandr1(['execute',
966                 ','.join(map(urllib.quote, ['sh','-xec',script]))],
967                 '/dev/null', so.write(True), se.write(True), work.write(True))
968         results = file(so.read()).read().split("\n")
969         if rc:
970                 badpkg_se("%s failed with exit code %d" % (which,rc), se)
971         if results_lines is not None and len(results) != results_lines:
972                 badpkg_se("got %d lines of results from %s where %d expected"
973                         % (len(results), which, results_lines), se)
974         if results_lines==1: return results[0]
975         return results
976
977 def build_source(act):
978         act.blame = 'arg:'+act.af.spec()
979         testbed.blame(act.blame)
980         testbed.needs_reset()
981
982         what = act.ah['what']
983         dsc = act.af
984         basename = dsc.spec; if basename is None: basename = 'source.dsc'
985         dsc_what = what+'/'+basename
986
987         dsc_file = open(dsc.read())
988         in_files = False
989         fre = regexp.compile('^\s+[0-9a-f]+\s+\d+\s+([^/.][^/]*)$')
990         for l in dsc_file():
991                 if l.startswith('Files:'): in_files = True
992                 elif l.startswith('#'): pass
993                 elif not l.startswith(' '):
994                         in_files = False
995                         if l.startswith('Source:'):
996                                 act.blame = 'dsc:'+l[7:].strip()
997                                 testbed.blame(act.blame)
998                 elif not in_files: pass
999                 if not dsc.spec_tb: continue
1000                 m = re.match(l)
1001                 if not m: badpkg(".dsc contains unparseable line"
1002                                 " in Files: `%s'" % (`dsc`,l))
1003                 subfile = dsc.enclosingdir().subpath(
1004                                 dsc_what+'/'+m.groups(0), m.groups(0),
1005                                 InputFile)
1006                 subfile.read(True)
1007         dsc.read(True)
1008         
1009         work = TemporaryDir(what+'-build')
1010
1011         script = [
1012                         'cd '+work.write(True),
1013                         'gdebi '+dsc.read(True),
1014                         'dpkg-source -x '+dsc.read(True),
1015                         'cd */.',
1016                         'pwd >&3',
1017                         opts.user_wrap('debian/rules build'),
1018         ]
1019         result_pwd = source_rules_command(act,script,what,'build',work,1)
1020
1021         if os.path.dirname(result_pwd) != work.read(True):
1022                 badpkg_se("results dir `%s' is not in expected parent dir `%s'"
1023                         % (results[0], work.read(True)), se)
1024
1025         act.tests_tree = InputDir(dsc_what+'tests-tree',
1026                                 work.read(True)+os.path.basename(results[0]),
1027                                 InputDir)
1028         if act.ah['dsc_tests']:
1029                 act.tests_tree.read()
1030                 act.tests_tree.invalidate(True)
1031
1032         act.blamed = testbed.blamed.copy()
1033
1034         act.binaries = []
1035         if act.ah['dsc_filter'] != '_':
1036                 script = [
1037                         'cd '+work.write(True)+'/*/.',
1038                         opts.user_wrap(opts.fakeroot+' debian/rules binary'),
1039                         'cd ..',
1040                         'echo *.deb >&3',
1041                         ]
1042                 result_debs = source_rules_command(act,script,what,
1043                                 'debian/rules binary',work,1)
1044                 if result_debs == '*': debs = []
1045                 else: debs = debs.split(' ')
1046                 re = regexp.compile('^([-+.0-9a-z]+)_[^_/]+(?:_[^_/]+)\.deb$')
1047                 for deb in debs:
1048                         m = re.match(deb)
1049                         if not m: badpkg("badly-named binary `%s'" % deb)
1050                         pkg = m.groups(0)
1051                         for pat in act.ah['dsc_filter'].split(','):
1052                                 if fnmatch.fnmatchcase(pkg,pat):
1053                                         deb_af = work.read()+'/'+deb
1054                                         deb_what = pkg+'_'+what+'.deb'
1055                                         bin = InputFile(deb_what,deb_af,True)
1056                                         bin.preserve_now()
1057                                         binaries.register(act,pkg,bin,'builds',
1058                                                 testbed.blamed)
1059                                         act.binaries.subpath((pkg,bin))
1060                                         break
1061
1062 #---------- main processing loop and main program
1063
1064 def process_actions():
1065         global binaries
1066         binaries = Binaries()
1067
1068         b.reset()
1069         for act in opts.actions:
1070                 testbed.prepare()
1071                 if act.kind == 'deb':
1072                         blame('arg:'+act.af.spec)
1073                         determine_package(act)
1074                         blame('deb:'+act.pkg)
1075                         binaries.register(act,act.pkg,act.af,'builds',
1076                                 testbed.blamed)
1077                 if act.kind == 'dsc':
1078                         build_source(act)
1079
1080         b.reset()
1081         control_override = None
1082         for act in opts.actions:
1083                 testbed.prepare()
1084                 if act.kind == 'control':
1085                         control_override = act.af
1086                 if act.kind == 'deb':
1087                         binaries.register(act,act.pkg,act.af,'tests',
1088                                 ['deb:'+act.pkg])
1089                 if act.kind == 'dsc':
1090                         for (pkg,bin) in act.binaries:
1091                                 binaries.register(act,pkg,bin,'tests',
1092                                         act.blamed)
1093                         if not act.ah['dsc_tests']: continue
1094                         stanzas = read_control(act, act.tests_tree,
1095                                         control_override)
1096                         testbed.blamed += act.blamed
1097                         run_tests(act, stanzas)
1098                         control_override = None
1099                 if act.kind == 'tree':
1100                         testbed.blame('arg:'+act.af.spec)
1101                         stanzas = read_control(act, act.af,
1102                                         control_override)
1103                         run_tests(act, stanzas)
1104                         control_override = None
1105
1106 def main():
1107         global testbed
1108         global tmpdir
1109         try:
1110                 parse_args()
1111         except SystemExit, se:
1112                 os._exit(20)
1113         try:
1114                 tmpdir = tempfile.mkdtemp()
1115                 testbed = Testbed()
1116                 testbed.start()
1117                 finalise_options()
1118                 process_actions()
1119         except:
1120                 ec = print_exception(sys.exc_info(), '')
1121                 cleanup()
1122                 os._exit(ec)
1123         cleanup()
1124         os._exit(errorcode)
1125
1126 main()