diff options
-rw-r--r-- | etc/TestCmd.py | 8 | ||||
-rw-r--r-- | etc/TestCommon.py | 286 | ||||
-rw-r--r-- | etc/TestSCons.py | 149 | ||||
-rw-r--r-- | test/ASFLAGS.py | 24 | ||||
-rw-r--r-- | test/CPPFLAGS.py | 34 | ||||
-rw-r--r-- | test/CXX.py | 20 |
6 files changed, 349 insertions, 172 deletions
diff --git a/etc/TestCmd.py b/etc/TestCmd.py index 61fa2a7..ca89ed9 100644 --- a/etc/TestCmd.py +++ b/etc/TestCmd.py @@ -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 index 0000000..0354e5b --- /dev/null +++ b/etc/TestCommon.py @@ -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 diff --git a/etc/TestSCons.py b/etc/TestSCons.py index 028a815..33049c5 100644 --- a/etc/TestSCons.py +++ b/etc/TestSCons.py @@ -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 diff --git a/test/ASFLAGS.py b/test/ASFLAGS.py index d750a6a..6396c45 100644 --- a/test/ASFLAGS.py +++ b/test/ASFLAGS.py @@ -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() diff --git a/test/CPPFLAGS.py b/test/CPPFLAGS.py index 87f5603..357a241 100644 --- a/test/CPPFLAGS.py +++ b/test/CPPFLAGS.py @@ -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() diff --git a/test/CXX.py b/test/CXX.py index ad2eb10..6f9f0ce 100644 --- a/test/CXX.py +++ b/test/CXX.py @@ -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() |