Commits

Steven Knight  committed f3ba0fe

Merged revisions 4727-4729,4731-4938,4940-5028 via svnmerge from
http://scons.tigris.org/svn/scons/branches/pending

........
r4942 | stevenknight | 2010-06-03 12:41:20 -0700 (Thu, 03 Jun 2010) | 13 lines

Isseu 2641: Latest drop of the TestCmd infrastructure, v. 1.3, including:

* Support for test timeouts.
* Ability to set separate match_stdout and match_stderr functions.
* Ability to set separate diff_stdout and diff_stderr functions.
* Static methods for the various underlying match* and diff* functionality.
* Ability to get at the various match* and diff* functions by attribute name.
* Got rid of checks for difflib now that Python 2.3 is the floor
(for this infrastructure, anyway).

Ripple effects in two test scripts. Added upstream unit test modules
(QMTest/Test{Cmd,Common}Tests.py). Added a README.txt file.
........
r4943 | stevenknight | 2010-06-03 13:00:31 -0700 (Thu, 03 Jun 2010) | 2 lines

Grab the correct TestCmd files with the updated version number.
........
r4946 | managan | 2010-06-04 09:39:20 -0700 (Fri, 04 Jun 2010) | 4 lines

On Windows add a '/D' to the command line so it recognizes drive letters in the
source or target file paths
........
r4947 | managan | 2010-06-04 09:51:48 -0700 (Fri, 04 Jun 2010) | 5 lines

The scanner was not parsing the dependencies in \includegraphics commands when there was whitespace (including carriage returns) in the command.

While we need a better long term fix this covers this concern.
........
r4948 | managan | 2010-06-04 11:13:12 -0700 (Fri, 04 Jun 2010) | 3 lines

Dropped an import line that is needed by the last commit of mine for Windows
depenedant option on latex command lines
........
r4949 | managan | 2010-06-04 12:27:48 -0700 (Fri, 04 Jun 2010) | 7 lines

Some latex packages break up commands where you normally could not by
using a comment character at the end of the first line.
Our current scanner broke on this and lost some dependecies.
While we need general fix, this patch solves this problem
........
r4950 | managan | 2010-06-04 15:51:36 -0700 (Fri, 04 Jun 2010) | 5 lines

Tweak how we handle comments within Latex source files when
scanning and looking for dependencies. We were adding a
space when a comment broke a line and we should not have.
........
r4984 | managan | 2010-06-07 09:37:40 -0700 (Mon, 07 Jun 2010) | 6 lines

The multi-line_include-options test failed to check for the
existence of latex. Added that so this test is skipped on
systems without latex.
........

  • Participants
  • Parent commits d10ad18

Comments (0)

Files changed (12)

File QMTest/README.txt

+This directory contains testing infrastructure.  Note that not all of
+the pieces here are local to SCons.
+
+    README.txt
+
+        What you're looking at right now.
+
+    SConscript
+
+        Configuration for our packaging build, to copy the necessary
+        parts of the infrastructure into a build directory.
+
+    TestCmd.py
+    TestCmdTests.py
+    TestCommon.py
+    TestCommonTests.py
+
+        The TestCmd infrastructure for testing external commands.
+        These are for generic command testing, are used by some
+        other projects, and are developed separately from SCons.
+        (They're developed by SK, but still...)
+
+        We've captured the unit tests (Test*Tests.py) for these files
+        along with the actual modules themselves to make it a little
+        easier to hack on them for our purposes.  Note, however,
+        that any SCons-specific functionality should be implemented
+        in one of the
+
+    TestRuntest.py
+
+        Test infrastructure for our runtest.py script.
+
+    TestSCons.py
+
+        Test infrastructure for SCons itself.
+
+    TestSConsMSVS.py
+
+        Test infrastructure for SCons' Visual Studio support.
+
+    TestSCons_time.py
+
+        Test infrastructure for the scons-time.py script.
+
+    TestSConsign.py
+
+        Test infrastructure for the sconsign.py script.
+
+    classes.qmc
+    configuration
+    scons-tdb.py
+
+        Pieces for the use of QMTest to test SCons.  We're moving away
+        from this infrastructure, in no small part because we're not
+        really using it as originally envisioned.

File QMTest/TestCmd.py

                            subdir = 'subdir',
                            verbose = Boolean,
                            match = default_match_function,
-                           diff = default_diff_function,
+                           match_stdout = default_match_stdout_function,
+                           match_stderr = default_match_stderr_function,
+                           diff = default_diff_stderr_function,
+                           diff_stdout = default_diff_stdout_function,
+                           diff_stderr = default_diff_stderr_function,
                            combine = Boolean)
 
 There are a bunch of methods that let you do different things:
 
     test.diff(actual, expected)
 
+    test.diff_stderr(actual, expected)
+
+    test.diff_stdout(actual, expected)
+
     test.match(actual, expected)
 
+    test.match_stderr(actual, expected)
+
+    test.match_stdout(actual, expected)
+
+    test.set_match_function(match, stdout, stderr)
+
     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"])
     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.
+The TestCmd module also provides unbound global functions that handle
+matching in the same way as the match_*() methods described above.
 
     import TestCmd
 
 
     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:
+These functions are also available as static methods:
+
+    import TestCmd
+
+    test = TestCmd.TestCmd(match = TestCmd.TestCmd.match_exact)
+
+    test = TestCmd.TestCmd(match = TestCmd.TestCmd.match_re)
+
+    test = TestCmd.TestCmd(match = TestCmd.TestCmd.match_re_dotall)
+
+These static methods can be accessed by a string naming the method:
+
+    import TestCmd
+
+    test = TestCmd.TestCmd(match = 'match_exact')
+
+    test = TestCmd.TestCmd(match = 'match_re')
+
+    test = TestCmd.TestCmd(match = 'match_re_dotall')
+
+The TestCmd module provides unbound global functions that can be used
+for the "diff" argument to TestCmd.TestCmd instantiation:
 
     import TestCmd
 
 
     test = TestCmd.TestCmd(diff = TestCmd.simple_diff)
 
+    test = TestCmd.TestCmd(diff = TestCmd.context_diff)
+
+    test = TestCmd.TestCmd(diff = TestCmd.unified_diff)
+
+These functions are also available as static methods:
+
+    import TestCmd
+
+    test = TestCmd.TestCmd(match = TestCmd.TestCmd.match_re,
+                           diff = TestCmd.TestCmd.diff_re)
+
+    test = TestCmd.TestCmd(diff = TestCmd.TestCmd.simple_diff)
+
+    test = TestCmd.TestCmd(diff = TestCmd.TestCmd.context_diff)
+
+    test = TestCmd.TestCmd(diff = TestCmd.TestCmd.unified_diff)
+
+These static methods can be accessed by a string naming the method:
+
+    import TestCmd
+
+    test = TestCmd.TestCmd(match = 'match_re', diff = 'diff_re')
+
+    test = TestCmd.TestCmd(diff = 'simple_diff')
+
+    test = TestCmd.TestCmd(diff = 'context_diff')
+
+    test = TestCmd.TestCmd(diff = 'unified_diff')
+
 The "diff" argument can also be used with standard difflib functions:
 
     import difflib
 from __future__ import division
 
 __author__ = "Steven Knight <knight at baldmt dot com>"
-__revision__ = "TestCmd.py 1.1.D002 2010/05/27 14:47:22 knight"
-__version__ = "1.1"
+__revision__ = "TestCmd.py 1.3.D001 2010/06/03 12:58:27 knight"
+__version__ = "1.3"
 
 import atexit
+import difflib
 import errno
 import os
 import re
 import shutil
+import signal
 import stat
 import sys
 import tempfile
+import threading
 import time
 import traceback
+import types
+
+class null(object):
+    pass
+_Null = null()
 
 try:
     from collections import UserList, UserString
     'TestCmd'
 ]
 
-try:
-    import difflib
-except ImportError:
-    __all__.append('simple_diff')
-
 def is_List(e):
     return isinstance(e, (list, UserList))
 
         raise re.error(msg % (repr(s), e.args[0]))
     return expr.match(lines)
 
-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 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'):
     # so we're going to cobble up something that looks just enough
     # like its API for our purposes below.
     import popen2
-    import types
     subprocess = types.ModuleType('subprocess')
 
     subprocess.PIPE = 'PIPE'
                 self.stderr.close()
             self.returncode = self.wait()
             return (out, err)
+        def terminate(self):
+            os.kill(self.pid, signal.SIGTERM)
         def wait(self, *args, **kw):
             resultcode = popen2.Popen3.wait(self, *args, **kw)
-            if os.WIFEXITED(resultcode):
+            if os.WIFSIGNALED(resultcode):
+                return (- os.WTERMSIG(resultcode))
+            elif os.WIFEXITED(resultcode):
                 return os.WEXITSTATUS(resultcode)
-            elif os.WIFSIGNALED(resultcode):
-                return os.WTERMSIG(resultcode)
             else:
                 return None
 
     subprocess.Popen = Popen
+else:
+    try:
+        subprocess.Popen.terminate
+    except AttributeError:
+        if sys.platform == 'win32':
+            import win32process
+            def terminate(self):
+                win32process.TerminateProcess(self._handle, 1)
+        else:
+            def terminate(self):
+                os.kill(self.pid, signal.SIGTERM)
+        method = types.MethodType(terminate, None, subprocess.Popen)
+        setattr(subprocess.Popen, 'terminate', method)
 
 
 
                        subdir = None,
                        verbose = None,
                        match = None,
+                       match_stdout = None,
+                       match_stderr = None,
                        diff = None,
+                       diff_stdout = None,
+                       diff_stderr = None,
                        combine = 0,
-                       universal_newlines = 1):
+                       universal_newlines = 1,
+                       timeout = None):
         self._cwd = os.getcwd()
         self.description_set(description)
         self.program_set(program)
         self.verbose_set(verbose)
         self.combine = combine
         self.universal_newlines = universal_newlines
-        if not match is None:
-            self.match_function = match
-        else:
-            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.process = None
+        self.set_timeout(timeout)
+        self.set_match_function(match, match_stdout, match_stderr)
+        self.set_diff_function(diff, diff_stdout, diff_stderr)
         self._dirlist = []
         self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
         if 'PRESERVE' in os.environ and not os.environ['PRESERVE'] is '':
         """
         self.description = description
 
-    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):
+    def set_diff_function(self, diff=_Null, stdout=_Null, stderr=_Null):
+        """Sets the specified diff functions.
+        """
+        if diff is not _Null:
+            self._diff_function = diff
+        if stdout is not _Null:
+            self._diff_stdout_function = stdout
+        if stderr is not _Null:
+            self._diff_stderr_function = stderr
+
+    def diff(self, a, b, name=None, diff_function=None, *args, **kw):
+        if diff_function is None:
+            try:
+                diff_function = getattr(self, self._diff_function)
+            except TypeError:
+                diff_function = self._diff_function
+                if diff_function is None:
+                    diff_function = self.simple_diff
+        if name is not None:
             print self.banner(name)
-            args = (a.splitlines(), b.splitlines()) + args
-            lines = self.diff_function(*args, **kw)
-            for l in lines:
-                print l
+        args = (a.splitlines(), b.splitlines()) + args
+        for line in diff_function(*args, **kw):
+            print line
+
+    def diff_stderr(self, a, b, *args, **kw):
+        """Compare actual and expected file contents.
+        """
+        try:
+            diff_stderr_function = getattr(self, self._diff_stderr_function)
+        except TypeError:
+            diff_stderr_function = self._diff_stderr_function
+        return self.diff(a, b, diff_function=diff_stderr_function, *args, **kw)
+
+    def diff_stdout(self, a, b, *args, **kw):
+        """Compare actual and expected file contents.
+        """
+        try:
+            diff_stdout_function = getattr(self, self._diff_stdout_function)
+        except TypeError:
+            diff_stdout_function = self._diff_stdout_function
+        return self.diff(a, b, diff_function=diff_stdout_function, *args, **kw)
+
+    simple_diff = staticmethod(simple_diff)
+
+    diff_re = staticmethod(diff_re)
+
+    context_diff = staticmethod(difflib.context_diff)
+
+    unified_diff = staticmethod(difflib.unified_diff)
 
     def fail_test(self, condition = 1, function = None, skip = 0):
         """Cause the test to fail.
         """
         self.interpreter = interpreter
 
+    def set_match_function(self, match=_Null, stdout=_Null, stderr=_Null):
+        """Sets the specified match functions.
+        """
+        if match is not _Null:
+            self._match_function = match
+        if stdout is not _Null:
+            self._match_stdout_function = stdout
+        if stderr is not _Null:
+            self._match_stderr_function = stderr
+
     def match(self, lines, matches):
         """Compare actual and expected file contents.
         """
-        return self.match_function(lines, matches)
+        try:
+            match_function = getattr(self, self._match_function)
+        except TypeError:
+            match_function = self._match_function
+            if match_function is None:
+                # Default is regular expression matches.
+                match_function = self.match_re
+        return match_function(lines, matches)
 
-    def match_exact(self, lines, matches):
+    def match_stderr(self, lines, matches):
         """Compare actual and expected file contents.
         """
-        return match_exact(lines, matches)
+        try:
+            match_stderr_function = getattr(self, self._match_stderr_function)
+        except TypeError:
+            match_stderr_function = self._match_stderr_function
+            if match_stderr_function is None:
+                # Default is to use whatever match= is set to.
+                match_stderr_function = self.match
+        return match_stderr_function(lines, matches)
 
-    def match_re(self, lines, res):
+    def match_stdout(self, lines, matches):
         """Compare actual and expected file contents.
         """
-        return match_re(lines, res)
+        try:
+            match_stdout_function = getattr(self, self._match_stdout_function)
+        except TypeError:
+            match_stdout_function = self._match_stdout_function
+            if match_stdout_function is None:
+                # Default is to use whatever match= is set to.
+                match_stdout_function = self.match
+        return match_stdout_function(lines, matches)
 
-    def match_re_dotall(self, lines, res):
-        """Compare actual and expected file contents.
-        """
-        return match_re_dotall(lines, res)
+    match_exact = staticmethod(match_exact)
+
+    match_re = staticmethod(match_re)
+
+    match_re_dotall = staticmethod(match_re_dotall)
 
     def no_result(self, condition = 1, function = None, skip = 0):
         """Report that the test could not be run.
         dir = self.canonicalize(dir)
         os.rmdir(dir)
 
+    def _timeout(self):
+        self.process.terminate()
+        self.timer.cancel()
+        self.timer = None
+
+    def set_timeout(self, timeout):
+        self.timeout = timeout
+        self.timer = None
+
     def start(self, program = None,
                     interpreter = None,
                     arguments = None,
                     universal_newlines = None,
+                    timeout = _Null,
                     **kw):
         """
         Starts a program or script for the test environment.
         else:
             stderr_value = subprocess.PIPE
 
-        return Popen(cmd,
-                     stdin=stdin,
-                     stdout=subprocess.PIPE,
-                     stderr=stderr_value,
-                     universal_newlines=universal_newlines)
+        if timeout is _Null:
+            timeout = self.timeout
+        if timeout:
+            self.timer = threading.Timer(float(timeout), self._timeout)
+            self.timer.start()
+        p = Popen(cmd,
+                  stdin=stdin,
+                  stdout=subprocess.PIPE,
+                  stderr=stderr_value,
+                  universal_newlines=universal_newlines)
+        self.process = p
+        return p
 
-    def finish(self, popen, **kw):
+    def finish(self, popen=None, **kw):
         """
         Finishes and waits for the process being run under control of
         the specified popen argument, recording the exit status,
         standard output and error output.
         """
+        if popen is None:
+            popen = self.process
         stdout, stderr = popen.communicate()
+        if self.timer:
+            self.timer.cancel()
+            self.timer = None
         self.status = popen.returncode
+        self.process = None
         self._stdout.append(stdout or '')
         self._stderr.append(stderr or '')
 
                   arguments = None,
                   chdir = None,
                   stdin = None,
-                  universal_newlines = None):
+                  universal_newlines = None,
+                  timeout = _Null):
         """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.
             if self.verbose:
                 sys.stderr.write("chdir(" + chdir + ")\n")
             os.chdir(chdir)
-        p = self.start(program,
-                       interpreter,
-                       arguments,
-                       universal_newlines,
-                       stdin=stdin)
+        p = self.start(program = program,
+                       interpreter = interpreter,
+                       arguments = arguments,
+                       universal_newlines = universal_newlines,
+                       timeout = timeout,
+                       stdin = stdin)
         if is_List(stdin):
             stdin = ''.join(stdin)
+        # TODO(sgk):  figure out how to re-use the logic in the .finish()
+        # method above.  Just calling it from here causes problems with
+        # subclasses that redefine .finish().  We could abstract this
+        # into Yet Another common method called both here and by .finish(),
+        # but that seems ill-thought-out.
         stdout, stderr = p.communicate(input=stdin)
