Skip to content

Commit d924a63

Browse files
Implement truncation thresholds config options (#12766)
Fixes #12765 Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 4508d0b commit d924a63

File tree

7 files changed

+177
-19
lines changed

7 files changed

+177
-19
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ Paul Müller
328328
Paul Reece
329329
Pauli Virtanen
330330
Pavel Karateev
331+
Pavel Zhukov
331332
Paweł Adamczak
332333
Pedro Algarvio
333334
Petter Strandmark

changelog/12765.feature.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Thresholds to trigger snippet truncation can now be set with :confval:`truncation_limit_lines` and :confval:`truncation_limit_chars`.
2+
3+
See :ref:`truncation-params` for more information.

doc/en/how-to/output.rst

+22
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,28 @@ captured output:
549549
By default, parametrized variants of skipped tests are grouped together if
550550
they share the same skip reason. You can use ``--no-fold-skipped`` to print each skipped test separately.
551551

552+
553+
.. _truncation-params:
554+
555+
Modifying truncation limits
556+
--------------------------------------------------
557+
558+
.. versionadded: 8.4
559+
560+
Default truncation limits are 8 lines or 640 characters, whichever comes first.
561+
To set custom truncation limits you can use following ``pytest.ini`` file options:
562+
563+
.. code-block:: ini
564+
565+
[pytest]
566+
truncation_limit_lines = 10
567+
truncation_limit_chars = 90
568+
569+
That will cause pytest to truncate the assertions to 10 lines or 90 characters, whichever comes first.
570+
571+
Setting both :confval:`truncation_limit_lines` and :confval:`truncation_limit_chars` to ``0`` will disable the truncation.
572+
However, setting only one of those values will disable one truncation mode, but will leave the other one intact.
573+
552574
Creating resultlog format files
553575
--------------------------------------------------
554576

doc/en/reference/reference.rst

+40
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,46 @@ passed multiple times. The expected format is ``name=value``. For example::
18731873
Default: ``all``
18741874

18751875

1876+
.. confval:: truncation_limit_chars
1877+
1878+
Controls maximum number of characters to truncate assertion message contents.
1879+
1880+
Setting value to ``0`` disables the character limit for truncation.
1881+
1882+
.. code-block:: ini
1883+
1884+
[pytest]
1885+
truncation_limit_chars = 640
1886+
1887+
pytest truncates the assert messages to a certain limit by default to prevent comparison with large data to overload the console output.
1888+
1889+
Default: ``640``
1890+
1891+
.. note::
1892+
1893+
If pytest detects it is :ref:`running on CI <ci-pipelines>`, truncation is disabled automatically.
1894+
1895+
1896+
.. confval:: truncation_limit_lines
1897+
1898+
Controls maximum number of linesto truncate assertion message contents.
1899+
1900+
Setting value to ``0`` disables the lines limit for truncation.
1901+
1902+
.. code-block:: ini
1903+
1904+
[pytest]
1905+
truncation_limit_lines = 8
1906+
1907+
pytest truncates the assert messages to a certain limit by default to prevent comparison with large data to overload the console output.
1908+
1909+
Default: ``8``
1910+
1911+
.. note::
1912+
1913+
If pytest detects it is :ref:`running on CI <ci-pipelines>`, truncation is disabled automatically.
1914+
1915+
18761916
.. confval:: usefixtures
18771917

18781918
List of fixtures that will be applied to all test functions; this is semantically the same to apply

src/_pytest/assertion/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ def pytest_addoption(parser: Parser) -> None:
4545
help="Enables the pytest_assertion_pass hook. "
4646
"Make sure to delete any previously generated pyc cache files.",
4747
)
48+
49+
parser.addini(
50+
"truncation_limit_lines",
51+
default=None,
52+
help="Set threshold of LINES after which truncation will take effect",
53+
)
54+
parser.addini(
55+
"truncation_limit_chars",
56+
default=None,
57+
help=("Set threshold of CHARS after which truncation will take effect"),
58+
)
59+
4860
Config._add_verbosity_ini(
4961
parser,
5062
Config.VERBOSITY_ASSERTIONS,

src/_pytest/assertion/truncate.py

+39-19
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,54 @@
1212

1313

1414
DEFAULT_MAX_LINES = 8
15-
DEFAULT_MAX_CHARS = 8 * 80
15+
DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80
1616
USAGE_MSG = "use '-vv' to show"
1717

1818

19-
def truncate_if_required(
20-
explanation: list[str], item: Item, max_length: int | None = None
21-
) -> list[str]:
19+
def truncate_if_required(explanation: list[str], item: Item) -> list[str]:
2220
"""Truncate this assertion explanation if the given test item is eligible."""
23-
if _should_truncate_item(item):
24-
return _truncate_explanation(explanation)
21+
should_truncate, max_lines, max_chars = _get_truncation_parameters(item)
22+
if should_truncate:
23+
return _truncate_explanation(
24+
explanation,
25+
max_lines=max_lines,
26+
max_chars=max_chars,
27+
)
2528
return explanation
2629

2730

28-
def _should_truncate_item(item: Item) -> bool:
29-
"""Whether or not this test item is eligible for truncation."""
31+
def _get_truncation_parameters(item: Item) -> tuple[bool, int, int]:
32+
"""Return the truncation parameters related to the given item, as (should truncate, max lines, max chars)."""
33+
# We do not need to truncate if one of conditions is met:
34+
# 1. Verbosity level is 2 or more;
35+
# 2. Test is being run in CI environment;
36+
# 3. Both truncation_limit_lines and truncation_limit_chars
37+
# .ini parameters are set to 0 explicitly.
38+
max_lines = item.config.getini("truncation_limit_lines")
39+
max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES)
40+
41+
max_chars = item.config.getini("truncation_limit_chars")
42+
max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS)
43+
3044
verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
31-
return verbose < 2 and not util.running_on_ci()
45+
46+
should_truncate = verbose < 2 and not util.running_on_ci()
47+
should_truncate = should_truncate and (max_lines > 0 or max_chars > 0)
48+
49+
return should_truncate, max_lines, max_chars
3250

3351

3452
def _truncate_explanation(
3553
input_lines: list[str],
36-
max_lines: int | None = None,
37-
max_chars: int | None = None,
54+
max_lines: int,
55+
max_chars: int,
3856
) -> list[str]:
3957
"""Truncate given list of strings that makes up the assertion explanation.
4058
41-
Truncates to either 8 lines, or 640 characters - whichever the input reaches
59+
Truncates to either max_lines, or max_chars - whichever the input reaches
4260
first, taking the truncation explanation into account. The remaining lines
4361
will be replaced by a usage message.
4462
"""
45-
if max_lines is None:
46-
max_lines = DEFAULT_MAX_LINES
47-
if max_chars is None:
48-
max_chars = DEFAULT_MAX_CHARS
49-
5063
# Check if truncation required
5164
input_char_count = len("".join(input_lines))
5265
# The length of the truncation explanation depends on the number of lines
@@ -71,16 +84,23 @@ def _truncate_explanation(
7184
):
7285
return input_lines
7386
# Truncate first to max_lines, and then truncate to max_chars if necessary
74-
truncated_explanation = input_lines[:max_lines]
87+
if max_lines > 0:
88+
truncated_explanation = input_lines[:max_lines]
89+
else:
90+
truncated_explanation = input_lines
7591
truncated_char = True
7692
# We reevaluate the need to truncate chars following removal of some lines
77-
if len("".join(truncated_explanation)) > tolerable_max_chars:
93+
if len("".join(truncated_explanation)) > tolerable_max_chars and max_chars > 0:
7894
truncated_explanation = _truncate_by_char_count(
7995
truncated_explanation, max_chars
8096
)
8197
else:
8298
truncated_char = False
8399

100+
if truncated_explanation == input_lines:
101+
# No truncation happened, so we do not need to add any explanations
102+
return truncated_explanation
103+
84104
truncated_line_count = len(input_lines) - len(truncated_explanation)
85105
if truncated_explanation[-1]:
86106
# Add ellipsis and take into account part-truncated final line

testing/test_assertion.py

+60
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,66 @@ def test_many_lines():
14351435
result = pytester.runpytest()
14361436
result.stdout.fnmatch_lines(["* 6*"])
14371437

1438+
@pytest.mark.parametrize(
1439+
["truncation_lines", "truncation_chars", "expected_lines_hidden"],
1440+
(
1441+
(3, None, 3),
1442+
(4, None, 0),
1443+
(0, None, 0),
1444+
(None, 8, 6),
1445+
(None, 9, 0),
1446+
(None, 0, 0),
1447+
(0, 0, 0),
1448+
(0, 1000, 0),
1449+
(1000, 0, 0),
1450+
),
1451+
)
1452+
def test_truncation_with_ini(
1453+
self,
1454+
monkeypatch,
1455+
pytester: Pytester,
1456+
truncation_lines: int | None,
1457+
truncation_chars: int | None,
1458+
expected_lines_hidden: int,
1459+
) -> None:
1460+
pytester.makepyfile(
1461+
"""\
1462+
string_a = "123456789\\n23456789\\n3"
1463+
string_b = "123456789\\n23456789\\n4"
1464+
1465+
def test():
1466+
assert string_a == string_b
1467+
"""
1468+
)
1469+
1470+
# This test produces 6 lines of diff output or 79 characters
1471+
# So the effect should be when threshold is < 4 lines (considering 2 additional lines for explanation)
1472+
# Or < 9 characters (considering 70 additional characters for explanation)
1473+
1474+
monkeypatch.delenv("CI", raising=False)
1475+
1476+
ini = "[pytest]\n"
1477+
if truncation_lines is not None:
1478+
ini += f"truncation_limit_lines = {truncation_lines}\n"
1479+
if truncation_chars is not None:
1480+
ini += f"truncation_limit_chars = {truncation_chars}\n"
1481+
pytester.makeini(ini)
1482+
1483+
result = pytester.runpytest()
1484+
1485+
if expected_lines_hidden != 0:
1486+
result.stdout.fnmatch_lines(
1487+
[f"*truncated ({expected_lines_hidden} lines hidden)*"]
1488+
)
1489+
else:
1490+
result.stdout.no_fnmatch_line("*truncated*")
1491+
result.stdout.fnmatch_lines(
1492+
[
1493+
"*- 4*",
1494+
"*+ 3*",
1495+
]
1496+
)
1497+
14381498

14391499
def test_python25_compile_issue257(pytester: Pytester) -> None:
14401500
pytester.makepyfile(

0 commit comments

Comments
 (0)