http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / QMTest / TestCmd.py
index 48ba850b2be6a8e0bfb5c6b83eba4deb30367ad8..7c8e1a509c3f937f8959ae1f64e2fd21a0b9615d 100644 (file)
@@ -25,6 +25,7 @@ There are a bunch of keyword arguments available at instantiation:
                            subdir = 'subdir',
                            verbose = Boolean,
                            match = default_match_function,
+                           diff = default_diff_function,
                            combine = Boolean)
 
 There are a bunch of methods that let you do different things:
@@ -103,6 +104,11 @@ There are a bunch of methods that let you do different things:
 
     test.symlink(target, link)
 
+    test.banner(string)
+    test.banner(string, width)
+
+    test.diff(actual, expected)
+
     test.match(actual, expected)
 
     test.match_exact("actual 1\nactual 2\n", "expected 1\nexpected 2\n")
@@ -164,6 +170,24 @@ in the same way as the match_*() methods described above.
 
     test = TestCmd.TestCmd(match = TestCmd.match_re_dotall)
 
+The TestCmd module provides unbound functions that can be used for the
+"diff" argument to TestCmd.TestCmd instantiation:
+
+    import TestCmd
+
+    test = TestCmd.TestCmd(match = TestCmd.match_re,
+                           diff = TestCmd.diff_re)
+
+    test = TestCmd.TestCmd(diff = TestCmd.simple_diff)
+
+The "diff" argument can also be used with standard difflib functions:
+
+    import difflib
+
+    test = TestCmd.TestCmd(diff = difflib.context_diff)
+
+    test = TestCmd.TestCmd(diff = difflib.unified_diff)
+
 Lastly, the where_is() method also exists in an unbound function
 version.
 
@@ -174,7 +198,7 @@ version.
     TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
 """
 
-# 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.
@@ -189,10 +213,11 @@ version.
 # 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__ = "TestCmd.py 0.35.D001 2009/02/08 07:10:39 knight"
-__version__ = "0.35"
+__revision__ = "TestCmd.py 0.37.D001 2010/01/11 16:55:50 knight"
+__version__ = "0.37"
 
 import errno
 import os
@@ -200,12 +225,10 @@ import os.path
 import re
 import shutil
 import stat
-import string
 import sys
 import tempfile
 import time
 import traceback
-import types
 import UserList
 
 __all__ = [
@@ -220,8 +243,13 @@ __all__ = [
     'TestCmd'
 ]
 
+try:
+    import difflib
+except ImportError:
+    __all__.append('simple_diff')
+
 def is_List(e):
-    return type(e) is types.ListType \
+    return isinstance(e, list) \
         or isinstance(e, UserList.UserList)
 
 try:
@@ -230,14 +258,15 @@ except ImportError:
     class UserString:
         pass
 
-if hasattr(types, 'UnicodeType'):
+try: unicode
+except NameError:
     def is_String(e):
-        return type(e) is types.StringType \
-            or type(e) is types.UnicodeType \
-            or isinstance(e, UserString)
+        return isinstance(e, str) or isinstance(e, UserString)
 else:
     def is_String(e):
-        return type(e) is types.StringType or isinstance(e, UserString)
+        return isinstance(e, str) \
+            or isinstance(e, unicode) \
+            or isinstance(e, UserString)
 
 tempfile.template = 'testcmd.'
 if os.name in ('posix', 'nt'):
@@ -253,7 +282,7 @@ _chain_to_exitfunc = None
 
 def _clean():
     global _Cleanup
-    cleanlist = filter(None, _Cleanup)
+    cleanlist = [_f for _f in _Cleanup if _f]
     del _Cleanup[:]
     cleanlist.reverse()
     for test in cleanlist:
@@ -278,16 +307,16 @@ try:
 except NameError:
     def zip(*lists):
         result = []
-        for i in xrange(min(map(len, lists))):
-            result.append(tuple(map(lambda l, i=i: l[i], lists)))
+        for i in xrange(min(list(map(len, lists)))):
+            result.append(tuple([l[i] for l in lists]))
         return result
 
 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))
+        pathjoin = lambda n: os.path.join(dirname, n)
+        self.entries.extend(list(map(pathjoin, names)))
 
 def _caller(tblist, skip):
     string = ""
@@ -378,9 +407,9 @@ def match_exact(lines = None, matches = None):
     """
     """
     if not is_List(lines):
-        lines = string.split(lines, "\n")
+        lines = lines.split("\n")
     if not is_List(matches):
-        matches = string.split(matches, "\n")
+        matches = matches.split("\n")
     if len(lines) != len(matches):
         return
     for i in range(len(lines)):
@@ -392,9 +421,9 @@ def match_re(lines = None, res = None):
     """
     """
     if not is_List(lines):
-        lines = string.split(lines, "\n")
+        lines = lines.split("\n")
     if not is_List(res):
-        res = string.split(res, "\n")
+        res = res.split("\n")
     if len(lines) != len(res):
         return
     for i in range(len(lines)):
@@ -411,10 +440,10 @@ def match_re(lines = None, res = None):
 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 not isinstance(lines, str):
+        lines = "\n".join(lines)
+    if not isinstance(res, str):
+        res = "\n".join(res)
     s = "^" + res + "$"
     try:
         expr = re.compile(s, re.DOTALL)
@@ -424,6 +453,36 @@ def match_re_dotall(lines = None, res = None):
     if expr.match(lines):
         return 1
 
+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(['< ' + l for l in a[a1:a2]])
+            elif op == 'insert':
+                result.append("%da%s" % (a1, comma(b1, b2)))
+                result.extend(['> ' + l for l in b[b1:b2]])
+            elif op == 'replace':
+                result.append("%sc%s" % (comma(a1, a2), comma(b1, b2)))
+                result.extend(['< ' + l for l in a[a1:a2]])
+                result.append('---')
+                result.extend(['> ' + l for l in b[b1:b2]])
+        return result
+
 def diff_re(a, b, fromfile='', tofile='',
                 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
     """
@@ -471,13 +530,13 @@ if sys.platform == 'win32':
         if path is None:
             path = os.environ['PATH']
         if is_String(path):
-            path = string.split(path, os.pathsep)
+            path = path.split(os.pathsep)
         if pathext is None:
             pathext = os.environ['PATHEXT']
         if is_String(pathext):
-            pathext = string.split(pathext, os.pathsep)
+            pathext = pathext.split(os.pathsep)
         for ext in pathext:
-            if string.lower(ext) == string.lower(file[-len(ext):]):
+            if ext.lower() == file[-len(ext):].lower():
                 pathext = ['']
                 break
         for dir in path:
@@ -494,7 +553,7 @@ else:
         if path is None:
             path = os.environ['PATH']
         if is_String(path):
-            path = string.split(path, os.pathsep)
+            path = path.split(os.pathsep)
         for dir in path:
             f = os.path.join(dir, file)
             if os.path.isfile(f):
@@ -590,14 +649,14 @@ except ImportError:
             universal_newlines = 1
             def __init__(self, command, **kw):
                 if kw.get('stderr') == 'STDOUT':
-                    apply(popen2.Popen4.__init__, (self, command, 1))
+                    popen2.Popen4.__init__(self, command, 1)
                 else:
-                    apply(popen2.Popen3.__init__, (self, command, 1))
+                    popen2.Popen3.__init__(self, command, 1)
                 self.stdin = self.tochild
                 self.stdout = self.fromchild
                 self.stderr = self.childerr
             def wait(self, *args, **kw):
-                resultcode = apply(popen2.Popen3.wait, (self,)+args, kw)
+                resultcode = popen2.Popen3.wait(self, *args, **kw)
                 if os.WIFEXITED(resultcode):
                     return os.WEXITSTATUS(resultcode)
                 elif os.WIFSIGNALED(resultcode):
@@ -759,6 +818,7 @@ def recv_some(p, t=.1, e=1, tr=5, stderr=0):
             time.sleep(max((x-time.time())/tr, 0))
     return ''.join(y)
 
+# TODO(3.0:  rewrite to use memoryview()
 def send_all(p, data):
     while len(data):
         sent = p.send(data)
@@ -768,7 +828,15 @@ def send_all(p, data):
 
 
 
-class TestCmd:
+try:
+    object
+except NameError:
+    class object:
+        pass
+
+
+
+class TestCmd(object):
     """Class TestCmd
     """
 
@@ -779,6 +847,7 @@ class TestCmd:
                        subdir = None,
                        verbose = None,
                        match = None,
+                       diff = None,
                        combine = 0,
                        universal_newlines = 1):
         self._cwd = os.getcwd()
@@ -794,12 +863,23 @@ class TestCmd:
         self.combine = combine
         self.universal_newlines = universal_newlines
         if not match is None:
-            self.match_func = match
+            self.match_function = match
         else:
-            self.match_func = match_re
+            self.match_function = match_re
+        if not diff is None:
+            self.diff_function = diff
+        else:
+            try:
+                difflib
+            except NameError:
+                pass
+            else:
+                self.diff_function = simple_diff
+                #self.diff_function = difflib.context_diff
+                #self.diff_function = difflib.unified_diff
         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 '':
+        if 'PRESERVE' in os.environ 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']
@@ -829,6 +909,14 @@ class TestCmd:
     def __repr__(self):
         return "%x" % id(self)
 
+    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))
+
     if os.name == 'posix':
 
         def escape(self, arg):
@@ -836,9 +924,9 @@ class TestCmd:
             slash = '\\'
             special = '"$'
 
-            arg = string.replace(arg, slash, slash+slash)
+            arg = arg.replace(slash, slash+slash)
             for c in special:
-                arg = string.replace(arg, c, slash+c)
+                arg = arg.replace(c, slash+c)
 
             if re_space.search(arg):
                 arg = '"' + arg + '"'
@@ -856,7 +944,7 @@ class TestCmd:
 
     def canonicalize(self, path):
         if is_List(path):
