http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / QMTest / TestCommon.py
index f4c7c541dd364e990a05e6f3b08343c78c545d83..6b452f79535e5e35c75a4105f59a1c0b56c337aa 100644 (file)
@@ -36,12 +36,20 @@ provided by the TestCommon class:
 
     test.must_contain('file', 'required text\n')
 
+    test.must_contain_all_lines(output, lines, ['title', find])
+
+    test.must_contain_any_line(output, lines, ['title', find])
+
     test.must_exist('file1', ['file2', ...])
 
     test.must_match('file', "expected contents\n")
 
     test.must_not_be_writable('file1', ['file2', ...])
 
+    test.must_not_contain('file', 'banned text\n')
+
+    test.must_not_contain_any_line(output, lines, ['title', find])
+
     test.must_not_exist('file1', ['file2', ...])
 
     test.run(options = "options to be prepended to arguments",
@@ -55,6 +63,7 @@ The TestCommon module also provides the following variables
     TestCommon.python_executable
     TestCommon.exe_suffix
     TestCommon.obj_suffix
+    TestCommon.shobj_prefix
     TestCommon.shobj_suffix
     TestCommon.lib_prefix
     TestCommon.lib_suffix
@@ -63,7 +72,7 @@ The TestCommon module also provides the following variables
 
 """
 
-# Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
+# Copyright 2000-2010 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.
@@ -78,27 +87,26 @@ The TestCommon module also provides the following variables
 # 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.
+from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
 
 __author__ = "Steven Knight <knight at baldmt dot com>"
-__revision__ = "TestCommon.py 0.26.D001 2007/08/20 21:58:58 knight"
-__version__ = "0.26"
+__revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight"
+__version__ = "0.37"
 
+import copy
 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_prefix',
                  'shobj_suffix',
                  'lib_prefix',
                  'lib_suffix',
@@ -111,6 +119,7 @@ if sys.platform == 'win32':
     exe_suffix   = '.exe'
     obj_suffix   = '.obj'
     shobj_suffix = '.obj'
+    shobj_prefix = ''
     lib_prefix   = ''
     lib_suffix   = '.lib'
     dll_prefix   = ''
@@ -119,22 +128,34 @@ elif sys.platform == 'cygwin':
     exe_suffix   = '.exe'
     obj_suffix   = '.o'
     shobj_suffix = '.os'
+    shobj_prefix = ''
     lib_prefix   = 'lib'
     lib_suffix   = '.a'
     dll_prefix   = ''
     dll_suffix   = '.dll'
-elif string.find(sys.platform, 'irix') != -1:
+elif sys.platform.find('irix') != -1:
     exe_suffix   = ''
     obj_suffix   = '.o'
     shobj_suffix = '.o'
+    shobj_prefix = ''
     lib_prefix   = 'lib'
     lib_suffix   = '.a'
     dll_prefix   = 'lib'
     dll_suffix   = '.so'
-elif string.find(sys.platform, 'darwin') != -1:
+elif sys.platform.find('darwin') != -1:
+    exe_suffix   = ''
+    obj_suffix   = '.o'
+    shobj_suffix = '.os'
+    shobj_prefix = ''
+    lib_prefix   = 'lib'
+    lib_suffix   = '.a'
+    dll_prefix   = 'lib'
+    dll_suffix   = '.dylib'
+elif sys.platform.find('sunos') != -1:
     exe_suffix   = ''
     obj_suffix   = '.o'
     shobj_suffix = '.os'
+    shobj_prefix = 'so_'
     lib_prefix   = 'lib'
     lib_suffix   = '.a'
     dll_prefix   = 'lib'
@@ -143,43 +164,14 @@ else:
     exe_suffix   = ''
     obj_suffix   = '.o'
     shobj_suffix = '.os'
+    shobj_prefix = ''
     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 \
+    return isinstance(e, list) \
         or isinstance(e, UserList.UserList)
 
 def is_writable(f):
@@ -196,26 +188,13 @@ def separate_files(flist):
             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
+        return self.status
 elif os.name == 'nt':
     def _failed(self, status = 0):
         return not (self.status is None or status is None) and \
@@ -237,40 +216,8 @@ class TestCommon(TestCmd):
         calling the base class initialization, and then changing directory
         to the workdir.
         """
-        apply(TestCmd.__init__, [self], kw)
+        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.
@@ -279,20 +226,20 @@ class TestCommon(TestCmd):
         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)
