mirror of https://github.com/python/cpython
- 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:
parent
7c748469c7
commit
34fcb14768
266
Lib/doctest.py
266
Lib/doctest.py
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue