Skip to content

Commit

Permalink
[fix] Truncate only if truncating means a smaller output
Browse files Browse the repository at this point in the history
Also fixes the dislayed line hidden message

Closes pytest-dev#6267
  • Loading branch information
Pierre-Sassoulas committed Oct 29, 2022
1 parent 8543247 commit 59b2ee7
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 28 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,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.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 40 additions & 22 deletions src/_pytest/assertion/truncate.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,45 +38,63 @@ 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.
The length of the truncation explanation depends on the number of line
removed.
"""
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 truncation explanation is at least 67 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)
# + 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
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)
# We reevaluate the need to truncate following removal of some lines
if len("".join(input_lines)) > tolerable_max_chars:
truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
)

# Add ellipsis to final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."

# 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
truncated_explanation.extend(
[
"", # Line break
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
]
)
if truncated_explanation[-1]:
truncated_explanation[-1] = truncated_explanation[-1] + "..."
truncated_line_count += 1 # Account for the part-truncated final line
else:
truncated_explanation[-1] = "..."
truncated_explanation += [
"",
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
]
return truncated_explanation


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
34 changes: 28 additions & 6 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
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 Down Expand Up @@ -1188,10 +1188,11 @@ 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("...")

Expand All @@ -1205,13 +1206,34 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
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:
expl = ["a" * 10 for x in range(2)]
result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10)
assert result == ["aaaaaaaaaa", "aaaaaaaaaa"]

def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines(
self,
) -> None:
expl = ["a" * 10 for x in range(2)]
result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100)
assert result == ["aaaaaaaaaa", "aaaaaaaaaa"]

def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None:
expl = ["a" * 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

0 comments on commit 59b2ee7

Please sign in to comment.