chiark / gitweb /
delete debug msgs; bomb surviving path better; get tbscratch default right
[autopkgtest.git] / runner / adt-run
index 21f97860a815db9e189600069f1c43e9b4aae532..e663a2a76a8e0070ed1ff3f9db5e266f946dc855 100755 (executable)
@@ -1,17 +1,26 @@
 #!/usr/bin/python2.4
-# usage:
-#      adt-run <options>... --- <virt-server> [<virt-server-arg>...]
 #
-# invoke in toplevel of package (not necessarily built)
-# with package installed
-
-# exit status:
-#  0 all tests passed
-#  4 at least one test failed
-#  8 no tests in this package
-# 12 erroneous package
-# 16 testbed failure
-# 20 other unexpected failures including bad usage
+# adt-run is part of autodebtest
+# autodebtest is a tool for testing Debian binary packages
+#
+# autodebtest is Copyright (C) 2006 Canonical Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# See the file CREDITS for a full list of credits information (often
+# installed as /usr/share/doc/autodebtest/CREDITS).
 
 import signal
 import optparse
@@ -22,11 +31,14 @@ import traceback
 import urllib
 import string
 import re as regexp
+import os
+import errno
 
 from optparse import OptionParser
 
 tmpdir = None
 testbed = None
+errorcode = 0
 
 signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing
 
@@ -35,13 +47,16 @@ class Quit:
 
 def bomb(m): raise Quit(20, "unexpected error: %s" % m)
 def badpkg(m): raise Quit(12, "erroneous package: %s" % m)
+def report(tname, result): print '%-20s %s' % (tname, result)
 
 class Unsupported:
  def __init__(u, lno, m):
        if lno >= 0: u.m = '%s (control line %d)' % (m, lno)
        else: u.m = m
  def report(u, tname):
-       print '%-20s SKIP %s' % (tname, u.m)
+       global errorcode
+       errorcode != 2
+       report(tname, 'SKIP %s' % u.m)
 
 def debug(m):
        global opts
@@ -52,36 +67,54 @@ def flatten(l):
        return reduce((lambda a,b: a + b), l, []) 
 
 class Path:
- def __init__(p, tb, path, what, dir=False):
+ def __init__(p, tb, path, what, dir=False, tbscratch=None):
        p.tb = tb
        p.p = path
        p.what = what
        p.dir = dir
+       p.tbscratch = tbscratch
        if p.tb:
                if p.p[:1] != '/':
                        bomb("path %s specified as being in testbed but"
                                " not absolute: `%s'" % (what, p.p))
                p.local = None
+               p.down = p.p
        else:
                p.local = p.p
+               p.down = None
        if p.dir: p.dirsfx = '/'
        else: p.dirsfx = ''
  def path(p):
        return p.p + p.dirsfx
  def append(p, suffix, what, dir=False):
-       return Path(p.tb, p.path() + suffix, what=what, dir=dir)
+       return Path(p.tb, p.path() + suffix, what=what, dir=dir,
+                       tbscratch=p.tbscratch)
  def __str__(p):
        if p.tb: pfx = '/VIRT'
        elif p.p[:1] == '/': pfx = '/HOST'
        else: pfx = './'
        return pfx + p.p
- def onhost(p):
-       if not p.tb: return p.p
-       if p.local is not None: return p.local
+ def onhost(p, lpath = None):
+       if p.local is not None:
+               if lpath is not None: assert(p.local == lpath)
+               return p.local
        testbed.open()
-       p.local = tmpdir + '/tb.' + p.what
+       p.local = lpath
+       if p.local is None: p.local = tmpdir + '/tb-' + p.what
        testbed.command('copyup', (p.path(), p.local + p.dirsfx))
        return p.local
+ def ontb(p):
+       testbed.open()
+       if p.tbscratch is not None:
+               if p.tbscratch != testbed.scratch:
+                       p.down = None
+       if p.down is not None: return p.down
+       if p.tb:
+               bomb("testbed scratch path " + str(p) + " survived testbed")
+       p.down = testbed.scratch.p + '/host-' + p.what
+       p.tbscratch = testbed.scratch
+       testbed.command('copydown', (p.path(), p.down + p.dirsfx))
+       return p.down
 
 def parse_args():
        global opts
@@ -108,10 +141,12 @@ def parse_args():
 
        pa_path('build-tree',   True, 'use build tree from PATH on %s')
        pa_path('control',      False, 'read control file PATH on %s')
+       pa_path('output-dir',   True, 'write stderr/out files in PATH on %s')
 
        pa('-d', '--debug', action='store_true', dest='debug');
-       pa('','--user', type='string',
-               help='run tests as USER (needs root on testbed)')
+       # pa('','--user', type='string',
+       #       help='run tests as USER (needs root on testbed)')
+       # nyi
 
        class SpecialOption(optparse.Option): pass
        vs_op = SpecialOption('','--VSERVER-DUMMY')
@@ -164,9 +199,11 @@ class Testbed:
        if tb.scratch is not None: return
        p = tb.commandr1('open')
        tb.scratch = Path(True, p, 'tb-scratch', dir=True)
+       tb.scratch.tbscratch = tb.scratch
  def close(tb):
        if tb.scratch is None: return
        tb.scratch = None
+       if tb.sp is None: return
        tb.command('close')
  def bomb(tb, m):
        if tb.sp is not None:
@@ -178,14 +215,16 @@ class Testbed:
        tb.sp = None
        raise Quit(16, 'testbed failed: %s' % m)
  def send(tb, string):
