Merge pull request #3257 from will133/continue-on-failure
Continue on doctest failure
This commit is contained in:
commit
5cb72b6188
1
AUTHORS
1
AUTHORS
|
@ -196,6 +196,7 @@ Victor Uriarte
|
|||
Vidar T. Fauske
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
William Lee
|
||||
Wouter van Ackooy
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
|
|
|
@ -24,6 +24,9 @@ DOCTEST_REPORT_CHOICES = (
|
|||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
|
||||
)
|
||||
|
||||
# Lazy definiton of runner class
|
||||
RUNNER_CLASS = None
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini('doctest_optionflags', 'option flags for doctests',
|
||||
|
@ -47,6 +50,10 @@ def pytest_addoption(parser):
|
|||
action="store_true", default=False,
|
||||
help="ignore doctest ImportErrors",
|
||||
dest="doctest_ignore_import_errors")
|
||||
group.addoption("--doctest-continue-on-failure",
|
||||
action="store_true", default=False,
|
||||
help="for a given doctest, continue to run after the first failure",
|
||||
dest="doctest_continue_on_failure")
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
|
@ -77,14 +84,63 @@ def _is_doctest(config, path, parent):
|
|||
|
||||
class ReprFailDoctest(TerminalRepr):
|
||||
|
||||
def __init__(self, reprlocation, lines):
|
||||
self.reprlocation = reprlocation
|
||||
self.lines = lines
|
||||
def __init__(self, reprlocation_lines):
|
||||
# List of (reprlocation, lines) tuples
|
||||
self.reprlocation_lines = reprlocation_lines
|
||||
|
||||
def toterminal(self, tw):
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
self.reprlocation.toterminal(tw)
|
||||
for reprlocation, lines in self.reprlocation_lines:
|
||||
for line in lines:
|
||||
tw.line(line)
|
||||
reprlocation.toterminal(tw)
|
||||
|
||||
|
||||
class MultipleDoctestFailures(Exception):
|
||||
def __init__(self, failures):
|
||||
super(MultipleDoctestFailures, self).__init__()
|
||||
self.failures = failures
|
||||
|
||||
|
||||
def _init_runner_class():
|
||||
import doctest
|
||||
|
||||
class PytestDoctestRunner(doctest.DebugRunner):
|
||||
"""
|
||||
Runner to collect failures. Note that the out variable in this case is
|
||||
a list instead of a stdout-like object
|
||||
"""
|
||||
def __init__(self, checker=None, verbose=None, optionflags=0,
|
||||
continue_on_failure=True):
|
||||
doctest.DebugRunner.__init__(
|
||||
self, checker=checker, verbose=verbose, optionflags=optionflags)
|
||||
self.continue_on_failure = continue_on_failure
|
||||
|
||||
def report_failure(self, out, test, example, got):
|
||||
failure = doctest.DocTestFailure(test, example, got)
|
||||
if self.continue_on_failure:
|
||||
out.append(failure)
|
||||
else:
|
||||
raise failure
|
||||
|
||||
def report_unexpected_exception(self, out, test, example, exc_info):
|
||||
failure = doctest.UnexpectedException(test, example, exc_info)
|
||||
if self.continue_on_failure:
|
||||
out.append(failure)
|
||||
else:
|
||||
raise failure
|
||||
|
||||
return PytestDoctestRunner
|
||||
|
||||
|
||||
def _get_runner(checker=None, verbose=None, optionflags=0,
|
||||
continue_on_failure=True):
|
||||
# We need this in order to do a lazy import on doctest
|
||||
global RUNNER_CLASS
|
||||
if RUNNER_CLASS is None:
|
||||
RUNNER_CLASS = _init_runner_class()
|
||||
return RUNNER_CLASS(
|
||||
checker=checker, verbose=verbose, optionflags=optionflags,
|
||||
continue_on_failure=continue_on_failure)
|
||||
|
||||
|
||||
class DoctestItem(pytest.Item):
|
||||
|
@ -106,7 +162,10 @@ class DoctestItem(pytest.Item):
|
|||
def runtest(self):
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
self.runner.run(self.dtest)
|
||||
failures = []
|
||||
self.runner.run(self.dtest, out=failures)
|
||||
if failures:
|
||||
raise MultipleDoctestFailures(failures)
|
||||
|
||||
def _disable_output_capturing_for_darwin(self):
|
||||
"""
|
||||
|
@ -122,42 +181,51 @@ class DoctestItem(pytest.Item):
|
|||
|
||||
def repr_failure(self, excinfo):
|
||||
import doctest
|
||||
failures = None
|
||||
if excinfo.errisinstance((doctest.DocTestFailure,
|
||||
doctest.UnexpectedException)):
|
||||
doctestfailure = excinfo.value
|
||||
example = doctestfailure.example
|
||||
test = doctestfailure.test
|
||||
filename = test.filename
|
||||
if test.lineno is None:
|
||||
lineno = None
|
||||
else:
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = excinfo.type.__name__
|
||||
reprlocation = ReprFileLocation(filename, lineno, message)
|
||||
checker = _get_checker()
|
||||
report_choice = _get_report_choice(self.config.getoption("doctestreport"))
|
||||
if lineno is not None:
|
||||
lines = doctestfailure.test.docstring.splitlines(False)
|
||||
# add line numbers to the left of the error message
|
||||
lines = ["%03d %s" % (i + test.lineno + 1, x)
|
||||
for (i, x) in enumerate(lines)]
|
||||
# trim docstring error lines to 10
|
||||
lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
|
||||
else:
|
||||
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
|
||||
indent = '>>>'
|
||||
for line in example.source.splitlines():
|
||||
lines.append('??? %s %s' % (indent, line))
|
||||
indent = '...'
|
||||
if excinfo.errisinstance(doctest.DocTestFailure):
|
||||
lines += checker.output_difference(example,
|
||||
doctestfailure.got, report_choice).split("\n")
|
||||
else:
|
||||
inner_excinfo = ExceptionInfo(excinfo.value.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
lines += traceback.format_exception(*excinfo.value.exc_info)
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
failures = [excinfo.value]
|
||||
elif excinfo.errisinstance(MultipleDoctestFailures):
|
||||
failures = excinfo.value.failures
|
||||
|
||||
if failures is not None:
|
||||
reprlocation_lines = []
|
||||
for failure in failures:
|
||||
example = failure.example
|
||||
test = failure.test
|
||||
filename = test.filename
|
||||
if test.lineno is None:
|
||||
lineno = None
|
||||
else:
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = type(failure).__name__
|
||||
reprlocation = ReprFileLocation(filename, lineno, message)
|
||||
checker = _get_checker()
|
||||
report_choice = _get_report_choice(self.config.getoption("doctestreport"))
|
||||
if lineno is not None:
|
||||
lines = failure.test.docstring.splitlines(False)
|
||||
# add line numbers to the left of the error message
|
||||
lines = ["%03d %s" % (i + test.lineno + 1, x)
|
||||
for (i, x) in enumerate(lines)]
|
||||
# trim docstring error lines to 10
|
||||
lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
|
||||
else:
|
||||
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
|
||||
indent = '>>>'
|
||||
for line in example.source.splitlines():
|
||||
lines.append('??? %s %s' % (indent, line))
|
||||
indent = '...'
|
||||
if isinstance(failure, doctest.DocTestFailure):
|
||||
lines += checker.output_difference(example,
|
||||
failure.got,
|
||||
report_choice).split("\n")
|
||||
else:
|
||||
inner_excinfo = ExceptionInfo(failure.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
lines += traceback.format_exception(*failure.exc_info)
|
||||
reprlocation_lines.append((reprlocation, lines))
|
||||
return ReprFailDoctest(reprlocation_lines)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
|
||||
|
@ -187,6 +255,16 @@ def get_optionflags(parent):
|
|||
return flag_acc
|
||||
|
||||
|
||||
def _get_continue_on_failure(config):
|
||||
continue_on_failure = config.getvalue('doctest_continue_on_failure')
|
||||
if continue_on_failure:
|
||||
# We need to turn off this if we use pdb since we should stop at
|
||||
# the first failure
|
||||
if config.getvalue("usepdb"):
|
||||
continue_on_failure = False
|
||||
return continue_on_failure
|
||||
|
||||
|
||||
class DoctestTextfile(pytest.Module):
|
||||
obj = None
|
||||
|
||||
|
@ -202,8 +280,11 @@ class DoctestTextfile(pytest.Module):
|
|||
globs = {'__name__': '__main__'}
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker())
|
||||
|
||||
runner = _get_runner(
|
||||
verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker(),
|
||||
continue_on_failure=_get_continue_on_failure(self.config))
|
||||
_fix_spoof_python2(runner, encoding)
|
||||
|
||||
parser = doctest.DocTestParser()
|
||||
|
@ -238,8 +319,10 @@ class DoctestModule(pytest.Module):
|
|||
# uses internal doctest module parsing mechanism
|
||||
finder = doctest.DocTestFinder()
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker())
|
||||
runner = _get_runner(
|
||||
verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker(),
|
||||
continue_on_failure=_get_continue_on_failure(self.config))
|
||||
|
||||
for test in finder.find(module, module.__name__):
|
||||
if test.examples: # skip empty doctests
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
New ``--doctest-continue-on-failure`` command-line option to enable doctests to show multiple failures for each snippet, instead of stopping at the first failure.
|
|
@ -115,6 +115,11 @@ itself::
|
|||
>>> get_unicode_greeting() # doctest: +ALLOW_UNICODE
|
||||
'Hello'
|
||||
|
||||
By default, pytest would report only the first failure for a given doctest. If
|
||||
you want to continue the test even when you have failures, do::
|
||||
|
||||
pytest --doctest-modules --doctest-continue-on-failure
|
||||
|
||||
|
||||
The 'doctest_namespace' fixture
|
||||
-------------------------------
|
||||
|
|
|
@ -756,6 +756,27 @@ class TestDoctestSkips(object):
|
|||
reprec = testdir.inline_run("--doctest-modules")
|
||||
reprec.assertoutcome(passed=0, skipped=0)
|
||||
|
||||
def test_continue_on_failure(self, testdir):
|
||||
testdir.maketxtfile(test_something="""
|
||||
>>> i = 5
|
||||
>>> def foo():
|
||||
... raise ValueError('error1')
|
||||
>>> foo()
|
||||
>>> i
|
||||
>>> i + 2
|
||||
7
|
||||
>>> i + 1
|
||||
""")
|
||||
result = testdir.runpytest("--doctest-modules", "--doctest-continue-on-failure")
|
||||
result.assert_outcomes(passed=0, failed=1)
|
||||
# The lines that contains the failure are 4, 5, and 8. The first one
|
||||
# is a stack trace and the other two are mismatches.
|
||||
result.stdout.fnmatch_lines([
|
||||
"*4: UnexpectedException*",
|
||||
"*5: DocTestFailure*",
|
||||
"*8: DocTestFailure*",
|
||||
])
|
||||
|
||||
|
||||
class TestDoctestAutoUseFixtures(object):
|
||||
|
||||
|
|
Loading…
Reference in New Issue