http://scons.tigris.org/issues/show_bug.cgi?id=2345
[scons.git] / QMTest / TestSCons.py
index 7fe68be3a9988cadc9494358f3ff34ca5bedfbfa..d61c0089fe77b1f0ff4cccc379bd4520cfd0a937 100644 (file)
@@ -13,26 +13,23 @@ attributes defined in this subclass.
 """
 
 # __COPYRIGHT__
+from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 import os
-import os.path
 import re
-import string
+import shutil
 import sys
 import time
 
-import __builtin__
 try:
-    __builtin__.zip
-except AttributeError:
-    def zip(*lists):
-        result = []
-        for i in xrange(len(lists[0])):
-            result.append(tuple(map(lambda l, i=i: l[i], lists)))
-        return result
-    __builtin__.zip = zip
+    x = True
+except NameError:
+    True = not 0
+    False = not 1
+else:
+    del x
 
 from TestCommon import *
 from TestCommon import __all__
@@ -42,9 +39,9 @@ from TestCommon import __all__
 # here provides some independent verification that what we packaged
 # conforms to what we expect.
 
-default_version = '0.98.1'
+default_version = '1.3.0'
 
-copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008'
+copyright_years = '2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010'
 
 # In the checked-in source, the value of SConsVersion in the following
 # line must remain "__ VERSION __" (without the spaces) so the built
@@ -100,16 +97,26 @@ def gccFortranLibs():
     a more reliable way, but using popen3 is relatively efficient."""
 
     libs = ['g2c']
+    cmd = 'gcc -v'
 
     try:
-        import popen2
-        stderr = popen2.popen3('gcc -v')[2]
-    except OSError:
-        return libs
+        import subprocess
+    except ImportError:
+        try:
+            import popen2
+            stderr = popen2.popen3(cmd)[2]
+        except OSError:
+            return libs
+    else:
+        p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE)
+        stderr = p.stderr
 
     for l in stderr.readlines():
-        list = string.split(l)
+        list = l.split()
         if len(list) > 3 and list[:2] == ['gcc', 'version']:
+            if list[2][:3] in ('4.1','4.2','4.3'):
+                libs = ['gfortranbegin']
+                break
             if list[2][:2] in ('3.', '4.'):
                 libs = ['frtbegin'] + libs
                 break
@@ -130,7 +137,7 @@ if sys.platform == 'win32':
     fortran_lib = gccFortranLibs()
 elif sys.platform == 'cygwin':
     fortran_lib = gccFortranLibs()
-elif string.find(sys.platform, 'irix') != -1:
+elif sys.platform.find('irix') != -1:
     fortran_lib = ['ftn']
 else:
     fortran_lib = gccFortranLibs()
@@ -143,7 +150,7 @@ file_expr = r"""File "[^"]*", line \d+, in .+
 # re.escape escapes too much.
 def re_escape(str):
     for c in ['.', '[', ']', '(', ')', '*', '+', '?']:  # Not an exhaustive list.
-        str = string.replace(str, c, '\\' + c)
+        str = str.replace(c, '\\' + c)
     return str
 
 
@@ -152,12 +159,12 @@ try:
     sys.version_info
 except AttributeError:
     # Pre-1.6 Python has no sys.version_info
-    version_string = string.split(sys.version)[0]
-    version_ints = map(int, string.split(version_string, '.'))
+    version_string = sys.version.split()[0]
+    version_ints = list(map(int, version_string.split('.')))
     sys.version_info = tuple(version_ints + ['final', 0])
 
 def python_version_string():
-    return string.split(sys.version)[0]
+    return sys.version.split()[0]
 
 def python_minor_version_string():
     return sys.version[:3]
@@ -166,11 +173,11 @@ def unsupported_python_version(version=sys.version_info):
     return version < (1, 5, 2)
 
 def deprecated_python_version(version=sys.version_info):
-    return version < (2, 2, 0)
+    return version < (2, 4, 0)
 
 if deprecated_python_version():
     msg = r"""
-scons: warning: Support for pre-2.2 Python (%s) is deprecated.
+scons: warning: Support for pre-2.4 Python (%s) is deprecated.
     If this will cause hardship, contact dev@scons.tigris.org.
 """
 
@@ -216,35 +223,41 @@ class TestSCons(TestCommon):
             pass
         else:
             os.chdir(script_dir)
-        if not kw.has_key('program'):
+        if 'program' not in kw:
             kw['program'] = os.environ.get('SCONS')
             if not kw['program']:
                 if os.path.exists('scons'):
                     kw['program'] = 'scons'
                 else:
                     kw['program'] = 'scons.py'
-        if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
+            elif not os.path.isabs(kw['program']):
+                kw['program'] = os.path.join(self.orig_cwd, kw['program'])
+        if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
             kw['interpreter'] = [python, '-tt']
-        if not kw.has_key('match'):
+        if 'match' not in kw:
             kw['match'] = match_exact
-        if not kw.has_key('workdir'):
+        if 'workdir' not in kw:
             kw['workdir'] = ''
 
-       # Term causing test failures due to bogus readline init
-       # control character output on FC8
+        # Term causing test failures due to bogus readline init
+        # control character output on FC8
         # TERM can cause test failures due to control chars in prompts etc.
         os.environ['TERM'] = 'dumb'
+        
+        self.ignore_python_version=kw.get('ignore_python_version',1)
+        if kw.get('ignore_python_version',-1) != -1:
+            del kw['ignore_python_version']
 
-        if deprecated_python_version():
+        if self.ignore_python_version and deprecated_python_version():
             sconsflags = os.environ.get('SCONSFLAGS')
             if sconsflags:
                 sconsflags = [sconsflags]
             else:
                 sconsflags = []
             sconsflags = sconsflags + ['--warn=no-python-version']
-            os.environ['SCONSFLAGS'] = string.join(sconsflags)
+            os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
 
-        apply(TestCommon.__init__, [self], kw)
+        TestCommon.__init__(self, **kw)
 
         import SCons.Node.FS
         if SCons.Node.FS.default_fs is None:
@@ -260,7 +273,7 @@ class TestSCons(TestCommon):
         if not ENV is None:
             kw['ENV'] = ENV
         try:
-            return apply(SCons.Environment.Environment, args, kw)
+            return SCons.Environment.Environment(*args, **kw)
         except (SCons.Errors.UserError, SCons.Errors.InternalError):
             return None
 
@@ -283,7 +296,7 @@ class TestSCons(TestCommon):
             return None
         result = env.WhereIs(prog)
         if norm and os.sep != '/':
-            result = string.replace(result, os.sep, '/')
+            result = result.replace(os.sep, '/')
         return result
 
     def detect_tool(self, tool, prog=None, ENV=None):
@@ -331,18 +344,47 @@ class TestSCons(TestCommon):
                build_str + \
                term
 
+    def run(self, *args, **kw):
+        """
+        Add the --warn=no-python-version option to SCONSFLAGS every
+        command so test scripts don't have to filter out Python version
+        deprecation warnings.
+        Same for --warn=no-visual-c-missing.
+        """
+        save_sconsflags = os.environ.get('SCONSFLAGS')
+        if save_sconsflags:
+            sconsflags = [save_sconsflags]
+        else:
+            sconsflags = []
+        if self.ignore_python_version and deprecated_python_version():
+            sconsflags = sconsflags + ['--warn=no-python-version']
+        # Provide a way to suppress or provide alternate flags for
+        # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
+        # (The intended use case is to set it to null when running
+        # timing tests of earlier versions of SCons which don't
+        # support the --warn=no-visual-c-missing warning.)
+        sconsflags = sconsflags + [os.environ.get('TESTSCONS_SCONSFLAGS',
+                                                  '--warn=no-visual-c-missing')]
+        os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
+        try:
+            result = TestCommon.run(self, *args, **kw)
+        finally:
+            sconsflags = save_sconsflags
+        return result
+
     def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
         s = ""
-        for arg in string.split(arguments):
+        for arg in arguments.split():
             s = s + "scons: `%s' is up to date.\n" % arg
             if options:
                 arguments = options + " " + arguments
         kw['arguments'] = arguments
-        kw['stdout'] = self.wrap_stdout(read_str = read_str, build_str = s)
-        kw['stdout'] = string.replace(kw['stdout'],'\n','\\n')
-        kw['stdout'] = string.replace(kw['stdout'],'.','\\.')
+        stdout = self.wrap_stdout(read_str = read_str, build_str = s)
+        # Append '.*' so that timing output that comes after the
+        # up-to-date output is okay.
+        kw['stdout'] = re.escape(stdout) + '.*'
         kw['match'] = self.match_re_dotall
-        apply(self.run, [], kw)
+        self.run(**kw)
 
     def not_up_to_date(self, options = None, arguments = None, **kw):
         """Asserts that none of the targets listed in arguments is
@@ -350,16 +392,34 @@ class TestSCons(TestCommon):
         This function is most useful in conjunction with the -n option.
         """
         s = ""
-        for  arg in string.split(arguments):
-            s = s + "(?!scons: `%s' is up to date.)" % arg
+        for arg in arguments.split():
+            s = s + "(?!scons: `%s' is up to date.)" % re.escape(arg)
             if options:
                 arguments = options + " " + arguments
+        s = '('+s+'[^\n]*\n)*'
         kw['arguments'] = arguments
-        kw['stdout'] = self.wrap_stdout(build_str="("+s+"[^\n]*\n)*")
-        kw['stdout'] = string.replace(kw['stdout'],'\n','\\n')
-        kw['stdout'] = string.replace(kw['stdout'],'.','\\.')
+        stdout = re.escape(self.wrap_stdout(build_str='ARGUMENTSGOHERE'))
+        kw['stdout'] = stdout.replace('ARGUMENTSGOHERE', s)
         kw['match'] = self.match_re_dotall
-        apply(self.run, [], kw)
+        self.run(**kw)
+
+    def option_not_yet_implemented(self, option, arguments=None, **kw):
+        """
+        Verifies expected behavior for options that are not yet implemented:
+        a warning message, and exit status 1.
+        """
+        msg = "Warning:  the %s option is not yet implemented\n" % option
+        kw['stderr'] = msg
+        if arguments:
+            # If it's a long option and the argument string begins with '=',
+            # it's of the form --foo=bar and needs no separating space.
+            if option[:2] == '--' and arguments[0] == '=':
+                kw['arguments'] = option + arguments
+            else:
+                kw['arguments'] = option + ' ' + arguments
+        # TODO(1.5)
+        #return self.run(**kw)
+        return self.run(**kw)
 
     def diff_substr(self, expect, actual, prelen=20, postlen=40):
         i = 0
@@ -390,9 +450,9 @@ class TestSCons(TestCommon):
         places, abstracting out the version difference.
         """
         exec 'import traceback; x = traceback.format_stack()[-1]'
-        x = string.lstrip(x)
-        x = string.replace(x, '<string>', file)
-        x = string.replace(x, 'line 1,', 'line %s,' % line)
+        x = x.lstrip()
+        x = x.replace('<string>', file)
+        x = x.replace('line 1,', 'line %s,' % line)
         return x
 
     def normalize_pdf(self, s):
@@ -415,12 +475,12 @@ class TestSCons(TestCommon):
             end_marker = 'endstream\nendobj'
 
             encoded = []
-            b = string.find(s, begin_marker, 0)
+            b = s.find(begin_marker, 0)
             while b != -1:
                 b = b + len(begin_marker)
-                e = string.find(s, end_marker, b)
+                e = s.find(end_marker, b)
                 encoded.append((b, e))
-                b = string.find(s, begin_marker, e + len(end_marker))
+                b = s.find(begin_marker, e + len(end_marker))
 
             x = 0
             r = []
@@ -436,7 +496,7 @@ class TestSCons(TestCommon):
                 r.append(d)
                 x = e
             r.append(s[x:])
-            s = string.join(r, '')
+            s = ''.join(r)
 
         return s
 
@@ -444,9 +504,7 @@ class TestSCons(TestCommon):
         import glob
         result = []
         for p in patterns:
-            paths = glob.glob(p)
-            paths.sort()
-            result.extend(paths)
+            result.extend(sorted(glob.glob(p)))
         return result
 
 
@@ -482,7 +540,7 @@ class TestSCons(TestCommon):
             ]
             java_path = self.paths(patterns) + [env['ENV']['PATH']]
 
-        env['ENV']['PATH'] = string.join(java_path, os.pathsep)
+        env['ENV']['PATH'] = os.pathsep.join(java_path)
         return env['ENV']
 
     def java_where_includes(self,version=None):
@@ -493,8 +551,12 @@ class TestSCons(TestCommon):
         import sys
         if not version:
             version=''
+            frame = '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h'
+        else:
+            frame = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/jni.h'%version
         jni_dirs = ['/usr/lib/jvm/java-*-sun-%s*/include/jni.h'%version,
                     '/usr/java/jdk%s*/include/jni.h'%version,
+                   frame,
                     ]
         dirs = self.paths(jni_dirs)
         if not dirs:
@@ -510,10 +572,18 @@ class TestSCons(TestCommon):
 
 
     def java_where_java_home(self,version=None):
-        import os.path
-        jar=self.java_where_jar(version)
-        home=os.path.normpath('%s/..'%jar)
-        return home
+        if sys.platform[:6] == 'darwin':
+            if version is None:
+                home = '/System/Library/Frameworks/JavaVM.framework/Home'
+            else:
+                home = '/System/Library/Frameworks/JavaVM.framework/Versions/%s/Home' % version
+        else:
+            jar = self.java_where_jar(version)
+            home = os.path.normpath('%s/..'%jar)
+        if os.path.isdir(home):
+            return home
+        print("Could not determine JAVA_HOME: %s is not a directory" % home)
+        self.fail_test()
 
     def java_where_jar(self, version=None):
         ENV = self.java_ENV(version)
@@ -551,7 +621,7 @@ class TestSCons(TestCommon):
                  stderr=None,
                  status=None)
         if version:
-            if string.find(self.stderr(), 'javac %s' % version) == -1:
+            if self.stderr().find('javac %s' % version) == -1:
                 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
                 self.skip_test(fmt % version)
         else:
@@ -590,9 +660,9 @@ class TestSCons(TestCommon):
         self.write([dir, 'bin', 'mymoc.py'], """\
 import getopt
 import sys
-import string
 import re
-cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
+# -w and -z are fake options used in test/QT/QTFLAGS.py
+cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
 output = None
 impl = 0
 opt_string = ''
@@ -600,13 +670,14 @@ for opt, arg in cmd_opts:
     if opt == '-o': output = open(arg, 'wb')
     elif opt == '-i': impl = 1
     else: opt_string = opt_string + ' ' + opt
+output.write("/* mymoc.py%s */\\n" % opt_string)
 for a in args:
     contents = open(a, 'rb').read()
-    a = string.replace(a, '\\\\', '\\\\\\\\')
+    a = a.replace('\\\\', '\\\\\\\\')
     subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
     if impl:
         contents = re.sub( r'#include.*', '', contents )
-    output.write(string.replace(contents, 'Q_OBJECT', subst))
+    output.write(contents.replace('Q_OBJECT', subst))
 output.close()
 sys.exit(0)
 """)
@@ -615,11 +686,11 @@ sys.exit(0)
 import os.path
 import re
 import sys
-import string
 output_arg = 0
 impl_arg = 0
 impl = None
 source = None
+opt_string = ''
 for arg in sys.argv[1:]:
     if output_arg:
         output = open(arg, 'wb')
@@ -631,11 +702,14 @@ for arg in sys.argv[1:]:
         output_arg = 1
     elif arg == "-impl":
         impl_arg = 1
+    elif arg[0:1] == "-":
+        opt_string = opt_string + ' ' + arg
     else:
         if source:
             sys.exit(1)
         source = open(arg, 'rb')
         sourceFile = arg
+output.write("/* myuic.py%s */\\n" % opt_string)
 if impl:
     output.write( '#include "' + impl + '"\\n' )
     includes = re.findall('<include.*?>(.*?)</include>', source.read())
@@ -658,7 +732,7 @@ void my_qt_symbol(const char *arg);
 #include "../include/my_qobject.h"
 #include <stdio.h>
 void my_qt_symbol(const char *arg) {
-  printf( arg );
+  fputs( arg, stdout );
 }
 """)
 
@@ -683,8 +757,8 @@ else:
         self.QT_LIB_DIR = self.workpath(dir, 'lib')
 
     def Qt_create_SConstruct(self, place):
-        if type(place) is type([]):
-            place = apply(test.workpath, place)
+        if isinstance(place, list):
+            place = test.workpath(*place)
         self.write(place, """\
 if ARGUMENTS.get('noqtdir', 0): QTDIR=None
 else: QTDIR=r'%s'
@@ -714,103 +788,6 @@ Export("env dup")
 SConscript( sconscript )
 """ % (self.QT, self.QT_LIB, self.QT_MOC, self.QT_UIC))
 
-    def msvs_versions(self):
-        if not hasattr(self, '_msvs_versions'):
-
-            # Determine the SCons version and the versions of the MSVS
-            # environments installed on the test machine.
-            #
-            # We do this by executing SCons with an SConstruct file
-            # (piped on stdin) that spits out Python assignments that
-            # we can just exec().  We construct the SCons.__"version"__
-            # string in the input here so that the SCons build itself
-            # doesn't fill it in when packaging SCons.
-            input = """\
-import SCons
-print "self._scons_version =", repr(SCons.__%s__)
-env = Environment();
-print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
-""" % 'version'
-        
-            self.run(arguments = '-n -q -Q -f -', stdin = input)
-            exec(self.stdout())
-
-        return self._msvs_versions
-
-    def vcproj_sys_path(self, fname):
-        """
-        """
-        orig = 'sys.path = [ join(sys'
-
-        enginepath = repr(os.path.join(self._cwd, '..', 'engine'))
-        replace = 'sys.path = [ %s, join(sys' % enginepath
-
-        contents = self.read(fname)
-        contents = string.replace(contents, orig, replace)
-        self.write(fname, contents)
-
-    def msvs_substitute(self, input, msvs_ver,
-                        subdir=None, sconscript=None,
-                        python=sys.executable,
-                        project_guid=None):
-        if not hasattr(self, '_msvs_versions'):
-            self.msvs_versions()
-
-        if subdir:
-            workpath = self.workpath(subdir)
-        else:
-            workpath = self.workpath()
-
-        if sconscript is None:
-            sconscript = self.workpath('SConstruct')
-
-        if project_guid is None:
-            project_guid = "{E5466E26-0003-F18B-8F8A-BCD76C86388D}"
-
-        if os.environ.has_key('SCONS_LIB_DIR'):
-            exec_script_main = "from os.path import join; import sys; sys.path = [ r'%s' ] + sys.path; import SCons.Script; SCons.Script.main()" % os.environ['SCONS_LIB_DIR']
-        else:
-            exec_script_main = "from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-%s'), join(sys.prefix, 'scons-%s'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()" % (self._scons_version, self._scons_version)
-        exec_script_main_xml = string.replace(exec_script_main, "'", "&apos;")
-
-        result = string.replace(input, r'<WORKPATH>', workpath)
-        result = string.replace(result, r'<PYTHON>', python)
-        result = string.replace(result, r'<SCONSCRIPT>', sconscript)
-        result = string.replace(result, r'<SCONS_SCRIPT_MAIN>', exec_script_main)
-        result = string.replace(result, r'<SCONS_SCRIPT_MAIN_XML>', exec_script_main_xml)
-        result = string.replace(result, r'<PROJECT_GUID>', project_guid)
-        return result
-
-    def get_msvs_executable(self, version):
-        """Returns a full path to the executable (MSDEV or devenv)
-        for the specified version of Visual Studio.
-        """
-        common_msdev98_bin_msdev_com = ['Common', 'MSDev98', 'Bin', 'MSDEV.COM']
-        common7_ide_devenv_com       = ['Common7', 'IDE', 'devenv.com']
-        common7_ide_vcexpress_exe    = ['Common7', 'IDE', 'VCExpress.exe']
-        sub_paths = {
-            '6.0' : [
-                common_msdev98_bin_msdev_com,
-            ],
-            '7.0' : [
-                common7_ide_devenv_com,
-            ],
-            '7.1' : [
-                common7_ide_devenv_com,
-            ],
-            '8.0' : [
-                common7_ide_devenv_com,
-                common7_ide_vcexpress_exe,
-            ],
-        }
-        from SCons.Tool.msvs import get_msvs_install_dirs
-        vs_path = get_msvs_install_dirs(version)['VSINSTALLDIR']
-        for sp in sub_paths[version]:
-            p = apply(os.path.join, [vs_path] + sp)
-            if os.path.exists(p):
-                return p
-        return apply(os.path.join, [vs_path] + sub_paths[version][0])
-
 
     NCR = 0 # non-cached rebuild
     CR  = 1 # cached rebuild (up to date)
@@ -833,10 +810,10 @@ print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
             def __init__(self, p):
                 self.pos = p
 
-        def matchPart(log, logfile, lastEnd):
+        def matchPart(log, logfile, lastEnd, NoMatch=NoMatch):
             m = re.match(log, logfile[lastEnd:])
             if not m:
-                raise NoMatch, lastEnd
+                raise NoMatch(lastEnd)
             return m.end() + lastEnd
         try:
             #print len(os.linesep)
@@ -853,7 +830,7 @@ print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
             lastEnd = 0
             logfile = self.read(self.workpath(logfile))
             if (doCheckLog and
-                string.find( logfile, "scons: warning: The stored build "
+                logfile.find( "scons: warning: The stored build "
                              "information has an unexpected class." ) >= 0):
                 self.fail_test()
             sconf_dir = sconf_dir
@@ -911,7 +888,7 @@ print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
                 log = ""
             if doCheckLog: lastEnd = matchPart(ls, logfile, lastEnd)
             if doCheckLog and lastEnd != len(logfile):
-                raise NoMatch, lastEnd
+                raise NoMatch(lastEnd)
             
         except NoMatch, m:
             print "Cannot match log file against log regexp."
@@ -944,75 +921,40 @@ print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
         # see also sys.prefix documentation
         return python_minor_version_string()
 
-    def get_platform_python(self):
+    def get_platform_python_info(self):
         """
         Returns a path to a Python executable suitable for testing on
-        this platform.
-
-        Mac OS X has no static libpython for SWIG to link against,
-        so we have to link against Apple's framwork version.  However,
-        testing must use the executable version that corresponds to the
-        framework we link against, or else we get interpreter errors.
-        """
-        if sys.platform == 'darwin':
-            return '/System/Library/Frameworks/Python.framework/Versions/Current/bin/python'
-        else:
-            global python
-            return python
-
-    def get_quoted_platform_python(self):
-        """
-        Returns a quoted path to a Python executable suitable for testing on
-        this platform.
-
-        Mac OS X has no static libpython for SWIG to link against,
-        so we have to link against Apple's framwork version.  However,
-        testing must use the executable version that corresponds to the
-        framework we link against, or else we get interpreter errors.
-        """
-        if sys.platform == 'darwin':
-            return '"' + self.get_platform_python() + '"'
-        else:
-            global _python_
-            return _python_
-
-    def get_platform_sys_prefix(self):
+        this platform and its associated include path, library path,
+        and library name.
         """
-        Returns a "sys.prefix" value suitable for linking on this platform.
+        python = self.where_is('python')
+        if not python:
+            self.skip_test('Can not find installed "python", skipping test.\n')
 
-        Mac OS X has a built-in Python but no static libpython,
-        so we must link to it using Apple's 'framework' scheme.
-        """
-        if sys.platform == 'darwin':
-            fmt = '/System/Library/Frameworks/Python.framework/Versions/%s/'
-            return fmt % self.get_python_version()
-        else:
-            return sys.prefix
+        self.run(program = python, stdin = """\
+import os, sys
+try:
+       py_ver = 'python%d.%d' % sys.version_info[:2]
+except AttributeError:
+       py_ver = 'python' + sys.version[:3]
+print os.path.join(sys.prefix, 'include', py_ver)
+print os.path.join(sys.prefix, 'lib', py_ver, 'config')
+print py_ver
+""")
 
-    def get_python_frameworks_flags(self):
-        """
-        Returns a FRAMEWORKSFLAGS value for linking with Python.
+        return [python] + self.stdout().strip().split('\n')
 
-        Mac OS X has a built-in Python but no static libpython,
-        so we must link to it using Apple's 'framework' scheme.
+    def start(self, *args, **kw):
         """
-        if sys.platform == 'darwin':
-            return '-framework Python'
-        else:
-            return ''
+        Starts SCons in the test environment.
 
-    def get_python_inc(self):
+        This method exists to tell Test{Cmd,Common} that we're going to
+        use standard input without forcing every .start() call in the
+        individual tests to do so explicitly.
         """
-        Returns a path to the Python include directory.
-        """
-        try:
-            import distutils.sysconfig
-        except ImportError:
-            return os.path.join(self.get_platform_sys_prefix(),
-                                'include',
-                                'python' + self.get_python_version())
-        else:
-            return distutils.sysconfig.get_python_inc()
+        if 'stdin' not in kw:
+            kw['stdin'] = True
+        return TestCommon.start(self, *args, **kw)
 
     def wait_for(self, fname, timeout=10.0, popen=None):
         """
@@ -1042,6 +984,297 @@ print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
         else:
             alt_cpp_suffix = '.C'
         return alt_cpp_suffix
+
+
+class Stat:
+    def __init__(self, name, units, expression, convert=None):
+        if convert is None:
+            convert = lambda x: x
+        self.name = name
+        self.units = units
+        self.expression = re.compile(expression)
+        self.convert = convert
+
+StatList = [
+    Stat('memory-initial', 'kbytes',
+         r'Memory before reading SConscript files:\s+(\d+)',
+         convert=lambda s: int(s) / 1024),
+    Stat('memory-prebuild', 'kbytes',
+         r'Memory before building targets:\s+(\d+)',
+         convert=lambda s: int(s) / 1024),
+    Stat('memory-final', 'kbytes',
+         r'Memory after building targets:\s+(\d+)',
+         convert=lambda s: int(s) / 1024),
+
+    Stat('time-sconscript', 'seconds',
+         r'Total SConscript file execution time:\s+([\d.]+) seconds'),
+    Stat('time-scons', 'seconds',
+         r'Total SCons execution time:\s+([\d.]+) seconds'),
+    Stat('time-commands', 'seconds',
+         r'Total command execution time:\s+([\d.]+) seconds'),
+    Stat('time-total', 'seconds',
+         r'Total build time:\s+([\d.]+) seconds'),
+]
+
+
+class TimeSCons(TestSCons):
+    """Class for timing SCons."""
+    def __init__(self, *args, **kw):
+        """
+        In addition to normal TestSCons.TestSCons intialization,
+        this enables verbose mode (which causes the command lines to
+        be displayed in the output) and copies the contents of the
+        directory containing the executing script to the temporary
+        working directory.
+        """
+        self.variables = kw.get('variables')
+        if self.variables is not None:
+            for variable, value in self.variables.items():
+                value = os.environ.get(variable, value)
+                try:
+                    value = int(value)
+                except ValueError:
+                    try:
+                        value = float(value)
+                    except ValueError:
+                        pass
+                self.variables[variable] = value
+            del kw['variables']
+
+        self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
+
+        if 'verbose' not in kw and not self.calibrate:
+            kw['verbose'] = True
+
+        # TODO(1.5)
+        #TestSCons.__init__(self, *args, **kw)
+        TestSCons.__init__(self, *args, **kw)
+
+        # TODO(sgk):    better way to get the script dir than sys.argv[0]
+        test_dir = os.path.dirname(sys.argv[0])
+        test_name = os.path.basename(test_dir)
+
+        if not os.path.isabs(test_dir):
+            test_dir = os.path.join(self.orig_cwd, test_dir)
+        self.copy_timing_configuration(test_dir, self.workpath())
+
+    def main(self, *args, **kw):
+        """
+        The main entry point for standard execution of timings.
+
+        This method run SCons three times:
+
+          Once with the --help option, to have it exit after just reading
+          the configuration.
+
+          Once as a full build of all targets.
+
+          Once again as a (presumably) null or up-to-date build of
+          all targets.
+
+        The elapsed time to execute each build is printed after
+        it has finished.
+        """
+        if 'options' not in kw and self.variables:
+            options = []
+            for variable, value in self.variables.items():
+                options.append('%s=%s' % (variable, value))
+            kw['options'] = ' '.join(options)
+        if self.calibrate:
+            # TODO(1.5)
+            #self.calibration(*args, **kw)
+            self.calibration(*args, **kw)
+        else:
+            self.uptime()
+            # TODO(1.5)
+            #self.startup(*args, **kw)
+            #self.full(*args, **kw)
+            #self.null(*args, **kw)
+            self.startup(*args, **kw)
+            self.full(*args, **kw)
+            self.null(*args, **kw)
+
+    def trace(self, graph, name, value, units, sort=None):
+        fmt = "TRACE: graph=%s name=%s value=%s units=%s"
+        line = fmt % (graph, name, value, units)
+        if sort is not None:
+          line = line + (' sort=%s' % sort)
+        line = line + '\n'
+        sys.stdout.write(line)
+        sys.stdout.flush()
+
+    def report_traces(self, trace, stats):
+        self.trace('TimeSCons-elapsed',
+                   trace,
+                   self.elapsed_time(),
+                   "seconds",
+                   sort=0)
+        for name, args in stats.items():
+            # TODO(1.5)
+            #self.trace(name, trace, *args)
+            self.trace(name, trace, **args)
+
+    def uptime(self):
+        try:
+            fp = open('/proc/loadavg')
+        except EnvironmentError:
+            pass
+        else:
+            avg1, avg5, avg15 = fp.readline().split(" ")[:3]
+            fp.close()
+            self.trace('load-average',  'average1', avg1, 'processes')
+            self.trace('load-average',  'average5', avg5, 'processes')
+            self.trace('load-average',  'average15', avg15, 'processes')
+
+    def collect_stats(self, input):
+        result = {}
+        for stat in StatList:
+            m = stat.expression.search(input)
+            if m:
+                value = stat.convert(m.group(1))
+                # The dict keys match the keyword= arguments
+                # of the trace() method above so they can be
+                # applied directly to that call.
+                result[stat.name] = {'value':value, 'units':stat.units}
+        return result
+
+    def startup(self, *args, **kw):
+        """
+        Runs scons with the --help option.
+
+        This serves as a way to isolate just the amount of startup time
+        spent reading up the configuration, since --help exits before any
+        "real work" is done.
+        """
+        kw['options'] = kw.get('options', '') + ' --help'
+        # Ignore the exit status.  If the --help run dies, we just
+        # won't report any statistics for it, but we can still execute
+        # the full and null builds.
+        kw['status'] = None
+        # TODO(1.5)
+        #self.run(*args, **kw)
+        self.run(*args, **kw)
+        sys.stdout.write(self.stdout())
+        stats = self.collect_stats(self.stdout())
+        # Delete the time-commands, since no commands are ever
+        # executed on the help run and it is (or should be) always 0.0.
+        del stats['time-commands']
+        self.report_traces('startup', stats)
+
+    def full(self, *args, **kw):
+        """
+        Runs a full build of SCons.
+        """
+        # TODO(1.5)
+        #self.run(*args, **kw)
+        self.run(*args, **kw)
+        sys.stdout.write(self.stdout())
+        stats = self.collect_stats(self.stdout())
+        self.report_traces('full', stats)
+        # TODO(1.5)
+        #self.trace('full-memory', 'initial', **stats['memory-initial'])
+        #self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
+        #self.trace('full-memory', 'final', **stats['memory-final'])
+        self.trace('full-memory', 'initial', **stats['memory-initial'])
+        self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
+        self.trace('full-memory', 'final', **stats['memory-final'])
+
+    def calibration(self, *args, **kw):
+        """
+        Runs a full build of SCons, but only reports calibration
+        information (the variable(s) that were set for this configuration,
+        and the elapsed time to run.
+        """
+        # TODO(1.5)
+        #self.run(*args, **kw)
+        self.run(*args, **kw)
+        if self.variables:
+            for variable, value in self.variables.items():
+                sys.stdout.write('VARIABLE: %s=%s\n' % (variable, value))
+        sys.stdout.write('ELAPSED: %s\n' % self.elapsed_time())
+
+    def null(self, *args, **kw):
+        """
+        Runs an up-to-date null build of SCons.
+        """
+        # TODO(sgk):  allow the caller to specify the target (argument)
+        # that must be up-to-date.
+        # TODO(1.5)
+        #self.up_to_date(arguments='.', **kw)
+        kw = kw.copy()
+        kw['arguments'] = '.'
+        self.up_to_date(**kw)
+        sys.stdout.write(self.stdout())
+        stats = self.collect_stats(self.stdout())
+        # time-commands should always be 0.0 on a null build, because
+        # no commands should be executed.  Remove it from the stats
+        # so we don't trace it, but only if it *is* 0 so that we'll
+        # get some indication if a supposedly-null build actually does
+        # build something.
+        if float(stats['time-commands']['value']) == 0.0:
+            del stats['time-commands']
+        self.report_traces('null', stats)
+        # TODO(1.5)
+        #self.trace('null-memory', 'initial', **stats['memory-initial'])
+        #self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
+        #self.trace('null-memory', 'final', **stats['memory-final'])
+        self.trace('null-memory', 'initial', **stats['memory-initial'])
+        self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
+        self.trace('null-memory', 'final', **stats['memory-final'])
+
+    def elapsed_time(self):
+        """
+        Returns the elapsed time of the most recent command execution.
+        """
+        return self.endTime - self.startTime
+
+    def run(self, *args, **kw):
+        """
+        Runs a single build command, capturing output in the specified file.
+
+        Because this class is about timing SCons, we record the start
+        and end times of the elapsed execution, and also add the
+        --debug=memory and --debug=time options to have SCons report
+        its own memory and timing statistics.
+        """
+        kw['options'] = kw.get('options', '') + ' --debug=memory --debug=time'
+        self.startTime = time.time()
+        try:
+            # TODO(1.5)
+            #result = TestSCons.run(self, *args, **kw)
+            result = TestSCons.run(self, *args, **kw)
+        finally:
+            self.endTime = time.time()
+        return result
+
+    def copy_timing_configuration(self, source_dir, dest_dir):
+        """
+        Copies the timing configuration from the specified source_dir (the
+        directory in which the controlling script lives) to the specified
+        dest_dir (a temporary working directory).
+
+        This ignores all files and directories that begin with the string
+        'TimeSCons-', and all '.svn' subdirectories.
+        """
+        for root, dirs, files in os.walk(source_dir):
+            if '.svn' in dirs:
+                dirs.remove('.svn')
+            # TODO(1.5)
+            #dirs = [ d for d in dirs if not d.startswith('TimeSCons-') ]
+            #files = [ f for f in files if not f.startswith('TimeSCons-') ]
+            not_timescons_entries = lambda s: not s.startswith('TimeSCons-')
+            dirs = list(filter(not_timescons_entries, dirs))
+            files = list(filter(not_timescons_entries, files))
+            for dirname in dirs:
+                source = os.path.join(root, dirname)
+                destination = source.replace(source_dir, dest_dir)
+                os.mkdir(destination)
+                if sys.platform != 'win32':
+                    shutil.copystat(source, destination)
+            for filename in files:
+                source = os.path.join(root, filename)
+                destination = source.replace(source_dir, dest_dir)
+                shutil.copy2(source, destination)
     
 
 # In some environments, $AR will generate a warning message to stderr
@@ -1054,3 +1287,9 @@ print "self._msvs_versions =", str(env['MSVS']['VERSIONS'])
 # test/AR.py for sample usage).
 
 noisy_ar=r'(ar: creating( archive)? \S+\n?)*'
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: