Fixed #3149 where doctest does not continue to run when there is a failure

This commit is contained in:
William Lee 2018-02-22 21:00:54 -06:00
parent 7336dbb979
commit fbc45be83f
3 changed files with 161 additions and 45 deletions

View File

@ -195,6 +195,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',
@ -77,14 +80,91 @@ 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 DoctestFailureContainer(object):
#
# NAME = 'DocTestFailure'
#
# def __init__(self, test, example, got):
# self.test = test
# self.example = example
# self.got = got
#
#
# class DoctestUnexpectedExceptionContainer(object):
#
# NAME = 'DoctestUnexpectedException'
#
# def __init__(self, test, example, exc_info):
# self.test = test
# self.example = example
# self.exc_info = exc_info
class MultipleDoctestFailures(Exception):
def __init__(self, failures):
super(MultipleDoctestFailures, self).__init__()
self.failures = failures
def _init_runner_class():
import doctest
class PytestDoctestRunner(doctest.DocTestRunner):
"""
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.DocTestRunner.__init__(
self, checker=checker, verbose=verbose, optionflags=optionflags)
self.continue_on_failure = continue_on_failure
def report_start(self, out, test, example):
pass
def report_success(self, out, test, example, got):
pass
def report_failure(self, out, test, example, got):
# failure = DoctestFailureContainer(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 = DoctestUnexpectedExceptionContainer(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 +186,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 +205,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)
@ -202,8 +294,10 @@ 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, continue_on_failure = not self.config.getvalue("usepdb")
checker=_get_checker()) runner = _get_runner(verbose=0, optionflags=optionflags,
checker=_get_checker(),
continue_on_failure=continue_on_failure)
_fix_spoof_python2(runner, encoding) _fix_spoof_python2(runner, encoding)
parser = doctest.DocTestParser() parser = doctest.DocTestParser()
@ -238,8 +332,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, continue_on_failure = not self.config.getvalue("usepdb")
checker=_get_checker()) runner = _get_runner(verbose=0, optionflags=optionflags,
checker=_get_checker(),
continue_on_failure=continue_on_failure)
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

View File

@ -756,6 +756,25 @@ 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")
result.assert_outcomes(passed=0, failed=1)
# We need to make sure we have two failure lines (4, 5, and 8) instead of
# one.
result.stdout.fnmatch_lines("*test_something.txt:4: DoctestUnexpectedException*")
result.stdout.fnmatch_lines("*test_something.txt:5: DocTestFailure*")
result.stdout.fnmatch_lines("*test_something.txt:8: DocTestFailure*")
class TestDoctestAutoUseFixtures(object): class TestDoctestAutoUseFixtures(object):