Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Doc/library/doctest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,12 @@ from text files and modules with doctests:
.. versionchanged:: 3.15
Run each example as a :ref:`subtest <subtests>`.

.. versionchanged:: next
Report every example, as in verbose mode, if the test runner reports
more than the test names, i.e. its
:attr:`~unittest.TestResult.verbosity` is 3 or higher (for example
with ``python -m unittest -vv``).

Under the covers, :func:`DocTestSuite` creates a :class:`unittest.TestSuite` out
of :class:`!doctest.DocTestCase` instances, and :class:`!DocTestCase` is a
subclass of :class:`unittest.TestCase`. :class:`!DocTestCase` isn't documented
Expand Down
7 changes: 4 additions & 3 deletions Doc/library/test.rst
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,10 @@ The :mod:`!test.support` module defines the following constants:

.. data:: verbose

``True`` when verbose output is enabled. Should be checked when more
detailed information is desired about a running test. *verbose* is set by
:mod:`test.regrtest`.
How verbose the output is: the number of :option:`!-v` options which
:mod:`test.regrtest` was run with, and therefore ``0`` when verbose output
is not enabled. Should be checked when more detailed information is
desired about a running test.


.. data:: is_jython
Expand Down
18 changes: 17 additions & 1 deletion Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ You can run tests with more detail (higher verbosity) by passing in the -v flag:

python -m unittest -v test_module

Repeat it for even more detail: ``-vv`` reports also the individual examples
of a :mod:`doctest`.

When executed without arguments :ref:`unittest-test-discovery` is started::

python -m unittest
Expand Down Expand Up @@ -291,7 +294,11 @@ The ``discover`` sub-command has the following options:

.. option:: -v, --verbose

Verbose output
Verbose output. May be repeated: ``-vv`` reports also the individual
examples of a :mod:`doctest`.

.. versionchanged:: next
The option can be repeated.

.. option:: -s, --start-directory directory

Expand Down Expand Up @@ -2149,6 +2156,15 @@ Loading and running tests

.. versionadded:: 3.5

.. attribute:: verbosity

The level of details which the test runner reports: ``0`` -- quiet,
``1`` -- progress dots, ``2`` -- test names, ``3`` -- also the
individual examples of a :mod:`doctest`. A test runner is expected to
set it to its own verbosity.

.. versionadded:: next

.. method:: wasSuccessful()

Return ``True`` if all tests run so far have passed, otherwise returns
Expand Down
41 changes: 31 additions & 10 deletions Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,18 @@ def _find_lineno(self, obj, source_lines):
## 5. DocTest Runner
######################################################################

def _make_output_function(stream):
"""Return a function writing to *stream*, whatever it can encode."""
encoding = getattr(stream, 'encoding', None)
if encoding is None or encoding.lower() == 'utf-8':
return stream.write
def out(s):
# Use backslashreplace error handling on write
s = str(s.encode(encoding, 'backslashreplace'), encoding)
stream.write(s)
return out


class DocTestRunner:
"""
A class used to run DocTest test cases, and accumulate statistics.
Expand Down Expand Up @@ -1560,14 +1572,7 @@ def run(self, test, compileflags=None, out=None, clear_globs=True):

save_stdout = sys.stdout
if out is None:
encoding = save_stdout.encoding
if encoding is None or encoding.lower() == 'utf-8':
out = save_stdout.write
else:
# Use backslashreplace error handling on write
def out(s):
s = str(s.encode(encoding, 'backslashreplace'), encoding)
save_stdout.write(s)
out = _make_output_function(save_stdout)
sys.stdout = self._fakeout

# Patch pdb.set_trace to restore sys.stdout during interactive
Expand Down Expand Up @@ -2321,6 +2326,9 @@ def report_skip(self, out, test, example):
unittest.case._addSkip(self._test_result, self._subTest(), '')

def report_success(self, out, test, example, got):
# Report "ok" if verbose, to close what report_start() opened. A
# failed or skipped example is reported by the test result instead.
super().report_success(out, test, example, got)
self._test_result.addSubTest(self._test_case, self._subTest(), None)

def report_unexpected_exception(self, out, test, example, exc_info):
Expand Down Expand Up @@ -2400,10 +2408,23 @@ def runTest(self):
if getattr(result, 'failfast', False):
optionflags |= FAIL_FAST

# Report every example only if the test runner is asked for more than
# the test names it reports at verbosity 2. Write them to its stream,
# so that they are not swallowed by result.buffer.
verbose = getattr(result, 'verbosity', 1) >= 3
stream = getattr(result, 'stream', None)
out = None
if verbose and stream is not None:
out = _make_output_function(stream)
if test.examples and not getattr(result, '_newline', True):
# End the line which startTest() left open.
out('\n')
result._newline = True

runner = _DocTestCaseRunner(optionflags=optionflags,
checker=self._dt_checker, verbose=False,
checker=self._dt_checker, verbose=verbose,
test_case=self, test_result=result)
results = runner.run(test, clear_globs=False)
results = runner.run(test, out=out, clear_globs=False)
if results.skipped == results.attempted:
raise unittest.SkipTest("all examples were skipped")

Expand Down
6 changes: 4 additions & 2 deletions Lib/test/libregrtest/testresult.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class RegressionTestResult(unittest.TextTestResult):

def __init__(self, stream, descriptions, verbosity):
super().__init__(stream=stream, descriptions=descriptions,
verbosity=2 if verbosity else 0)
verbosity=verbosity)
self.buffer = True
if self.USE_XML:
from xml.etree import ElementTree as ET
Expand Down Expand Up @@ -150,10 +150,12 @@ def run(self, test):

def get_test_runner_class(verbosity, buffer=False):
if verbosity:
# The verbosity of regrtest is one less than the verbosity of
# unittest: -v reports the test names, -vv also the doctest examples.
return functools.partial(unittest.TextTestRunner,
resultclass=RegressionTestResult,
buffer=buffer,
verbosity=verbosity)
verbosity=verbosity + 1)
return functools.partial(QuietRegressionTestRunner, buffer=buffer)

def get_test_runner(stream, verbosity, capture_output=False):
Expand Down
56 changes: 55 additions & 1 deletion Lib/test/test_doctest/test_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from test.support import import_helper
import doctest
import functools
import io
import os
import sys
import importlib
Expand Down Expand Up @@ -469,7 +470,7 @@ def basics(): r"""
>>> tests = finder.find(sample_func)

