Merged revisions 1968-2115 via svnmerge from
[scons.git] / runtest.py
index 1530415c922b123f149251454ca06ee8ee907b2a..ffaa68b468ba760336d7d312b000fe8a9e924a5e 100644 (file)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 #
-# Copyright (c) 2001, 2002, 2003, 2004 The SCons Foundation
+# __COPYRIGHT__
 #
 # runtest.py - wrapper script for running SCons tests
 #
@@ -8,7 +8,7 @@
 # directories to test the SCons modules.
 #
 # By default, it directly uses the modules in the local tree:
-# ./src/ (source files we ship) and ./etc/ (other modules we don't).
+# ./src/ (source files we ship) and ./QMTest/ (other modules we don't).
 #
 # HOWEVER, now that SCons has Repository support, we don't have
 # Aegis copy all of the files into the local tree.  So if you're
 #
 #       -h              Print the help and exit.
 #
+#       -l              List available tests and exit.
+#
+#       -n              No execute, just print command lines.
+#
 #       -o file         Print test results to the specified file.
 #                       The --aegis and --xml options specify the
 #                       output format.
 #                       command line it will execute before
 #                       executing it.  This suppresses that print.
 #
+#       --sp            The Aegis search path.
+#
+#       --spe           The Aegis executable search path.
+#
 #       -t              Print the execution time of each test.
 #
 #       -X              The scons "script" is an executable; don't
@@ -88,10 +96,18 @@ import string
 import sys
 import time
 
+if not hasattr(os, 'WEXITSTATUS'):
+    os.WEXITSTATUS = lambda x: x
+
+cwd = os.getcwd()
+
 all = 0
+baseline = 0
+builddir = os.path.join(cwd, 'build')
 debug = ''
+execute_tests = 1
 format = None
-tests = []
+list_only = None
 printcommand = 1
 package = None
 print_passed_summary = None
@@ -100,30 +116,24 @@ scons_exec = None
 outputfile = None
 testlistfile = None
 version = ''
-print_time = lambda fmt, time: None
-
-if os.name == 'java':
-    python = os.path.join(sys.prefix, 'jython')
-else:
-    python = sys.executable
-
-cwd = os.getcwd()
-
-if sys.platform == 'win32' or os.name == 'java':
-    lib_dir = os.path.join(sys.exec_prefix, "Lib")
-else:
-    # The hard-coded "python" here is the directory name,
-    # not an executable, so it's all right.
-    lib_dir = os.path.join(sys.exec_prefix, "lib", "python" + sys.version[0:3])
+print_times = None
+python = None
+sp = None
+spe = None
 
 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.
+  -l, --list                  List available tests and exit.
+  -n, --no-exec               No execute, just print command lines.
+  --noqmtest                  Execute tests directly, not using QMTest.
   -o FILE, --output FILE      Print test results to FILE.
   -P Python                   Use the specified Python interpreter.
   -p PACKAGE, --package PACKAGE
@@ -137,7 +147,10 @@ Options:
                                 tar-gz        .tar.gz distribution
                                 zip           .zip distribution
   --passed                    Summarize which tests passed.
+  --qmtest                    Run using the QMTest harness.
   -q, --quiet                 Don't print the test being executed.
+  --sp PATH                   The Aegis search path.
+  --spe PATH                  The Aegis executable search path.
   -t, --time                  Print test execution time.
   -v version                  Specify the SCons version.
   --verbose=LEVEL             Set verbose level: 1 = print executed commands,
@@ -148,74 +161,141 @@ Options:
   --xml                       Print results in SCons XML format.
 """
 
-opts, args = getopt.getopt(sys.argv[1:], "adf:ho:P:p:qv:Xx:t",
-                            ['all', 'aegis',
-                             'debug', 'file=', 'help', 'output=',
-                             'package=', 'passed', 'python=', 'quiet',
+opts, args = getopt.getopt(sys.argv[1:], "ab:df:hlno:P:p:qv:Xx:t",
+                            ['all', 'aegis', 'baseline=', 'builddir=',
+                             'debug', 'file=', 'help',
+                             'list', 'no-exec', 'noqmtest', 'output=',
+                             'package=', 'passed', 'python=',
+                             'qmtest', 'quiet', 'spe=',
                              'version=', 'exec=', 'time',
                              'verbose=', 'xml'])
 
 for o, a in opts:
-    if o == '-a' or o == '--all':
+    if o in ['-a', '--all']:
         all = 1
-    elif o == '-d' or o == '--debug':
-        debug = os.path.join(lib_dir, "pdb.py")
-    elif o == '-f' or o == '--file':
+    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')
+            if os.path.exists(pdb):
+                debug = pdb
+                break
+    elif o in ['-f', '--file']:
         if not os.path.isabs(a):
             a = os.path.join(cwd, a)
         testlistfile = a
-    elif o == '-h' or o == '--help':
+    elif o in ['-h', '--help']:
         print helpstr
         sys.exit(0)
-    elif o == '-o' or o == '--output':
+    elif o in ['-l', '--list']:
+        list_only = 1
+    elif o in ['-n', '--no-exec']:
+        execute_tests = None
+    elif o in ['--noqmtest']:
+        qmtest = None
+    elif o in ['-o', '--output']:
         if a != '-' and not os.path.isabs(a):
             a = os.path.join(cwd, a)
         outputfile = a
-    elif o == '-p' or o == '--package':
+    elif o in ['-p', '--package']:
         package = a
-    elif o == '--passed':
+    elif o in ['--passed']:
         print_passed_summary = 1
-    elif o == '-P' or o == '--python':
+    elif o in ['-P', '--python']:
         python = a
-    elif o == '-q' or o == '--quiet':
+    elif o in ['--qmtest']:
+        qmtest = 'qmtest.py'
+    elif o in ['-q', '--quiet']:
         printcommand = 0
-    elif o == '-t' or o == '--time':
-        print_time = lambda fmt, time: sys.stdout.write(fmt % time)
+    elif o in ['--sp']:
+        sp = string.split(a, os.pathsep)
+    elif o in ['--spe']:
+        spe = string.split(a, os.pathsep)
+    elif o in ['-t', '--time']:
+        print_times = 1
     elif o in ['--verbose']:
         os.environ['TESTCMD_VERBOSE'] = a
-    elif o == '-v' or o == '--version':
+    elif o in ['-v', '--version']:
         version = a
-    elif o == '-X':
+    elif o in ['-X']:
         scons_exec = 1
-    elif o == '-x' or o == '--exec':
+    elif o in ['-x', '--exec']:
         scons = a
     elif o in ['--aegis', '--xml']:
         format = o
 
-def whereis(file):
-    for dir in string.split(os.environ['PATH'], os.pathsep):
-        f = os.path.join(dir, file)
-        if os.path.isfile(f):
-            try:
-                st = os.stat(f)
-            except OSError:
-                continue
-            if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
-                return f
-    return None
+if not args and not all and not testlistfile:
+    sys.stderr.write("""\
+runtest.py:  No tests were specified.
+             List one or more tests on the command line, use the
+             -f option to specify a file containing a list of tests,
+             or use the -a option to find and run all tests.
 
