Increase truncation threshold with -v, disable with -vv

Fix #6682
Fix #8403
This commit is contained in:
Bruno Oliveira 2021-03-02 21:47:37 -03:00
parent 9e8a6b6eeb
commit be8d63e33b
8 changed files with 352 additions and 9 deletions

View File

@ -0,0 +1,5 @@
By default, pytest will truncate long strings in assert errors so they don't clutter the output too much,
currently at ``240`` characters by default.
However, in some cases the longer output helps, or is even crucial, to diagnose a failure. Using ``-v`` will
now increase the truncation threshold to ``2400`` characters, and ``-vv`` or higher will disable truncation entirely.

View File

@ -161,6 +161,243 @@ will be shown (because KeyboardInterrupt is caught by pytest). By using this
option you make sure a trace is shown.
Verbosity
---------
The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion
details when tests fail, fixtures details with ``--fixtures``, etc.
.. regendoc:wipe
Consider this simple file:
.. code-block:: python
# content of test_verbosity_example.py
def test_ok():
pass
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
assert fruits1 == fruits2
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
assert number_to_text1 == number_to_text2
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
assert "hello world" in long_text
Executing pytest normally gives us this output (we are skipping the header to focus on the rest):
.. code-block:: pytest
$ pytest --no-header
=========================== test session starts ===========================
collected 4 items
test_verbosity_example.py .FFF [100%]
================================ FAILURES =================================
_____________________________ test_words_fail _____________________________
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E At index 2 diff: 'grapes' != 'orange'
E Use -v to get the full diff
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail ____________________________
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
> assert number_to_text1 == number_to_text2
E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
E Omitting 1 identical items, use -vv to show
E Left contains 4 more items:
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Use -v to get the full diff
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ___________________________
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
> assert "hello world" in long_text
E AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ips... sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
test_verbosity_example.py:19: AssertionError
========================= short test summary info =========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.08s =======================
Notice that:
* Each test inside the file is shown by a single character in the output: ``.`` for passing, ``F`` for failure.
* ``test_words_fail`` failed, and we are shown a short summary indicating the index 2 of the two lists differ.
* ``test_numbers_fail`` failed, and we are shown a summary of left/right differences on dictionary items. Identical items are omitted.
* ``test_long_text_fail`` failed, and the right hand side of the ``in`` statement is truncated using ``...```
because it is longer than an internal threshold (240 characters currently).
Now we can increase pytest's verbosity:
.. code-block:: pytest
$ pytest --no-header -v
=========================== test session starts ===========================
collecting ... collected 4 items
test_verbosity_example.py::test_ok PASSED [ 25%]
test_verbosity_example.py::test_words_fail FAILED [ 50%]
test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
test_verbosity_example.py::test_long_text_fail FAILED [100%]
================================ FAILURES =================================
_____________________________ test_words_fail _____________________________
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E At index 2 diff: 'grapes' != 'orange'
E Full diff:
E - ['banana', 'apple', 'orange', 'melon', 'kiwi']
E ? ^ ^^
E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
E ? ^ ^ +
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail ____________________________
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
> assert number_to_text1 == number_to_text2
E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
E Omitting 1 identical items, use -vv to show
E Left contains 4 more items:
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Full diff:
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ___________________________
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
> assert "hello world" in long_text
E AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
test_verbosity_example.py:19: AssertionError
========================= short test summary info =========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.07s =======================
Notice now that:
* Each test inside the file gets its own line in the output.
* ``test_words_fail`` now shows the two failing lists in full, in addition to which index differs.
* ``test_numbers_fail`` now shows a text diff of the two dictionaries, truncated.
* ``test_long_text_fail`` no longer truncates the right hand side of the ``in`` statement, because the internal
threshold for truncation is larger now (2400 characters currently).
Now if we increase verbosity even more:
.. code-block:: pytest
$ pytest --no-header -vv
=========================== test session starts ===========================
collecting ... collected 4 items
test_verbosity_example.py::test_ok PASSED [ 25%]
test_verbosity_example.py::test_words_fail FAILED [ 50%]
test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
test_verbosity_example.py::test_long_text_fail FAILED [100%]
================================ FAILURES =================================
_____________________________ test_words_fail _____________________________
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
E At index 2 diff: 'grapes' != 'orange'
E Full diff:
E - ['banana', 'apple', 'orange', 'melon', 'kiwi']
E ? ^ ^^
E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
E ? ^ ^ +
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail ____________________________
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
> assert number_to_text1 == number_to_text2
E AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E Common items:
E {'0': 0}
E Left contains 4 more items:
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Full diff:
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E ? - - - - - - - -
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ___________________________
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
> assert "hello world" in long_text
E AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
test_verbosity_example.py:19: AssertionError
========================= short test summary info =========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.07s =======================
Notice now that:
* Each test inside the file gets its own line in the output.
* ``test_words_fail`` gives the same output as before in this case.
* ``test_numbers_fail`` now shows a full text diff of the two dictionaries.
* ``test_long_text_fail`` also doesn't truncate on the right hand side as before, but now pytest won't truncate any
text at all, regardless of its size.
Those were examples of how verbosity affects normal test session output, but verbosity also is used in other
situations, for example you are shown even fixtures that start with ``_`` if you use ``pytest --fixtures -v``.
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
however some plugins might make use of higher verbosity.
.. _`pytest.detailed_failed_tests_usage`:
Producing a detailed summary report
@ -171,6 +408,8 @@ making it easy in large test suites to get a clear picture of all failures, skip
It defaults to ``fE`` to list failures and errors.
.. regendoc:wipe
Example:
.. code-block:: python

View File

@ -36,12 +36,23 @@ def _ellipsize(s: str, maxsize: int) -> str:
class SafeRepr(reprlib.Repr):
"""repr.Repr that limits the resulting size of repr() and includes
information on exceptions raised during the call."""
"""
repr.Repr that limits the resulting size of repr() and includes
information on exceptions raised during the call.
"""
def __init__(self, maxsize: int) -> None:
def __init__(self, maxsize: Optional[int]) -> None:
"""
:param maxsize:
If not None, will truncate the resulting repr to that specific size, using ellipsis
somewhere in the middle to hide the extra text.
If None, will not impose any size limits on the returning repr.
"""
super().__init__()
self.maxstring = maxsize
# ``maxstring`` is used by the superclass, and needs to be an int; using a
# very large number in case maxsize is None, meaning we want to disable
# truncation.
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
self.maxsize = maxsize
def repr(self, x: object) -> str:
@ -51,7 +62,9 @@ class SafeRepr(reprlib.Repr):
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
return _ellipsize(s, self.maxsize)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)
return s
def repr_instance(self, x: object, level: int) -> str:
try:
@ -60,7 +73,9 @@ class SafeRepr(reprlib.Repr):
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
return _ellipsize(s, self.maxsize)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)
return s
def safeformat(obj: object) -> str:
@ -75,7 +90,11 @@ def safeformat(obj: object) -> str:
return _format_repr_exception(exc, obj)
def saferepr(obj: object, maxsize: int = 240) -> str:
# Maximum size of overall repr of objects to display during assertion errors.
DEFAULT_REPR_MAX_SIZE = 240
def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str:
"""Return a size-limited safe repr-string for the given object.
Failing __repr__ functions of user instances will be represented
@ -83,7 +102,7 @@ def saferepr(obj: object, maxsize: int = 240) -> str:
care to never raise exceptions itself.
This function is a wrapper around the Repr/reprlib functionality of the
standard 2.6 lib.
stdlib.
"""
return SafeRepr(maxsize).repr(obj)

View File

@ -153,6 +153,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
saved_assert_hooks = util._reprcompare, util._assertion_pass
util._reprcompare = callbinrepr
util._config = item.config
if ihook.pytest_assertion_pass.get_hookimpls():
@ -164,6 +165,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
yield
util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None
def pytest_sessionfinish(session: "Session") -> None:

View File

@ -27,6 +27,7 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr
from _pytest._version import version
from _pytest.assertion import util
@ -427,7 +428,18 @@ def _saferepr(obj: object) -> str:
sequences, especially '\n{' and '\n}' are likely to be present in
JSON reprs.
"""
return saferepr(obj).replace("\n", "\\n")
maxsize = _get_maxsize_for_saferepr(util._config)
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
"""Get `maxsize` configuration for saferepr based on the given config object."""
verbosity = config.getoption("verbose") if config is not None else 0
if verbosity >= 2:
return None
if verbosity >= 1:
return DEFAULT_REPR_MAX_SIZE * 10
return DEFAULT_REPR_MAX_SIZE
def _format_assertmsg(obj: object) -> str:

View File

@ -15,6 +15,8 @@ from _pytest import outcomes
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
@ -26,6 +28,9 @@ _reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
# when pytest_runtest_setup is called.
_assertion_pass: Optional[Callable[[int, str, str], None]] = None
# Config object which is assigned during pytest_runtest_protocol.
_config: Optional[Config] = None
def format_explanation(explanation: str) -> str:
r"""Format an explanation.

View File

@ -1,5 +1,6 @@
import pytest
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr
@ -15,6 +16,13 @@ def test_maxsize():
assert s == expected
def test_no_maxsize():
text = "x" * DEFAULT_REPR_MAX_SIZE * 10
s = saferepr(text, maxsize=None)
expected = repr(text)
assert s == expected
def test_maxsize_error_on_instance():
class A:
def __repr__(self):

View File

@ -11,6 +11,7 @@ import textwrap
import zipfile
from functools import partial
from pathlib import Path
from typing import cast
from typing import Dict
from typing import List
from typing import Mapping
@ -19,13 +20,16 @@ from typing import Set
import _pytest._code
import pytest
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest.assertion import util
from _pytest.assertion.rewrite import _get_assertion_exprs
from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
from _pytest.assertion.rewrite import AssertionRewritingHook
from _pytest.assertion.rewrite import get_cache_dir
from _pytest.assertion.rewrite import PYC_TAIL
from _pytest.assertion.rewrite import PYTEST_TAG
from _pytest.assertion.rewrite import rewrite_asserts
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.pathlib import make_numbered_dir
from _pytest.pytester import Pytester
@ -1706,3 +1710,52 @@ class TestPyCacheDir:
cache_tag=sys.implementation.cache_tag
)
assert bar_init_pyc.is_file()
class TestReprSizeVerbosity:
"""
Check that verbosity also controls the string length threshold to shorten it using
ellipsis.
"""
@pytest.mark.parametrize(
"verbose, expected_size",
[
(0, DEFAULT_REPR_MAX_SIZE),
(1, DEFAULT_REPR_MAX_SIZE * 10),
(2, None),
(3, None),
],
)
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
class FakeConfig:
def getoption(self, name: str) -> int:
assert name == "verbose"
return verbose
config = FakeConfig()
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
def create_test_file(self, pytester: Pytester, size: int) -> None:
pytester.makepyfile(
f"""
def test_very_long_string():
text = "x" * {size}
assert "hello world" in text
"""
)
def test_default_verbosity(self, pytester: Pytester) -> None:
self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*xxx...xxx*"])
def test_increased_verbosity(self, pytester: Pytester) -> None:
self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE)
result = pytester.runpytest("-v")
result.stdout.no_fnmatch_line("*xxx...xxx*")
def test_max_increased_verbosity(self, pytester: Pytester) -> None:
self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10)
result = pytester.runpytest("-vv")
result.stdout.no_fnmatch_line("*xxx...xxx*")