>>> print(tests) # doctest: +ELLIPSIS
[<DocTest sample_func from test_doctest.py:36 (1 example)>]
[<DocTest sample_func from test_doctest.py:37 (1 example)>]

The exact name depends on how test_doctest was invoked, so allow for
leading path components.
Expand Down Expand Up @@ -803,6 +804,59 @@ def myfunc():
self.assertEqual((x, y), (2, 3))


class TestDocTestSuiteVerbosity(unittest.TestCase):

def run_suite(self, module='test.test_doctest.sample_doctest', **kwargs):
"""Return what the test runner wrote and what leaked to stdout."""
suite = doctest.DocTestSuite(module)
stream = io.StringIO()
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
unittest.TextTestRunner(stream=stream, **kwargs).run(suite)
return stream.getvalue(), stdout.getvalue()

def test_quiet(self):
for verbosity in range(3):
with self.subTest(verbosity=verbosity):
output, stdout = self.run_suite(verbosity=verbosity)
self.assertNotIn('Trying:', output)
self.assertNotIn('Expecting:', output)
self.assertEqual(stdout, '')

def test_verbose(self):
output, stdout = self.run_suite(verbosity=3)
self.assertIn('Trying:\n 2+2\n', output)
self.assertIn('Expecting:\n 4\n', output)
self.assertIn('\nok\n', output)
# Reported to the stream of the test runner, not to the stdout.
self.assertEqual(stdout, '')

def test_verbose_buffered(self):
# result.buffer replaces sys.stdout, which would swallow the examples.
output, stdout = self.run_suite(verbosity=3, buffer=True)
self.assertIn('Trying:\n 2+2\n', output)
self.assertEqual(stdout, '')

def test_verbose_failure_not_duplicated(self):
module = 'test.test_doctest.sample_doctest_errors'
quiet, _ = self.run_suite(module, verbosity=2)
verbose, _ = self.run_suite(module, verbosity=3)
self.assertIn('Trying:', verbose)
self.assertNotIn('Trying:', quiet)
# Reporting the examples does not report the failures once more.
self.assertEqual(verbose.count('Failed example:'),
quiet.count('Failed example:'))
self.assertGreater(quiet.count('Failed example:'), 0)

def test_plain_result(self):
# A result which is not from a text test runner has no stream.
suite = doctest.DocTestSuite('test.test_doctest.sample_doctest')
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
suite.run(unittest.TestResult())
self.assertEqual(stdout.getvalue(), '')


class TestDocTestFinder(unittest.TestCase):

def test_issue35753(self):
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_regrtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,18 @@ def load_tests(loader, tests, pattern):
failed=[testname],
parallel=True,
stats=TestStats(1, 2, 1))
# A single -v reports the test names, not the examples.
self.assertNotIn('Trying:', output)

# -vv reports every example, without changing what is run.
output = self.run_tests("--fail-env-changed", "-vv", "-j1", testname,
exitcode=EXITCODE_BAD_TEST)
self.check_executed_tests(output, [testname],
failed=[testname],
parallel=True,
stats=TestStats(1, 2, 1))
self.assertIn('Trying:\n 1 + 1\n', output)
self.assertIn('Expecting:\n 2\n', output)

def _check_random_seed(self, run_workers: bool):
# gh-109276: When -r/--randomize is used, random.seed() is called
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_unittest/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ def test_command_line_handling_discover_by_default_with_options(self):
program._do_discovery = args.append
program.parseArgs(['something', '-v', '-b', '-v', '-c', '-f'])
self.assertEqual(args, [[]])
self.assertEqual(program.verbosity, 2)
self.assertEqual(program.verbosity, 3) # -v is passed twice
self.assertIs(program.buffer, True)
self.assertIs(program.catchbreak, True)
self.assertIs(program.failfast, True)
Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test_unittest/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,32 @@ def testVerbosity(self):
program.parseArgs([None, opt])
self.assertEqual(program.verbosity, 2)

# -v can be repeated to ask for more details.
for args, verbosity in (
(['-vv'], 3),
(['-v', '-v'], 3),
(['--verbose', '--verbose'], 3),
(['-vvv'], 4),
# -q overrides any number of -v.
(['-v', '-q'], 0),
):
with self.subTest(args=args):
program.verbosity = 1
program.parseArgs([None, *args])
self.assertEqual(program.verbosity, verbosity)

def testVerbosityCountedOnce(self):
# "python -m unittest -v" falls back to test discovery, which parses
# arguments again: -v must not be counted twice.
program = self.program
program.verbosity = 1
program.parseArgs([None, '-v'])
self.assertEqual(program.verbosity, 2)

program.verbosity = 1
program.parseArgs([None, 'discover', '-vv'])
self.assertEqual(program.verbosity, 3)

def testBufferCatchFailfast(self):
program = self.program
for arg, attr in (('buffer', 'buffer'), ('failfast', 'failfast'),
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/test_unittest/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ def test_init(self):
self.assertEqual(result.shouldStop, False)
self.assertIsNone(result._stdout_buffer)
self.assertIsNone(result._stderr_buffer)
self.assertEqual(result.verbosity, 1)

def test_init_verbosity(self):
for verbosity in range(4):
with self.subTest(verbosity=verbosity):
result = unittest.TestResult(None, None, verbosity)
self.assertEqual(result.verbosity, verbosity)

# "This method can be called to signal that the set of tests being
# run should be aborted by setting the TestResult's shouldStop
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_unittest/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,35 @@ def MockResultClass(*args):
expectedresult = (runner.stream, DESCRIPTIONS, VERBOSITY)
self.assertEqual(runner._makeResult(), expectedresult)

def test_verbosity_set_on_result(self):
class Suite:
def __call__(self, result):
pass

for verbosity in range(4):
with self.subTest(verbosity=verbosity):
runner = unittest.TextTestRunner(io.StringIO(),
verbosity=verbosity)
result = runner.run(Suite())
self.assertEqual(result.verbosity, verbosity)

def test_verbosity_set_on_filtering_result(self):
# A result class is free to filter the verbosity which its
# constructor gets, as test.libregrtest does.
class FilteringResult(unittest.TextTestResult):
def __init__(self, stream, descriptions, verbosity):
super().__init__(stream, descriptions,
2 if verbosity else 0)

class Suite:
def __call__(self, result):
pass

runner = unittest.TextTestRunner(io.StringIO(), verbosity=3,
resultclass=FilteringResult)
result = runner.run(Suite())
self.assertEqual(result.verbosity, 3)

@support.force_not_colorized
@support.requires_subprocess()
def test_warnings(self):
Expand Down
7 changes: 5 additions & 2 deletions Lib/unittest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,12 @@ def _initArgParsers(self):
def _getParentArgParser(self):
parser = argparse.ArgumentParser(add_help=False)

# Counted, not a constant: the namespace is the TestProgram, whose
# verbosity is already 1, so -v still gives 2 and -vv gives 3.
parser.add_argument('-v', '--verbose', dest='verbosity',
action='store_const', const=2,
help='Verbose output')
action='count', default=1,
help='Verbose output, twice to also report '
'the examples of a doctest')
parser.add_argument('-q', '--quiet', dest='verbosity',
action='store_const', const=0,
help='Quiet output')
Expand Down
3 changes: 3 additions & 0 deletions Lib/unittest/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class TestResult(object):
_moduleSetUpFailed = False
def __init__(self, stream=None, descriptions=None, verbosity=None):
self.failfast = False
# How much the test runner reports: 0 -- quiet, 1 -- progress dots,
# 2 -- test names, 3 -- also the examples of a doctest.
self.verbosity = 1 if verbosity is None else verbosity
self.failures = []
self.errors = []
self.testsRun = 0
Expand Down
3 changes: 3 additions & 0 deletions Lib/unittest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ def run(self, test):
result.failfast = self.failfast
result.buffer = self.buffer
result.tb_locals = self.tb_locals
# Not left to _makeResult(): a result class is free to filter the
# verbosity which its constructor gets.
result.verbosity = self.verbosity
with warnings.catch_warnings():
if self.warnings:
# if self.warnings is set, use it to filter all the warnings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Doctests run by the :mod:`unittest` test runner now report every example, as
in verbose mode, if the runner reports more than the test names, i.e. its
verbosity is 3 or higher. The :option:`!-v` option of :mod:`unittest` can now
be repeated, so ``python -m unittest -vv`` asks for this. Added also the
:attr:`~unittest.TestResult.verbosity` attribute of
:class:`unittest.TestResult`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Running the Python test suite with ``-vv`` now reports every example of a
doctest. A single ``-v`` reports the test names, as before.
Loading