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