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