+        if self.timer:
+            self.timer.cancel()
+            self.timer = None
         self.status = p.returncode
+        self.process = None
         self._stdout.append(stdout or '')
         self._stderr.append(stderr or '')
 

File QMTest/TestCmdTests.py

+#!/usr/bin/env python
+"""
+TestCmdTests.py:  Unit tests for the TestCmd.py module.
+
+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.
+
+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__ = "TestCmdTests.py 1.3.D001 2010/06/03 12:58:27 knight"
+
+import os
+import shutil
+import signal
+import stat
+import StringIO
+import sys
+import tempfile
+import time
+import types
+import unittest
+import UserList
+
+# Strip the current directory so we get the right TestCmd.py module.
+sys.path = sys.path[1:]
+
+import TestCmd
+
+def _is_readable(path):
+    # XXX this doesn't take into account UID, it assumes it's our file
+    return os.stat(path)[stat.ST_MODE] & stat.S_IREAD
+
+def _is_writable(path):
+    # XXX this doesn't take into account UID, it assumes it's our file
+    return os.stat(path)[stat.ST_MODE] & stat.S_IWRITE
+
+def _is_executable(path):
+    # XXX this doesn't take into account UID, it assumes it's our file
+    return os.stat(path)[stat.ST_MODE] & stat.S_IEXEC
+
+def _clear_dict(dict, *keys):
+    for key in keys:
+        try:
+            dict[key] = ''  # del dict[key]
+        except KeyError:
+            pass
+
+try:
+    import subprocess
+except ImportError:
+    # The subprocess module doesn't exist in this version of Python,
+    # so we're going to cobble up something that looks just enough
+    # like its API for our purposes below.
+    import popen2
+    subprocess = types.ModuleType('subprocess')
+
+    subprocess.PIPE = 'PIPE'
+    subprocess.STDOUT = 'STDOUT'
+    subprocess.mswindows = (sys.platform == 'win32')
+
+    class Popen(popen2.Popen3, popen2.Popen4):
+        universal_newlines = 1
+        def __init__(self, command, **kw):
+            if kw.get('stderr') == 'STDOUT':
+                popen2.Popen4.__init__(self, command, 1)
+            else:
+                popen2.Popen3.__init__(self, command, 1)
+            self.stdin = self.tochild
+            self.stdout = self.fromchild
+            self.stderr = self.childerr
+        def communicate(self, input=None):
+            if input:
+                self.stdin.write(input)
+            self.stdin.close()
+            out = self.stdout.read()
+            if self.stderr is None:
+                err = None
+            else:
+                err = self.stderr.read()
+            self.stdout.close()
+            if self.stderr is not None:
+                self.stderr.close()
+            self.returncode = self.wait()
+            return (out, err)
+        def terminate(self):
+            os.kill(self.pid, signal.SIGTERM)
+        def wait(self, *args, **kw):
+            resultcode = popen2.Popen3.wait(self, *args, **kw)
+            if os.WIFEXITED(resultcode):
+                return os.WEXITSTATUS(resultcode)
+            elif os.WIFSIGNALED(resultcode):
+                return os.WTERMSIG(resultcode)
+            else:
+                return None
+
+    subprocess.Popen = Popen
+else:
+    try:
+        subprocess.Popen.terminate
+    except AttributeError:
+        if sys.platform == 'win32':
+            import win32process
+            def terminate(self):
+                win32process.TerminateProcess(self._handle, 1)
+        else:
+            def terminate(self):
+                os.kill(self.pid, signal.SIGTERM)
+        method = types.MethodType(terminate, None, subprocess.Popen)
+        setattr(subprocess.Popen, 'terminate', method)
+
+class ExitError(Exception):
+    pass
+
+class TestCmdTestCase(unittest.TestCase):
+    """Base class for TestCmd test cases, with fixture and utility methods."""
+
+    def setUp(self):
+        self.orig_cwd = os.getcwd()
+
+    def tearDown(self):
+        os.chdir(self.orig_cwd)
+
+    def setup_run_scripts(self):
+        class T:
+            pass
+
+        t = T()
+
+        t.script = 'script'
+        t.scriptx = 'scriptx.bat'
+        t.script1 = 'script_1.txt'
+        t.scriptout = 'scriptout'
+        t.scripterr = 'scripterr'
+        fmt = "import os, sys; cwd = os.getcwd(); " + \
+              "sys.stdout.write('%s:  STDOUT:  %%s:  %%s\\n' %% (cwd, sys.argv[1:])); " + \
+              "sys.stderr.write('%s:  STDERR:  %%s:  %%s\\n' %% (cwd, sys.argv[1:]))"
+        fmtout = "import os, sys; cwd = os.getcwd(); " + \
+                 "sys.stdout.write('%s:  STDOUT:  %%s:  %%s\\n' %% (cwd, sys.argv[1:]))"
+        fmterr = "import os, sys; cwd = os.getcwd(); " + \
+                 "sys.stderr.write('%s:  STDERR:  %%s:  %%s\\n' %% (cwd, sys.argv[1:]))"
+        text = fmt % (t.script, t.script)
+        textx = fmt % (t.scriptx, t.scriptx)
+        if sys.platform == 'win32':
+            textx = textx.replace('%', '%%')
+            textx = '@python -c "%s"' % textx + ' %1 %2 %3 %4 %5 %6 %7 %8 %9\n'
+        else:
+            textx = '#! /usr/bin/env python\n' + textx + '\n'
+        text1 = 'A first line to be ignored!\n' + fmt % (t.script1, t.script1)
+        textout = fmtout % (t.scriptout)
+        texterr = fmterr % (t.scripterr)
+
+        run_env = TestCmd.TestCmd(workdir = '')
+        run_env.subdir('sub dir')
+        t.run_env = run_env
+
+        t.sub_dir = run_env.workpath('sub dir')
+        t.script_path = run_env.workpath('sub dir', t.script)
+        t.scriptx_path = run_env.workpath('sub dir', t.scriptx)
+        t.script1_path = run_env.workpath('sub dir', t.script1)
+        t.scriptout_path = run_env.workpath('sub dir', t.scriptout)
+        t.scripterr_path = run_env.workpath('sub dir', t.scripterr)
+
+        run_env.write(t.script_path, text)
+        run_env.write(t.scriptx_path, textx)
+        run_env.write(t.script1_path, text1)
+        run_env.write(t.scriptout_path, textout)
+        run_env.write(t.scripterr_path, texterr)
+
+        os.chmod(t.script_path, 0644)  # XXX UNIX-specific
+        os.chmod(t.scriptx_path, 0755)  # XXX UNIX-specific
+        os.chmod(t.script1_path, 0644)  # XXX UNIX-specific
+        os.chmod(t.scriptout_path, 0644)  # XXX UNIX-specific
+        os.chmod(t.scripterr_path, 0644)  # XXX UNIX-specific
+
+        t.orig_cwd = os.getcwd()
+
+        t.workdir = run_env.workpath('sub dir')
+        os.chdir(t.workdir)
+
+        return t
+
+    def translate_newlines(self, data):
+        data = data.replace("\r\n", "\n")
+        return data
+
+    def call_python(self, input, python=None):
+        if python is None:
+            python = sys.executable
+        p = subprocess.Popen(python,
+                             stdin=subprocess.PIPE,
+                             stderr=subprocess.PIPE,
+                             stdout=subprocess.PIPE)
+        stdout, stderr = p.communicate(input)
+        stdout = self.translate_newlines(stdout)
+        stderr = self.translate_newlines(stderr)
+        return stdout, stderr, p.returncode
+
+    def popen_python(self, input, status=0, stdout="", stderr="", python=None):
+        if python is None:
+            python = sys.executable
+        _stdout, _stderr, _status = self.call_python(input, python)
+        _stdout = self.translate_newlines(_stdout)
+        _stderr = self.translate_newlines(_stderr)
+        assert _status == status, \
+                "status = %s, expected %s\n" % (str(_status), str(status)) + \
+                "STDOUT ===================\n" + _stdout + \
+                "STDERR ===================\n" + _stderr
+        assert _stdout == stdout, \
+                "Expected STDOUT ==========\n" + stdout + \
+                "Actual STDOUT ============\n" + _stdout + \
+                "STDERR ===================\n" + _stderr
+        assert _stderr == stderr, \
+                "Expected STDERR ==========\n" + stderr + \
+                "Actual STDERR ============\n" + _stderr
+
+    def run_match(self, content, *args):
+        expect = "%s:  %s:  %s:  %s\n" % args
+        content = self.translate_newlines(content)
+        assert content == expect, \
+                "Expected %s ==========\n" % args[1] + expect + \
+                "Actual %s ============\n" % args[1] + content
+
+
+
+class __init__TestCase(TestCmdTestCase):
+    def test_init(self):
+        """Test init()"""
+        test = TestCmd.TestCmd()
+        test = TestCmd.TestCmd(description = 'test')
+        test = TestCmd.TestCmd(description = 'test', program = 'foo')
+        test = TestCmd.TestCmd(description = 'test',
+                               program = 'foo',
+                               universal_newlines=None)
+
+
+
+class basename_TestCase(TestCmdTestCase):
+    def test_basename(self):
+        """Test basename() [XXX TO BE WRITTEN]"""
+        assert 1 == 1
+
+
+
+class cleanup_TestCase(TestCmdTestCase):
+    def test_cleanup(self):
+        """Test cleanup()"""
+        test = TestCmd.TestCmd(workdir = '')
+        wdir = test.workdir
+        test.write('file1', "Test file #1\n")
+        test.cleanup()
+        assert not os.path.exists(wdir)
+
+    def test_writable(self):
+        """Test cleanup() when the directory isn't writable"""
+        test = TestCmd.TestCmd(workdir = '')
+        wdir = test.workdir
+        test.write('file2', "Test file #2\n")
+        os.chmod(test.workpath('file2'), 0400)
+        os.chmod(wdir, 0500)
+        test.cleanup()
+        assert not os.path.exists(wdir)
+
+    def test_shutil(self):
+        """Test cleanup() when used with shutil"""
+        test = TestCmd.TestCmd(workdir = '')
+        wdir = test.workdir
+        os.chdir(wdir)
+
+        import shutil
+        save_rmtree = shutil.rmtree
+        def my_rmtree(dir, ignore_errors=0, wdir=wdir, _rmtree=save_rmtree):
+            assert os.getcwd() != wdir
+            return _rmtree(dir, ignore_errors=ignore_errors)
+        try:
+            shutil.rmtree = my_rmtree
+            test.cleanup()
+        finally:
+            shutil.rmtree = save_rmtree
+
+    def test_atexit(self):
+        """Test cleanup() when atexit is used"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import atexit
+def my_exitfunc():
+    print "my_exitfunc()"
+atexit.register(my_exitfunc)
+import TestCmd
+result = TestCmd.TestCmd(workdir = '')
+sys.exit(0)
+""" % self.orig_cwd, stdout='my_exitfunc()\n')
+
+    def test_exitfunc(self):
+        """Test cleanup() when sys.exitfunc is set"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+def my_exitfunc():
+    print "my_exitfunc()"
+sys.exitfunc = my_exitfunc
+import TestCmd
+result = TestCmd.TestCmd(workdir = '')
+sys.exit(0)
+""" % self.orig_cwd, stdout='my_exitfunc()\n')
+
+
+
+class chmod_TestCase(TestCmdTestCase):
+    def test_chmod(self):
+        """Test chmod()"""
+        test = TestCmd.TestCmd(workdir = '', subdir = 'sub')
+
+        wdir_file1 = os.path.join(test.workdir, 'file1')
+        wdir_sub_file2 = os.path.join(test.workdir, 'sub', 'file2')
+
+        open(wdir_file1, 'w').write("")
+        open(wdir_sub_file2, 'w').write("")
+
+        if sys.platform == 'win32':
+
+            test.chmod(wdir_file1, stat.S_IREAD)
+            test.chmod(['sub', 'file2'], stat.S_IWRITE)
+
+            file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE])
+            assert file1_mode == 0444, '0%o' % file1_mode
+            file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE])
+            assert file2_mode == 0666, '0%o' % file2_mode
+
+            test.chmod('file1', stat.S_IWRITE)
+            test.chmod(wdir_sub_file2, stat.S_IREAD)
+
+            file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE])
+            assert file1_mode == 0666, '0%o' % file1_mode
+            file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE])
+            assert file2_mode == 0444, '0%o' % file2_mode
+
+        else:
+
+            test.chmod(wdir_file1, 0700)
+            test.chmod(['sub', 'file2'], 0760)
+
+            file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE])
+            assert file1_mode == 0700, '0%o' % file1_mode
+            file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE])
+            assert file2_mode == 0760, '0%o' % file2_mode
+
+            test.chmod('file1', 0765)
+            test.chmod(wdir_sub_file2, 0567)
+
+            file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE])
+            assert file1_mode == 0765, '0%o' % file1_mode
+            file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE])
+            assert file2_mode == 0567, '0%o' % file2_mode
+
+
+
+class combine_TestCase(TestCmdTestCase):
+    def test_combine(self):
+        """Test combining stdout and stderr"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        run_env.write('run1', """import sys
+sys.stdout.write("run1 STDOUT %s\\n" % sys.argv[1:])
+sys.stdout.write("run1 STDOUT second line\\n")
+sys.stderr.write("run1 STDERR %s\\n" % sys.argv[1:])
+sys.stderr.write("run1 STDERR second line\\n")
+sys.stdout.write("run1 STDOUT third line\\n")
+sys.stderr.write("run1 STDERR third line\\n")
+""")
+        run_env.write('run2', """import sys
+sys.stdout.write("run2 STDOUT %s\\n" % sys.argv[1:])
+sys.stdout.write("run2 STDOUT second line\\n")
+sys.stderr.write("run2 STDERR %s\\n" % sys.argv[1:])
+sys.stderr.write("run2 STDERR second line\\n")
+sys.stdout.write("run2 STDOUT third line\\n")
+sys.stderr.write("run2 STDERR third line\\n")
+""")
+        cwd = os.getcwd()
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        try:
+            test = TestCmd.TestCmd(interpreter = 'python',
+                                   workdir = '',
+                                   combine = 1)
+            try:
+                output = test.stdout()
+            except IndexError:
+                pass
+            else:
+                raise IndexError("got unexpected output:\n\t`%s'\n" % output)
+
+            # The underlying system subprocess implementations can combine
+            # stdout and stderr in different orders, so we accomodate both.
+
+            test.program_set('run1')
+            test.run(arguments = 'foo bar')
+            stdout_lines = """\
+run1 STDOUT ['foo', 'bar']
+run1 STDOUT second line
+run1 STDOUT third line
+"""
+            stderr_lines = """\
+run1 STDERR ['foo', 'bar']
+run1 STDERR second line
+run1 STDERR third line
+"""
+            foo_bar_expect = (stdout_lines + stderr_lines,
+                              stderr_lines + stdout_lines)
+
+            test.program_set('run2')
+            test.run(arguments = 'snafu')
+            stdout_lines = """\
+run2 STDOUT ['snafu']
+run2 STDOUT second line
+run2 STDOUT third line
+"""
+            stderr_lines = """\
+run2 STDERR ['snafu']
+run2 STDERR second line
+run2 STDERR third line
+"""
+            snafu_expect = (stdout_lines + stderr_lines,
+                            stderr_lines + stdout_lines)
+
+            # XXX SHOULD TEST ABSOLUTE NUMBER AS WELL
+            output = test.stdout()
+            output = self.translate_newlines(output)
+            assert output in snafu_expect, output
+            error = test.stderr()
+            assert error == '', error
+
+            output = test.stdout(run = -1)
+            output = self.translate_newlines(output)
+            assert output in foo_bar_expect, output
+            error = test.stderr(-1)
+            assert error == '', error
+        finally:
+            os.chdir(cwd)
+
+
+
+class description_TestCase(TestCmdTestCase):
+    def test_description(self):
+        """Test description()"""
+        test = TestCmd.TestCmd()
+        assert test.description is None, 'initialized description?'
+        test = TestCmd.TestCmd(description = 'test')
+        assert test.description == 'test', 'uninitialized description'
+        test.description_set('foo')
+        assert test.description == 'foo', 'did not set description'
+
+
+
+class diff_TestCase(TestCmdTestCase):
+    def test_diff_re(self):
+        """Test diff_re()"""
+        result = TestCmd.diff_re(["abcde"], ["abcde"])
+        assert result == [], result
+        result = TestCmd.diff_re(["a.*e"], ["abcde"])
+        assert result == [], result
+        result = TestCmd.diff_re(["a.*e"], ["xxx"])
+        assert result == ['1c1', "< 'a.*e'", '---', "> 'xxx'"], result
+
+    def test_diff_custom_function(self):
+        """Test diff() using a custom function"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def my_diff(a, b):
+    return [
+        '*****',
+        a,
+        '*****',
+        b,
+        '*****',
+    ]
+test = TestCmd.TestCmd(diff = my_diff)
+test.diff("a\\nb1\\nc\\n", "a\\nb2\\nc\\n", "STDOUT")
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout = """\
+STDOUT==========================================================================
+*****
+['a', 'b1', 'c']
+*****
+['a', 'b2', 'c']
+*****
+""")
+
+    def test_diff_string(self):
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff = 'diff_re')
+test.diff("a\\nb1\\nc\\n", "a\\nb2\\nc\\n", 'STDOUT')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout = """\
+STDOUT==========================================================================
+2c2
+< 'b1'
+---
+> 'b2'
+""")
+
+    def test_error(self):
+        """Test handling a compilation error in TestCmd.diff_re()"""
+        script_input = """import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+assert TestCmd.diff_re(["a.*(e"], ["abcde"])
+sys.exit(0)
+""" % self.orig_cwd
+        stdout, stderr, status = self.call_python(script_input)
+        assert status == 1, status
+        expect1 = "Regular expression error in '^a.*(e$': missing )\n"
+        expect2 = "Regular expression error in '^a.*(e$': unbalanced parenthesis\n"
+        assert (stderr.find(expect1) != -1 or
+                stderr.find(expect2) != -1), repr(stderr)
+
+    def test_simple_diff_static_method(self):
+        """Test calling the TestCmd.TestCmd.simple_diff() static method"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+result = TestCmd.TestCmd.simple_diff(['a', 'b', 'c', 'e', 'f1'],
+                                     ['a', 'c', 'd', 'e', 'f2'])
+expect = ['2d1', '< b', '3a3', '> d', '5c5', '< f1', '---', '> f2']
+assert result == expect, result
+sys.exit(0)
+""" % self.orig_cwd)
+
+    def test_context_diff_static_method(self):
+        """Test calling the TestCmd.TestCmd.context_diff() static method"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+result = TestCmd.TestCmd.context_diff(['a\\n', 'b\\n', 'c\\n', 'e\\n', 'f1\\n'],
+                                      ['a\\n', 'c\\n', 'd\\n', 'e\\n', 'f2\\n'])
+result = list(result)
+expect = [
+    '***  \\n',
+    '---  \\n',
+    '***************\\n',
+    '*** 1,5 ****\\n',
+    '  a\\n',
+    '- b\\n',
+    '  c\\n',
+    '  e\\n',
+    '! f1\\n',
+    '--- 1,5 ----\\n',
+    '  a\\n',
+    '  c\\n',
+    '+ d\\n',
+    '  e\\n',
+    '! f2\\n',
+]
+assert result == expect, result
+sys.exit(0)
+""" % self.orig_cwd)
+
+    def test_unified_diff_static_method(self):
+        """Test calling the TestCmd.TestCmd.unified_diff() static method"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+result = TestCmd.TestCmd.unified_diff(['a\\n', 'b\\n', 'c\\n', 'e\\n', 'f1\\n'],
+                                      ['a\\n', 'c\\n', 'd\\n', 'e\\n', 'f2\\n'])
+result = list(result)
+expect = [
+    '---  \\n',
+    '+++  \\n',
+    '@@ -1,5 +1,5 @@\\n',
+    ' a\\n',
+    '-b\\n',
+    ' c\\n',
+    '+d\\n',
+    ' e\\n',
+    '-f1\\n',
+    '+f2\\n'
+]
+assert result == expect, result
+sys.exit(0)
+""" % self.orig_cwd)
+
+    def test_diff_re_static_method(self):
+        """Test calling the TestCmd.TestCmd.diff_re() static method"""
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+result = TestCmd.TestCmd.diff_re(['a', 'b', 'c', '.', 'f1'],
+                                 ['a', 'c', 'd', 'e', 'f2'])
+expect = [
+    '2c2',
+    "< 'b'",
+    '---',
+    "> 'c'",
+    '3c3',
+    "< 'c'",
+    '---',
+    "> 'd'",
+    '5c5',
+    "< 'f1'",
+    '---',
+    "> 'f2'"
+]
+assert result == expect, result
+sys.exit(0)
+""" % self.orig_cwd)
+
+
+
+class diff_stderr_TestCase(TestCmdTestCase):
+    def test_diff_stderr_default(self):
+        """Test diff_stderr() default behavior"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd()
+test.diff_stderr('a\nb1\nc\n', 'a\nb2\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+2c2
+< b1
+---
+> b2
+""")
+
+    def test_diff_stderr_not_affecting_diff_stdout(self):
+        """Test diff_stderr() not affecting diff_stdout() behavior"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stderr='diff_re')
+print "diff_stderr:"
+test.diff_stderr('a\nb.\nc\n', 'a\nbb\nc\n')
+print "diff_stdout:"
+test.diff_stdout('a\nb.\nc\n', 'a\nbb\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+diff_stderr:
+diff_stdout:
+2c2
+< b.
+---
+> bb
+""")
+
+    def test_diff_stderr_custom_function(self):
+        """Test diff_stderr() using a custom function"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def my_diff(a, b):
+    return ["a:"] + a + ["b:"] + b
+test = TestCmd.TestCmd(diff_stderr=my_diff)
+test.diff_stderr('abc', 'def')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+a:
+abc
+b:
+def
+""")
+
+    def test_diff_stderr_TestCmd_function(self):
+        """Test diff_stderr() using a TestCmd function"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stderr = TestCmd.diff_re)
+test.diff_stderr('a\n.\n', 'b\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+1c1
+< 'a'
+---
+> 'b'
+""")
+
+    def test_diff_stderr_static_method(self):
+        """Test diff_stderr() using a static method"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stderr=TestCmd.TestCmd.diff_re)
+test.diff_stderr('a\n.\n', 'b\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+1c1
+< 'a'
+---
+> 'b'
+""")
+
+    def test_diff_stderr_string(self):
+        """Test diff_stderr() using a string to fetch the diff method"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stderr='diff_re')
+test.diff_stderr('a\n.\n', 'b\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+1c1
+< 'a'
+---
+> 'b'
+""")
+
+
+
+class diff_stdout_TestCase(TestCmdTestCase):
+    def test_diff_stdout_default(self):
+        """Test diff_stdout() default behavior"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd()
+test.diff_stdout('a\nb1\nc\n', 'a\nb2\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+2c2
+< b1
+---
+> b2
+""")
+
+    def test_diff_stdout_not_affecting_diff_stderr(self):
+        """Test diff_stdout() not affecting diff_stderr() behavior"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stdout='diff_re')
+print "diff_stdout:"
+test.diff_stdout('a\nb.\nc\n', 'a\nbb\nc\n')
+print "diff_stderr:"
+test.diff_stderr('a\nb.\nc\n', 'a\nbb\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+diff_stdout:
+diff_stderr:
+2c2
+< b.
+---
+> bb
+""")
+
+    def test_diff_stdout_custom_function(self):
+        """Test diff_stdout() using a custom function"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def my_diff(a, b):
+    return ["a:"] + a + ["b:"] + b
+test = TestCmd.TestCmd(diff_stdout=my_diff)
+test.diff_stdout('abc', 'def')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+a:
+abc
+b:
+def
+""")
+
+    def test_diff_stdout_TestCmd_function(self):
+        """Test diff_stdout() using a TestCmd function"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stdout = TestCmd.diff_re)
+test.diff_stdout('a\n.\n', 'b\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+1c1
+< 'a'
+---
+> 'b'
+""")
+
+    def test_diff_stdout_static_method(self):
+        """Test diff_stdout() using a static method"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stdout=TestCmd.TestCmd.diff_re)
+test.diff_stdout('a\n.\n', 'b\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+1c1
+< 'a'
+---
+> 'b'
+""")
+
+    def test_diff_stdout_string(self):
+        """Test diff_stdout() using a string to fetch the diff method"""
+        self.popen_python(r"""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(diff_stdout='diff_re')
+test.diff_stdout('a\n.\n', 'b\nc\n')
+sys.exit(0)
+""" % self.orig_cwd,
+                          stdout="""\
+1c1
+< 'a'
+---
+> 'b'
+""")
+
+
+
+class exit_TestCase(TestCmdTestCase):
+    def test_exit(self):
+        """Test exit()"""
+        def _test_it(cwd, tempdir, condition, preserved):
+            close_true = {'pass_test': 1, 'fail_test': 0, 'no_result': 0}
+            exit_status = {'pass_test': 0, 'fail_test': 1, 'no_result': 2}
+            result_string = {'pass_test': "PASSED\n",
+                             'fail_test': "FAILED test at line 5 of <stdin>\n",
+                             'no_result': "NO RESULT for test at line 5 of <stdin>\n"}
+            global ExitError
+            input = """import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(workdir = '%s')
+test.%s()
+""" % (cwd, tempdir, condition)
+            stdout, stderr, status = self.call_python(input, python="python")
+            if close_true[condition]:
+                unexpected = (status != 0)
+            else:
+                unexpected = (status == 0)
+            if unexpected:
+                msg = "Unexpected exit status from python:  %s\n"
+                raise ExitError(msg % status + stdout + stderr)
+            if status != exit_status[condition]:
+                        msg = "Expected exit status %d, got %d\n"
+                        raise ExitError(msg % (exit_status[condition], status))
+            if stderr != result_string[condition]:
+                msg = "Expected error output:\n%sGot error output:\n%s"
+                raise ExitError(msg % (result_string[condition], stderr))
+            if preserved:
+                if not os.path.exists(tempdir):
+                    msg = "Working directory %s was mistakenly removed\n"
+                    raise ExitError(msg % tempdir + stdout)
+            else:
+                if os.path.exists(tempdir):
+                    msg = "Working directory %s was mistakenly preserved\n"
+                    raise ExitError(msg % tempdir + stdout)
+
+        run_env = TestCmd.TestCmd(workdir = '')
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        try:
+            cwd = self.orig_cwd
+            _clear_dict(os.environ, 'PRESERVE', 'PRESERVE_PASS', 'PRESERVE_FAIL', 'PRESERVE_NO_RESULT')
+            _test_it(cwd, 'dir01', 'pass_test', 0)
+            _test_it(cwd, 'dir02', 'fail_test', 0)
+            _test_it(cwd, 'dir03', 'no_result', 0)
+            os.environ['PRESERVE'] = '1'
+            _test_it(cwd, 'dir04', 'pass_test', 1)
+            _test_it(cwd, 'dir05', 'fail_test', 1)
+            _test_it(cwd, 'dir06', 'no_result', 1)
+            os.environ['PRESERVE'] = ''  # del os.environ['PRESERVE']
+            os.environ['PRESERVE_PASS'] = '1'
+            _test_it(cwd, 'dir07', 'pass_test', 1)
+            _test_it(cwd, 'dir08', 'fail_test', 0)
+            _test_it(cwd, 'dir09', 'no_result', 0)
+            os.environ['PRESERVE_PASS'] = ''  # del os.environ['PRESERVE_PASS']
+            os.environ['PRESERVE_FAIL'] = '1'
+            _test_it(cwd, 'dir10', 'pass_test', 0)
+            _test_it(cwd, 'dir11', 'fail_test', 1)
+            _test_it(cwd, 'dir12', 'no_result', 0)
+            os.environ['PRESERVE_FAIL'] = ''  # del os.environ['PRESERVE_FAIL']
+            os.environ['PRESERVE_NO_RESULT'] = '1'
+            _test_it(cwd, 'dir13', 'pass_test', 0)
+            _test_it(cwd, 'dir14', 'fail_test', 0)
+            _test_it(cwd, 'dir15', 'no_result', 1)
+            os.environ['PRESERVE_NO_RESULT'] = '' # del os.environ['PRESERVE_NO_RESULT']
+        finally:
+            _clear_dict(os.environ, 'PRESERVE', 'PRESERVE_PASS', 'PRESERVE_FAIL', 'PRESERVE_NO_RESULT')
+
+
+
+class fail_test_TestCase(TestCmdTestCase):
+    def test_fail_test(self):
+        """Test fail_test()"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        run_env.write('run', """import sys
+sys.stdout.write("run:  STDOUT\\n")
+sys.stderr.write("run:  STDERR\\n")
+""")
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+TestCmd.fail_test(condition = 1)
+""" % self.orig_cwd, status = 1, stderr = "FAILED test at line 4 of <stdin>\n")
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')
+test.run()
+test.fail_test(condition = (test.status == 0))
+""" % self.orig_cwd, status = 1, stderr = "FAILED test of %s\n\tat line 6 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', description = 'xyzzy', workdir = '')
+test.run()
+test.fail_test(condition = (test.status == 0))
+""" % self.orig_cwd, status = 1, stderr = "FAILED test of %s [xyzzy]\n\tat line 6 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')
+test.run()
+def xxx():
+    sys.stderr.write("printed on failure\\n")
+test.fail_test(condition = (test.status == 0), function = xxx)
+""" % self.orig_cwd, status = 1, stderr = "printed on failure\nFAILED test of %s\n\tat line 8 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def test1(self):
+    self.run()
+    self.fail_test(condition = (self.status == 0))
+def test2(self):
+    test1(self)
+test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = ''))
+""" % self.orig_cwd, status = 1, stderr = "FAILED test of %s\n\tat line 6 of <stdin> (test1)\n\tfrom line 8 of <stdin> (test2)\n\tfrom line 9 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def test1(self):
+    self.run()
+    self.fail_test(condition = (self.status == 0), skip = 1)
+def test2(self):
+    test1(self)
+test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = ''))
+""" % self.orig_cwd, status = 1, stderr = "FAILED test of %s\n\tat line 8 of <stdin> (test2)\n\tfrom line 9 of <stdin>\n" % run_env.workpath('run'))
+
+
+
+class interpreter_TestCase(TestCmdTestCase):
+    def test_interpreter(self):
+        """Test interpreter()"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        run_env.write('run', """import sys
+sys.stdout.write("run:  STDOUT\\n")
+sys.stderr.write("run:  STDERR\\n")
+""")
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        test = TestCmd.TestCmd(program = 'run', workdir = '')
+        test.interpreter_set('foo')
+        assert test.interpreter == 'foo', 'did not set interpreter'
+        test.interpreter_set('python')
+        assert test.interpreter == 'python', 'did not set interpreter'
+        test.run()
+
+
+
+class match_TestCase(TestCmdTestCase):
+    def test_match_default(self):
+        """Test match() default behavior"""
+        test = TestCmd.TestCmd()
+        assert test.match("abcde\n", "a.*e\n")
+        assert test.match("12345\nabcde\n", "1\\d+5\na.*e\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match(lines, regexes)
+
+    def test_match_custom_function(self):
+        """Test match() using a custom function"""
+        def match_length(lines, matches):
+            return len(lines) == len(matches)
+        test = TestCmd.TestCmd(match=match_length)
+        assert not test.match("123\n", "1\n")
+        assert test.match("123\n", "111\n")
+        assert not test.match("123\n123\n", "1\n1\n")
+        assert test.match("123\n123\n", "111\n111\n")
+        lines = ["123\n", "123\n"]
+        regexes = ["1\n", "1\n"]
+        assert test.match(lines, regexes)       # due to equal numbers of lines
+
+    def test_match_TestCmd_function(self):
+        """Test match() using a TestCmd function"""
+        test = TestCmd.TestCmd(match = TestCmd.match_exact)
+        assert not test.match("abcde\n", "a.*e\n")
+        assert test.match("abcde\n", "abcde\n")
+        assert not test.match("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match(lines, regexes)
+        assert test.match(lines, lines)
+
+    def test_match_static_method(self):
+        """Test match() using a static method"""
+        test = TestCmd.TestCmd(match=TestCmd.TestCmd.match_exact)
+        assert not test.match("abcde\n", "a.*e\n")
+        assert test.match("abcde\n", "abcde\n")
+        assert not test.match("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match(lines, regexes)
+        assert test.match(lines, lines)
+
+    def test_match_string(self):
+        """Test match() using a string to fetch the match method"""
+        test = TestCmd.TestCmd(match='match_exact')
+        assert not test.match("abcde\n", "a.*e\n")
+        assert test.match("abcde\n", "abcde\n")
+        assert not test.match("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match(lines, regexes)
+        assert test.match(lines, lines)
+
+
+
+class match_exact_TestCase(TestCmdTestCase):
+    def test_match_exact_function(self):
+        """Test calling the TestCmd.match_exact() function"""
+        assert not TestCmd.match_exact("abcde\\n", "a.*e\\n")
+        assert TestCmd.match_exact("abcde\\n", "abcde\\n")
+
+    def test_match_exact_instance_method(self):
+        """Test calling the TestCmd.TestCmd().match_exact() instance method"""
+        test = TestCmd.TestCmd()
+        assert not test.match_exact("abcde\\n", "a.*e\\n")
+        assert test.match_exact("abcde\\n", "abcde\\n")
+
+    def test_match_exact_static_method(self):
+        """Test calling the TestCmd.TestCmd.match_exact() static method"""
+        assert not TestCmd.TestCmd.match_exact("abcde\\n", "a.*e\\n")
+        assert TestCmd.TestCmd.match_exact("abcde\\n", "abcde\\n")
+
+    def test_evaluation(self):
+        """Test match_exact() evaluation"""
+        test = TestCmd.TestCmd()
+        assert not test.match_exact("abcde\n", "a.*e\n")
+        assert test.match_exact("abcde\n", "abcde\n")
+        assert not test.match_exact(["12345\n", "abcde\n"], ["1[0-9]*5\n", "a.*e\n"])
+        assert test.match_exact(["12345\n", "abcde\n"], ["12345\n", "abcde\n"])
+        assert not test.match_exact(UserList.UserList(["12345\n", "abcde\n"]),
+                                    ["1[0-9]*5\n", "a.*e\n"])
+        assert test.match_exact(UserList.UserList(["12345\n", "abcde\n"]),
+                                ["12345\n", "abcde\n"])
+        assert not test.match_exact(["12345\n", "abcde\n"],
+                                    UserList.UserList(["1[0-9]*5\n", "a.*e\n"]))
+        assert test.match_exact(["12345\n", "abcde\n"],
+                                UserList.UserList(["12345\n", "abcde\n"]))
+        assert not test.match_exact("12345\nabcde\n", "1[0-9]*5\na.*e\n")
+        assert test.match_exact("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_exact(lines, regexes)
+        assert test.match_exact(lines, lines)
+
+
+
+class match_re_dotall_TestCase(TestCmdTestCase):
+    def test_match_re_dotall_function(self):
+        """Test calling the TestCmd.match_re_dotall() function"""
+        assert TestCmd.match_re_dotall("abcde\nfghij\n", "a.*j\n")
+
+    def test_match_re_dotall_instance_method(self):
+        """Test calling the TestCmd.TestCmd().match_re_dotall() instance method"""
+        test = TestCmd.TestCmd()
+        test.match_re_dotall("abcde\\nfghij\\n", "a.*j\\n")
+
+    def test_match_re_dotall_static_method(self):
+        """Test calling the TestCmd.TestCmd.match_re_dotall() static method"""
+        assert TestCmd.TestCmd.match_re_dotall("abcde\nfghij\n", "a.*j\n")
+
+    def test_error(self):
+        """Test handling a compilation error in TestCmd.match_re_dotall()"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        cwd = os.getcwd()
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        try:
+            script_input = """import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+assert TestCmd.match_re_dotall("abcde", "a.*(e")
+sys.exit(0)
+""" % cwd
+            stdout, stderr, status = self.call_python(script_input)
+            assert status == 1, status
+            expect1 = "Regular expression error in '^a.*(e$': missing )\n"
+            expect2 = "Regular expression error in '^a.*(e$': unbalanced parenthesis\n"
+            assert (stderr.find(expect1) != -1 or
+                    stderr.find(expect2) != -1), repr(stderr)
+        finally:
+            os.chdir(cwd)
+
+    def test_evaluation(self):
+        """Test match_re_dotall() evaluation"""
+        test = TestCmd.TestCmd()
+        assert test.match_re_dotall("abcde\nfghij\n", "a.*e\nf.*j\n")
+        assert test.match_re_dotall("abcde\nfghij\n", "a[^j]*j\n")
+        assert test.match_re_dotall("abcde\nfghij\n", "abcde\nfghij\n")
+        assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"],
+                                    ["1[0-9]*5\n", "a.*e\n", "f.*j\n"])
+        assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"],
+                                    ["1.*j\n"])
+        assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"],
+                                    ["12345\n", "abcde\n", "fghij\n"])
+        assert test.match_re_dotall(UserList.UserList(["12345\n",
+                                                       "abcde\n",
+                                                       "fghij\n"]),
+                                    ["1[0-9]*5\n", "a.*e\n", "f.*j\n"])
+        assert test.match_re_dotall(UserList.UserList(["12345\n",
+                                                       "abcde\n",
+                                                       "fghij\n"]),
+                                    ["1.*j\n"])
+        assert test.match_re_dotall(UserList.UserList(["12345\n",
+                                                       "abcde\n",
+                                                       "fghij\n"]),
+                                    ["12345\n", "abcde\n", "fghij\n"])
+        assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"],
+                                    UserList.UserList(["1[0-9]*5\n",
+                                                       "a.*e\n",
+                                                       "f.*j\n"]))
+        assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"],
+                                    UserList.UserList(["1.*j\n"]))
+        assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"],
+                                    UserList.UserList(["12345\n",
+                                                       "abcde\n",
+                                                       "fghij\n"]))
+        assert test.match_re_dotall("12345\nabcde\nfghij\n",
+                                    "1[0-9]*5\na.*e\nf.*j\n")
+        assert test.match_re_dotall("12345\nabcde\nfghij\n", "1.*j\n")
+        assert test.match_re_dotall("12345\nabcde\nfghij\n",
+                                    "12345\nabcde\nfghij\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match_re_dotall(lines, regexes)
+        assert test.match_re_dotall(lines, lines)
+
+
+
+class match_re_TestCase(TestCmdTestCase):
+    def test_match_re_function(self):
+        """Test calling the TestCmd.match_re() function"""
+        assert TestCmd.match_re("abcde\n", "a.*e\n")
+
+    def test_match_re_instance_method(self):
+        """Test calling the TestCmd.TestCmd().match_re() instance method"""
+        test = TestCmd.TestCmd()
+        assert test.match_re("abcde\n", "a.*e\n")
+
+    def test_match_re_static_method(self):
+        """Test calling the TestCmd.TestCmd.match_re() static method"""
+        assert TestCmd.TestCmd.match_re("abcde\n", "a.*e\n")
+
+    def test_error(self):
+        """Test handling a compilation error in TestCmd.match_re()"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        cwd = os.getcwd()
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        try:
+            script_input = """import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+assert TestCmd.match_re("abcde\\n", "a.*(e\\n")
+sys.exit(0)
+""" % cwd
+            stdout, stderr, status = self.call_python(script_input)
+            assert status == 1, status
+            expect1 = "Regular expression error in '^a.*(e$': missing )\n"
+            expect2 = "Regular expression error in '^a.*(e$': unbalanced parenthesis\n"
+            assert (stderr.find(expect1) != -1 or
+                    stderr.find(expect2) != -1), repr(stderr)
+        finally:
+            os.chdir(cwd)
+
+    def test_evaluation(self):
+        """Test match_re() evaluation"""
+        test = TestCmd.TestCmd()
+        assert test.match_re("abcde\n", "a.*e\n")
+        assert test.match_re("abcde\n", "abcde\n")
+        assert test.match_re(["12345\n", "abcde\n"], ["1[0-9]*5\n", "a.*e\n"])
+        assert test.match_re(["12345\n", "abcde\n"], ["12345\n", "abcde\n"])
+        assert test.match_re(UserList.UserList(["12345\n", "abcde\n"]),
+                             ["1[0-9]*5\n", "a.*e\n"])
+        assert test.match_re(UserList.UserList(["12345\n", "abcde\n"]),
+                             ["12345\n", "abcde\n"])
+        assert test.match_re(["12345\n", "abcde\n"],
+                             UserList.UserList(["1[0-9]*5\n", "a.*e\n"]))
+        assert test.match_re(["12345\n", "abcde\n"],
+                             UserList.UserList(["12345\n", "abcde\n"]))
+        assert test.match_re("12345\nabcde\n", "1[0-9]*5\na.*e\n")
+        assert test.match_re("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match_re(lines, regexes)
+        assert test.match_re(lines, lines)
+
+
+
+class match_stderr_TestCase(TestCmdTestCase):
+    def test_match_stderr_default(self):
+        """Test match_stderr() default behavior"""
+        test = TestCmd.TestCmd()
+        assert test.match_stderr("abcde\n", "a.*e\n")
+        assert test.match_stderr("12345\nabcde\n", "1\\d+5\na.*e\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match_stderr(lines, regexes)
+
+    def test_match_stderr_not_affecting_match_stdout(self):
+        """Test match_stderr() not affecting match_stdout() behavior"""
+        test = TestCmd.TestCmd(match_stderr=TestCmd.TestCmd.match_exact)
+
+        assert not test.match_stderr("abcde\n", "a.*e\n")
+        assert test.match_stderr("abcde\n", "abcde\n")
+        assert not test.match_stderr("12345\nabcde\n", "1\\d+5\na.*e\n")
+        assert test.match_stderr("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stderr(lines, regexes)
+        assert test.match_stderr(lines, lines)
+
+        assert test.match_stdout("abcde\n", "a.*e\n")
+        assert test.match_stdout("12345\nabcde\n", "1\\d+5\na.*e\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match_stdout(lines, regexes)
+
+    def test_match_stderr_custom_function(self):
+        """Test match_stderr() using a custom function"""
+        def match_length(lines, matches):
+            return len(lines) == len(matches)
+        test = TestCmd.TestCmd(match_stderr=match_length)
+        assert not test.match_stderr("123\n", "1\n")
+        assert test.match_stderr("123\n", "111\n")
+        assert not test.match_stderr("123\n123\n", "1\n1\n")
+        assert test.match_stderr("123\n123\n", "111\n111\n")
+        lines = ["123\n", "123\n"]
+        regexes = ["1\n", "1\n"]
+        assert test.match_stderr(lines, regexes)    # equal numbers of lines
+
+    def test_match_stderr_TestCmd_function(self):
+        """Test match_stderr() using a TestCmd function"""
+        test = TestCmd.TestCmd(match_stderr = TestCmd.match_exact)
+        assert not test.match_stderr("abcde\n", "a.*e\n")
+        assert test.match_stderr("abcde\n", "abcde\n")
+        assert not test.match_stderr("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match_stderr("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stderr(lines, regexes)
+        assert test.match_stderr(lines, lines)
+
+    def test_match_stderr_static_method(self):
+        """Test match_stderr() using a static method"""
+        test = TestCmd.TestCmd(match_stderr=TestCmd.TestCmd.match_exact)
+        assert not test.match_stderr("abcde\n", "a.*e\n")
+        assert test.match_stderr("abcde\n", "abcde\n")
+        assert not test.match_stderr("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match_stderr("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stderr(lines, regexes)
+        assert test.match_stderr(lines, lines)
+
+    def test_match_stderr_string(self):
+        """Test match_stderr() using a string to fetch the match method"""
+        test = TestCmd.TestCmd(match_stderr='match_exact')
+        assert not test.match_stderr("abcde\n", "a.*e\n")
+        assert test.match_stderr("abcde\n", "abcde\n")
+        assert not test.match_stderr("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match_stderr("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stderr(lines, regexes)
+        assert test.match_stderr(lines, lines)
+
+
+
+class match_stdout_TestCase(TestCmdTestCase):
+    def test_match_stdout_default(self):
+        """Test match_stdout() default behavior"""
+        test = TestCmd.TestCmd()
+        assert test.match_stdout("abcde\n", "a.*e\n")
+        assert test.match_stdout("12345\nabcde\n", "1\\d+5\na.*e\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match_stdout(lines, regexes)
+
+    def test_match_stdout_not_affecting_match_stderr(self):
+        """Test match_stdout() not affecting match_stderr() behavior"""
+        test = TestCmd.TestCmd(match_stdout=TestCmd.TestCmd.match_exact)
+
+        assert not test.match_stdout("abcde\n", "a.*e\n")
+        assert test.match_stdout("abcde\n", "abcde\n")
+        assert not test.match_stdout("12345\nabcde\n", "1\\d+5\na.*e\n")
+        assert test.match_stdout("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stdout(lines, regexes)
+        assert test.match_stdout(lines, lines)
+
+        assert test.match_stderr("abcde\n", "a.*e\n")
+        assert test.match_stderr("12345\nabcde\n", "1\\d+5\na.*e\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert test.match_stderr(lines, regexes)
+
+    def test_match_stdout_custom_function(self):
+        """Test match_stdout() using a custom function"""
+        def match_length(lines, matches):
+            return len(lines) == len(matches)
+        test = TestCmd.TestCmd(match_stdout=match_length)
+        assert not test.match_stdout("123\n", "1\n")
+        assert test.match_stdout("123\n", "111\n")
+        assert not test.match_stdout("123\n123\n", "1\n1\n")
+        assert test.match_stdout("123\n123\n", "111\n111\n")
+        lines = ["123\n", "123\n"]
+        regexes = ["1\n", "1\n"]
+        assert test.match_stdout(lines, regexes)    # equal numbers of lines
+
+    def test_match_stdout_TestCmd_function(self):
+        """Test match_stdout() using a TestCmd function"""
+        test = TestCmd.TestCmd(match_stdout = TestCmd.match_exact)
+        assert not test.match_stdout("abcde\n", "a.*e\n")
+        assert test.match_stdout("abcde\n", "abcde\n")
+        assert not test.match_stdout("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match_stdout("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stdout(lines, regexes)
+        assert test.match_stdout(lines, lines)
+
+    def test_match_stdout_static_method(self):
+        """Test match_stdout() using a static method"""
+        test = TestCmd.TestCmd(match_stdout=TestCmd.TestCmd.match_exact)
+        assert not test.match_stdout("abcde\n", "a.*e\n")
+        assert test.match_stdout("abcde\n", "abcde\n")
+        assert not test.match_stdout("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match_stdout("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stdout(lines, regexes)
+        assert test.match_stdout(lines, lines)
+
+    def test_match_stdout_string(self):
+        """Test match_stdout() using a string to fetch the match method"""
+        test = TestCmd.TestCmd(match_stdout='match_exact')
+        assert not test.match_stdout("abcde\n", "a.*e\n")
+        assert test.match_stdout("abcde\n", "abcde\n")
+        assert not test.match_stdout("12345\nabcde\n", "1\d+5\na.*e\n")
+        assert test.match_stdout("12345\nabcde\n", "12345\nabcde\n")
+        lines = ["vwxyz\n", "67890\n"]
+        regexes = ["v[^a-u]*z\n", "6[^ ]+0\n"]
+        assert not test.match_stdout(lines, regexes)
+        assert test.match_stdout(lines, lines)
+
+
+
+class no_result_TestCase(TestCmdTestCase):
+    def test_no_result(self):
+        """Test no_result()"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        run_env.write('run', """import sys
+sys.stdout.write("run:  STDOUT\\n")
+sys.stderr.write("run:  STDERR\\n")
+""")
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+TestCmd.no_result(condition = 1)
+""" % self.orig_cwd, status = 2, stderr = "NO RESULT for test at line 4 of <stdin>\n")
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')
+test.run()
+test.no_result(condition = (test.status == 0))
+""" % self.orig_cwd, status = 2, stderr = "NO RESULT for test of %s\n\tat line 6 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', description = 'xyzzy', workdir = '')
+test.run()
+test.no_result(condition = (test.status == 0))
+""" % self.orig_cwd, status = 2, stderr = "NO RESULT for test of %s [xyzzy]\n\tat line 6 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')
+test.run()
+def xxx():
+    sys.stderr.write("printed on no result\\n")
+test.no_result(condition = (test.status == 0), function = xxx)
+""" % self.orig_cwd, status = 2, stderr = "printed on no result\nNO RESULT for test of %s\n\tat line 8 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def test1(self):
+    self.run()
+    self.no_result(condition = (self.status == 0))
+def test2(self):
+    test1(self)
+test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = ''))
+""" % self.orig_cwd, status = 2, stderr = "NO RESULT for test of %s\n\tat line 6 of <stdin> (test1)\n\tfrom line 8 of <stdin> (test2)\n\tfrom line 9 of <stdin>\n" % run_env.workpath('run'))
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+def test1(self):
+    self.run()
+    self.no_result(condition = (self.status == 0), skip = 1)
+def test2(self):
+    test1(self)
+test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = ''))
+""" % self.orig_cwd, status = 2, stderr = "NO RESULT for test of %s\n\tat line 8 of <stdin> (test2)\n\tfrom line 9 of <stdin>\n" % run_env.workpath('run'))
+
+
+
+class pass_test_TestCase(TestCmdTestCase):
+    def test_pass_test(self):
+        """Test pass_test()"""
+        run_env = TestCmd.TestCmd(workdir = '')
+        run_env.write('run', """import sys
+sys.stdout.write("run:  STDOUT\\n")
+sys.stderr.write("run:  STDERR\\n")
+""")
+        os.chdir(run_env.workdir)
+        # Everything before this prepared our "source directory."
+        # Now do the real test.
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+TestCmd.pass_test(condition = 1)
+""" % self.orig_cwd, stderr = "PASSED\n")
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd
+test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')
+test.run()
+test.pass_test(condition = (test.status == 0))
+""" % self.orig_cwd, stderr = "PASSED\n")
+
+        self.popen_python("""import sys
+sys.path = ['%s'] + sys.path
+import TestCmd