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