-            path = apply(os.path.join, tuple(path))
+            path = os.path.join(*tuple(path))
         if not os.path.isabs(path):
             path = os.path.join(self.workdir, path)
         return path
@@ -909,22 +997,22 @@ class TestCmd:
                            interpreter = None,
                            arguments = None):
         if program:
-            if type(program) == type('') and not os.path.isabs(program):
+            if isinstance(program, str) 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(())]:
+        if not type(program) in [list, tuple]:
             program = [program]
         cmd = list(program)
         if interpreter:
-            if not type(interpreter) in [type([]), type(())]:
+            if not type(interpreter) in [list, tuple]:
                 interpreter = [interpreter]
             cmd = list(interpreter) + cmd
         if arguments:
-            if type(arguments) == type(''):
-                arguments = string.split(arguments)
+            if isinstance(arguments, str):
+                arguments = arguments.split()
             cmd.extend(arguments)
         return cmd
 
@@ -933,9 +1021,21 @@ class TestCmd:
         """
         self.description = description
 
-#    def diff(self):
-#        """Diff two arrays.
-#        """
+    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 = self.diff_function(*args, **kw)
+            for l in lines:
+                print l
 
     def fail_test(self, condition = 1, function = None, skip = 0):
         """Cause the test to fail.
@@ -957,7 +1057,7 @@ class TestCmd:
     def match(self, lines, matches):
         """Compare actual and expected file contents.
         """
-        return self.match_func(lines, matches)
+        return self.match_function(lines, matches)
 
     def match_exact(self, lines, matches):
         """Compare actual and expected file contents.
@@ -1049,12 +1149,20 @@ class TestCmd:
         prepended unless it is enclosed in a [list].
         """
         cmd = self.command_args(program, interpreter, arguments)
-        cmd_string = string.join(map(self.escape, cmd), ' ')
+        cmd_string = ' '.join(map(self.escape, cmd))
         if self.verbose:
             sys.stderr.write(cmd_string + "\n")
         if universal_newlines is None:
             universal_newlines = self.universal_newlines
 
+        # On Windows, if we make stdin a pipe when we plan to send 
+        # no input, and the test program exits before
+        # Popen calls msvcrt.open_osfhandle, that call will fail.
+        # So don't use a pipe for stdin if we don't need one.
+        stdin = kw.get('stdin', None)
+        if stdin is not None:
+            stdin = subprocess.PIPE
+
         combine = kw.get('combine', self.combine)
         if combine:
             stderr_value = subprocess.STDOUT
@@ -1062,7 +1170,7 @@ class TestCmd:
             stderr_value = subprocess.PIPE
 
         return Popen(cmd,
-                     stdin=subprocess.PIPE,
+                     stdin=stdin,
                      stdout=subprocess.PIPE,
                      stderr=stderr_value,
                      universal_newlines=universal_newlines)
@@ -1104,14 +1212,18 @@ class TestCmd:
             if self.verbose:
                 sys.stderr.write("chdir(" + chdir + ")\n")
             os.chdir(chdir)
-        p = self.start(program, interpreter, arguments, universal_newlines)
+        p = self.start(program,
+                       interpreter,
+                       arguments,
+                       universal_newlines,
+                       stdin=stdin)
         if stdin:
             if is_List(stdin):
                 for line in stdin:
                     p.stdin.write(line)
             else:
                 p.stdin.write(stdin)
-        p.stdin.close()
+            p.stdin.close()
 
         out = p.stdout.read()
         if p.stderr is None:
@@ -1202,7 +1314,7 @@ class TestCmd:
             if sub is None:
                 continue
             if is_List(sub):
-                sub = apply(os.path.join, tuple(sub))
+                sub = os.path.join(*tuple(sub))
             new = os.path.join(self.workdir, sub)
             try:
                 os.mkdir(new)
@@ -1250,7 +1362,7 @@ class TestCmd:
         # letters is pretty much random on win32:
         drive,rest = os.path.splitdrive(path)
         if drive:
-            path = string.upper(drive) + rest
+            path = drive.upper() + rest
 
         #
         self._dirlist.append(path)
@@ -1292,7 +1404,7 @@ class TestCmd:
         """Find an executable file.
         """
         if is_List(file):
-            file = apply(os.path.join, tuple(file))
+            file = os.path.join(*tuple(file))
         if not os.path.isabs(file):
             file = where_is(file, path, pathext)
         return file
@@ -1314,7 +1426,7 @@ class TestCmd:
         the temporary working directory name with the specified
         arguments using the os.path.join() method.
         """
-        return apply(os.path.join, (self.workdir,) + tuple(args))
+        return os.path.join(self.workdir, *tuple(args))
 
     def readable(self, top, read=1):
         """Make the specified directory tree readable (read == 1)
@@ -1471,3 +1583,9 @@ class TestCmd:
         if mode[0] != 'w':
             raise ValueError, "mode must begin with 'w'"
         open(file, mode).write(content)
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: