Added fix for TeX includes with same name as subdirs.
[scons.git] / runtest.py
index 7ee1fea5682dcc3c277a206918e2b48529d9288f..11f87e1af2ad89779a80d20a282de814b6246040 100644 (file)
 # you can find the appropriate code in the 0.04 version of this script,
 # rather than reinventing that wheel.)
 #
+from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
 
 import getopt
 import glob
 import os
-import os.path
-import popen2
 import re
 import stat
-import string
 import sys
 import time
 
 if not hasattr(os, 'WEXITSTATUS'):
     os.WEXITSTATUS = lambda x: x
 
+try:
+    sorted
+except NameError:
+    # Pre-2.4 Python has no sorted() function.
+    #
+    # The pre-2.4 Python list.sort() method does not support
+    # list.sort(key=) nor list.sort(reverse=) keyword arguments, so
+    # we must implement the functionality of those keyword arguments
+    # by hand instead of passing them to list.sort().
+    def sorted(iterable, cmp=None, key=None, reverse=0):
+        if key is not None:
+            result = [(key(x), x) for x in iterable]
+        else:
+            result = iterable[:]
+        if cmp is None:
+            # Pre-2.3 Python does not support list.sort(None).
+            result.sort()
+        else:
+            result.sort(cmp)
+        if key is not None:
+            result = [t1 for t0,t1 in result]
+        if reverse:
+            result.reverse()
+        return result
+
+cwd = os.getcwd()
+
 all = 0
 baseline = 0
+builddir = os.path.join(cwd, 'build')
 debug = ''
 execute_tests = 1
 format = None
@@ -118,14 +144,13 @@ python = None
 sp = None
 spe = None
 
-cwd = os.getcwd()
-
 helpstr = """\
 Usage: runtest.py [OPTIONS] [TEST ...]
 Options:
   -a, --all                   Run all tests.
   --aegis                     Print results in Aegis format.
   -b BASE, --baseline BASE    Run test scripts against baseline BASE.
+  --builddir DIR              Directory in which packages were built.
   -d, --debug                 Run test scripts under the Python debugger.
   -f FILE, --file FILE        Run tests in specified FILE.
   -h, --help                  Print this message and exit.
@@ -160,12 +185,12 @@ Options:
 """
 
 opts, args = getopt.getopt(sys.argv[1:], "ab:df:hlno:P:p:qv:Xx:t",
-                            ['all', 'aegis', 'baseline=',
+                            ['all', 'aegis', 'baseline=', 'builddir=',
                              'debug', 'file=', 'help',
                              'list', 'no-exec', 'noqmtest', 'output=',
-                             'package=', 'passed', 'python=',
-                             'qmtest', 'quiet', 'spe=',
-                             'version=', 'exec=', 'time',
+                             'package=', 'passed', 'python=', 'qmtest',
+                             'quiet', 'sp=', 'spe=', 'time',
+                             'version=', 'exec=',
                              'verbose=', 'xml'])
 
 for o, a in opts:
@@ -173,6 +198,10 @@ for o, a in opts:
         all = 1
     elif o in ['-b', '--baseline']:
         baseline = a
+    elif o in ['--builddir']:
+        builddir = a
+        if not os.path.isabs(builddir):
+            builddir = os.path.normpath(os.path.join(cwd, builddir))
     elif o in ['-d', '--debug']:
         for dir in sys.path:
             pdb = os.path.join(dir, 'pdb.py')
@@ -203,13 +232,17 @@ for o, a in opts:
     elif o in ['-P', '--python']:
         python = a
     elif o in ['--qmtest']:
-        qmtest = 'qmtest.py'
+        if sys.platform == 'win32':
+            # typically in c:/PythonXX/Scripts
+            qmtest = 'qmtest.py'
+        else:
+            qmtest = 'qmtest'
     elif o in ['-q', '--quiet']:
         printcommand = 0
     elif o in ['--sp']:
-        sp = string.split(a, os.pathsep)
+        sp = a.split(os.pathsep)
     elif o in ['--spe']:
-        spe = string.split(a, os.pathsep)
+        spe = a.split(os.pathsep)
     elif o in ['-t', '--time']:
         print_times = 1
     elif o in ['--verbose']:
@@ -236,8 +269,8 @@ runtest.py:  No tests were specified.
 if sys.platform in ('win32', 'cygwin'):
 
     def whereis(file):
-        pathext = [''] + string.split(os.environ['PATHEXT'])
-        for dir in string.split(os.environ['PATH'], os.pathsep):
+        pathext = [''] + os.environ['PATHEXT'].split(os.pathsep)
+        for dir in os.environ['PATH'].split(os.pathsep):
             f = os.path.join(dir, file)
             for ext in pathext:
                 fext = f + ext
@@ -248,7 +281,7 @@ if sys.platform in ('win32', 'cygwin'):
 else:
 
     def whereis(file):
-        for dir in string.split(os.environ['PATH'], os.pathsep):
+        for dir in os.environ['PATH'].split(os.pathsep):
             f = os.path.join(dir, file)
             if os.path.isfile(f):
                 try:
@@ -259,15 +292,25 @@ else:
                     return f
         return None
 
+# See if --qmtest or --noqmtest specified
 try:
     qmtest
 except NameError:
-    q = 'qmtest.py'
-    qmtest = whereis(q)
-    if qmtest:
-        qmtest = q
-    else:
-        sys.stderr.write('Warning:  %s not found on $PATH, assuming --noqmtest option.\n' % q)
+    qmtest = None
+    # Old code for using QMTest by default if it's installed.
+    # We now default to not using QMTest unless explicitly asked for.
+    #for q in ['qmtest', 'qmtest.py']:
+    #    path = whereis(q)
+    #    if path:
+    #        # The name was found on $PATH; just execute the found name so
+    #        # we don't have to worry about paths containing white space.
+    #        qmtest = q
+    #        break
+    #if not qmtest:
+    #    msg = ('Warning:  found neither qmtest nor qmtest.py on $PATH;\n' +
+    #           '\tassuming --noqmtest option.\n')
+    #    sys.stderr.write(msg)
+    #    sys.stderr.flush()
 
 aegis = whereis('aegis')
 
@@ -276,10 +319,10 @@ if format == '--aegis' and aegis:
     if change:
         if sp is None:
             paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
-            sp = string.split(paths, os.pathsep)
+            sp = paths.split(os.pathsep)
         if spe is None:
             spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
-            spe = string.split(spe, os.pathsep)
+            spe = spe.split(os.pathsep)
     else:
         aegis = None
 
@@ -288,6 +331,7 @@ if sp is None:
 if spe is None:
     spe = []
 
+sp.append(builddir)
 sp.append(cwd)
 
 #
@@ -296,6 +340,7 @@ _ws = re.compile('\s')
 def escape(s):
     if _ws.search(s):
         s = '"' + s + '"'
+    s = s.replace('\\', '\\\\')
     return s
 
 # Set up lowest-common-denominator spawning of a process on both Windows
@@ -313,7 +358,7 @@ except AttributeError:
 else:
     def spawn_it(command_args):
         command = command_args[0]
-        command_args = map(escape, command_args)
+        command_args = list(map(escape, command_args))
         return os.spawnv(os.P_WAIT, command, command_args)
 
 class Base:
@@ -336,25 +381,42 @@ class SystemExecutor(Base):
             sys.stdout.write("Unexpected exit status %d\n" % s)
 
 try:
-    popen2.Popen3
-except AttributeError:
-    class PopenExecutor(Base):
-        def execute(self):
-            (tochild, fromchild, childerr) = os.popen3(self.command_str)
-            tochild.close()
-            self.stderr = childerr.read()
-            self.stdout = fromchild.read()
-            fromchild.close()
-            self.status = childerr.close()
-            if not self.status:
-                self.status = 0
+    import subprocess
+except ImportError:
+    import popen2
+    try:
+        popen2.Popen3
+    except AttributeError:
+        class PopenExecutor(Base):
+            def execute(self):
+                (tochild, fromchild, childerr) = os.popen3(self.command_str)
+                tochild.close()
+                self.stderr = childerr.read()
+                self.stdout = fromchild.read()
+                fromchild.close()
+                self.status = childerr.close()
+                if not self.status:
+                    self.status = 0
+                else:
+                    self.status = self.status >> 8
+    else:
+        class PopenExecutor(Base):
+            def execute(self):
+                p = popen2.Popen3(self.command_str, 1)
+                p.tochild.close()
+                self.stdout = p.fromchild.read()
+                self.stderr = p.childerr.read()
+                self.status = p.wait()
+                self.status = self.status >> 8
 else:
     class PopenExecutor(Base):
         def execute(self):
-            p = popen2.Popen3(self.command_str, 1)
-            p.tochild.close()
-            self.stdout = p.fromchild.read()
-            self.stderr = p.childerr.read()
+            p = subprocess.Popen(self.command_str,
+                                 stdout=subprocess.PIPE,
+                                 stderr=subprocess.PIPE,
+                                 shell=True)
+            self.stdout = p.stdout.read()
+            self.stderr = p.stderr.read()
             self.status = p.wait()
 
 class Aegis(SystemExecutor):
@@ -409,11 +471,11 @@ if package:
         'deb'        : os.path.join('python2.1', 'site-packages')
     }
 
-    if not dir.has_key(package):
+    if package not in dir:
         sys.stderr.write("Unknown package '%s'\n" % package)
         sys.exit(2)
 
-    test_dir = os.path.join(cwd, 'build', 'test-%s' % package)
+    test_dir = os.path.join(builddir, 'test-%s' % package)
 
     if dir[package] is None:
         scons_script_dir = test_dir
@@ -433,7 +495,7 @@ if package:
         scons_lib_dir = os.path.join(test_dir, dir[package], 'lib', l)
         pythonpath_dir = scons_lib_dir
 
-    scons_runtest_dir = os.path.join(cwd, 'build')
+    scons_runtest_dir = builddir
 
 else:
     sd = None
@@ -452,7 +514,7 @@ else:
     #                sd = d
     #                scons = f
     #    spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
-    #    ld = string.join(spe, os.pathsep)
+    #    ld = os.pathsep.join(spe)
 
     if not baseline or baseline == '.':
         base = cwd
@@ -517,6 +579,11 @@ os.environ['SCONS_VERSION'] = version
 
 old_pythonpath = os.environ.get('PYTHONPATH')
 
+# FIXME: the following is necessary to pull in half of the testing
+#        harness from $srcdir/etc. Those modules should be transfered
+#        to QMTest/ once we completely cut over to using that as
+#        the harness, in which case this manipulation of PYTHONPATH
+#        should be able to go away.
 pythonpaths = [ pythonpath_dir ]
 
 for dir in sp:
@@ -526,9 +593,9 @@ for dir in sp:
         q = os.path.join(dir, 'QMTest')
     pythonpaths.append(q)
 
-os.environ['SCONS_SOURCE_PATH_EXECUTABLE'] = string.join(spe, os.pathsep)
+os.environ['SCONS_SOURCE_PATH_EXECUTABLE'] = os.pathsep.join(spe)
 
-os.environ['PYTHONPATH'] = string.join(pythonpaths, os.pathsep)
+os.environ['PYTHONPATH'] = os.pathsep.join(pythonpaths)
 
 if old_pythonpath:
     os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \
@@ -537,6 +604,25 @@ if old_pythonpath:
 
 tests = []
 
+def find_Tests_py(tdict, dirname, names):
+    for n in [n for n in names if n[-8:] == "Tests.py"]:
+        tdict[os.path.join(dirname, n)] = 1
+
+def find_py(tdict, dirname, names):
+    tests = [n for n in names if n[-3:] == ".py"]
+    try:
+        excludes = open(os.path.join(dirname,".exclude_tests")).readlines()
+    except (OSError, IOError):
+        pass
+    else:
+        for exclude in excludes:
+            exclude = exclude.split('#' , 1)[0]
+            exclude = exclude.strip()
+            if not exclude: continue
+            tests = [n for n in tests if n != exclude]
+    for n in tests:
+        tdict[os.path.join(dirname, n)] = 1
+
 if args:
     if spe:
         for a in args:
@@ -551,11 +637,20 @@ if args:
                         break
     else:
         for a in args:
-            tests.extend(glob.glob(a))
+            for path in glob.glob(a):
+                if os.path.isdir(path):
+                    tdict = {}
+                    if path[:3] == 'src':
+                        os.path.walk(path, find_Tests_py, tdict)
+                    elif path[:4] == 'test':
+                        os.path.walk(path, find_py, tdict)
+                    tests.extend(sorted(tdict.keys()))
+                else:
+                    tests.append(path)
 elif testlistfile:
     tests = open(testlistfile, 'r').readlines()
-    tests = filter(lambda x: x[0] != '#', tests)
-    tests = map(lambda x: x[:-1], tests)
+    tests = [x for x in tests if x[0] != '#']
+    tests = [x[:-1] for x in tests]
 elif all and not qmtest:
     # Find all of the SCons functional tests in the local directory
     # tree.  This is anything under the 'src' subdirectory that ends
@@ -569,38 +664,24 @@ elif all and not qmtest:
     # by the Aegis packaging build to make sure that we're building
     # things correctly.)
     tdict = {}
-
-    def find_Tests_py(tdict, dirname, names):
-        for n in filter(lambda n: n[-8:] == "Tests.py", names):
-            t = os.path.join(dirname, n)
-            if not tdict.has_key(t):
-                tdict[t] = 1
     os.path.walk('src', find_Tests_py, tdict)
-
-    def find_py(tdict, dirname, names):
-        for n in filter(lambda n: n[-3:] == ".py", names):
-            t = os.path.join(dirname, n)
-            if not tdict.has_key(t):
-                tdict[t] = 1
     os.path.walk('test', find_py, tdict)
-
     if format == '--aegis' and aegis:
         cmd = "aegis -list -unf pf 2>/dev/null"
         for line in os.popen(cmd, "r").readlines():
