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