-aegis = whereis('aegis')
+""")
+    sys.exit(1)
+
+if sys.platform in ('win32', 'cygwin'):
 
-sp = []
+    def whereis(file):
+        pathext = [''] + string.split(os.environ['PATHEXT'])
+        for dir in string.split(os.environ['PATH'], os.pathsep):
+            f = os.path.join(dir, file)
+            for ext in pathext:
+                fext = f + ext
+                if os.path.isfile(fext):
+                    return fext
+        return None
 
-if aegis:
-    paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
-    sp.extend(string.split(paths, os.pathsep))
-    spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
-    spe = string.split(spe, os.pathsep)
 else:
+
+    def whereis(file):
+        for dir in string.split(os.environ['PATH'], os.pathsep):
+            f = os.path.join(dir, file)
+            if os.path.isfile(f):
+                try:
+                    st = os.stat(f)
+                except OSError:
+                    continue
+                if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
+                    return f
+        return None
+
+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)
+        sys.stderr.flush()
+
+aegis = whereis('aegis')
+
+if format == '--aegis' and aegis:
+    change = os.popen("aesub '$c' 2>/dev/null", "r").read()
+    if change:
+        if sp is None:
+            paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
+            sp = string.split(paths, os.pathsep)
+        if spe is None:
+            spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
+            spe = string.split(spe, os.pathsep)
+    else:
+        aegis = None
+
+if sp is None:
+    sp = []
+if spe is None:
     spe = []
 
+sp.append(builddir)
 sp.append(cwd)
 
 #
@@ -240,9 +320,9 @@ except AttributeError:
             return status >> 8
 else:
     def spawn_it(command_args):
+        command = command_args[0]
         command_args = map(escape, command_args)
-        command_args = map(lambda s: string.replace(s, '\\','\\\\'), command_args)
-        return os.spawnv(os.P_WAIT, command_args[0], command_args)
+        return os.spawnv(os.P_WAIT, command, command_args)
 
 class Base:
     def __init__(self, path, spe=None):
@@ -315,84 +395,8 @@ format_class = {
     '--aegis'   : Aegis,
     '--xml'     : XML,
 }
-Test = format_class[format]
-
-if args:
-    if spe:
-        for a in args:
-            if os.path.isabs(a):
-                for g in glob.glob(a):
-                    tests.append(Test(g))
-            else:
-                for dir in spe:
-                    x = os.path.join(dir, a)
-                    globs = glob.glob(x)
-                    if globs:
-                        for g in globs:
-                            tests.append(Test(g))
-                        break
-    else:
-        for a in args:
-            for g in glob.glob(a):
-                tests.append(Test(g))
-elif all:
-    # Find all of the SCons functional tests in the local directory
-    # tree.  This is anything under the 'src' subdirectory that ends
-    # with 'Tests.py', or any Python script (*.py) under the 'test'
-    # subdirectory.
-    #
-    # Note that there are some tests under 'src' that *begin* with
-    # 'test_', but they're packaging and installation tests, not
-    # functional tests, so we don't execute them by default.  (They can
-    # still be executed by hand, though, and are routinely executed
-    # by the Aegis packaging build to make sure that we're building
-    # things correctly.)
-    tdict = {}
-
-    def find_Tests_py(arg, dirname, names, tdict=tdict):
-        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] = Test(t)
-    os.path.walk('src', find_Tests_py, 0)
-
-    def find_py(arg, dirname, names, tdict=tdict):
-        for n in filter(lambda n: n[-3:] == ".py", names):
-            t = os.path.join(dirname, n)
-            if not tdict.has_key(t):
-                tdict[t] = Test(t)
-    os.path.walk('test', find_py, 0)
-
-    if 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]):
-                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)
-            if a[0] == "test":
-                if a[1] == "remove":
-                    del tdict[a[-1]]
-                elif not tdict.has_key(a[-1]):
-                    tdict[a[-1]] = Test(a[-1], spe)
-
-    keys = tdict.keys()
-    keys.sort()
-    tests = map(tdict.get, keys)
-elif testlistfile:
-    tests = open(testlistfile, 'r').readlines()
-    tests = filter(lambda x: x[0] != '#', tests)
-    tests = map(lambda x: x[:-1], tests)
-    tests = map(Test, tests)
-else:
-    sys.stderr.write("""\
-runtest.py:  No tests were specified on the command line.
-             List one or more tests, or use the -a option
-             to find and run all tests.
-""")
 
+Test = format_class[format]
 
 if package:
 
@@ -417,7 +421,7 @@ if package:
         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
@@ -437,6 +441,8 @@ if package:
         scons_lib_dir = os.path.join(test_dir, dir[package], 'lib', l)
         pythonpath_dir = scons_lib_dir
 
+    scons_runtest_dir = builddir
+
 else:
     sd = None
     ld = None
@@ -456,9 +462,45 @@ else:
     #    spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
     #    ld = string.join(spe, os.pathsep)
 
-    scons_script_dir = sd or os.path.join(cwd, 'src', 'script')
+    if not baseline or baseline == '.':
+        base = cwd
+    elif baseline == '-':
+        # Tentative code for fetching information directly from the
+        # QMTest context file.
+        #
+        #import qm.common
+        #import qm.test.context
+        #qm.rc.Load("test")
+        #context = qm.test.context.Context()
+        #context.Read('context')
+
+        url = None
+        svn_info =  os.popen("svn info 2>&1", "r").read()
+        match = re.search('URL: (.*)', svn_info)
+        if match:
+            url = match.group(1)
+        if not url:
+            sys.stderr.write('runtest.py: could not find a URL:\n')
+            sys.stderr.write(svn_info)
+            sys.exit(1)
+        import tempfile
+        base = tempfile.mkdtemp(prefix='runtest-tmp-')
+
+        command = 'cd %s && svn co -q %s' % (base, url)
+
+        base = os.path.join(base, os.path.split(url)[1])
+        if printcommand:
+            print command
+        if execute_tests:
+            os.system(command)
+    else:
+        base = baseline
+
+    scons_runtest_dir = base
+
+    scons_script_dir = sd or os.path.join(base, 'src', 'script')
 
-    scons_lib_dir = ld or os.path.join(cwd, 'src', 'engine')
+    scons_lib_dir = ld or os.path.join(base, 'src', 'engine')
 
     pythonpath_dir = scons_lib_dir
 
@@ -475,6 +517,7 @@ elif scons_lib_dir:
 if scons_exec:
     os.environ['SCONS_EXEC'] = '1'
 
+os.environ['SCONS_RUNTEST_DIR'] = scons_runtest_dir
 os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
 os.environ['SCONS_CWD'] = cwd
 
@@ -483,9 +526,16 @@ os.environ['SCONS_VERSION'] = version
 old_pythonpath = os.environ.get('PYTHONPATH')
 
 pythonpaths = [ pythonpath_dir ]
-for p in sp:
-    pythonpaths.append(os.path.join(p, 'build', 'etc'))
-    pythonpaths.append(os.path.join(p, 'etc'))
+
+for dir in sp:
+    if format == '--aegis':
+        q = os.path.join(dir, 'build', 'QMTest')
+    else:
+        q = os.path.join(dir, 'QMTest')
+    pythonpaths.append(q)
+
+os.environ['SCONS_SOURCE_PATH_EXECUTABLE'] = string.join(spe, os.pathsep)
+
 os.environ['PYTHONPATH'] = string.join(pythonpaths, os.pathsep)
 
 if old_pythonpath:
@@ -493,10 +543,131 @@ if old_pythonpath:
                                os.pathsep + \
                                old_pythonpath
 
-try:
-    os.chdir(scons_script_dir)
-except OSError:
-    pass
+tests = []
+
+if args:
+    if spe:
+        for a in args:
+            if os.path.isabs(a):
+                tests.extend(glob.glob(a))
+            else:
+                for dir in spe:
+                    x = os.path.join(dir, a)
+                    globs = glob.glob(x)
+                    if globs:
+                        tests.extend(globs)
+                        break
+    else:
+        for a in args:
+            tests.extend(glob.glob(a))
+elif testlistfile:
+    tests = open(testlistfile, 'r').readlines()
+    tests = filter(lambda x: x[0] != '#', tests)
+    tests = map(lambda x: x[:-1], 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
+    # with 'Tests.py', or any Python script (*.py) under the 'test'
+    # subdirectory.
+    #
+    # Note that there are some tests under 'src' that *begin* with
+    # 'test_', but they're packaging and installation tests, not
+    # functional tests, so we don't execute them by default.  (They can
+    # still be executed by hand, though, and are routinely executed
+    # 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]):
+                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)
+            if a[0] == "test":
+                if a[1] == "remove":
+                    del tdict[a[-1]]
+                elif not tdict.has_key(a[-1]):
+                    tdict[a[-1]] = Test(a[-1], spe)
+
+    tests = tdict.keys()
+    tests.sort()
+
+if qmtest:
+    if baseline:
+        aegis_result_stream = 'scons_tdb.AegisBaselineStream'
+        qmr_file = 'baseline.qmr'
+    else:
+        aegis_result_stream = 'scons_tdb.AegisChangeStream'
+        qmr_file = 'results.qmr'
+
+    if print_times:
+        aegis_result_stream = aegis_result_stream + "(print_time='1')"
+
+    qmtest_args = [ qmtest, ]
+
+    if format == '--aegis':
+        dir = builddir
+        if not os.path.isdir(dir):
+            dir = cwd
+        qmtest_args.extend(['-D', dir])
+
+    qmtest_args.extend([
+                'run',
+                '--output %s' % qmr_file,
+                '--format none',
+                '--result-stream="%s"' % aegis_result_stream,
+              ])
+
+    if python:
+        qmtest_args.append('--context python="%s"' % python)
+
+    if outputfile:
+        if format == '--xml':
+            rsclass = 'scons_tdb.SConsXMLResultStream'
+        else:
+            rsclass = 'scons_tdb.AegisBatchStream'
+        qof = "r'" + outputfile + "'"
+        rs = '--result-stream="%s(filename=%s)"' % (rsclass, qof)
+        qmtest_args.append(rs)
+
+    if format == '--aegis':
+        tests = map(lambda x: string.replace(x, cwd+os.sep, ''), tests)
+    else:
+        os.environ['SCONS'] = os.path.join(cwd, 'src', 'script', 'scons.py')
+
+    cmd = string.join(qmtest_args + tests, ' ')
+    if printcommand:
+        sys.stdout.write(cmd + '\n')
+        sys.stdout.flush()
+    status = 0
+    if execute_tests:
+        status = os.WEXITSTATUS(os.system(cmd))
+    sys.exit(status)
+
+#try:
+#    os.chdir(scons_script_dir)
+#except OSError:
+#    pass
+
+tests = map(Test, tests)
 
 class Unbuffered:
     def __init__(self, file):
@@ -509,14 +680,32 @@ class Unbuffered:
 
 sys.stdout = Unbuffered(sys.stdout)
 
+if list_only:
+    for t in tests:
+        sys.stdout.write(t.abspath + "\n")
+    sys.exit(0)
+
+#
+if not python:
+    if os.name == 'java':
+        python = os.path.join(sys.prefix, 'jython')
+    else:
+        python = sys.executable
+
 # time.clock() is the suggested interface for doing benchmarking timings,
 # but time.time() does a better job on Linux systems, so let that be
 # the non-Windows default.
+
 if sys.platform == 'win32':
     time_func = time.clock
 else:
     time_func = time.time
 
+if print_times:
+    print_time_func = lambda fmt, time: sys.stdout.write(fmt % time)
+else:
+    print_time_func = lambda fmt, time: None
+
 total_start_time = time_func()
 for t in tests:
     t.command_args = [python, '-tt']
@@ -527,18 +716,19 @@ for t in tests:
     if printcommand:
         sys.stdout.write(t.command_str + "\n")
     test_start_time = time_func()
-    t.execute()
+    if execute_tests:
+        t.execute()
     t.test_time = time_func() - test_start_time
-    print_time("Test execution time: %.1f seconds\n",  t.test_time)
+    print_time_func("Test execution time: %.1f seconds\n", t.test_time)
 if len(tests) > 0:
     tests[0].total_time = time_func() - total_start_time
-    print_time("Total execution time for all tests: %.1f seconds\n",  tests[0].total_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)
 
-if len(tests) != 1:
+if len(tests) != 1 and execute_tests:
     if passed and print_passed_summary:
         if len(passed) == 1:
             sys.stdout.write("\nPassed the following test:\n")