- Split DocTestRunner's check_output and output_difference methods off

into their own class, OutputChecker.
- Added optional OutputChecker arguments to DocTestRunner,
  DocTestCase, DocTestSuite.
This commit is contained in:
Edward Loper 2004-08-09 02:45:41 +00:00
parent 7c748469c7
commit 34fcb14768
1 changed files with 139 additions and 127 deletions

View File

@ -1062,9 +1062,6 @@ class DocTestFinder:
## 5. DocTest Runner ## 5. DocTest Runner
###################################################################### ######################################################################
# [XX] Should overridable methods (eg DocTestRunner.check_output) be
# named with a leading underscore?
class DocTestRunner: class DocTestRunner:
""" """
A class used to run DocTest test cases, and accumulate statistics. A class used to run DocTest test cases, and accumulate statistics.
@ -1105,12 +1102,11 @@ class DocTestRunner:
0 0
The comparison between expected outputs and actual outputs is done The comparison between expected outputs and actual outputs is done
by the `check_output` method. This comparison may be customized by an `OutputChecker`. This comparison may be customized with a
with a number of option flags; see the documentation for `testmod` number of option flags; see the documentation for `testmod` for
for more information. If the option flags are insufficient, then more information. If the option flags are insufficient, then the
the comparison may also be customized by subclassing comparison may also be customized by passing a subclass of
DocTestRunner, and overriding the methods `check_output` and `OutputChecker` to the constructor.
`output_difference`.
The test runner's display output can be controlled in two ways. The test runner's display output can be controlled in two ways.
First, an output function (`out) can be passed to First, an output function (`out) can be passed to
@ -1125,10 +1121,14 @@ class DocTestRunner:
# separate sections of the summary. # separate sections of the summary.
DIVIDER = "*" * 70 DIVIDER = "*" * 70
def __init__(self, verbose=None, optionflags=0): def __init__(self, checker=None, verbose=None, optionflags=0):
""" """
Create a new test runner. Create a new test runner.
Optional keyword arg `checker` is the `OutputChecker` that
should be used to compare the expected outputs and actual
outputs of doctest examples.
Optional keyword arg 'verbose' prints lots of stuff if true, Optional keyword arg 'verbose' prints lots of stuff if true,
only failures if false; by default, it's true iff '-v' is in only failures if false; by default, it's true iff '-v' is in
sys.argv. sys.argv.
@ -1138,6 +1138,7 @@ class DocTestRunner:
it displays failures. See the documentation for `testmod` for it displays failures. See the documentation for `testmod` for
more information. more information.
""" """
self._checker = checker or OutputChecker()
if verbose is None: if verbose is None:
verbose = '-v' in sys.argv verbose = '-v' in sys.argv
self._verbose = verbose self._verbose = verbose
@ -1151,114 +1152,6 @@ class DocTestRunner:
# Create a fake output target for capturing doctest output. # Create a fake output target for capturing doctest output.
self._fakeout = _SpoofOut() self._fakeout = _SpoofOut()
#/////////////////////////////////////////////////////////////////
# Output verification methods
#/////////////////////////////////////////////////////////////////
# These two methods should be updated together, since the
# output_difference method needs to know what should be considered
# to match by check_output.
def check_output(self, want, got):
"""
Return True iff the actual output (`got`) matches the expected
output (`want`). These strings are always considered to match
if they are identical; but depending on what option flags the
test runner is using, several non-exact match types are also
possible. See the documentation for `TestRunner` for more
information about option flags.
"""
# Handle the common case first, for efficiency:
# if they're string-identical, always return true.
if got == want:
return True
# The values True and False replaced 1 and 0 as the return
# value for boolean comparisons in Python 2.3.
if not (self.optionflags & DONT_ACCEPT_TRUE_FOR_1):
if (got,want) == ("True\n", "1\n"):
return True
if (got,want) == ("False\n", "0\n"):
return True
# <BLANKLINE> can be used as a special sequence to signify a
# blank line, unless the DONT_ACCEPT_BLANKLINE flag is used.
if not (self.optionflags & DONT_ACCEPT_BLANKLINE):
# Replace <BLANKLINE> in want with a blank line.
want = re.sub('(?m)^%s\s*?$' % re.escape(BLANKLINE_MARKER),
'', want)
# If a line in got contains only spaces, then remove the
# spaces.
got = re.sub('(?m)^\s*?$', '', got)
if got == want:
return True
# This flag causes doctest to ignore any differences in the
# contents of whitespace strings. Note that this can be used
# in conjunction with the ELLISPIS flag.
if (self.optionflags & NORMALIZE_WHITESPACE):
got = ' '.join(got.split())
want = ' '.join(want.split())
if got == want:
return True
# The ELLIPSIS flag says to let the sequence "..." in `want`
# match any substring in `got`. We implement this by
# transforming `want` into a regular expression.
if (self.optionflags & ELLIPSIS):
# Escape any special regexp characters
want_re = re.escape(want)
# Replace ellipsis markers ('...') with .*
want_re = want_re.replace(re.escape(ELLIPSIS_MARKER), '.*')
# Require that it matches the entire string; and set the
# re.DOTALL flag (with '(?s)').
want_re = '(?s)^%s$' % want_re
# Check if the `want_re` regexp matches got.
if re.match(want_re, got):
return True
# We didn't find any match; return false.
return False
def output_difference(self, want, got):
"""
Return a string describing the differences between the
expected output (`want`) and the actual output (`got`).
"""
# If <BLANKLINE>s are being used, then replace <BLANKLINE>
# with blank lines in the expected output string.
if not (self.optionflags & DONT_ACCEPT_BLANKLINE):
want = re.sub('(?m)^%s$' % re.escape(BLANKLINE_MARKER), '', want)
# Check if we should use diff. Don't use diff if the actual
# or expected outputs are too short, or if the expected output
# contains an ellipsis marker.
if ((self.optionflags & (UNIFIED_DIFF | CONTEXT_DIFF)) and
want.count('\n') > 2 and got.count('\n') > 2 and
not (self.optionflags & ELLIPSIS and '...' in want)):
# Split want & got into lines.
want_lines = [l+'\n' for l in want.split('\n')]
got_lines = [l+'\n' for l in got.split('\n')]
# Use difflib to find their differences.
if self.optionflags & UNIFIED_DIFF:
diff = difflib.unified_diff(want_lines, got_lines, n=2,
fromfile='Expected', tofile='Got')
kind = 'unified'
elif self.optionflags & CONTEXT_DIFF:
diff = difflib.context_diff(want_lines, got_lines, n=2,
fromfile='Expected', tofile='Got')
kind = 'context'
else:
assert 0, 'Bad diff option'
# Remove trailing whitespace on diff output.
diff = [line.rstrip() + '\n' for line in diff]
return _tag_msg("Differences (" + kind + " diff)",
''.join(diff))
# If we're not using diff, then simply list the expected
# output followed by the actual output.
return (_tag_msg("Expected", want or "Nothing") +
_tag_msg("Got", got))
#///////////////////////////////////////////////////////////////// #/////////////////////////////////////////////////////////////////
# Reporting methods # Reporting methods
#///////////////////////////////////////////////////////////////// #/////////////////////////////////////////////////////////////////
@ -1286,7 +1179,8 @@ class DocTestRunner:
""" """
# Print an error message. # Print an error message.
out(self.__failure_header(test, example) + out(self.__failure_header(test, example) +
self.output_difference(example.want, got)) self._checker.output_difference(example.want, got,
self.optionflags))
def report_unexpected_exception(self, out, test, example, exc_info): def report_unexpected_exception(self, out, test, example, exc_info):
""" """
@ -1412,7 +1306,8 @@ class DocTestRunner:
# If the example executed without raising any exceptions, # If the example executed without raising any exceptions,
# then verify its output and report its outcome. # then verify its output and report its outcome.
if exception is None: if exception is None:
if self.check_output(example.want, got): if self._checker.check_output(example.want, got,
self.optionflags):
self.report_success(out, test, example, got) self.report_success(out, test, example, got)
else: else:
self.report_failure(out, test, example, got) self.report_failure(out, test, example, got)
@ -1436,8 +1331,10 @@ class DocTestRunner:
# The test passes iff the pre-exception output and # The test passes iff the pre-exception output and
# the exception description match the values given # the exception description match the values given
# in `want`. # in `want`.
if (self.check_output(m.group('out'), got) and if (self._checker.check_output(m.group('out'), got,
self.check_output(m.group('exc'), exc_msg)): self.optionflags) and
self._checker.check_output(m.group('exc'), exc_msg,
self.optionflags)):
# Is +exc_msg the right thing here?? # Is +exc_msg the right thing here??
self.report_success(out, test, example, self.report_success(out, test, example,
got+exc_hdr+exc_msg) got+exc_hdr+exc_msg)
@ -1554,6 +1451,115 @@ class DocTestRunner:
print "Test passed." print "Test passed."
return totalf, totalt return totalf, totalt
class OutputChecker:
"""
A class used to check the whether the actual output from a doctest
example matches the expected output. `OutputChecker` defines two
methods: `check_output`, which compares a given pair of outputs,
and returns true if they match; and `output_difference`, which
returns a string describing the differences between two outputs.
"""
def check_output(self, want, got, optionflags):
"""
Return True iff the actual output (`got`) matches the expected
output (`want`). These strings are always considered to match
if they are identical; but depending on what option flags the
test runner is using, several non-exact match types are also
possible. See the documentation for `TestRunner` for more
information about option flags.
"""
# Handle the common case first, for efficiency:
# if they're string-identical, always return true.
if got == want:
return True
# The values True and False replaced 1 and 0 as the return
# value for boolean comparisons in Python 2.3.
if not (optionflags & DONT_ACCEPT_TRUE_FOR_1):
if (got,want) == ("True\n", "1\n"):
return True
if (got,want) == ("False\n", "0\n"):
return True
# <BLANKLINE> can be used as a special sequence to signify a
# blank line, unless the DONT_ACCEPT_BLANKLINE flag is used.
if not (optionflags & DONT_ACCEPT_BLANKLINE):
# Replace <BLANKLINE> in want with a blank line.
want = re.sub('(?m)^%s\s*?$' % re.escape(BLANKLINE_MARKER),
'', want)
# If a line in got contains only spaces, then remove the
# spaces.
got = re.sub('(?m)^\s*?$', '', got)
if got == want:
return True
# This flag causes doctest to ignore any differences in the
# contents of whitespace strings. Note that this can be used
# in conjunction with the ELLISPIS flag.
if (optionflags & NORMALIZE_WHITESPACE):
got = ' '.join(got.split())
want = ' '.join(want.split())
if got == want:
return True
# The ELLIPSIS flag says to let the sequence "..." in `want`
# match any substring in `got`. We implement this by
# transforming `want` into a regular expression.
if (optionflags & ELLIPSIS):
# Escape any special regexp characters
want_re = re.escape(want)
# Replace ellipsis markers ('...') with .*
want_re = want_re.replace(re.escape(ELLIPSIS_MARKER), '.*')
# Require that it matches the entire string; and set the
# re.DOTALL flag (with '(?s)').
want_re = '(?s)^%s$' % want_re
# Check if the `want_re` regexp matches got.
if re.match(want_re, got):
return True
# We didn't find any match; return false.
return False
def output_difference(self, want, got, optionflags):
"""
Return a string describing the differences between the
expected output (`want`) and the actual output (`got`).
"""
# If <BLANKLINE>s are being used, then replace <BLANKLINE>
# with blank lines in the expected output string.
if not (optionflags & DONT_ACCEPT_BLANKLINE):
want = re.sub('(?m)^%s$' % re.escape(BLANKLINE_MARKER), '', want)
# Check if we should use diff. Don't use diff if the actual
# or expected outputs are too short, or if the expected output
# contains an ellipsis marker.
if ((optionflags & (UNIFIED_DIFF | CONTEXT_DIFF)) and
want.count('\n') > 2 and got.count('\n') > 2 and
not (optionflags & ELLIPSIS and '...' in want)):
# Split want & got into lines.
want_lines = [l+'\n' for l in want.split('\n')]
got_lines = [l+'\n' for l in got.split('\n')]
# Use difflib to find their differences.
if optionflags & UNIFIED_DIFF:
diff = difflib.unified_diff(want_lines, got_lines, n=2,
fromfile='Expected', tofile='Got')
kind = 'unified'
elif optionflags & CONTEXT_DIFF:
diff = difflib.context_diff(want_lines, got_lines, n=2,
fromfile='Expected', tofile='Got')
kind = 'context'
else:
assert 0, 'Bad diff option'
# Remove trailing whitespace on diff output.
diff = [line.rstrip() + '\n' for line in diff]
return _tag_msg("Differences (" + kind + " diff)",
''.join(diff))
# If we're not using diff, then simply list the expected
# output followed by the actual output.
return (_tag_msg("Expected", want or "Nothing") +
_tag_msg("Got", got))
class DocTestFailure(Exception): class DocTestFailure(Exception):
"""A DocTest example has failed in debugging mode. """A DocTest example has failed in debugging mode.
@ -1937,9 +1943,11 @@ class Tester:
class DocTestCase(unittest.TestCase): class DocTestCase(unittest.TestCase):
def __init__(self, test, optionflags=0, setUp=None, tearDown=None): def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
checker=None):
unittest.TestCase.__init__(self) unittest.TestCase.__init__(self)
self._dt_optionflags = optionflags self._dt_optionflags = optionflags
self._dt_checker = checker
self._dt_test = test self._dt_test = test
self._dt_setUp = setUp self._dt_setUp = setUp
self._dt_tearDown = tearDown self._dt_tearDown = tearDown
@ -1956,7 +1964,8 @@ class DocTestCase(unittest.TestCase):
test = self._dt_test test = self._dt_test
old = sys.stdout old = sys.stdout
new = StringIO() new = StringIO()
runner = DocTestRunner(optionflags=self._dt_optionflags, verbose=False) runner = DocTestRunner(optionflags=self._dt_optionflags,
checker=self._dt_checker, verbose=False)
try: try:
runner.DIVIDER = "-"*70 runner.DIVIDER = "-"*70
@ -2045,7 +2054,8 @@ class DocTestCase(unittest.TestCase):
""" """
runner = DebugRunner(verbose = False, optionflags=self._dt_optionflags) runner = DebugRunner(optionflags=self._dt_optionflags,
checker=self._dt_checker, verbose=False)
runner.run(self._dt_test, out=nooutput) runner.run(self._dt_test, out=nooutput)
def id(self): def id(self):
@ -2065,7 +2075,8 @@ def nooutput(*args):
def DocTestSuite(module=None, globs=None, extraglobs=None, def DocTestSuite(module=None, globs=None, extraglobs=None,
optionflags=0, test_finder=None, optionflags=0, test_finder=None,
setUp=lambda: None, tearDown=lambda: None): setUp=lambda: None, tearDown=lambda: None,
checker=None):
""" """
Convert doctest tests for a mudule to a unittest test suite. Convert doctest tests for a mudule to a unittest test suite.
@ -2103,7 +2114,8 @@ def DocTestSuite(module=None, globs=None, extraglobs=None,
elif filename.endswith(".pyo"): elif filename.endswith(".pyo"):
filename = filename[:-1] filename = filename[:-1]
test.filename = filename test.filename = filename
suite.addTest(DocTestCase(test, optionflags, setUp, tearDown)) suite.addTest(DocTestCase(test, optionflags, setUp, tearDown,
checker))
return suite return suite