-            a = string.split(line)
-            if a[0] == "test" and not tdict.has_key(a[-1]):
+            a = line.split()
+            if a[0] == "test" and a[-1] not in tdict:
                 tdict[a[-1]] = Test(a[-1], spe)
         cmd = "aegis -list -unf cf 2>/dev/null"
         for line in os.popen(cmd, "r").readlines():
-            a = string.split(line)
+            a = line.split()
             if a[0] == "test":
                 if a[1] == "remove":
                     del tdict[a[-1]]
-                elif not tdict.has_key(a[-1]):
+                elif a[-1] not in tdict:
                     tdict[a[-1]] = Test(a[-1], spe)
 
-    tests = tdict.keys()
-    tests.sort()
+    tests = sorted(tdict.keys())
 
 if qmtest:
     if baseline:
@@ -616,7 +697,7 @@ if qmtest:
     qmtest_args = [ qmtest, ]
 
     if format == '--aegis':
-        dir = os.path.join(cwd, 'build')
+        dir = builddir
         if not os.path.isdir(dir):
             dir = cwd
         qmtest_args.extend(['-D', dir])
@@ -641,11 +722,11 @@ if qmtest:
         qmtest_args.append(rs)
 
     if format == '--aegis':
-        tests = map(lambda x: string.replace(x, cwd+os.sep, ''), tests)
+        tests = [x.replace(cwd+os.sep, '') for x in tests]
     else:
         os.environ['SCONS'] = os.path.join(cwd, 'src', 'script', 'scons.py')
 
-    cmd = string.join(qmtest_args + tests, ' ')
+    cmd = ' '.join(qmtest_args + tests)
     if printcommand:
         sys.stdout.write(cmd + '\n')
         sys.stdout.flush()
@@ -659,7 +740,7 @@ if qmtest:
 #except OSError:
 #    pass
 
-tests = map(Test, tests)
+tests = list(map(Test, tests))
 
 class Unbuffered:
     def __init__(self, file):
@@ -671,10 +752,11 @@ class Unbuffered:
         return getattr(self.file, attr)
 
 sys.stdout = Unbuffered(sys.stdout)
+sys.stderr = Unbuffered(sys.stderr)
 
 if list_only:
     for t in tests:
-        sys.stdout.write(t.abspath + "\n")
+        sys.stdout.write(t.path + "\n")
     sys.exit(0)
 
 #
@@ -700,11 +782,12 @@ else:
 
 total_start_time = time_func()
 for t in tests:
-    t.command_args = [python, '-tt']
+    command_args = ['-tt']
     if debug:
-        t.command_args.append(debug)
-    t.command_args.append(t.abspath)
-    t.command_str = string.join(map(escape, t.command_args), " ")
+        command_args.append(debug)
+    command_args.append(t.path)
+    t.command_args = [python] + command_args
+    t.command_str = " ".join([escape(python)] + command_args)
     if printcommand:
         sys.stdout.write(t.command_str + "\n")
     test_start_time = time_func()
@@ -716,9 +799,9 @@ if len(tests) > 0:
     tests[0].total_time = time_func() - total_start_time
     print_time_func("Total execution time for all tests: %.1f seconds\n", tests[0].total_time)
 
-passed = filter(lambda t: t.status == 0, tests)
-fail = filter(lambda t: t.status == 1, tests)
-no_result = filter(lambda t: t.status == 2, tests)
+passed = [t for t in tests if t.status == 0]
+fail = [t for t in tests if t.status == 1]
+no_result = [t for t in tests if t.status == 2]
 
 if len(tests) != 1 and execute_tests:
     if passed and print_passed_summary:
@@ -726,22 +809,22 @@ if len(tests) != 1 and execute_tests:
             sys.stdout.write("\nPassed the following test:\n")
         else:
             sys.stdout.write("\nPassed the following %d tests:\n" % len(passed))
-        paths = map(lambda x: x.path, passed)
-        sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
+        paths = [x.path for x in passed]
+        sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
     if fail:
         if len(fail) == 1:
             sys.stdout.write("\nFailed the following test:\n")
         else:
             sys.stdout.write("\nFailed the following %d tests:\n" % len(fail))
-        paths = map(lambda x: x.path, fail)
-        sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
+        paths = [x.path for x in fail]
+        sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
     if no_result:
         if len(no_result) == 1:
             sys.stdout.write("\nNO RESULT from the following test:\n")
         else:
             sys.stdout.write("\nNO RESULT from the following %d tests:\n" % len(no_result))
-        paths = map(lambda x: x.path, no_result)
-        sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
+        paths = [x.path for x in no_result]
+        sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
 
 if outputfile:
     if outputfile == '-':
@@ -765,3 +848,9 @@ elif len(no_result):
     sys.exit(2)
 else:
     sys.exit(0)
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: