Anonymous avatar Anonymous committed aa23d6b

Merged revisions 1441-1539 via svnmerge from
http://scons.tigris.org/svn/scons/branches/core

........
r1441 | stevenknight | 2006-04-22 23:06:53 -0400 (Sat, 22 Apr 2006) | 1 line

0.96.D397 - The scons command, branch 0.96.91.
........
r1442 | stevenknight | 2006-04-27 00:45:12 -0400 (Thu, 27 Apr 2006) | 1 line

0.96.D398 - The scons command, branch 0.96.92.
........
r1443 | stevenknight | 2006-04-27 00:49:25 -0400 (Thu, 27 Apr 2006) | 1 line

0.96.D399 - Taskmaster clean-ups in anticipation of refactoring speedups.
........
r1450 | stevenknight | 2006-05-02 00:04:55 -0400 (Tue, 02 May 2006) | 1 line

0.96.D400 - Fix VC+++ 2005 Express detection. (Atul Varma) Fix parsing Intel C compiler Li
........
r1451 | stevenknight | 2006-05-02 01:14:24 -0400 (Tue, 02 May 2006) | 1 line

0.96.D401 - Enhance ParseConfig() to understand -arch and -isysroot options. (Gary Oberbrun
........
r1458 | stevenknight | 2006-05-02 23:21:04 -0400 (Tue, 02 May 2006) | 1 line

0.96.D402 - Make strfunction handling consistent. (David Gruener)
........
r1459 | stevenknight | 2006-05-02 23:37:08 -0400 (Tue, 02 May 2006) | 1 line

0.96.D403 - Comment out the test of CVS checkout from the old tigris.org repository.
........
r1460 | stevenknight | 2006-05-03 23:47:54 -0400 (Wed, 03 May 2006) | 1 line

0.96.D404 - Preserve white space in display Action string. (David Gruener)
........
r1461 | stevenknight | 2006-05-04 09:16:15 -0400 (Thu, 04 May 2006) | 1 line

0.96.D405 - Add MergeFlags() and AddFlags() methods. (Greg Noel) Support recognizing compi
........
r1462 | stevenknight | 2006-05-04 23:46:53 -0400 (Thu, 04 May 2006) | 1 line

0.96.D406 - Fix stack trace when ParseFlags has a null string.
........
r1464 | stevenknight | 2006-05-05 17:21:27 -0400 (Fri, 05 May 2006) | 1 line

0.96.D408 - Fix the string displayed by InstallAs() when called through the default construc
........
r1465 | stevenknight | 2006-05-05 18:30:28 -0400 (Fri, 05 May 2006) | 1 line

0.96.D409 - Fix test/ParseConfig.py, broken in the previous checkin by ParseFlags() changes.
........
r1466 | stevenknight | 2006-05-05 20:42:35 -0400 (Fri, 05 May 2006) | 1 line

0.96.D407 - Avoid recursive calls to main() in SConf test programs. (Karol Pietrzak)
........
r1467 | stevenknight | 2006-05-06 00:27:21 -0400 (Sat, 06 May 2006) | 1 line

0.96.D410 - Catch errors from commands that ParseConfig() calls. (John Pye)
........
r1468 | stevenknight | 2006-05-06 10:55:38 -0400 (Sat, 06 May 2006) | 1 line

0.96.D411 - Significant taskmaster speedup by using reference counts, not list manipulation.
........
r1469 | stevenknight | 2006-05-06 18:38:02 -0400 (Sat, 06 May 2006) | 1 line

0.96.D413 - TeX improvements.
........
r1471 | stevenknight | 2006-05-07 09:07:58 -0400 (Sun, 07 May 2006) | 2 lines

Delete properties interfering with clean .jpg checkout.
........
r1472 | stevenknight | 2006-05-07 09:23:54 -0400 (Sun, 07 May 2006) | 1 line

0.96.D412 - Windows portability fixes for two tests and ParseConfig() execution.
........
r1473 | stevenknight | 2006-05-07 09:30:11 -0400 (Sun, 07 May 2006) | 1 line

0.96.D414 - Various man page and documentation updates.
........
r1474 | stevenknight | 2006-05-07 23:53:12 -0400 (Sun, 07 May 2006) | 1 line

0.96.D415 - Initial infrastructure for executing tests under QMTest. (Stefan Seefeld)
........
r1476 | stevenknight | 2006-05-09 00:03:47 -0400 (Tue, 09 May 2006) | 1 line

0.96.D416 - Fix QMTest infrastructure to avoid listing directories with no tests and to find
........
r1477 | stevenknight | 2006-05-16 06:47:51 -0400 (Tue, 16 May 2006) | 1 line

0.96.D417 - Fix Alias turning Entries into Nodes or Dirs too soon.
........
r1478 | stevenknight | 2006-05-17 08:32:58 -0400 (Wed, 17 May 2006) | 1 line

0.96.D418 - Next QMTest changes (including fixing copyrights).
........
r1479 | stevenknight | 2006-05-18 05:07:06 -0400 (Thu, 18 May 2006) | 1 line

0.96.D419 - Fix DVIPDF tests after recent changes.
........
r1497 | stevenknight | 2006-05-23 08:47:01 -0400 (Tue, 23 May 2006) | 1 line

0.96.D420 - Better error message when trying to build a file from an unknown sufix. (Gary O
........
r1498 | stevenknight | 2006-05-23 09:38:52 -0400 (Tue, 23 May 2006) | 1 line

0.96.D421 - Suppress duplicate entries in latest TeX patch. (Joel B. Mohler)
........
r1499 | stevenknight | 2006-05-23 22:00:06 -0400 (Tue, 23 May 2006) | 1 line

0.96.D422 - Add tests for tuple variable expansion. (Gary Oberbrunner)
........
r1515 | stevenknight | 2006-06-12 06:44:24 -0400 (Mon, 12 Jun 2006) | 1 line

0.96.D423 - More QMTest work: start giving runtest.py its own tests, more functionality for
........
r1517 | stevenknight | 2006-06-21 07:34:30 -0400 (Wed, 21 Jun 2006) | 1 line

0.96.D424 - Move test/Configure.py and test/Options.py to avoid confusion with similarly-nam
........
r1518 | stevenknight | 2006-06-21 12:40:37 -0400 (Wed, 21 Jun 2006) | 1 line

0.96.D425 - Change the QMTest infrastructure to use File naming, not Python. Rename tests w
........
r1533 | stevenknight | 2006-07-23 20:10:08 -0400 (Sun, 23 Jul 2006) | 1 line

0.96.D426 - Fix ramifications of changing when Node disambiguation happens.
........
r1535 | stevenknight | 2006-07-24 06:40:43 -0400 (Mon, 24 Jul 2006) | 3 lines

Initialized merge tracking via "svnmerge" with revisions "1-1534" from
http://scons.tigris.org/svn/scons/trunk
........
r1536 | stevenknight | 2006-07-24 21:45:40 -0400 (Mon, 24 Jul 2006) | 2 lines

Remove svnmerge-integrated property to start over.
........
r1538 | stevenknight | 2006-07-24 21:51:32 -0400 (Mon, 24 Jul 2006) | 3 lines

Initialized merge tracking via "svnmerge" with revisions "1-1440" from
http://scons.tigris.org/svn/scons/trunk
........

Comments (0)

Files changed (95)

HOWTO/subrelease.txt

                 aecp rpm/scons.spec.in
                 vi rpm/scons.spec.in
 
+                aecp src/setup.py
+                vi src/setup.py
+
                 aecp src/test_setup.py
                 vi src/test_setup.py
 
+*,D
+*.pyc
+.*.swp
+.consign
+.sconsign

QMTest/SConscript

+#
+# SConscript file for external packages we need.
+#
+
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+import os.path
+
+Import('env')
+
+files = [
+    'classes.qmc',
+    'configuration',
+    'scons_tdb.py',
+    'TestCmd.py',
+    'TestCommon.py',
+    'TestRuntest.py',
+    'TestSCons.py',
+    'unittest.py',
+]
+
+def copy(target, source, env):
+    t = str(target[0])
+    s = str(source[0])
+    open(t, 'wb').write(open(s, 'rb').read())
+
+for file in files:
+    # Guarantee that real copies of these files always exist in
+    # build/QMTest.  If there's a symlink there, then this is an Aegis
+    # build and we blow them away now so that they'll get "built" later.
+    p = os.path.join('build', 'QMTest', file)
+    if os.path.islink(p):
+        os.unlink(p)
+    sp = '#' + p
+    env.Command(sp, file, copy)
+    Local(sp)

QMTest/TestCmd.py

+"""
+TestCmd.py:  a testing framework for commands and scripts.
+
+The TestCmd module provides a framework for portable automated testing
+of executable commands and scripts (in any language, not just Python),
+especially commands and scripts that require file system interaction.
+
+In addition to running tests and evaluating conditions, the TestCmd
+module manages and cleans up one or more temporary workspace
+directories, and provides methods for creating files and directories in
+those workspace directories from in-line data, here-documents), allowing
+tests to be completely self-contained.
+
+A TestCmd environment object is created via the usual invocation:
+
+    import TestCmd
+    test = TestCmd.TestCmd()
+
+There are a bunch of keyword arguments that you can use at instantiation
+time:
+
+    test = TestCmd.TestCmd(description = 'string',
+                           program = 'program_or_script_to_test',
+                           interpreter = 'script_interpreter',
+                           workdir = 'prefix',
+                           subdir = 'subdir',
+                           verbose = Boolean,
+                           match = default_match_function,
+                           combine = Boolean)
+
+There are a bunch of methods that let you do a bunch of different
+things.  Here is an overview of them:
+
+    test.verbose_set(1)
+
+    test.description_set('string')
+
+    test.program_set('program_or_script_to_test')
+
+    test.interpreter_set('script_interpreter')
+    test.interpreter_set(['script_interpreter', 'arg'])
+
+    test.workdir_set('prefix')
+    test.workdir_set('')
+
+    test.workpath('file')
+    test.workpath('subdir', 'file')
+
+    test.subdir('subdir', ...)
+
+    test.write('file', "contents\n")
+    test.write(['subdir', 'file'], "contents\n")
+
+    test.read('file')
+    test.read(['subdir', 'file'])
+    test.read('file', mode)
+    test.read(['subdir', 'file'], mode)
+
+    test.writable('dir', 1)
+    test.writable('dir', None)
+
+    test.preserve(condition, ...)
+
+    test.cleanup(condition)
+
+    test.run(program = 'program_or_script_to_run',
+             interpreter = 'script_interpreter',
+             arguments = 'arguments to pass to program',
+             chdir = 'directory_to_chdir_to',
+             stdin = 'input to feed to the program\n')
+
+    test.pass_test()
+    test.pass_test(condition)
+    test.pass_test(condition, function)
+
+    test.fail_test()
+    test.fail_test(condition)
+    test.fail_test(condition, function)
+    test.fail_test(condition, function, skip)
+
+    test.no_result()
+    test.no_result(condition)
+    test.no_result(condition, function)
+    test.no_result(condition, function, skip)
+
+    test.stdout()
+    test.stdout(run)
+
+    test.stderr()
+    test.stderr(run)
+
+    test.symlink(target, link)
+
+    test.match(actual, expected)
+
+    test.match_exact("actual 1\nactual 2\n", "expected 1\nexpected 2\n")
+    test.match_exact(["actual 1\n", "actual 2\n"],
+                     ["expected 1\n", "expected 2\n"])
+
+    test.match_re("actual 1\nactual 2\n", regex_string)
+    test.match_re(["actual 1\n", "actual 2\n"], list_of_regexes)
+
+    test.match_re_dotall("actual 1\nactual 2\n", regex_string)
+    test.match_re_dotall(["actual 1\n", "actual 2\n"], list_of_regexes)
+
+    test.sleep()
+    test.sleep(seconds)
+
+    test.where_is('foo')
+    test.where_is('foo', 'PATH1:PATH2')
+    test.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
+
+    test.unlink('file')
+    test.unlink('subdir', 'file')
+
+The TestCmd module provides pass_test(), fail_test(), and no_result()
+unbound functions that report test results for use with the Aegis change
+management system.  These methods terminate the test immediately,
+reporting PASSED, FAILED, or NO RESULT respectively, and exiting with
+status 0 (success), 1 or 2 respectively.  This allows for a distinction
+between an actual failed test and a test that could not be properly
+evaluated because of an external condition (such as a full file system
+or incorrect permissions).
+
+    import TestCmd
+
+    TestCmd.pass_test()
+    TestCmd.pass_test(condition)
+    TestCmd.pass_test(condition, function)
+
+    TestCmd.fail_test()
+    TestCmd.fail_test(condition)
+    TestCmd.fail_test(condition, function)
+    TestCmd.fail_test(condition, function, skip)
+
+    TestCmd.no_result()
+    TestCmd.no_result(condition)
+    TestCmd.no_result(condition, function)
+    TestCmd.no_result(condition, function, skip)
+
+The TestCmd module also provides unbound functions that handle matching
+in the same way as the match_*() methods described above.
+
+    import TestCmd
+
+    test = TestCmd.TestCmd(match = TestCmd.match_exact)
+
+    test = TestCmd.TestCmd(match = TestCmd.match_re)
+
+    test = TestCmd.TestCmd(match = TestCmd.match_re_dotall)
+
+Lastly, the where_is() method also exists in an unbound function
+version.
+
+    import TestCmd
+
+    TestCmd.where_is('foo')
+    TestCmd.where_is('foo', 'PATH1:PATH2')
+    TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
+"""
+
+# Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
+# This module is free software, and you may redistribute it and/or modify
+# it under the same terms as Python itself, so long as this copyright message
+# and disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
+# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
+# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+#
+# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
+# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+__author__ = "Steven Knight <knight at baldmt dot com>"
+__revision__ = "TestCmd.py 0.22.D001 2006/02/26 15:45:18 knight"
+__version__ = "0.22"
+
+import os
+import os.path
+import popen2
+import re
+import shutil
+import stat
+import string
+import sys
+import tempfile
+import time
+import traceback
+import types
+import UserList
+
+__all__ = [ 'fail_test', 'no_result', 'pass_test',
+            'match_exact', 'match_re', 'match_re_dotall',
+            'python_executable', 'TestCmd' ]
+
+def is_List(e):
+    return type(e) is types.ListType \
+        or isinstance(e, UserList.UserList)
+
+try:
+    from UserString import UserString
+except ImportError:
+    class UserString:
+        pass
+
+if hasattr(types, 'UnicodeType'):
+    def is_String(e):
+        return type(e) is types.StringType \
+            or type(e) is types.UnicodeType \
+            or isinstance(e, UserString)
+else:
+    def is_String(e):
+        return type(e) is types.StringType or isinstance(e, UserString)
+
+tempfile.template = 'testcmd.'
+
+re_space = re.compile('\s')
+
+_Cleanup = []
+
+def _clean():
+    global _Cleanup
+    cleanlist = filter(None, _Cleanup)
+    del _Cleanup[:]
+    cleanlist.reverse()
+    for test in cleanlist:
+        test.cleanup()
+
+sys.exitfunc = _clean
+
+class Collector:
+    def __init__(self, top):
+        self.entries = [top]
+    def __call__(self, arg, dirname, names):
+        pathjoin = lambda n, d=dirname: os.path.join(d, n)
+        self.entries.extend(map(pathjoin, names))
+
+def _caller(tblist, skip):
+    string = ""
+    arr = []
+    for file, line, name, text in tblist:
+        if file[-10:] == "TestCmd.py":
+                break
+        arr = [(file, line, name, text)] + arr
+    atfrom = "at"
+    for file, line, name, text in arr[skip:]:
+        if name == "?":
+            name = ""
+        else:
+            name = " (" + name + ")"
+        string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
+        atfrom = "\tfrom"
+    return string
+
+def fail_test(self = None, condition = 1, function = None, skip = 0):
+    """Cause the test to fail.
+
+    By default, the fail_test() method reports that the test FAILED
+    and exits with a status of 1.  If a condition argument is supplied,
+    the test fails only if the condition is true.
+    """
+    if not condition:
+        return
+    if not function is None:
+        function()
+    of = ""
+    desc = ""
+    sep = " "
+    if not self is None:
+        if self.program:
+            of = " of " + self.program
+            sep = "\n\t"
+        if self.description:
+            desc = " [" + self.description + "]"
+            sep = "\n\t"
+
+    at = _caller(traceback.extract_stack(), skip)
+    sys.stderr.write("FAILED test" + of + desc + sep + at)
+
+    sys.exit(1)
+
+def no_result(self = None, condition = 1, function = None, skip = 0):
+    """Causes a test to exit with no valid result.
+
+    By default, the no_result() method reports NO RESULT for the test
+    and exits with a status of 2.  If a condition argument is supplied,
+    the test fails only if the condition is true.
+    """
+    if not condition:
+        return
+    if not function is None:
+        function()
+    of = ""
+    desc = ""
+    sep = " "
+    if not self is None:
+        if self.program:
+            of = " of " + self.program
+            sep = "\n\t"
+        if self.description:
+            desc = " [" + self.description + "]"
+            sep = "\n\t"
+
+    at = _caller(traceback.extract_stack(), skip)
+    sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
+
+    sys.exit(2)
+
+def pass_test(self = None, condition = 1, function = None):
+    """Causes a test to pass.
+
+    By default, the pass_test() method reports PASSED for the test
+    and exits with a status of 0.  If a condition argument is supplied,
+    the test passes only if the condition is true.
+    """
+    if not condition:
+        return
+    if not function is None:
+        function()
+    sys.stderr.write("PASSED\n")
+    sys.exit(0)
+
+def match_exact(lines = None, matches = None):
+    """
+    """
+    if not is_List(lines):
+        lines = string.split(lines, "\n")
+    if not is_List(matches):
+        matches = string.split(matches, "\n")
+    if len(lines) != len(matches):
+        return
+    for i in range(len(lines)):
+        if lines[i] != matches[i]:
+            return
+    return 1
+
+def match_re(lines = None, res = None):
+    """
+    """
+    if not is_List(lines):
+        lines = string.split(lines, "\n")
+    if not is_List(res):
+        res = string.split(res, "\n")
+    if len(lines) != len(res):
+        return
+    for i in range(len(lines)):
+        if not re.compile("^" + res[i] + "$").search(lines[i]):
+            return
+    return 1
+
+def match_re_dotall(lines = None, res = None):
+    """
+    """
+    if not type(lines) is type(""):
+        lines = string.join(lines, "\n")
+    if not type(res) is type(""):
+        res = string.join(res, "\n")
+    if re.compile("^" + res + "$", re.DOTALL).match(lines):
+        return 1
+
+if os.name == 'java':
+
+    python_executable = os.path.join(sys.prefix, 'jython')
+
+else:
+
+    python_executable = sys.executable
+
+if sys.platform == 'win32':
+
+    default_sleep_seconds = 2
+
+    def where_is(file, path=None, pathext=None):
+        if path is None:
+            path = os.environ['PATH']
+        if is_String(path):
+            path = string.split(path, os.pathsep)
+        if pathext is None:
+            pathext = os.environ['PATHEXT']
+        if is_String(pathext):
+            pathext = string.split(pathext, os.pathsep)
+        for ext in pathext:
+            if string.lower(ext) == string.lower(file[-len(ext):]):
+                pathext = ['']
+                break
+        for dir in path:
+            f = os.path.join(dir, file)
+            for ext in pathext:
+                fext = f + ext
+                if os.path.isfile(fext):
+                    return fext
+        return None
+
+else:
+
+    def where_is(file, path=None, pathext=None):
+        if path is None:
+            path = os.environ['PATH']
+        if is_String(path):
+            path = string.split(path, os.pathsep)
+        for dir in path:
+            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
+
+    default_sleep_seconds = 1
+
+class TestCmd:
+    """Class TestCmd
+    """
+
+    def __init__(self, description = None,
+                       program = None,
+                       interpreter = None,
+                       workdir = None,
+                       subdir = None,
+                       verbose = None,
+                       match = None,
+                       combine = 0):
+        self._cwd = os.getcwd()
+        self.description_set(description)
+        self.program_set(program)
+        self.interpreter_set(interpreter)
+        if verbose is None:
+            try:
+                verbose = max( 0, int(os.environ.get('TESTCMD_VERBOSE', 0)) )
+            except ValueError:
+                verbose = 0
+        self.verbose_set(verbose)
+        self.combine = combine
+        if not match is None:
+            self.match_func = match
+        else:
+            self.match_func = match_re
+        self._dirlist = []
+        self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
+        if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
+            self._preserve['pass_test'] = os.environ['PRESERVE']
+            self._preserve['fail_test'] = os.environ['PRESERVE']
+            self._preserve['no_result'] = os.environ['PRESERVE']
+        else:
+            try:
+                self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
+            except KeyError:
+                pass
+            try:
+                self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
+            except KeyError:
+                pass
+            try:
+                self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
+            except KeyError:
+                pass
+        self._stdout = []
+        self._stderr = []
+        self.status = None
+        self.condition = 'no_result'
+        self.workdir_set(workdir)
+        self.subdir(subdir)
+
+    def __del__(self):
+        self.cleanup()
+
+    def __repr__(self):
+        return "%x" % id(self)
+
+    if os.name == 'posix':
+
+        def escape(self, arg):
+            "escape shell special characters"
+            slash = '\\'
+            special = '"$'
+
+            arg = string.replace(arg, slash, slash+slash)
+            for c in special:
+                arg = string.replace(arg, c, slash+c)
+
+            if re_space.search(arg):
+                arg = '"' + arg + '"'
+            return arg
+
+    else:
+
+        # Windows does not allow special characters in file names
+        # anyway, so no need for an escape function, we will just quote
+        # the arg.
+        def escape(self, arg):
+            if re_space.search(arg):
+                arg = '"' + arg + '"'
+            return arg
+
+    def canonicalize(self, path):
+        if is_List(path):
+            path = apply(os.path.join, tuple(path))
+        if not os.path.isabs(path):
+            path = os.path.join(self.workdir, path)
+        return path
+
+    def cleanup(self, condition = None):
+        """Removes any temporary working directories for the specified
+        TestCmd environment.  If the environment variable PRESERVE was
+        set when the TestCmd environment was created, temporary working
+        directories are not removed.  If any of the environment variables
+        PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set
+        when the TestCmd environment was created, then temporary working
+        directories are not removed if the test passed, failed, or had
+        no result, respectively.  Temporary working directories are also
+        preserved for conditions specified via the preserve method.
+
+        Typically, this method is not called directly, but is used when
+        the script exits to clean up temporary working directories as
+        appropriate for the exit status.
+        """
+        if not self._dirlist:
+            return
+        os.chdir(self._cwd)
+        self.workdir = None
+        if condition is None:
+            condition = self.condition
+        if self._preserve[condition]:
+            for dir in self._dirlist:
+                print "Preserved directory", dir
+        else:
+            list = self._dirlist[:]
+            list.reverse()
+            for dir in list:
+                self.writable(dir, 1)
+                shutil.rmtree(dir, ignore_errors = 1)
+            self._dirlist = []
+
+        try:
+            global _Cleanup
+            _Cleanup.remove(self)
+        except (AttributeError, ValueError):
+            pass
+
+    def chmod(self, path, mode):
+        """Changes permissions on the specified file or directory
+        path name."""
+        path = self.canonicalize(path)
+        os.chmod(path, mode)
+
+    def description_set(self, description):
+        """Set the description of the functionality being tested.
+        """
+        self.description = description
+
+#    def diff(self):
+#        """Diff two arrays.
+#        """
+
+    def fail_test(self, condition = 1, function = None, skip = 0):
+        """Cause the test to fail.
+        """
+        if not condition:
+            return
+        self.condition = 'fail_test'
+        fail_test(self = self,
+                  condition = condition,
+                  function = function,
+                  skip = skip)
+
+    def interpreter_set(self, interpreter):
+        """Set the program to be used to interpret the program
+        under test as a script.
+        """
+        self.interpreter = interpreter
+
+    def match(self, lines, matches):
+        """Compare actual and expected file contents.
+        """
+        return self.match_func(lines, matches)
+
+    def match_exact(self, lines, matches):
+        """Compare actual and expected file contents.
+        """
+        return match_exact(lines, matches)
+
+    def match_re(self, lines, res):
+        """Compare actual and expected file contents.
+        """
+        return match_re(lines, res)
+
+    def match_re_dotall(self, lines, res):
+        """Compare actual and expected file contents.
+        """
+        return match_re_dotall(lines, res)
+
+    def no_result(self, condition = 1, function = None, skip = 0):
+        """Report that the test could not be run.
+        """
+        if not condition:
+            return
+        self.condition = 'no_result'
+        no_result(self = self,
+                  condition = condition,
+                  function = function,
+                  skip = skip)
+
+    def pass_test(self, condition = 1, function = None):
+        """Cause the test to pass.
+        """
+        if not condition:
+            return
+        self.condition = 'pass_test'
+        pass_test(self = self, condition = condition, function = function)
+
+    def preserve(self, *conditions):
+        """Arrange for the temporary working directories for the
+        specified TestCmd environment to be preserved for one or more
+        conditions.  If no conditions are specified, arranges for
+        the temporary working directories to be preserved for all
+        conditions.
+        """
+        if conditions is ():
+            conditions = ('pass_test', 'fail_test', 'no_result')
+        for cond in conditions:
+            self._preserve[cond] = 1
+
+    def program_set(self, program):
+        """Set the executable program or script to be tested.
+        """
+        if program and not os.path.isabs(program):
+            program = os.path.join(self._cwd, program)
+        self.program = program
+
+    def read(self, file, mode = 'rb'):
+        """Reads and returns the contents of the specified file name.
+        The file name may be a list, in which case the elements are
+        concatenated with the os.path.join() method.  The file is
+        assumed to be under the temporary working directory unless it
+        is an absolute path name.  The I/O mode for the file may
+        be specified; it must begin with an 'r'.  The default is
+        'rb' (binary read).
+        """
+        file = self.canonicalize(file)
+        if mode[0] != 'r':
+            raise ValueError, "mode must begin with 'r'"
+        return open(file, mode).read()
+
+    def run(self, program = None,
+                  interpreter = None,
+                  arguments = None,
+                  chdir = None,
+                  stdin = None):
+        """Runs a test of the program or script for the test
+        environment.  Standard output and error output are saved for
+        future retrieval via the stdout() and stderr() methods.
+
+        The specified program will have the original directory
+        prepending unless it is enclosed in a [list].
+        """
+        if chdir:
+            oldcwd = os.getcwd()
+            if not os.path.isabs(chdir):
+                chdir = os.path.join(self.workpath(chdir))
+            if self.verbose:
+                sys.stderr.write("chdir(" + chdir + ")\n")
+            os.chdir(chdir)
+        if program:
+            if type(program) == type('') and not os.path.isabs(program):
+                program = os.path.join(self._cwd, program)
+        else:
+            program = self.program
+            if not interpreter:
+                interpreter = self.interpreter
+        if not type(program) in [type([]), type(())]:
+            program = [program]
+        cmd = list(program)
+        if interpreter:
+            if not type(interpreter) in [type([]), type(())]:
+                interpreter = [interpreter]
+            cmd = list(interpreter) + cmd
+        if arguments:
+            if type(arguments) == type(''):
+                arguments = string.split(arguments)
+            cmd.extend(arguments)
+        cmd_string = string.join(map(self.escape, cmd), ' ')
+        if self.verbose:
+            sys.stderr.write(cmd_string + "\n")
+        try:
+            p = popen2.Popen3(cmd, 1)
+        except AttributeError:
+            if sys.platform == 'win32' and cmd_string[0] == '"':
+                cmd_string = '"' + cmd_string + '"'
+            (tochild, fromchild, childerr) = os.popen3(' ' + cmd_string)
+            if stdin:
+                if is_List(stdin):
+                    for line in stdin:
+                        tochild.write(line)
+                else:
+                    tochild.write(stdin)
+            tochild.close()
+            out = fromchild.read()
+            err = childerr.read()
+            if self.combine:
+                self._stdout.append(out + err)
+            else:
+                self._stdout.append(out)
+                self._stderr.append(err)
+            fromchild.close()
+            self.status = childerr.close()
+            if not self.status:
+                self.status = 0
+        except:
+            raise
+        else:
+            if stdin:
+                if is_List(stdin):
+                    for line in stdin:
+                        p.tochild.write(line)
+                else:
+                    p.tochild.write(stdin)
+            p.tochild.close()
+            out = p.fromchild.read()
+            err = p.childerr.read()
+            if self.combine:
+                self._stdout.append(out + err)
+            else:
+                self._stdout.append(out)
+                self._stderr.append(err)
+            self.status = p.wait()
+        if chdir:
+            os.chdir(oldcwd)
+        if self.verbose >= 2:
+            write = sys.stdout.write
+            write('============ STATUS: %d\n' % self.status)
+            out = self.stdout()
+            if out or self.verbose >= 3:
+                write('============ BEGIN STDOUT (len=%d):\n' % len(out))
+                write(out)
+                write('============ END STDOUT\n')
+            err = self.stderr()
+            if err or self.verbose >= 3:
+                write('============ BEGIN STDERR (len=%d)\n' % len(err))
+                write(err)
+                write('============ END STDERR\n')
+
+    def sleep(self, seconds = default_sleep_seconds):
+        """Sleeps at least the specified number of seconds.  If no
+        number is specified, sleeps at least the minimum number of
+        seconds necessary to advance file time stamps on the current
+        system.  Sleeping more seconds is all right.
+        """
+        time.sleep(seconds)
+
+    def stderr(self, run = None):
+        """Returns the error output from the specified run number.
+        If there is no specified run number, then returns the error
+        output of the last run.  If the run number is less than zero,
+        then returns the error output from that many runs back from the
+        current run.
+        """
+        if not run:
+            run = len(self._stderr)
+        elif run < 0:
+            run = len(self._stderr) + run
+        run = run - 1
+        return self._stderr[run]
+
+    def stdout(self, run = None):
+        """Returns the standard output from the specified run number.
+        If there is no specified run number, then returns the standard
+        output of the last run.  If the run number is less than zero,
+        then returns the standard output from that many runs back from
+        the current run.
+        """
+        if not run:
+            run = len(self._stdout)
+        elif run < 0:
+            run = len(self._stdout) + run
+        run = run - 1
+        return self._stdout[run]
+
+    def subdir(self, *subdirs):
+        """Create new subdirectories under the temporary working
+        directory, one for each argument.  An argument may be a list,
+        in which case the list elements are concatenated using the
+        os.path.join() method.  Subdirectories multiple levels deep
+        must be created using a separate argument for each level:
+
+                test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
+
+        Returns the number of subdirectories actually created.
+        """
+        count = 0
+        for sub in subdirs:
+            if sub is None:
+                continue
+            if is_List(sub):
+                sub = apply(os.path.join, tuple(sub))
+            new = os.path.join(self.workdir, sub)
+            try:
+                os.mkdir(new)
+            except OSError:
+                pass
+            else:
+                count = count + 1
+        return count
+
+    def symlink(self, target, link):
+        """Creates a symlink to the specified target.
+        The link name may be a list, in which case the elements are
+        concatenated with the os.path.join() method.  The link is
+        assumed to be under the temporary working directory unless it
+        is an absolute path name. The target is *not* assumed to be
+        under the temporary working directory.
+        """
+        link = self.canonicalize(link)
+        os.symlink(target, link)
+
+    def touch(self, path, mtime=None):
+        """Updates the modification time on the specified file or
+        directory path name.  The default is to update to the
+        current time if no explicit modification time is specified.
+        """
+        path = self.canonicalize(path)
+        atime = os.path.getatime(path)
+        if mtime is None:
+            mtime = time.time()
+        os.utime(path, (atime, mtime))
+
+    def unlink(self, file):
+        """Unlinks the specified file name.
+        The file name may be a list, in which case the elements are
+        concatenated with the os.path.join() method.  The file is
+        assumed to be under the temporary working directory unless it
+        is an absolute path name.
+        """
+        file = self.canonicalize(file)
+        os.unlink(file)
+
+    def verbose_set(self, verbose):
+        """Set the verbose level.
+        """
+        self.verbose = verbose
+
+    def where_is(self, file, path=None, pathext=None):
+        """Find an executable file.
+        """
+        if is_List(file):
+            file = apply(os.path.join, tuple(file))
+        if not os.path.isabs(file):
+            file = where_is(file, path, pathext)
+        return file
+
+    def workdir_set(self, path):
+        """Creates a temporary working directory with the specified
+        path name.  If the path is a null string (''), a unique
+        directory name is created.
+        """
+        if (path != None):
+            if path == '':
+                path = tempfile.mktemp()
+            if path != None:
+                os.mkdir(path)
+            # We'd like to set self.workdir like this:
+            #     self.workdir = path
+            # But symlinks in the path will report things
+            # differently from os.getcwd(), so chdir there
+            # and back to fetch the canonical path.
+            cwd = os.getcwd()
+            os.chdir(path)
+            self.workdir = os.getcwd()
+            os.chdir(cwd)
+            # Uppercase the drive letter since the case of drive
+            # letters is pretty much random on win32:
+            drive,rest = os.path.splitdrive(self.workdir)
+            if drive:
+                self.workdir = string.upper(drive) + rest
+            #
+            self._dirlist.append(self.workdir)
+            global _Cleanup
+            try:
+                _Cleanup.index(self)
+            except ValueError:
+                _Cleanup.append(self)
+        else:
+            self.workdir = None
+
+    def workpath(self, *args):
+        """Returns the absolute path name to a subdirectory or file
+        within the current temporary working directory.  Concatenates
+        the temporary working directory name with the specified
+        arguments using the os.path.join() method.
+        """
+        return apply(os.path.join, (self.workdir,) + tuple(args))
+
+    def readable(self, top, read=1):
+        """Make the specified directory tree readable (read == 1)
+        or not (read == None).
+        """
+
+        if read:
+            def do_chmod(fname):
+                try: st = os.stat(fname)
+                except OSError: pass
+                else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0400))
+        else:
+            def do_chmod(fname):
+                try: st = os.stat(fname)
+                except OSError: pass
+                else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0400))
+
+        if os.path.isfile(top):
+            # If it's a file, that's easy, just chmod it.
+            do_chmod(top)
+        elif read:
+            # It's a directory and we're trying to turn on read
+            # permission, so it's also pretty easy, just chmod the
+            # directory and then chmod every entry on our walk down the
+            # tree.  Because os.path.walk() is top-down, we'll enable
+            # read permission on any directories that have it disabled
+            # before os.path.walk() tries to list their contents.
+            do_chmod(top)
+
+            def chmod_entries(arg, dirname, names, do_chmod=do_chmod):
+                pathnames = map(lambda n, d=dirname: os.path.join(d, n),
+                                names)
+                map(lambda p, do=do_chmod: do(p), pathnames)
+
+            os.path.walk(top, chmod_entries, None)
+        else:
+            # It's a directory and we're trying to turn off read
+            # permission, which means we have to chmod the directoreis
+            # in the tree bottom-up, lest disabling read permission from
+            # the top down get in the way of being able to get at lower
+            # parts of the tree.  But os.path.walk() visits things top
+            # down, so we just use an object to collect a list of all
+            # of the entries in the tree, reverse the list, and then
+            # chmod the reversed (bottom-up) list.
+            col = Collector(top)
+            os.path.walk(top, col, None)
+            col.entries.reverse()
+            map(lambda d, do=do_chmod: do(d), col.entries)
+
+    def writable(self, top, write=1):
+        """Make the specified directory tree writable (write == 1)
+        or not (write == None).
+        """
+
+        if write:
+            def do_chmod(fname):
+                try: st = os.stat(fname)
+                except OSError: pass
+                else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0200))
+        else:
+            def do_chmod(fname):
+                try: st = os.stat(fname)
+                except OSError: pass
+                else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0200))
+
+        if os.path.isfile(top):
+            do_chmod(top)
+        else:
+            col = Collector(top)
+            os.path.walk(top, col, None)
+            map(lambda d, do=do_chmod: do(d), col.entries)
+
+    def executable(self, top, execute=1):
+        """Make the specified directory tree executable (execute == 1)
+        or not (execute == None).
+        """
+
+        if execute:
+            def do_chmod(fname):
+                try: st = os.stat(fname)
+                except OSError: pass
+                else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0100))
+        else:
+            def do_chmod(fname):
+                try: st = os.stat(fname)
+                except OSError: pass
+                else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0100))
+
+        if os.path.isfile(top):
+            # If it's a file, that's easy, just chmod it.
+            do_chmod(top)
+        elif execute:
+            # It's a directory and we're trying to turn on execute
+            # permission, so it's also pretty easy, just chmod the
+            # directory and then chmod every entry on our walk down the
+            # tree.  Because os.path.walk() is top-down, we'll enable
+            # execute permission on any directories that have it disabled
+            # before os.path.walk() tries to list their contents.
+            do_chmod(top)
+
+            def chmod_entries(arg, dirname, names, do_chmod=do_chmod):
+                pathnames = map(lambda n, d=dirname: os.path.join(d, n),
+                                names)
+                map(lambda p, do=do_chmod: do(p), pathnames)
+
+            os.path.walk(top, chmod_entries, None)
+        else:
+            # It's a directory and we're trying to turn off execute
+            # permission, which means we have to chmod the directories
+            # in the tree bottom-up, lest disabling execute permission from
+            # the top down get in the way of being able to get at lower
+            # parts of the tree.  But os.path.walk() visits things top
+            # down, so we just use an object to collect a list of all
+            # of the entries in the tree, reverse the list, and then
+            # chmod the reversed (bottom-up) list.
+            col = Collector(top)
+            os.path.walk(top, col, None)
+            col.entries.reverse()
+            map(lambda d, do=do_chmod: do(d), col.entries)
+
+    def write(self, file, content, mode = 'wb'):
+        """Writes the specified content text (second argument) to the
+        specified file name (first argument).  The file name may be
+        a list, in which case the elements are concatenated with the
+        os.path.join() method.  The file is created under the temporary
+        working directory.  Any subdirectories in the path must already
+        exist.  The I/O mode for the file may be specified; it must
+        begin with a 'w'.  The default is 'wb' (binary write).
+        """
+        file = self.canonicalize(file)
+        if mode[0] != 'w':
+            raise ValueError, "mode must begin with 'w'"
+        open(file, mode).write(content)

QMTest/TestCommon.py

+"""
+TestCommon.py:  a testing framework for commands and scripts
+                with commonly useful error handling
+
+The TestCommon module provides a simple, high-level interface for writing
+tests of executable commands and scripts, especially commands and scripts
+that interact with the file system.  All methods throw exceptions and
+exit on failure, with useful error messages.  This makes a number of
+explicit checks unnecessary, making the test scripts themselves simpler
+to write and easier to read.
+
+The TestCommon class is a subclass of the TestCmd class.  In essence,
+TestCommon is a wrapper that handles common TestCmd error conditions in
+useful ways.  You can use TestCommon directly, or subclass it for your
+program and add additional (or override) methods to tailor it to your
+program's specific needs.  Alternatively, the TestCommon class serves
+as a useful example of how to define your own TestCmd subclass.
+
+As a subclass of TestCmd, TestCommon provides access to all of the
+variables and methods from the TestCmd module.  Consequently, you can
+use any variable or method documented in the TestCmd module without
+having to explicitly import TestCmd.
+
+A TestCommon environment object is created via the usual invocation:
+
+    import TestCommon
+    test = TestCommon.TestCommon()
+
+You can use all of the TestCmd keyword arguments when instantiating a
+TestCommon object; see the TestCmd documentation for details.
+
+Here is an overview of the methods and keyword arguments that are
+provided by the TestCommon class:
+
+    test.must_be_writable('file1', ['file2', ...])
+
+    test.must_contain('file', 'required text\n')
+
+    test.must_exist('file1', ['file2', ...])
+
+    test.must_match('file', "expected contents\n")
+
+    test.must_not_be_writable('file1', ['file2', ...])
+
+    test.must_not_exist('file1', ['file2', ...])
+
+    test.run(options = "options to be prepended to arguments",
+             stdout = "expected standard output from the program",
+             stderr = "expected error output from the program",
+             status = expected_status,
+             match = match_function)
+
+The TestCommon module also provides the following variables
+
+    TestCommon.python_executable
+    TestCommon.exe_suffix
+    TestCommon.obj_suffix
+    TestCommon.shobj_suffix
+    TestCommon.lib_prefix
+    TestCommon.lib_suffix
+    TestCommon.dll_prefix
+    TestCommon.dll_suffix
+
+"""
+
+# Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
+# This module is free software, and you may redistribute it and/or modify
+# it under the same terms as Python itself, so long as this copyright message
+# and disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
+# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
+# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+#
+# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
+# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+__author__ = "Steven Knight <knight at baldmt dot com>"
+__revision__ = "TestCommon.py 0.22.D001 2006/02/26 15:45:18 knight"
+__version__ = "0.22"
+
+import os
+import os.path
+import stat
+import string
+import sys
+import types
+import UserList
+
+from TestCmd import *
+from TestCmd import __all__
+
+__all__.extend([ 'TestCommon',
+                 'TestFailed',
+                 'TestNoResult',
+                 'exe_suffix',
+                 'obj_suffix',
+                 'shobj_suffix',
+                 'lib_prefix',
+                 'lib_suffix',
+                 'dll_prefix',
+                 'dll_suffix',
+               ])
+
+# Variables that describe the prefixes and suffixes on this system.
+if sys.platform == 'win32':
+    exe_suffix   = '.exe'
+    obj_suffix   = '.obj'
+    shobj_suffix = '.obj'
+    lib_prefix   = ''
+    lib_suffix   = '.lib'
+    dll_prefix   = ''
+    dll_suffix   = '.dll'
+elif sys.platform == 'cygwin':
+    exe_suffix   = '.exe'
+    obj_suffix   = '.o'
+    shobj_suffix = '.os'
+    lib_prefix   = 'lib'
+    lib_suffix   = '.a'
+    dll_prefix   = ''
+    dll_suffix   = '.dll'
+elif string.find(sys.platform, 'irix') != -1:
+    exe_suffix   = ''
+    obj_suffix   = '.o'
+    shobj_suffix = '.o'
+    lib_prefix   = 'lib'
+    lib_suffix   = '.a'
+    dll_prefix   = 'lib'
+    dll_suffix   = '.so'
+elif string.find(sys.platform, 'darwin') != -1:
+    exe_suffix   = ''
+    obj_suffix   = '.o'
+    shobj_suffix = '.os'
+    lib_prefix   = 'lib'
+    lib_suffix   = '.a'
+    dll_prefix   = 'lib'
+    dll_suffix   = '.dylib'
+else:
+    exe_suffix   = ''
+    obj_suffix   = '.o'
+    shobj_suffix = '.os'
+    lib_prefix   = 'lib'
+    lib_suffix   = '.a'
+    dll_prefix   = 'lib'
+    dll_suffix   = '.so'
+
+try:
+    import difflib
+except ImportError:
+    pass
+else:
+    def simple_diff(a, b, fromfile='', tofile='',
+                    fromfiledate='', tofiledate='', n=3, lineterm='\n'):
+        """
+        A function with the same calling signature as difflib.context_diff
+        (diff -c) and difflib.unified_diff (diff -u) but which prints
+        output like the simple, unadorned 'diff" command.
+        """
+        sm = difflib.SequenceMatcher(None, a, b)
+        def comma(x1, x2):
+            return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2)
+        result = []
+        for op, a1, a2, b1, b2 in sm.get_opcodes():
+            if op == 'delete':
+                result.append("%sd%d" % (comma(a1, a2), b1))
+                result.extend(map(lambda l: '< ' + l, a[a1:a2]))
+            elif op == 'insert':
+                result.append("%da%s" % (a1, comma(b1, b2)))
+                result.extend(map(lambda l: '> ' + l, b[b1:b2]))
+            elif op == 'replace':
+                result.append("%sc%s" % (comma(a1, a2), comma(b1, b2)))
+                result.extend(map(lambda l: '< ' + l, a[a1:a2]))
+                result.append('---')
+                result.extend(map(lambda l: '> ' + l, b[b1:b2]))
+        return result
+
+def is_List(e):
+    return type(e) is types.ListType \
+        or isinstance(e, UserList.UserList)
+
+def is_writable(f):
+    mode = os.stat(f)[stat.ST_MODE]
+    return mode & stat.S_IWUSR
+
+def separate_files(flist):
+    existing = []
+    missing = []
+    for f in flist:
+        if os.path.exists(f):
+            existing.append(f)
+        else:
+            missing.append(f)
+    return existing, missing
+
+class TestFailed(Exception):
+    def __init__(self, args=None):
+        self.args = args
+
+class TestNoResult(Exception):
+    def __init__(self, args=None):
+        self.args = args
+
+if os.name == 'posix':
+    def _failed(self, status = 0):
+        if self.status is None or status is None:
+            return None
+        if os.WIFSIGNALED(self.status):
+            return None
+        return _status(self) != status
+    def _status(self):
+        if os.WIFEXITED(self.status):
+            return os.WEXITSTATUS(self.status)
+        else:
+            return None
+elif os.name == 'nt':
+    def _failed(self, status = 0):
+        return not (self.status is None or status is None) and \
+               self.status != status
+    def _status(self):
+        return self.status
+
+class TestCommon(TestCmd):
+
+    # Additional methods from the Perl Test::Cmd::Common module
+    # that we may wish to add in the future:
+    #
+    #  $test->subdir('subdir', ...);
+    #
+    #  $test->copy('src_file', 'dst_file');
+
+    def __init__(self, **kw):
+        """Initialize a new TestCommon instance.  This involves just
+        calling the base class initialization, and then changing directory
+        to the workdir.
+        """
+        apply(TestCmd.__init__, [self], kw)
+        os.chdir(self.workdir)
+        try:
+            difflib
+        except NameError:
+            pass
+        else:
+            self.diff_function = simple_diff
+            #self.diff_function = difflib.context_diff
+            #self.diff_function = difflib.unified_diff
+
+    banner_char = '='
+    banner_width = 80
+
+    def banner(self, s, width=None):
+        if width is None:
+            width = self.banner_width
+        return s + self.banner_char * (width - len(s))
+
+    try:
+        difflib
+    except NameError:
+        def diff(self, a, b, name, *args, **kw):
+            print self.banner('Expected %s' % name)
+            print a
+            print self.banner('Actual %s' % name)
+            print b
+    else:
+        def diff(self, a, b, name, *args, **kw):
+            print self.banner(name)
+            args = (a.splitlines(), b.splitlines()) + args
+            lines = apply(self.diff_function, args, kw)
+            for l in lines:
+                print l
+
+    def must_be_writable(self, *files):
+        """Ensures that the specified file(s) exist and are writable.
+        An individual file can be specified as a list of directory names,
+        in which case the pathname will be constructed by concatenating
+        them.  Exits FAILED if any of the files does not exist or is
+        not writable.
+        """
+        files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
+        existing, missing = separate_files(files)
+        unwritable = filter(lambda x, iw=is_writable: not iw(x), existing)
+        if missing:
+            print "Missing files: `%s'" % string.join(missing, "', `")
+        if unwritable:
+            print "Unwritable files: `%s'" % string.join(unwritable, "', `")
+        self.fail_test(missing + unwritable)
+
+    def must_contain(self, file, required, mode = 'rb'):
+        """Ensures that the specified file contains the required text.
+        """
+        file_contents = self.read(file, mode)
+        contains = (string.find(file_contents, required) != -1)
+        if not contains:
+            print "File `%s' does not contain required string." % file
+            print self.banner('Required string ')
+            print required
+            print self.banner('%s contents ' % file)
+            print file_contents
+            self.fail_test(not contains)
+
+    def must_exist(self, *files):
+        """Ensures that the specified file(s) must exist.  An individual
+        file be specified as a list of directory names, in which case the
+        pathname will be constructed by concatenating them.  Exits FAILED
+        if any of the files does not exist.
+        """
+        files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
+        missing = filter(lambda x: not os.path.exists(x), files)
+        if missing:
+            print "Missing files: `%s'" % string.join(missing, "', `")
+            self.fail_test(missing)
+
+    def must_match(self, file, expect, mode = 'rb'):
+        """Matches the contents of the specified file (first argument)
+        against the expected contents (second argument).  The expected
+        contents are a list of lines or a string which will be split
+        on newlines.
+        """
+        file_contents = self.read(file, mode)
+        try:
+            self.fail_test(not self.match(file_contents, expect))
+        except KeyboardInterrupt:
+            raise
+        except:
+            print "Unexpected contents of `%s'" % file
+            self.diff(expect, file_contents, 'contents ')
+            raise
+
+    def must_not_exist(self, *files):
+        """Ensures that the specified file(s) must not exist.
+        An individual file be specified as a list of directory names, in
+        which case the pathname will be constructed by concatenating them.
+        Exits FAILED if any of the files exists.
+        """
+        files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
+        existing = filter(os.path.exists, files)
+        if existing:
+            print "Unexpected files exist: `%s'" % string.join(existing, "', `")
+            self.fail_test(existing)
+
+
+    def must_not_be_writable(self, *files):
+        """Ensures that the specified file(s) exist and are not writable.
+        An individual file can be specified as a list of directory names,
+        in which case the pathname will be constructed by concatenating
+        them.  Exits FAILED if any of the files does not exist or is
+        writable.
+        """
+        files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
+        existing, missing = separate_files(files)
+        writable = filter(is_writable, existing)
+        if missing:
+            print "Missing files: `%s'" % string.join(missing, "', `")
+        if writable:
+            print "Writable files: `%s'" % string.join(writable, "', `")
+        self.fail_test(missing + writable)
+
+    def run(self, options = None, arguments = None,
+                  stdout = None, stderr = '', status = 0, **kw):
+        """Runs the program under test, checking that the test succeeded.
+
+        The arguments are the same as the base TestCmd.run() method,
+        with the addition of:
+
+                options Extra options that get appended to the beginning
+                        of the arguments.
+
+                stdout  The expected standard output from
+                        the command.  A value of None means
+                        don't test standard output.
+
+                stderr  The expected error output from
+                        the command.  A value of None means
+                        don't test error output.
+
+                status  The expected exit status from the
+                        command.  A value of None means don't
+                        test exit status.
+
+        By default, this expects a successful exit (status = 0), does
+        not test standard output (stdout = None), and expects that error
+        output is empty (stderr = "").
+        """
+        if options:
+            if arguments is None:
+                arguments = options
+            else:
+                arguments = options + " " + arguments
+        kw['arguments'] = arguments
+        try:
+            match = kw['match']
+            del kw['match']
+        except KeyError:
+            match = self.match
+        try:
+            apply(TestCmd.run, [self], kw)
+        except KeyboardInterrupt:
+            raise
+        except:
+            print self.banner('STDOUT ')
+            print self.stdout()
+            print self.banner('STDERR ')
+            print self.stderr()
+            raise
+        if _failed(self, status):
+            expect = ''
+            if status != 0:
+                expect = " (expected %s)" % str(status)
+            print "%s returned %s%s" % (self.program, str(_status(self)), expect)
+            print self.banner('STDOUT ')
+            print self.stdout()
+            print self.banner('STDERR ')
+            print self.stderr()
+            raise TestFailed
+        if not stdout is None and not match(self.stdout(), stdout):
+            self.diff(stdout, self.stdout(), 'STDOUT ')
+            stderr = self.stderr()
+            if stderr:
+                print self.banner('STDERR ')
+                print stderr
+            raise TestFailed
+        if not stderr is None and not match(self.stderr(), stderr):
+            print self.banner('STDOUT ')
+            print self.stdout()
+            self.diff(stderr, self.stderr(), 'STDERR ')
+            raise TestFailed

QMTest/TestRuntest.py

+"""
+TestRuntest.py:  a testing framework for the runtest.py command used to
+invoke SCons tests.
+
+A TestRuntest environment object is created via the usual invocation:
+
+    test = TestRuntest()
+
+TestRuntest is a subclass of TestCommon, which is in turn is a subclass
+of TestCmd), and hence has available all of the methods and attributes
+from those classes, as well as any overridden or additional methods or
+attributes defined in this subclass.
+"""
+
+# __COPYRIGHT__
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import os.path
+import string
+import shutil
+import sys
+
+from TestCommon import *
+from TestCommon import __all__
+
+__all__.extend([ 'TestRuntest',
+                 'python',
+               ])
+
+python = python_executable
+
+
+failing_test_template = """\
+import sys
+sys.stdout.write('FAILING TEST STDOUT\\n')
+sys.stderr.write('FAILING TEST STDERR\\n')
+sys.exit(1)
+"""
+
+no_result_test_template = """\
+import sys
+sys.stdout.write('NO RESULT TEST STDOUT\\n')
+sys.stderr.write('NO RESULT TEST STDERR\\n')
+sys.exit(2)
+"""
+
+passing_test_template = """\
+import sys
+sys.stdout.write('PASSING TEST STDOUT\\n')
+sys.stderr.write('PASSING TEST STDERR\\n')
+sys.exit(0)
+"""
+
+class TestRuntest(TestCommon):
+    """Class for testing the runtest.py script.
+
+    This provides a common place for initializing Runtest tests,
+    eliminating the need to begin every test with the same repeated
+    initializations.
+    """
+
+    def __init__(self, **kw):
+        """Initialize a Runtest testing object.
+
+        If they're not overridden by keyword arguments, this
+        initializes the object with the following default values:
+
+                program = 'runtest.py'
+                interpreter = ['python', '-tt']
+                match = match_exact
+                workdir = ''
+
+        The workdir value means that, by default, a temporary
+        workspace directory is created for a TestRuntest environment.
+        The superclass TestCommon.__init__() will change directory (chdir)
+        to the workspace directory, so an explicit "chdir = '.'" on all
+        of the run() method calls is not necessary.  This initialization
+        also copies the runtest.py and QMTest/ subdirectory tree to the
+        temporary directory, duplicating how this test infrastructure
+        appears in a normal workspace.
+        """
+        set_workpath_runtest = None
+        if not kw.has_key('program'):
+            kw['program'] = 'runtest.py'
+            set_workpath_runtest = 1
+        if not kw.has_key('interpreter'):
+            kw['interpreter'] = [python, '-tt']
+        if not kw.has_key('match'):
+            kw['match'] = match_exact
+        if not kw.has_key('workdir'):
+            kw['workdir'] = ''
+        orig_cwd = os.getcwd()
+        apply(TestCommon.__init__, [self], kw)
+
+        things_to_copy = [
+            'runtest.py',
+            'QMTest',
+        ]
+
+        dirs = []
+        
+        spe = os.environ.get('SCONS_SOURCE_PATH_EXECUTABLE', orig_cwd)
+        for d in string.split(spe, os.pathsep):
+            dirs.append(os.path.join(d, 'build'))
+            dirs.append(d)
+
+        for thing in things_to_copy:
+            for dir in dirs:
+                t = os.path.join(dir, thing)
+                if os.path.exists(t):
+                    if os.path.isdir(t):
+                        copy_func = shutil.copytree
+                    else:
+                        copy_func = shutil.copyfile
+                    copy_func(t, self.workpath(thing))
+                    break
+
+        if set_workpath_runtest:
+            self.program_set(self.workpath('runtest.py'))
+
+        for key in os.environ.keys():
+            if key[:5] == 'AEGIS':
+                os.environ[key] = ''
+
+        os.environ['PYTHONPATH'] = ''
+        os.environ['SCONS_SOURCE_PATH_EXECUTABLE'] = ''
+
+    def write_failing_test(self, name):
+        self.write(name, failing_test_template)
+
+    def write_no_result_test(self, name):
+        self.write(name, no_result_test_template)
+
+    def write_passing_test(self, name):
+        self.write(name, passing_test_template)

QMTest/TestSCons.py

+"""
+TestSCons.py:  a testing framework for the SCons software construction
+tool.
+
+A TestSCons environment object is created via the usual invocation:
+
+    test = TestSCons()
+
+TestScons is a subclass of TestCommon, which is in turn is a subclass
+of TestCmd), and hence has available all of the methods and attributes
+from those classes, as well as any overridden or additional methods or
+attributes defined in this subclass.
+"""
+
+# __COPYRIGHT__
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import os.path
+import string
+import sys
+
+from TestCommon import *
+from TestCommon import __all__
+
+__all__.extend([ 'TestSCons',
+                 'python',
+                 '_exe',
+                 '_obj',
+                 '_shobj',
+                 'lib_',
+                 '_lib',
+                 'dll_',
+                 '_dll'
+               ])
+
+python = python_executable
+_exe = exe_suffix
+_obj = obj_suffix
+_shobj = shobj_suffix
+_lib = lib_suffix
+lib_ = lib_prefix
+_dll = dll_suffix
+dll_ = dll_prefix
+
+def gccFortranLibs():
+    """Test whether -lfrtbegin is required.  This can probably be done in
+    a more reliable way, but using popen3 is relatively efficient."""
+
+    libs = ['g2c']
+
+    try:
+        import popen2
+        stderr = popen2.popen3('gcc -v')[2]
+    except OSError:
+        return libs
+
+    for l in stderr.readlines():
+        list = string.split(l)
+        if len(list) > 3 and list[:2] == ['gcc', 'version']:
+            if list[2][:2] == '3.':
+                libs = ['frtbegin'] + libs
+                break
+    return libs
+
+
+if sys.platform == 'cygwin':
+    # On Cygwin, os.path.normcase() lies, so just report back the
+    # fact that the underlying Win32 OS is case-insensitive.
+    def case_sensitive_suffixes(s1, s2):
+        return 0
+else:
+    def case_sensitive_suffixes(s1, s2):
+        return (os.path.normcase(s1) != os.path.normcase(s2))
+
+
+if sys.platform == 'win32':
+    fortran_lib = gccFortranLibs()
+elif sys.platform == 'cygwin':
+    fortran_lib = gccFortranLibs()
+elif string.find(sys.platform, 'irix') != -1:
+    fortran_lib = ['ftn']
+else:
+    fortran_lib = gccFortranLibs()
+
+
+
+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)
+    return str
+
+
+
+class TestSCons(TestCommon):
+    """Class for testing SCons.
+
+    This provides a common place for initializing SCons tests,
+    eliminating the need to begin every test with the same repeated
+    initializations.
+    """
+
+    def __init__(self, **kw):
+        """Initialize an SCons testing object.
+
+        If they're not overridden by keyword arguments, this
+        initializes the object with the following default values:
+
+                program = 'scons' if it exists,
+                          else 'scons.py'
+                interpreter = 'python'
+                match = match_exact
+                workdir = ''
+
+        The workdir value means that, by default, a temporary workspace
+        directory is created for a TestSCons environment.  In addition,
+        this method changes directory (chdir) to the workspace directory,
+        so an explicit "chdir = '.'" on all of the run() method calls
+        is not necessary.
+        """
+        self.orig_cwd = os.getcwd()
+        try:
+            script_dir = os.environ['SCONS_SCRIPT_DIR']
+        except KeyError:
+            pass
+        else:
+            os.chdir(script_dir)
+        if not kw.has_key('program'):
+            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'):
+            kw['interpreter'] = [python, '-tt']
+        if not kw.has_key('match'):
+            kw['match'] = match_exact
+        if not kw.has_key('workdir'):
+            kw['workdir'] = ''
+        apply(TestCommon.__init__, [self], kw)
+
+    def Environment(self, ENV=None, *args, **kw):
+        """
+        Return a construction Environment that optionally overrides
+        the default external environment with the specified ENV.
+        """
+        import SCons.Environment
+        import SCons.Errors
+        if not ENV is None:
+            kw['ENV'] = ENV
+        try:
+            return apply(SCons.Environment.Environment, args, kw)
+        except (SCons.Errors.UserError, SCons.Errors.InternalError):
+            return None
+
+    def detect(self, var, prog=None, ENV=None):
+        """
+        Detect a program named 'prog' by first checking the construction
+        variable named 'var' and finally searching the path used by
+        SCons. If either method fails to detect the program, then false
+        is returned, otherwise the full path to prog is returned. If
+        prog is None, then the value of the environment variable will be
+        used as prog.
+        """
+        env = self.Environment(ENV)
+        v = env.subst('$'+var)
+        if not v:
+            return None
+        if prog is None:
+            prog = v
+        if v != prog:
+            return None
+        return env.WhereIs(prog)
+
+    def detect_tool(self, tool, prog=None, ENV=None):
+        """
+        Given a tool (i.e., tool specification that would be passed
+        to the "tools=" parameter of Environment()) and a program that
+        corresponds to that tool, return true if and only if we can find
+        that tool using Environment.Detect().
+
+        By default, prog is set to the value passed into the tools parameter.
+        """
+
+        if not prog:
+            prog = tool
+        env = self.Environment(ENV, tools=[tool])
+        if env is None:
+            return None
+        return env.Detect([prog])
+
+    def where_is(self, prog, path=None):
+        """
+        Given a program, search for it in the specified external PATH,
+        or in the actual external PATH is none is specified.
+        """
+        import SCons.Environment
+        env = SCons.Environment.Environment()
+        if path is None:
+            path = os.environ['PATH']
+        return env.WhereIs(prog, path)
+
+    def wrap_stdout(self, build_str = "", read_str = "", error = 0, cleaning = 0):
+        """Wraps standard output string(s) in the normal
+        "Reading ... done" and "Building ... done" strings
+        """
+        cap,lc = [ ('Build','build'),
+                   ('Clean','clean') ][cleaning]
+        if error:
+            term = "scons: %sing terminated because of errors.\n" % lc
+        else:
+            term = "scons: done %sing targets.\n" % lc
+        return "scons: Reading SConscript files ...\n" + \
+               read_str + \
+               "scons: done reading SConscript files.\n" + \
+               "scons: %sing targets ...\n" % cap + \
+               build_str + \
+               term
+
+    def up_to_date(self, options = None, arguments = None, read_str = "", **kw):
+        s = ""
+        for arg in string.split(arguments):
+            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['match'] = self.match_exact
+        apply(self.run, [], kw)
+
+    def not_up_to_date(self, options = None, arguments = None, **kw):
+        """Asserts that none of the targets listed in arguments is
+        up to date, but does not make any assumptions on other targets.
+        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
+            if options:
+                arguments = options + " " + arguments
+        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'],'.','\\.')
+        kw['match'] = self.match_re_dotall
+        apply(self.run, [], kw)
+
+    def skip_test(self, message="Skipping test.\n"):
+        """Skips a test.
+
+        Proper test-skipping behavior is dependent on whether we're being
+        executed as part of development of a change under Aegis.
+
+        Technically, skipping a test is a NO RESULT, but Aegis will
+        treat that as a test failure and prevent the change from going
+        to the next step.  We don't want to force anyone using Aegis
+        to have to install absolutely every tool used by the tests,
+        so we actually report to Aegis that a skipped test has PASSED
+        so that the workflow isn't held up.
+        """
+        if message:
+            sys.stdout.write(message)
+            sys.stdout.flush()
+        devdir = os.popen("aesub '$dd' 2>/dev/null", "r").read()[:-1]
+        intdir = os.popen("aesub '$intd' 2>/dev/null", "r").read()[:-1]
+        if devdir and self._cwd[:len(devdir)] == devdir or \
+           intdir and self._cwd[:len(intdir)] == intdir:
+            # We're under the development directory for this change,
+            # so this is an Aegis invocation; pass the test (exit 0).
+            self.pass_test()
+        else:
+            # skip=1 means skip this function when showing where this
+            # result came from.  They only care about the line where the
+            # script called test.skip_test(), not the line number where
+            # we call test.no_result().
+            self.no_result(skip=1)
+
+    def diff_substr(self, expect, actual):
+        i = 0
+        for x, y in zip(expect, actual):
+            if x != y:
+                return "Actual did not match expect at char %d:\n" \
+                       "    Expect:  %s\n" \
+                       "    Actual:  %s\n" \
+                       % (i, repr(expect[i-20:i+40]), repr(actual[i-20:i+40]))
+            i = i + 1
+        return "Actual matched the expected output???"
+
+    def java_ENV(self):
+        """
+        Return a default external environment that uses a local Java SDK
+        in preference to whatever's found in the default PATH.
+        """
+        import SCons.Environment
+        env = SCons.Environment.Environment()
+        java_path = [
+            '/usr/local/j2sdk1.4.2/bin',
+            '/usr/local/j2sdk1.4.1/bin',
+            '/usr/local/j2sdk1.3.1/bin',
+            '/usr/local/j2sdk1.3.0/bin',
+            '/usr/local/j2sdk1.2.2/bin',
+            '/usr/local/j2sdk1.2/bin',
+            '/usr/local/j2sdk1.1.8/bin',
+            '/usr/local/j2sdk1.1.7/bin',
+            '/usr/local/j2sdk1.1.6/bin',
+            '/usr/local/j2sdk1.1.5/bin',
+            '/usr/local/j2sdk1.1.4/bin',
+            '/usr/local/j2sdk1.1.3/bin',
+            '/usr/local/j2sdk1.1.2/bin',
+            '/usr/local/j2sdk1.1.1/bin',
+            env['ENV']['PATH'],
+        ]
+        env['ENV']['PATH'] = string.join(java_path, os.pathsep)
+        return env['ENV']
+
+    def Qt_dummy_installation(self, dir='qt'):
+        # create a dummy qt installation
+
+        self.subdir( dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'] )
+
+        self.write([dir, 'bin', 'mymoc.py'], """\
+import getopt
+import sys
+import string
+import re
+cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:', [])
+output = None
+impl = 0
+opt_string = ''
+for opt, arg in cmd_opts:
+    if opt == '-o': output = open(arg, 'wb')
+    elif opt == '-i': impl = 1
+    else: opt_string = opt_string + ' ' + opt
+for a in args:
+    contents = open(a, 'rb').read()
+    subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
+    if impl:
+        contents = re.sub( r'#include.*', '', contents )
+    output.write(string.replace(contents, 'Q_OBJECT', subst))
+output.close()
+sys.exit(0)
+""")
+
+        self.write([dir, 'bin', 'myuic.py'], """\
+import os.path
+import re
+import sys
+import string
+output_arg = 0
+impl_arg = 0
+impl = None
+source = None
+for arg in sys.argv[1:]:
+    if output_arg:
+        output = open(arg, 'wb')
+        output_arg = 0
+    elif impl_arg:
+        impl = arg
+        impl_arg = 0
+    elif arg == "-o":
+        output_arg = 1
+    elif arg == "-impl":
+        impl_arg = 1
+    else:
+        if source:
+            sys.exit(1)
+        source = open(arg, 'rb')
+        sourceFile = arg
+if impl:
+    output.write( '#include "' + impl + '"\\n' )
+    includes = re.findall('<include.*?>(.*?)</include>', source.read())
+    for incFile in includes:
+        # this is valid for ui.h files, at least
+        if os.path.exists(incFile):
+            output.write('#include "' + incFile + '"\\n')
+else:
+    output.write( '#include "my_qobject.h"\\n' + source.read() + " Q_OBJECT \\n" )
+output.close()
+sys.exit(0)
+""" )
+
+        self.write([dir, 'include', 'my_qobject.h'], r"""
+#define Q_OBJECT ;
+void my_qt_symbol(const char *arg);
+""")
+
+        self.write([dir, 'lib', 'my_qobject.cpp'], r"""
+#include "../include/my_qobject.h"
+#include <stdio.h>
+void my_qt_symbol(const char *arg) {
+  printf( arg );
+}
+""")
+
+        self.write(['qt', 'lib', 'SConstruct'], r"""
+env = Environment()
+env.StaticLibrary( 'myqt', 'my_qobject.cpp' )
+""")
+
+        self.run(chdir = self.workpath('qt', 'lib'),
+                 arguments = '.',
+                 stderr = noisy_ar,
+                 match = self.match_re_dotall)
+
+        self.QT = self.workpath(dir)
+        self.QT_LIB = 'myqt'
+        self.QT_MOC = '%s %s' % (python, self.workpath(dir, 'bin', 'mymoc.py'))
+        self.QT_UIC = '%s %s' % (python, self.workpath(dir, 'bin', 'myuic.py'))
+
+    def Qt_create_SConstruct(self, place):
+        if type(place) is type([]):
+            place = apply(test.workpath, place)
+        self.write(place, """\
+if ARGUMENTS.get('noqtdir', 0): QTDIR=None
+else: QTDIR=r'%s'
+env = Environment(QTDIR = QTDIR,
+                  QT_LIB = r'%s',
+                  QT_MOC = r'%s',
+                  QT_UIC = r'%s',
+                  tools=['default','qt'])
+dup = 1
+if ARGUMENTS.get('build_dir', 0):
+    if ARGUMENTS.get('chdir', 0):
+        SConscriptChdir(1)
+    else:
+        SConscriptChdir(0)
+    dup=int(ARGUMENTS.get('dup', 1))
+    if dup == 0:
+        builddir = 'build_dup0'
+        env['QT_DEBUG'] = 1
+    else:
+        builddir = 'build'
+    BuildDir(builddir, '.', duplicate=dup)
+    print builddir, dup
+    sconscript = Dir(builddir).File('SConscript')
+else:
+    sconscript = File('SConscript')
+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();