+        files = [is_List(x) and os.path.join(*x) or x for x in files]
         existing, missing = separate_files(files)
-        unwritable = filter(lambda x, iw=is_writable: not iw(x), existing)
+        unwritable = [x for x in existing if not is_writable(x)]
         if missing:
-            print "Missing files: `%s'" % string.join(missing, "', `")
+            print "Missing files: `%s'" % "', `".join(missing)
         if unwritable:
-            print "Unwritable files: `%s'" % string.join(unwritable, "', `")
+            print "Unwritable files: `%s'" % "', `".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)
+        contains = (file_contents.find(required) != -1)
         if not contains:
             print "File `%s' does not contain required string." % file
             print self.banner('Required string ')
@@ -301,16 +248,74 @@ class TestCommon(TestCmd):
             print file_contents
             self.fail_test(not contains)
 
+    def must_contain_all_lines(self, output, lines, title=None, find=None):
+        """Ensures that the specified output string (first argument)
+        contains all of the specified lines (second argument).
+
+        An optional third argument can be used to describe the type
+        of output being searched, and only shows up in failure output.
+
+        An optional fourth argument can be used to supply a different
+        function, of the form "find(line, output), to use when searching
+        for lines in the output.
+        """
+        if find is None:
+            find = lambda o, l: o.find(l) != -1
+        missing = []
+        for line in lines:
+            if not find(output, line):
+                missing.append(line)
+
+        if missing:
+            if title is None:
+                title = 'output'
+            sys.stdout.write("Missing expected lines from %s:\n" % title)
+            for line in missing:
+                sys.stdout.write('    ' + repr(line) + '\n')
+            sys.stdout.write(self.banner(title + ' '))
+            sys.stdout.write(output)
+            self.fail_test()
+
+    def must_contain_any_line(self, output, lines, title=None, find=None):
+        """Ensures that the specified output string (first argument)
+        contains at least one of the specified lines (second argument).
+
+        An optional third argument can be used to describe the type
+        of output being searched, and only shows up in failure output.
+
+        An optional fourth argument can be used to supply a different
+        function, of the form "find(line, output), to use when searching
+        for lines in the output.
+        """
+        if find is None:
+            find = lambda o, l: o.find(l) != -1
+        for line in lines:
+            if find(output, line):
+                return
+
+        if title is None:
+            title = 'output'
+        sys.stdout.write("Missing any expected line from %s:\n" % title)
+        for line in lines:
+            sys.stdout.write('    ' + repr(line) + '\n')
+        sys.stdout.write(self.banner(title + ' '))
+        sys.stdout.write(output)
+        self.fail_test()
+
+    def must_contain_lines(self, lines, output, title=None):
+        # Deprecated; retain for backwards compatibility.
+        return self.must_contain_all_lines(output, lines, title)
+
     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)
+        files = [is_List(x) and os.path.join(*x) or x for x in files]
+        missing = [x for x in files if not os.path.exists(x)]
         if missing:
-            print "Missing files: `%s'" % string.join(missing, "', `")
+            print "Missing files: `%s'" % "', `".join(missing)
             self.fail_test(missing)
 
     def must_match(self, file, expect, mode = 'rb'):
@@ -329,16 +334,60 @@ class TestCommon(TestCmd):
             self.diff(expect, file_contents, 'contents ')
             raise
 
+    def must_not_contain(self, file, banned, mode = 'rb'):
+        """Ensures that the specified file doesn't contain the banned text.
+        """
+        file_contents = self.read(file, mode)
+        contains = (file_contents.find(banned) != -1)
+        if contains:
+            print "File `%s' contains banned string." % file
+            print self.banner('Banned string ')
+            print banned
+            print self.banner('%s contents ' % file)
+            print file_contents
+            self.fail_test(contains)
+
+    def must_not_contain_any_line(self, output, lines, title=None, find=None):
+        """Ensures that the specified output string (first argument)
+        does not contain any of the specified lines (second argument).
+
+        An optional third argument can be used to describe the type
+        of output being searched, and only shows up in failure output.
+
+        An optional fourth argument can be used to supply a different
+        function, of the form "find(line, output), to use when searching
+        for lines in the output.
+        """
+        if find is None:
+            find = lambda o, l: o.find(l) != -1
+        unexpected = []
+        for line in lines:
+            if find(output, line):
+                unexpected.append(line)
+
+        if unexpected:
+            if title is None:
+                title = 'output'
+            sys.stdout.write("Unexpected lines in %s:\n" % title)
+            for line in unexpected:
+                sys.stdout.write('    ' + repr(line) + '\n')
+            sys.stdout.write(self.banner(title + ' '))
+            sys.stdout.write(output)
+            self.fail_test()
+
+    def must_not_contain_lines(self, lines, output, title=None):
+        return self.must_not_contain_any_line(output, lines, title)
+
     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)
+        files = [is_List(x) and os.path.join(*x) or x for x in files]
+        existing = list(filter(os.path.exists, files))
         if existing:
-            print "Unexpected files exist: `%s'" % string.join(existing, "', `")
+            print "Unexpected files exist: `%s'" % "', `".join(existing)
             self.fail_test(existing)
 
 
@@ -349,15 +398,107 @@ class TestCommon(TestCmd):
         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)
+        files = [is_List(x) and os.path.join(*x) or x for x in files]
         existing, missing = separate_files(files)
-        writable = filter(is_writable, existing)
+        writable = list(filter(is_writable, existing))
         if missing:
-            print "Missing files: `%s'" % string.join(missing, "', `")
+            print "Missing files: `%s'" % "', `".join(missing)
         if writable:
-            print "Writable files: `%s'" % string.join(writable, "', `")
+            print "Writable files: `%s'" % "', `".join(writable)
         self.fail_test(missing + writable)
 
+    def _complete(self, actual_stdout, expected_stdout,
+                        actual_stderr, expected_stderr, status, match):
+        """
+        Post-processes running a subcommand, checking for failure
+        status and displaying output appropriately.
+        """
+        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 actual_stdout
+            print self.banner('STDERR ')
+            print actual_stderr
+            self.fail_test()
+        if not expected_stdout is None and not match(actual_stdout, expected_stdout):
+            self.diff(expected_stdout, actual_stdout, 'STDOUT ')
+            if actual_stderr:
+                print self.banner('STDERR ')
+                print actual_stderr
+            self.fail_test()
+        if not expected_stderr is None and not match(actual_stderr, expected_stderr):
+            print self.banner('STDOUT ')
+            print actual_stdout
+            self.diff(expected_stderr, actual_stderr, 'STDERR ')
+            self.fail_test()
+
+    def start(self, program = None,
+                    interpreter = None,
+                    arguments = None,
+                    universal_newlines = None,
+                    **kw):
+        """
+        Starts a program or script for the test environment.
+
+        This handles the "options" keyword argument and exceptions.
+        """
+        try:
+            options = kw['options']
+            del kw['options']
+        except KeyError:
+            pass
+        else:
+            if options:
+                if arguments is None:
+                    arguments = options
+                else:
+                    arguments = options + " " + arguments
+        try:
+            return TestCmd.start(self, program, interpreter, arguments, universal_newlines,
+                         **kw)
+        except KeyboardInterrupt:
+            raise
+        except Exception, e:
+            print self.banner('STDOUT ')
+            try:
+                print self.stdout()
+            except IndexError:
+                pass
+            print self.banner('STDERR ')
+            try:
+                print self.stderr()
+            except IndexError:
+                pass
+            cmd_args = self.command_args(program, interpreter, arguments)
+            sys.stderr.write('Exception trying to execute: %s\n' % cmd_args)
+            raise e
+
+    def finish(self, popen, stdout = None, stderr = '', status = 0, **kw):
+        """
+        Finishes and waits for the process being run under control of
+        the specified popen argument.  Additional arguments are similar
+        to those of the run() method:
+
+                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.
+        """
+        TestCmd.finish(self, popen, **kw)
+        match = kw.get('match', self.match)
+        self._complete(self.stdout(), stdout,
+                       self.stderr(), stderr, status, match)
+
     def run(self, options = None, arguments = None,
                   stdout = None, stderr = '', status = 0, **kw):
         """Runs the program under test, checking that the test succeeded.
@@ -395,38 +536,9 @@ class TestCommon(TestCmd):
             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
+        TestCmd.run(self, **kw)
+        self._complete(self.stdout(), stdout,
+                       self.stderr(), stderr, status, match)
 
     def skip_test(self, message="Skipping test.\n"):
         """Skips a test.
@@ -459,3 +571,9 @@ class TestCommon(TestCmd):
             # We're under the development directory for this change,
             # so this is an Aegis invocation; pass the test (exit 0).
             self.pass_test()
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: