Skip to content

Commit 25fbd3f

Browse files
[fix] Truncate only if truncating means a smaller output
Also fixes the dislayed line hidden message Closes #6267
1 parent 8543247 commit 25fbd3f

File tree

4 files changed

+71
-30
lines changed

4 files changed

+71
-30
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ Paweł Adamczak
276276
Pedro Algarvio
277277
Petter Strandmark
278278
Philipp Loose
279+
Pierre Sassoulas
279280
Pieter Mulder
280281
Piotr Banaszkiewicz
281282
Piotr Helm

changelog/6267.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The full output of a test is no longer truncated if the truncation message would be longer than
2+
the hidden text. The line number shown has also been fixed.

src/_pytest/assertion/truncate.py

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,45 +38,61 @@ def _truncate_explanation(
3838
"""Truncate given list of strings that makes up the assertion explanation.
3939
4040
Truncates to either 8 lines, or 640 characters - whichever the input reaches
41-
first. The remaining lines will be replaced by a usage message.
41+
first, taking the truncation explanation into account. The remaining lines
42+
will be replaced by a usage message.
4243
"""
43-
4444
if max_lines is None:
4545
max_lines = DEFAULT_MAX_LINES
4646
if max_chars is None:
4747
max_chars = DEFAULT_MAX_CHARS
4848

4949
# Check if truncation required
5050
input_char_count = len("".join(input_lines))
51-
if len(input_lines) <= max_lines and input_char_count <= max_chars:
51+
# The length of the truncation explanation depends on the number of line
52+
# removed but is at least 67 characters:
53+
# The real value is
54+
# 64 (for the base message:
55+
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
56+
# )
57+
# + 1 (for plural)
58+
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line)
59+
# + 3 for the '...' added to the truncated line
60+
# But if there's more than 100 lines it's very likely that we're going to
61+
# truncate, so we don't need the exact value using log10.
62+
tolerable_max_chars = (
63+
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
64+
)
65+
# The truncation explanation add two lines to the output
66+
tolerable_max_lines = max_lines + 2
67+
if (
68+
len(input_lines) <= tolerable_max_lines
69+
and input_char_count <= tolerable_max_chars
70+
):
5271
return input_lines
53-
54-
# Truncate first to max_lines, and then truncate to max_chars if max_chars
55-
# is exceeded.
72+
# Truncate first to max_lines, and then truncate to max_chars if necessary
5673
truncated_explanation = input_lines[:max_lines]
57-
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
58-
59-
# Add ellipsis to final line
60-
truncated_explanation[-1] = truncated_explanation[-1] + "..."
74+
# We reevaluate the need to truncate following removal of some lines
75+
if len("".join(input_lines)) > tolerable_max_chars:
76+
truncated_explanation = _truncate_by_char_count(
77+
truncated_explanation, max_chars
78+
)
6179

62-
# Append useful message to explanation
6380
truncated_line_count = len(input_lines) - len(truncated_explanation)
64-
truncated_line_count += 1 # Account for the part-truncated final line
65-
truncated_explanation.extend(
66-
[
67-
"", # Line break
68-
f"...Full output truncated ({truncated_line_count} line"
69-
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
70-
]
71-
)
72-
return truncated_explanation
81+
if truncated_explanation[-1]:
82+
# Add ellipsis and take into account part-truncated final line
83+
truncated_explanation[-1] = truncated_explanation[-1] + "..."
84+
truncated_line_count += 1
85+
else:
86+
# Add proper ellipsis when we were able to fit a full line exactly
87+
truncated_explanation[-1] = "..."
88+
return truncated_explanation + [
89+
"",
90+
f"...Full output truncated ({truncated_line_count} line"
91+
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
92+
]
7393

7494

7595
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
76-
# Check if truncation required
77-
if len("".join(input_lines)) <= max_chars:
78-
return input_lines
79-
8096
# Find point at which input length exceeds total allowed length
8197
iterated_char_count = 0
8298
for iterated_index, input_line in enumerate(input_lines):

testing/test_assertion.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -807,9 +807,9 @@ def test_dataclasses(self, pytester: Pytester) -> None:
807807
"E ['field_b']",
808808
"E ",
809809
"E Drill down into differing attribute field_b:",
810-
"E field_b: 'b' != 'c'...",
811-
"E ",
812-
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
810+
"E field_b: 'b' != 'c'",
811+
"E - c",
812+
"E + b",
813813
],
814814
consecutive=True,
815815
)
@@ -1188,10 +1188,11 @@ def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None
11881188
def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
11891189
expl = ["" for x in range(50)]
11901190
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
1191+
assert len(result) != len(expl)
11911192
assert result != expl
11921193
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
11931194
assert "Full output truncated" in result[-1]
1194-
assert "43 lines hidden" in result[-1]
1195+
assert "42 lines hidden" in result[-1]
11951196
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
11961197
assert last_line_before_trunc_msg.endswith("...")
11971198

@@ -1205,13 +1206,34 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
12051206
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
12061207
assert last_line_before_trunc_msg.endswith("...")
12071208

1209+
def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None:
1210+
"""The number of line in the result is 9, the same number as if we truncated."""
1211+
expl = ["a" for x in range(9)]
1212+
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
1213+
assert result == expl
1214+
assert "truncated" not in result[-1]
1215+
1216+
def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars(
1217+
self,
1218+
) -> None:
1219+
expl = ["a" * 10 for x in range(2)]
1220+
result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10)
1221+
assert result == ["aaaaaaaaaa", "aaaaaaaaaa"]
1222+
1223+
def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines(
1224+
self,
1225+
) -> None:
1226+
expl = ["a" * 10 for x in range(2)]
1227+
result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100)
1228+
assert result == ["aaaaaaaaaa", "aaaaaaaaaa"]
1229+
12081230
def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None:
12091231
expl = ["a" * 80 for x in range(16)]
12101232
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
12111233
assert result != expl
1212-
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
1234+
assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG
12131235
assert "Full output truncated" in result[-1]
1214-
assert "9 lines hidden" in result[-1]
1236+
assert "8 lines hidden" in result[-1]
12151237
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
12161238
assert last_line_before_trunc_msg.endswith("...")
12171239

0 commit comments

Comments
 (0)