Skip to content
Merged
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
5 changes: 5 additions & 0 deletions changelog/8403.improvement.rst
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This consults the Config at the time the test is run. Alternative is to consult the config at the time the test is rewritten, that is, make the AssertionRewriter get the maxsize, add a maxsize argument to _saferepr and make AssertionRewriter pass it as a constant in the AST. Do you have any preference? The reason I'm talking about that is trying to avoid the util._config mutable global. These tend to be not great...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative is to consult the config at the time the test is rewritten, that is, make the AssertionRewriter get the maxsize

I thought that too, however I realized assertion rewritten is cached, so if you invoke pytest the first time, the .pyc file is rewritten and cached; if you invoke pytest -v next, the .pyc file is cached so assertion rewriting is not triggered. Unfortunately this information needs to be available at runtime.

I injected the Config object in the same way we inject the comparison functions today, so I think this at least aligns with how things work today, however I'm definitely open for alternative ideas. 👍

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
Loading