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