+       tb.sp.stdin
        try:
                debug('>> '+string)
                print >>tb.sp.stdin, string
                tb.sp.stdin.flush()
                tb.lastsend = string
        except:
-               tb.bomb('cannot send to testbed: %s' %
-                       formatexception_only(sys.last_type, sys.last_value))
+               (type, value, dummy) = sys.exc_info()
+               tb.bomb('cannot send to testbed: %s' % traceback.
+                       format_exception_only(type, value))
  def expect(tb, keyword, nresults=-1):
        l = tb.sp.stdout.readline()
        if not l: tb.bomb('unexpected eof from the testbed')
@@ -221,6 +260,7 @@ class Testbed:
 
 class FieldBase:
  def __init__(f, fname, stz, base, tnames, vl):
+       assert(vl)
        f.stz = stz
        f.base = base
        f.tnames = tnames
@@ -232,11 +272,8 @@ class FieldBase:
                r = map((lambda w: (lno, w)), r)
                return r
        return flatten(map(distribute, f.vl))
- def atmostone(f, default):
-       if not vl:
-               f.v = default
-               f.lno = -1
-       elif len(vl) == 1:
+ def atmostone(f):
+       if len(vl) == 1:
                (f.lno, f.v) = vl[0]
        else:
                raise Unsupported(f.vl[1][0],
@@ -266,16 +303,76 @@ class Field_Tests(FieldIgnore): pass
 
 class Field_Tests_directory(FieldBase):
  def parse(f):
-       base['testsdir'] = oneonly(f)
+       td = atmostone(f)
+       if td.startswith('/'): raise Unspported(f.lno,
+               'Tests-Directory may not be absolute')
+       base['testsdir'] = td
+
+def run_tests():
+       testbed.close()
+       for t in tests:
+               t.run()
+       if not tests:
+               global errorcode
+               report('*', 'SKIP no tests in this package')
+               errorcode |= 8
 
 class Test:
  def __init__(t, tname, base):
-       t.tname = tname
+       if '/' in tname: raise Unsupported(base[' lno'],
+               'test name may not contain / character')
        for k in base: setattr(t,k,base[k])
+       t.tname = tname
+       if len(base['testsdir']): tpath = base['testsdir'] + '/' + tname
+       else: tpath = tname
+       t.p = opts.build_tree.append(tpath, 'test-'+tname)
+ def report(t, m):
+       report(t.tname, m)
+ def reportfail(t, m):
+       global errorcode
+       errorcode |= 4
+       report(t.tname, 'FAIL ' + m)
+ def run(t):
+       testbed.open()
+       def stdouterr(oe):
+               idstr = oe + '-' + t.tname
+               if opts.output_dir is not None and opts.output_dir.tb:
+                       return opts.output_dir.append(idstr)
+               else:
+                       return testbed.scratch.append(idstr, idstr)
+       def stdouterrh(p, oe):
+               idstr = oe + '-' + t.tname
+               if opts.output_dir is None or opts.output_dir.tb:
+                       return p.onhost()
+               else:
+                       return p.onhost(opts.output_dir.onhost() + '/' + idstr)
+       so = stdouterr('stdout')
+       se = stdouterr('stderr')
+       rc = testbed.commandr1('execute',(t.p.ontb(),
+               '/dev/null', so.ontb(), se.ontb()))
+       soh = stdouterrh(so, 'stdout')
+       soe = stdouterrh(se, 'stderr')
+       testbed.close()
+       rc = int(rc)
+       stab = os.stat(soh)
+       if stab.st_size != 0:
+               l = file(seh).readline()
+               l = l.rstrip('\n \t\r')
+               if len(l) > 40: l = l[:40] + '...'
+               t.reportfail('stderr: %s' % l)
+       elif rc != 0:
+               t.reportfail('non-zero exit status %d' % rc)
+       else:
+               t.report('PASS')
 
 def read_control():
        global tests
-       control = file(opts.control.onhost(), 'r')
+       try:
+               control = file(opts.control.onhost(), 'r')
+       except IOError, oe:
+               if oe[0] != errno.ENOENT: raise
+               tests = []
+               return
        lno = 0
        def badctrl(m): testbed.badpkg('tests/control line %d: %s' % (lno, m))
        stz = None # stz[field_name][index] = (lno, value)
@@ -339,14 +436,13 @@ def read_control():
                                        'unknown metadata field %s' % fname)
                                f = fclass(stz, fname, base, tnames, vl)
                                f.parse()
+                       tests = []
+                       for tname in tnames:
+                               t = Test(tname, base)
+                               tests.append(t)
                except Unsupported, u:
                        for tname in tnames: u.report(tname)
                        continue
-               tests = []
-               for tname in tnames:
-                       t = Test(tname, base)
-                       tests.append(t)
-       testbed.close()
 
 def print_exception(ei, msgprefix=''):
        if msgprefix: print >>sys.stderr, msgprefix
@@ -370,21 +466,28 @@ def cleanup():
        except:
                print_exception(sys.exc_info(),
                        '\nadt-run: error cleaning up:\n')
-               sys.exit(20)
+               os._exit(20)
 
 def main():
        global testbed
        global tmpdir
        try:
                parse_args()
+       except SystemExit, se:
+               os._exit(20)
+       try:
                tmpdir = tempfile.mkdtemp()
                testbed = Testbed()
                testbed.start()
+               testbed.open()
+               testbed.close()
                read_control()
+               run_tests()
        except:
                ec = print_exception(sys.exc_info(), '')
                cleanup()
-               sys.exit(ec)
+               os._exit(ec)
        cleanup()
+       os._exit(errorcode)
 
 main()