Skip to content

Commit

Permalink
Avoid truncation when truncating means longer output (#10446)
Browse files Browse the repository at this point in the history
Fixes #6267
  • Loading branch information
Pierre-Sassoulas committed Dec 17, 2022
1 parent f6adebb commit b31db48
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 35 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -280,6 +280,7 @@ Paweł Adamczak
Pedro Algarvio
Petter Strandmark
Philipp Loose
Pierre Sassoulas
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
Expand Down
2 changes: 2 additions & 0 deletions changelog/6267.improvement.rst
@@ -0,0 +1,2 @@
The full output of a test is no longer truncated if the truncation message would be longer than
the hidden text. The line number shown has also been fixed.
67 changes: 44 additions & 23 deletions src/_pytest/assertion/truncate.py
Expand Up @@ -38,45 +38,66 @@ def _truncate_explanation(
"""Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
first. The remaining lines will be replaced by a usage message.
first, taking the truncation explanation into account. The remaining lines
will be replaced by a usage message.
"""

if max_lines is None:
max_lines = DEFAULT_MAX_LINES
if max_chars is None:
max_chars = DEFAULT_MAX_CHARS

# Check if truncation required
input_char_count = len("".join(input_lines))
if len(input_lines) <= max_lines and input_char_count <= max_chars:
# The length of the truncation explanation depends on the number of lines
# removed but is at least 68 characters:
# The real value is
# 64 (for the base message:
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
# )
# + 1 (for plural)
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
# + 3 for the '...' added to the truncated line
# But if there's more than 100 lines it's very likely that we're going to
# truncate, so we don't need the exact value using log10.
tolerable_max_chars = (
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
)
# The truncation explanation add two lines to the output
tolerable_max_lines = max_lines + 2
if (
len(input_lines) <= tolerable_max_lines
and input_char_count <= tolerable_max_chars
):
return input_lines

# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
# Truncate first to max_lines, and then truncate to max_chars if necessary
truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)

# Add ellipsis to final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
truncated_char = True
# We reevaluate the need to truncate chars following removal of some lines
if len("".join(truncated_explanation)) > tolerable_max_chars:
truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
)
else:
truncated_char = False

# Append useful message to explanation
truncated_line_count = len(input_lines) - len(truncated_explanation)
truncated_line_count += 1 # Account for the part-truncated final line
msg = "...Full output truncated"
if truncated_line_count == 1:
msg += f" ({truncated_line_count} line hidden)"
if truncated_explanation[-1]:
# Add ellipsis and take into account part-truncated final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
if truncated_char:
# It's possible that we did not remove any char from this line
truncated_line_count += 1
else:
msg += f" ({truncated_line_count} lines hidden)"
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)])
return truncated_explanation
# Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..."
return truncated_explanation + [
"",
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
]


def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
# Check if truncation required
if len("".join(input_lines)) <= max_chars:
return input_lines

# Find point at which input length exceeds total allowed length
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):
Expand Down
49 changes: 37 additions & 12 deletions testing/test_assertion.py
Expand Up @@ -807,9 +807,9 @@ def test_dataclasses(self, pytester: Pytester) -> None:
"E ['field_b']",
"E ",
"E Drill down into differing attribute field_b:",
"E field_b: 'b' != 'c'...",
"E ",
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
"E field_b: 'b' != 'c'",
"E - c",
"E + b",
],
consecutive=True,
)
Expand All @@ -827,7 +827,7 @@ def test_recursive_dataclasses(self, pytester: Pytester) -> None:
"E Drill down into differing attribute g:",
"E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
"E ",
"E ...Full output truncated (52 lines hidden), use '-vv' to show",
"E ...Full output truncated (51 lines hidden), use '-vv' to show",
],
consecutive=True,
)
Expand Down Expand Up @@ -1188,30 +1188,55 @@ def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None
def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
expl = ["" for x in range(50)]
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
assert len(result) != len(expl)
assert result != expl
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
assert "Full output truncated" in result[-1]
assert "43 lines hidden" in result[-1]
assert "42 lines hidden" in result[-1]
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
assert last_line_before_trunc_msg.endswith("...")

def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
expl = ["a" for x in range(100)]
total_lines = 100
expl = ["a" for x in range(total_lines)]
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
assert result != expl
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
assert "Full output truncated" in result[-1]
assert "93 lines hidden" in result[-1]
assert f"{total_lines - 8} lines hidden" in result[-1]
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
assert last_line_before_trunc_msg.endswith("...")

def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None:
"""The number of line in the result is 9, the same number as if we truncated."""
expl = ["a" for x in range(9)]
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
assert result == expl
assert "truncated" not in result[-1]

def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars(
self,
) -> None:
line = "a" * 10
expl = [line, line]
result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10)
assert result == [line, line]

def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines(
self,
) -> None:
line = "a" * 10
expl = [line, line]
result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100)
assert result == [line, line]

def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None:
expl = ["a" * 80 for x in range(16)]
expl = [chr(97 + x) * 80 for x in range(16)]
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
assert result != expl
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG
assert "Full output truncated" in result[-1]
assert "9 lines hidden" in result[-1]
assert "8 lines hidden" in result[-1]
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
assert last_line_before_trunc_msg.endswith("...")

Expand Down Expand Up @@ -1240,7 +1265,7 @@ def test_full_output_truncated(self, monkeypatch, pytester: Pytester) -> None:

line_count = 7
line_len = 100
expected_truncated_lines = 2
expected_truncated_lines = 1
pytester.makepyfile(
r"""
def test_many_lines():
Expand All @@ -1261,7 +1286,7 @@ def test_many_lines():
"*+ 1*",
"*+ 3*",
"*+ 5*",
"*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
"*truncated (%d line hidden)*use*-vv*" % expected_truncated_lines,
]
)

Expand Down

0 comments on commit b31db48

Please sign in to comment.