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