Skip to content

Commit

Permalink
Merge pull request #8391 from nicoddemus/ellipsis-verbose-6682
Browse files Browse the repository at this point in the history
Increase truncation threshold with -v, disable with -vv
  • Loading branch information
nicoddemus committed Mar 26, 2021
2 parents 9e8a6b6 + c1e0570 commit ddbc00d
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 13 deletions.
5 changes: 5 additions & 0 deletions changelog/8403.improvement.rst
@@ -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.
239 changes: 239 additions & 0 deletions doc/en/how-to/usage.rst
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions doc/en/reference/plugin_list.rst
@@ -1,5 +1,7 @@
Plugins List
============
.. _plugin-list:

Plugin List
===========

PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded.
Expand Down
7 changes: 5 additions & 2 deletions scripts/update-plugin-list.py
Expand Up @@ -6,8 +6,11 @@
import requests
import tabulate

FILE_HEAD = r"""Plugins List
============
FILE_HEAD = r"""\
.. _plugin-list:
Plugin List
===========
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded.
Expand Down
35 changes: 27 additions & 8 deletions src/_pytest/_io/saferepr.py
Expand Up @@ -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:
Expand All @@ -51,7 +62,9 @@ def repr(self, x: object) -> str:
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:
Expand All @@ -60,7 +73,9 @@ def repr_instance(self, x: object, level: int) -> str:
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:
Expand All @@ -75,15 +90,19 @@ 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
with a short exception info and 'saferepr' generally takes
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)

Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/assertion/__init__.py
Expand Up @@ -153,6 +153,7 @@ def callbinrepr(op, left: object, right: object) -> Optional[str]:

saved_assert_hooks = util._reprcompare, util._assertion_pass
util._reprcompare = callbinrepr
util._config = item.config

if ihook.pytest_assertion_pass.get_hookimpls():

Expand All @@ -164,6 +165,7 @@ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
yield

util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None


def pytest_sessionfinish(session: "Session") -> None:
Expand Down
14 changes: 13 additions & 1 deletion src/_pytest/assertion/rewrite.py
Expand Up @@ -27,6 +27,7 @@
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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit ddbc00d

Please sign in to comment.