Merge pull request #3257 from will133/continue-on-failure

Continue on doctest failure
This commit is contained in:
Bruno Oliveira 2018-02-27 07:45:41 -03:00 committed by GitHub
commit 5cb72b6188
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 45 deletions

View File

@ -196,6 +196,7 @@ Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Vitaly Lashmanov Vitaly Lashmanov
Vlad Dragos Vlad Dragos
William Lee
Wouter van Ackooy Wouter van Ackooy
Xuan Luong Xuan Luong
Xuecong Liao Xuecong Liao

View File

@ -24,6 +24,9 @@ DOCTEST_REPORT_CHOICES = (
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
) )
# Lazy definiton of runner class
RUNNER_CLASS = None
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addini('doctest_optionflags', 'option flags for doctests', parser.addini('doctest_optionflags', 'option flags for doctests',
@ -47,6 +50,10 @@ def pytest_addoption(parser):
action="store_true", default=False, action="store_true", default=False,
help="ignore doctest ImportErrors", help="ignore doctest ImportErrors",
dest="doctest_ignore_import_errors") 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): def pytest_collect_file(path, parent):
@ -77,14 +84,63 @@ def _is_doctest(config, path, parent):
class ReprFailDoctest(TerminalRepr): class ReprFailDoctest(TerminalRepr):
def __init__(self, reprlocation, lines): def __init__(self, reprlocation_lines):
self.reprlocation = reprlocation # List of (reprlocation, lines) tuples
self.lines = lines self.reprlocation_lines = reprlocation_lines
def toterminal(self, tw): def toterminal(self, tw):
for line in self.lines: for reprlocation, lines in self.reprlocation_lines:
tw.line(line) for line in lines:
self.reprlocation.toterminal(tw) 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): class DoctestItem(pytest.Item):
@ -106,7 +162,10 @@ class DoctestItem(pytest.Item):
def runtest(self): def runtest(self):
_check_all_skipped(self.dtest) _check_all_skipped(self.dtest)
self._disable_output_capturing_for_darwin() 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): def _disable_output_capturing_for_darwin(self):
""" """
@ -122,42 +181,51 @@ class DoctestItem(pytest.Item):
def repr_failure(self, excinfo): def repr_failure(self, excinfo):
import doctest import doctest
failures = None
if excinfo.errisinstance((doctest.DocTestFailure, if excinfo.errisinstance((doctest.DocTestFailure,
doctest.UnexpectedException)): doctest.UnexpectedException)):
doctestfailure = excinfo.value failures = [excinfo.value]
example = doctestfailure.example elif excinfo.errisinstance(MultipleDoctestFailures):
test = doctestfailure.test failures = excinfo.value.failures
filename = test.filename
if test.lineno is None: if failures is not None:
lineno = None reprlocation_lines = []
else: for failure in failures:
lineno = test.lineno + example.lineno + 1 example = failure.example
message = excinfo.type.__name__ test = failure.test
reprlocation = ReprFileLocation(filename, lineno, message) filename = test.filename
checker = _get_checker() if test.lineno is None:
report_choice = _get_report_choice(self.config.getoption("doctestreport")) lineno = None
if lineno is not None: else:
lines = doctestfailure.test.docstring.splitlines(False) lineno = test.lineno + example.lineno + 1
# add line numbers to the left of the error message message = type(failure).__name__
lines = ["%03d %s" % (i + test.lineno + 1, x) reprlocation = ReprFileLocation(filename, lineno, message)
for (i, x) in enumerate(lines)] checker = _get_checker()
# trim docstring error lines to 10 report_choice = _get_report_choice(self.config.getoption("doctestreport"))
lines = lines[max(example.lineno - 9, 0):example.lineno + 1] if lineno is not None:
else: lines = failure.test.docstring.splitlines(False)
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example'] # add line numbers to the left of the error message
indent = '>>>' lines = ["%03d %s" % (i + test.lineno + 1, x)
for line in example.source.splitlines(): for (i, x) in enumerate(lines)]
lines.append('??? %s %s' % (indent, line)) # trim docstring error lines to 10
indent = '...' lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
if excinfo.errisinstance(doctest.DocTestFailure): else:
lines += checker.output_difference(example, lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
doctestfailure.got, report_choice).split("\n") indent = '>>>'
else: for line in example.source.splitlines():
inner_excinfo = ExceptionInfo(excinfo.value.exc_info) lines.append('??? %s %s' % (indent, line))
lines += ["UNEXPECTED EXCEPTION: %s" % indent = '...'
repr(inner_excinfo.value)] if isinstance(failure, doctest.DocTestFailure):
lines += traceback.format_exception(*excinfo.value.exc_info) lines += checker.output_difference(example,
return ReprFailDoctest(reprlocation, lines) 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: else:
return super(DoctestItem, self).repr_failure(excinfo) return super(DoctestItem, self).repr_failure(excinfo)
@ -187,6 +255,16 @@ def get_optionflags(parent):
return flag_acc 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): class DoctestTextfile(pytest.Module):
obj = None obj = None
@ -202,8 +280,11 @@ class DoctestTextfile(pytest.Module):
globs = {'__name__': '__main__'} globs = {'__name__': '__main__'}
optionflags = get_optionflags(self) 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) _fix_spoof_python2(runner, encoding)
parser = doctest.DocTestParser() parser = doctest.DocTestParser()
@ -238,8 +319,10 @@ class DoctestModule(pytest.Module):
# uses internal doctest module parsing mechanism # uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder() finder = doctest.DocTestFinder()
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, runner = _get_runner(
checker=_get_checker()) verbose=0, optionflags=optionflags,
checker=_get_checker(),
continue_on_failure=_get_continue_on_failure(self.config))
for test in finder.find(module, module.__name__): for test in finder.find(module, module.__name__):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests

1
changelog/3149.feature Normal file
View File

@ -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.

View File

@ -115,6 +115,11 @@ itself::
>>> get_unicode_greeting() # doctest: +ALLOW_UNICODE >>> get_unicode_greeting() # doctest: +ALLOW_UNICODE
'Hello' '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 The 'doctest_namespace' fixture
------------------------------- -------------------------------

View File

@ -756,6 +756,27 @@ class TestDoctestSkips(object):
reprec = testdir.inline_run("--doctest-modules") reprec = testdir.inline_run("--doctest-modules")
reprec.assertoutcome(passed=0, skipped=0) 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): class TestDoctestAutoUseFixtures(object):