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