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