Cygwin portability fixes. (Chad Austin) Improved testing infrastructure.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sun, 21 Mar 2004 05:37:23 +0000 (05:37 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sun, 21 Mar 2004 05:37:23 +0000 (05:37 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@927 fdb21ef1-2011-0410-befe-b5e4ea1792b1

etc/TestCmd.py
etc/TestCommon.py [new file with mode: 0644]
etc/TestSCons.py
test/ASFLAGS.py
test/CPPFLAGS.py
test/CXX.py

index 61fa2a7d2d1074edb8f2670cb9babfcdd5056ee6..ca89ed948f03f70340288c6457951e2b492eecdd 100644 (file)
@@ -173,8 +173,8 @@ version.
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 
 __author__ = "Steven Knight <knight at baldmt dot com>"
-__revision__ = "TestCmd.py 0.04.D010 2004/01/27 00:11:44 knight"
-__version__ = "0.04"
+__revision__ = "TestCmd.py 0.6.D001 2004/03/20 17:39:42 knight"
+__version__ = "0.6"
 
 import os
 import os.path
@@ -190,6 +190,10 @@ 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)
diff --git a/etc/TestCommon.py b/etc/TestCommon.py
new file mode 100644 (file)
index 0000000..0354e5b
--- /dev/null
@@ -0,0 +1,286 @@
+"""
+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_exist('file1', ['file2', ...])
+
+    test.must_match('file', "expected contents\n")
+
+    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)
+
+The TestCommon module also provides the following variables
+
+    TestCommon.python_executable
+    TestCommon._exe
+    TestCommon._obj
+    TestCommon._shobj
+    TestCommon.lib_
+    TestCommon._lib
+    TestCommon.dll_
+    TestCommon._dll
+
+"""
+
+# 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.6.D001 2004/03/20 17:39:42 knight"
+__version__ = "0.6"
+
+import os
+import os.path
+import string
+import sys
+import types
+import UserList
+
+from TestCmd import *
+from TestCmd import __all__
+
+__all__.extend([ 'TestCommon',
+                 '_exe', '_obj', '_shobj', 'lib_', '_lib', 'dll_', '_dll', ])
+
+# Variables that describe the prefixes and suffixes on this system.
+if sys.platform == 'win32':
+    _exe   = '.exe'
+    _obj   = '.obj'
+    _shobj = '.obj'
+    lib_   = ''
+    _lib   = '.lib'
+    dll_   = ''
+    _dll   = '.dll'
+elif sys.platform == 'cygwin':
+    _exe   = '.exe'
+    _obj   = '.o'
+    _shobj = '.os'
+    lib_   = 'lib'
+    _lib   = '.a'
+    dll_   = ''
+    _dll   = '.dll'
+elif string.find(sys.platform, 'irix') != -1:
+    _exe   = ''
+    _obj   = '.o'
+    _shobj = '.o'
+    lib_   = 'lib'
+    _lib   = '.a'
+    dll_   = 'lib'
+    _dll   = '.so'
+else:
+    _exe   = ''
+    _obj   = '.o'
+    _shobj = '.os'
+    lib_   = 'lib'
+    _lib   = '.a'
+    dll_   = 'lib'
+    _dll   = '.so'
+
+def is_List(e):
+    return type(e) is types.ListType \
+        or isinstance(e, UserList.UserList)
+
+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');
+    #
+    #  $test->chmod($mode, 'file', ...);
+    #
+    #  $test->touch('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)
+
+    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 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):
+        """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)
+        try:
+            self.fail_test(not self.match(file_contents, expect))
+        except:
+            print "Unexpected contents of `%s'" % file
+           print "EXPECTED contents ======"
+            print expect
+            print "ACTUAL contents ========"
+            print file_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 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 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:
+           apply(TestCmd.run, [self], kw)
+       except:
+           print "STDOUT ============"
+           print self.stdout()
+           print "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 "STDOUT ============"
+            print self.stdout()
+           print "STDERR ============"
+           print self.stderr()
+           raise TestFailed
+       if not stdout is None and not self.match(self.stdout(), stdout):
+            print "Expected STDOUT =========="
+            print stdout
+            print "Actual STDOUT ============"
+            print self.stdout()
+            stderr = self.stderr()
+            if stderr:
+                print "STDERR ==================="
+                print stderr
+            raise TestFailed
+       if not stderr is None and not self.match(self.stderr(), stderr):
+            print "STDOUT ==================="
+            print self.stdout()
+           print "Expected STDERR =========="
+           print stderr
+           print "Actual STDERR ============"
+           print self.stderr()
+           raise TestFailed
index 028a81520bdeee7a067ae904878d1a165b1989b8..33049c560430ef48c944d369f3b3af2be6622e09 100644 (file)
@@ -6,9 +6,10 @@ A TestSCons environment object is created via the usual invocation:
 
     test = TestSCons()
 
-TestScons is a subclass of TestCmd, and hence has available all of its
-methods and attributes, as well as any overridden or additional methods
-or attributes defined in this subclass.
+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 2001, 2002, 2003 Steven Knight
@@ -20,9 +21,9 @@ import os.path
 import string
 import sys
 
-import TestCmd
+from TestCommon import *
 
-python = TestCmd.python_executable
+python = python_executable
 
 
 def gccFortranLibs():
@@ -46,72 +47,27 @@ def gccFortranLibs():
     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':
-    _exe   = '.exe'
-    _obj   = '.obj'
-    _shobj = '.obj'
-    lib_   = ''
-    _lib   = '.lib'
-    dll_   = ''
-    _dll   = '.dll'
     fortran_lib = gccFortranLibs()
 elif sys.platform == 'cygwin':
-    _exe   = '.exe'
-    _obj   = '.o'
-    _shobj = '.os'
-    lib_   = 'lib'
-    _lib   = '.a'
-    dll_   = ''
-    _dll   = '.dll'
     fortran_lib = gccFortranLibs()
 elif string.find(sys.platform, 'irix') != -1:
-    _exe   = ''
-    _obj   = '.o'
-    _shobj = '.o'
-    lib_   = 'lib'
-    _lib   = '.a'
-    dll_   = 'lib'
-    _dll   = '.so'
     fortran_lib = ['ftn']
 else:
-    _exe   = ''
-    _obj   = '.o'
-    _shobj = '.os'
-    lib_   = 'lib'
-    _lib   = '.a'
-    dll_   = 'lib'
-    _dll   = '.so'
     fortran_lib = gccFortranLibs()
 
 
-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 TestSCons(TestCmd.TestCmd):
+class TestSCons(TestCommon):
     """Class for testing SCons.
 
     This provides a common place for initializing SCons tests,
@@ -128,7 +84,7 @@ class TestSCons(TestCmd.TestCmd):
                program = 'scons' if it exists,
                          else 'scons.py'
                interpreter = 'python'
-               match = TestCmd.match_exact
+               match = match_exact
                workdir = ''
 
         The workdir value means that, by default, a temporary workspace
@@ -147,73 +103,10 @@ class TestSCons(TestCmd.TestCmd):
        if not kw.has_key('interpreter') and not os.environ.get('SCONS_EXEC'):
            kw['interpreter'] = python
        if not kw.has_key('match'):
-           kw['match'] = TestCmd.match_exact
+           kw['match'] = match_exact
        if not kw.has_key('workdir'):
            kw['workdir'] = ''
-       apply(TestCmd.TestCmd.__init__, [self], kw)
-       os.chdir(self.workdir)
-
-    def run(self, options = None, arguments = None,
-                  stdout = None, stderr = '', status = 0, **kw):
-       """Runs SCons.
-
-        This is the same as the base TestCmd.run() method, with
-        the addition of:
-
-               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 does not test standard output (stdout = None),
-        and expects that error output is empty (stderr = "").
-       """
-        if options:
-            arguments = options + " " + arguments
-        kw['arguments'] = arguments
-       try:
-           apply(TestCmd.TestCmd.run, [self], kw)
-       except:
-           print "STDOUT ============"
-           print self.stdout()
-           print "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 "STDOUT ============"
-            print self.stdout()
-           print "STDERR ============"
-           print self.stderr()
-           raise TestFailed
-       if not stdout is None and not self.match(self.stdout(), stdout):
-                print "Expected STDOUT =========="
-                print stdout
-                print "Actual STDOUT ============"
-                print self.stdout()
-                stderr = self.stderr()
-                if stderr:
-                    print "STDERR ==================="
-                    print stderr
-                raise TestFailed
-       if not stderr is None and not self.match(self.stderr(), stderr):
-            print "STDOUT ==================="
-            print self.stdout()
-           print "Expected STDERR =========="
-           print stderr
-           print "Actual STDERR ============"
-           print self.stderr()
-           raise TestFailed
+       apply(TestCommon.__init__, [self], kw)
 
     def detect(self, var, prog=None):
         """
@@ -294,6 +187,6 @@ class TestSCons(TestCmd.TestCmd):
         kw['stdout'] = string.replace(kw['stdout'],'\n','\\n')
         kw['stdout'] = string.replace(kw['stdout'],'.','\\.')
         old_match_func = self.match_func
-        self.match_func = TestCmd.match_re_dotall
+        self.match_func = match_re_dotall
         apply(self.run, [], kw)
         self.match_func = old_match_func
index d750a6a46d1d948be570c18ed43cec6f5fe9b08a..6396c45deff40cf5560351c137d79794c59c6e96 100644 (file)
@@ -170,18 +170,16 @@ test.write('test6.SPP', r"""This is a .SPP file.
 
 test.run(arguments = '.', stderr = None)
 
-test.fail_test(test.read('test1' + _exe) != "%s\nThis is a .s file.\n" % o)
-
-test.fail_test(test.read('test2' + _exe) != "%s\nThis is a .S file.\n" % o_c)
-
-test.fail_test(test.read('test3' + _exe) != "%s\nThis is a .asm file.\n" % o)
-
-test.fail_test(test.read('test4' + _exe) != "%s\nThis is a .ASM file.\n" % o)
-
-test.fail_test(test.read('test5' + _exe) != "%s\nThis is a .spp file.\n" % o_c)
-
-test.fail_test(test.read('test6' + _exe) != "%s\nThis is a .SPP file.\n" % o_c)
-
-
+if TestSCons.case_sensitive_suffixes('.s', '.S'):
+    o_css = o_c
+else:
+    o_css = o
+
+test.must_match('test1' + _exe, "%s\nThis is a .s file.\n" % o)
+test.must_match('test2' + _exe, "%s\nThis is a .S file.\n" % o_css)
+test.must_match('test3' + _exe, "%s\nThis is a .asm file.\n" % o)
+test.must_match('test4' + _exe, "%s\nThis is a .ASM file.\n" % o)
+test.must_match('test5' + _exe, "%s\nThis is a .spp file.\n" % o_c)
+test.must_match('test6' + _exe, "%s\nThis is a .SPP file.\n" % o_c)
 
 test.pass_test()
index 87f5603b8e57b9aef3ee9b65a28041b05a18e67e..357a2413d64ab1982599d9bfc8c2e941bcbf06e5 100644 (file)
@@ -128,15 +128,14 @@ test.write('test3.F', r"""test3.F
 
 test.run(arguments = '.', stderr=None)
 
-test.fail_test(test.read('test1' + _obj) != "test1.c\n#link\n")
-
-test.fail_test(test.read('test2' + _obj) != "test2.cpp\n#link\n")
-
-test.fail_test(test.read('test3' + _obj) != "test3.F\n#link\n")
-
-test.fail_test(test.read('foo' + _exe) != "test1.c\ntest2.cpp\ntest3.F\n")
-
-test.fail_test(test.read('mygcc.out') != "cc\nc++\ng77\n")
+test.must_match('test1' + _obj, "test1.c\n#link\n")
+test.must_match('test2' + _obj, "test2.cpp\n#link\n")
+test.must_match('test3' + _obj, "test3.F\n#link\n")
+test.must_match('foo' + _exe,   "test1.c\ntest2.cpp\ntest3.F\n")
+if TestSCons.case_sensitive_suffixes('.F', '.f'):
+    test.must_match('mygcc.out', "cc\nc++\ng77\n")
+else:
+    test.must_match('mygcc.out', "cc\nc++\n")   
 
 test.write('SConstruct', """
 env = Environment(CPPFLAGS = '-x',
@@ -172,14 +171,13 @@ test.unlink('test3' + _obj)
 
 test.run(arguments = '.', stderr = None)
 
-test.fail_test(test.read('test1' + _shobj) != "test1.c\n#link\n")
-
-test.fail_test(test.read('test2' + _shobj) != "test2.cpp\n#link\n")
-
-test.fail_test(test.read('test3' + _shobj) != "test3.F\n#link\n")
-
-test.fail_test(test.read('foo.bar') != "test1.c\ntest2.cpp\ntest3.F\n")
-
-test.fail_test(test.read('mygcc.out') != "cc\nc++\ng77\n")
+test.must_match('test1' + _shobj, "test1.c\n#link\n")
+test.must_match('test2' + _shobj, "test2.cpp\n#link\n")
+test.must_match('test3' + _shobj, "test3.F\n#link\n")
+test.must_match('foo.bar',        "test1.c\ntest2.cpp\ntest3.F\n")
+if TestSCons.case_sensitive_suffixes('.F', '.f'):
+    test.must_match('mygcc.out', "cc\nc++\ng77\n")
+else:
+    test.must_match('mygcc.out', "cc\nc++\n")   
 
 test.pass_test()
index ad2eb10b872b43782dc7a6890efa4633cab269fd..6f9f0ce86d6906ea7168c3fdae5d7587de915fde 100644 (file)
@@ -145,19 +145,17 @@ test.write('test5.C++', r"""This is a .C++ file.
 
 test.run(arguments = '.', stderr = None)
 
-test.fail_test(test.read('test1' + _exe) != "This is a .cc file.\n")
+test.must_match('test1' + _exe, "This is a .cc file.\n")
 
-test.fail_test(test.read('test2' + _exe) != "This is a .cpp file.\n")
+test.must_match('test2' + _exe, "This is a .cpp file.\n")
 
-test.fail_test(test.read('test3' + _exe) != "This is a .cxx file.\n")
+test.must_match('test3' + _exe, "This is a .cxx file.\n")
 
-test.fail_test(test.read('test4' + _exe) != "This is a .c++ file.\n")
+test.must_match('test4' + _exe, "This is a .c++ file.\n")
 
-test.fail_test(test.read('test5' + _exe) != "This is a .C++ file.\n")
+test.must_match('test5' + _exe, "This is a .C++ file.\n")
 
-# Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
-_is_cygwin = sys.platform == "cygwin"
-if os.path.normcase('.c') != os.path.normcase('.C') and not _is_cygwin:
+if TestSCons.case_sensitive_suffixes('.c', '.C'):
 
     test.write('SConstruct', """
 env = Environment(LINK = r'%s mylink.py',
@@ -174,7 +172,7 @@ env.Program(target = 'test6', source = 'test6.C')
 
     test.run(arguments = '.', stderr = None)
 
-    test.fail_test(test.read('test6' + _exe) != "This is a .C file.\n")
+    test.must_match('test6' + _exe, "This is a .C file.\n")
 
 
 
@@ -222,10 +220,10 @@ main(int argc, char *argv[])
 
 test.run(arguments = 'foo' + _exe)
 
-test.fail_test(os.path.exists(test.workpath('wrapper.out')))
+test.must_not_exist(test.workpath('wrapper.out'))
 
 test.run(arguments = 'bar' + _exe)
 
-test.fail_test(test.read('wrapper.out') != "wrapper.py\n")
+test.must_match('wrapper.out', "wrapper.py\n")
 
 test.pass_test()