Skip to content

Commit ddbc00d

Browse files
authored
Merge pull request #8391 from nicoddemus/ellipsis-verbose-6682
Increase truncation threshold with -v, disable with -vv
2 parents 9e8a6b6 + c1e0570 commit ddbc00d

File tree

10 files changed

+361
-13
lines changed

10 files changed

+361
-13
lines changed

changelog/8403.improvement.rst

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

doc/en/how-to/usage.rst

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,243 @@ will be shown (because KeyboardInterrupt is caught by pytest). By using this
161161
option you make sure a trace is shown.
162162

163163

164+
Verbosity
165+
---------
166+
167+
The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion
168+
details when tests fail, fixtures details with ``--fixtures``, etc.
169+
170+
.. regendoc:wipe
171+
172+
Consider this simple file:
173+
174+
.. code-block:: python
175+
176+
# content of test_verbosity_example.py
177+
def test_ok():
178+
pass
179+
180+
181+
def test_words_fail():
182+
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
183+
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
184+
assert fruits1 == fruits2
185+
186+
187+
def test_numbers_fail():
188+
number_to_text1 = {str(x): x for x in range(5)}
189+
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
190+
assert number_to_text1 == number_to_text2
191+
192+
193+
def test_long_text_fail():
194+
long_text = "Lorem ipsum dolor sit amet " * 10
195+
assert "hello world" in long_text
196+
197+
Executing pytest normally gives us this output (we are skipping the header to focus on the rest):
198+
199+
.. code-block:: pytest
200+
201+
$ pytest --no-header
202+
=========================== test session starts ===========================
203+
collected 4 items
204+
205+
test_verbosity_example.py .FFF [100%]
206+
207+
================================ FAILURES =================================
208+
_____________________________ test_words_fail _____________________________
209+
210+
def test_words_fail():
211+
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
212+
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
213+
> assert fruits1 == fruits2
214+
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
215+
E At index 2 diff: 'grapes' != 'orange'
216+
E Use -v to get the full diff
217+
218+
test_verbosity_example.py:8: AssertionError
219+
____________________________ test_numbers_fail ____________________________
220+
221+
def test_numbers_fail():
222+
number_to_text1 = {str(x): x for x in range(5)}
223+
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
224+
> assert number_to_text1 == number_to_text2
225+
E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
226+
E Omitting 1 identical items, use -vv to show
227+
E Left contains 4 more items:
228+
E {'1': 1, '2': 2, '3': 3, '4': 4}
229+
E Right contains 4 more items:
230+
E {'10': 10, '20': 20, '30': 30, '40': 40}
231+
E Use -v to get the full diff
232+
233+
test_verbosity_example.py:14: AssertionError
234+
___________________________ test_long_text_fail ___________________________
235+
236+
def test_long_text_fail():
237+
long_text = "Lorem ipsum dolor sit amet " * 10
238+
> assert "hello world" in long_text
239+
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 '
240+
241+
test_verbosity_example.py:19: AssertionError
242+
========================= short test summary info =========================
243+
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
244+
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
245+
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
246+
======================= 3 failed, 1 passed in 0.08s =======================
247+
248+
Notice that:
249+
250+
* Each test inside the file is shown by a single character in the output: ``.`` for passing, ``F`` for failure.
251+
* ``test_words_fail`` failed, and we are shown a short summary indicating the index 2 of the two lists differ.
252+
* ``test_numbers_fail`` failed, and we are shown a summary of left/right differences on dictionary items. Identical items are omitted.
253+
* ``test_long_text_fail`` failed, and the right hand side of the ``in`` statement is truncated using ``...```
254+
because it is longer than an internal threshold (240 characters currently).
255+
256+
Now we can increase pytest's verbosity:
257+
258+
.. code-block:: pytest
259+
260+
$ pytest --no-header -v
261+
=========================== test session starts ===========================
262+
collecting ... collected 4 items
263+
264+
test_verbosity_example.py::test_ok PASSED [ 25%]
265+
test_verbosity_example.py::test_words_fail FAILED [ 50%]
266+
test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
267+
test_verbosity_example.py::test_long_text_fail FAILED [100%]
268+
269+
================================ FAILURES =================================
270+
_____________________________ test_words_fail _____________________________
271+
272+
def test_words_fail():
273+
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
274+
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
275+
> assert fruits1 == fruits2
276+
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
277+
E At index 2 diff: 'grapes' != 'orange'
278+
E Full diff:
279+
E - ['banana', 'apple', 'orange', 'melon', 'kiwi']
280+
E ? ^ ^^
281+
E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
282+
E ? ^ ^ +
283+
284+
test_verbosity_example.py:8: AssertionError
285+
____________________________ test_numbers_fail ____________________________
286+
287+
def test_numbers_fail():
288+
number_to_text1 = {str(x): x for x in range(5)}
289+
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
290+
> assert number_to_text1 == number_to_text2
291+
E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
292+
E Omitting 1 identical items, use -vv to show
293+
E Left contains 4 more items:
294+
E {'1': 1, '2': 2, '3': 3, '4': 4}
295+
E Right contains 4 more items:
296+
E {'10': 10, '20': 20, '30': 30, '40': 40}
297+
E Full diff:
298+
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
299+
E
300+
E ...Full output truncated (3 lines hidden), use '-vv' to show
301+
302+
test_verbosity_example.py:14: AssertionError
303+
___________________________ test_long_text_fail ___________________________
304+
305+
def test_long_text_fail():
306+
long_text = "Lorem ipsum dolor sit amet " * 10
307+
> assert "hello world" in long_text
308+
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 '
309+
310+
test_verbosity_example.py:19: AssertionError
311+
========================= short test summary info =========================
312+
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
313+
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
314+
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
315+
======================= 3 failed, 1 passed in 0.07s =======================
316+
317+
Notice now that:
318+
319+
* Each test inside the file gets its own line in the output.
320+
* ``test_words_fail`` now shows the two failing lists in full, in addition to which index differs.
321+
* ``test_numbers_fail`` now shows a text diff of the two dictionaries, truncated.
322+
* ``test_long_text_fail`` no longer truncates the right hand side of the ``in`` statement, because the internal
323+
threshold for truncation is larger now (2400 characters currently).
324+
325+
Now if we increase verbosity even more:
326+
327+
.. code-block:: pytest
328+
329+
$ pytest --no-header -vv
330+
=========================== test session starts ===========================
331+
collecting ... collected 4 items
332+
333+
test_verbosity_example.py::test_ok PASSED [ 25%]
334+
test_verbosity_example.py::test_words_fail FAILED [ 50%]
335+
test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
336+
test_verbosity_example.py::test_long_text_fail FAILED [100%]
337+
338+
================================ FAILURES =================================
339+
_____________________________ test_words_fail _____________________________
340+
341+
def test_words_fail():
342+
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
343+
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
344+
> assert fruits1 == fruits2
345+
E AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
346+
E At index 2 diff: 'grapes' != 'orange'
347+
E Full diff:
348+
E - ['banana', 'apple', 'orange', 'melon', 'kiwi']
349+
E ? ^ ^^
350+
E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
351+
E ? ^ ^ +
352+
353+
test_verbosity_example.py:8: AssertionError
354+
____________________________ test_numbers_fail ____________________________
355+
356+
def test_numbers_fail():
357+
number_to_text1 = {str(x): x for x in range(5)}
358+
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
359+
> assert number_to_text1 == number_to_text2
360+
E AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
361+
E Common items:
362+
E {'0': 0}
363+
E Left contains 4 more items:
364+
E {'1': 1, '2': 2, '3': 3, '4': 4}
365+
E Right contains 4 more items:
366+
E {'10': 10, '20': 20, '30': 30, '40': 40}
367+
E Full diff:
368+
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
369+
E ? - - - - - - - -
370+
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
371+
372+
test_verbosity_example.py:14: AssertionError
373+
___________________________ test_long_text_fail ___________________________
374+
375+
def test_long_text_fail():
376+
long_text = "Lorem ipsum dolor sit amet " * 10
377+
> assert "hello world" in long_text
378+
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 '
379+
380+
test_verbosity_example.py:19: AssertionError
381+
========================= short test summary info =========================
382+
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
383+
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
384+
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
385+
======================= 3 failed, 1 passed in 0.07s =======================
386+
387+
Notice now that:
388+
389+
* Each test inside the file gets its own line in the output.
390+
* ``test_words_fail`` gives the same output as before in this case.
391+
* ``test_numbers_fail`` now shows a full text diff of the two dictionaries.
392+
* ``test_long_text_fail`` also doesn't truncate on the right hand side as before, but now pytest won't truncate any
393+
text at all, regardless of its size.
394+
395+
Those were examples of how verbosity affects normal test session output, but verbosity also is used in other
396+
situations, for example you are shown even fixtures that start with ``_`` if you use ``pytest --fixtures -v``.
397+
398+
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
399+
however some plugins might make use of higher verbosity.
400+
164401
.. _`pytest.detailed_failed_tests_usage`:
165402

166403
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
171408

172409
It defaults to ``fE`` to list failures and errors.
173410

411+
.. regendoc:wipe
412+
174413
Example:
175414

176415
.. code-block:: python

doc/en/reference/plugin_list.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
Plugins List
2-
============
1+
.. _plugin-list:
2+
3+
Plugin List
4+
===========
35

46
PyPI projects that match "pytest-\*" are considered plugins and are listed
57
automatically. Packages classified as inactive are excluded.

scripts/update-plugin-list.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
import requests
77
import tabulate
88

9-
FILE_HEAD = r"""Plugins List
10-
============
9+
FILE_HEAD = r"""\
10+
.. _plugin-list:
11+
12+
Plugin List
13+
===========
1114
1215
PyPI projects that match "pytest-\*" are considered plugins and are listed
1316
automatically. Packages classified as inactive are excluded.

src/_pytest/_io/saferepr.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,23 @@ def _ellipsize(s: str, maxsize: int) -> str:
3636

3737

3838
class SafeRepr(reprlib.Repr):
39-
"""repr.Repr that limits the resulting size of repr() and includes
40-
information on exceptions raised during the call."""
39+
"""
40+
repr.Repr that limits the resulting size of repr() and includes
41+
information on exceptions raised during the call.
42+
"""
4143

42-
def __init__(self, maxsize: int) -> None:
44+
def __init__(self, maxsize: Optional[int]) -> None:
45+
"""
46+
:param maxsize:
47+
If not None, will truncate the resulting repr to that specific size, using ellipsis
48+
somewhere in the middle to hide the extra text.
49+
If None, will not impose any size limits on the returning repr.
50+
"""
4351
super().__init__()
44-
self.maxstring = maxsize
52+
# ``maxstring`` is used by the superclass, and needs to be an int; using a
53+
# very large number in case maxsize is None, meaning we want to disable
54+
# truncation.
55+
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
4556
self.maxsize = maxsize
4657

4758
def repr(self, x: object) -> str:
@@ -51,7 +62,9 @@ def repr(self, x: object) -> str:
5162
raise
5263
except BaseException as exc:
5364
s = _format_repr_exception(exc, x)
54-
return _ellipsize(s, self.maxsize)
65+
if self.maxsize is not None:
66+
s = _ellipsize(s, self.maxsize)
67+
return s
5568

5669
def repr_instance(self, x: object, level: int) -> str:
5770
try:
@@ -60,7 +73,9 @@ def repr_instance(self, x: object, level: int) -> str:
6073
raise
6174
except BaseException as exc:
6275
s = _format_repr_exception(exc, x)
63-
return _ellipsize(s, self.maxsize)
76+
if self.maxsize is not None:
77+
s = _ellipsize(s, self.maxsize)
78+
return s
6479

6580

6681
def safeformat(obj: object) -> str:
@@ -75,15 +90,19 @@ def safeformat(obj: object) -> str:
7590
return _format_repr_exception(exc, obj)
7691

7792

78-
def saferepr(obj: object, maxsize: int = 240) -> str:
93+
# Maximum size of overall repr of objects to display during assertion errors.
94+
DEFAULT_REPR_MAX_SIZE = 240
95+
96+
97+
def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str:
7998
"""Return a size-limited safe repr-string for the given object.
8099
81100
Failing __repr__ functions of user instances will be represented
82101
with a short exception info and 'saferepr' generally takes
83102
care to never raise exceptions itself.
84103
85104
This function is a wrapper around the Repr/reprlib functionality of the
86-
standard 2.6 lib.
105+
stdlib.
87106
"""
88107
return SafeRepr(maxsize).repr(obj)
89108

src/_pytest/assertion/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def callbinrepr(op, left: object, right: object) -> Optional[str]:
153153

154154
saved_assert_hooks = util._reprcompare, util._assertion_pass
155155
util._reprcompare = callbinrepr
156+
util._config = item.config
156157

157158
if ihook.pytest_assertion_pass.get_hookimpls():
158159

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

166167
util._reprcompare, util._assertion_pass = saved_assert_hooks
168+
util._config = None
167169

168170

169171
def pytest_sessionfinish(session: "Session") -> None:

src/_pytest/assertion/rewrite.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import TYPE_CHECKING
2828
from typing import Union
2929

30+
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
3031
from _pytest._io.saferepr import saferepr
3132
from _pytest._version import version
3233
from _pytest.assertion import util
@@ -427,7 +428,18 @@ def _saferepr(obj: object) -> str:
427428
sequences, especially '\n{' and '\n}' are likely to be present in
428429
JSON reprs.
429430
"""
430-
return saferepr(obj).replace("\n", "\\n")
431+
maxsize = _get_maxsize_for_saferepr(util._config)
432+
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
433+
434+
435+
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
436+
"""Get `maxsize` configuration for saferepr based on the given config object."""
437+
verbosity = config.getoption("verbose") if config is not None else 0
438+
if verbosity >= 2:
439+
return None
440+
if verbosity >= 1:
441+
return DEFAULT_REPR_MAX_SIZE * 10
442+
return DEFAULT_REPR_MAX_SIZE
431443

432444

433445
def _format_assertmsg(obj: object) -> str:

0 commit comments

Comments
 (0)