Avoid truncation when truncating means longer output (#10446)
Fixes #6267
This commit is contained in:
parent
f6adebb990
commit
b31db4809b
1
AUTHORS
1
AUTHORS
|
@ -280,6 +280,7 @@ Paweł Adamczak
|
|||
Pedro Algarvio
|
||||
Petter Strandmark
|
||||
Philipp Loose
|
||||
Pierre Sassoulas
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
|
|
|
@ -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.
|
|
@ -38,9 +38,9 @@ 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:
|
||||
|
@ -48,35 +48,56 @@ def _truncate_explanation(
|
|||
|
||||
# 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] + "..."
|
||||
|
||||
# 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)"
|
||||
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:
|
||||
msg += f" ({truncated_line_count} lines hidden)"
|
||||
msg += f", {USAGE_MSG}"
|
||||
truncated_explanation.extend(["", str(msg)])
|
||||
return truncated_explanation
|
||||
truncated_char = False
|
||||
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
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:
|
||||
# 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):
|
||||
|
|
|
@ -807,9 +807,9 @@ class TestAssert_reprcompare_dataclass:
|
|||
"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,
|
||||
)
|
||||
|
@ -827,7 +827,7 @@ class TestAssert_reprcompare_dataclass:
|
|||
"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,
|
||||
)
|
||||
|
@ -1188,30 +1188,55 @@ class TestTruncateExplanation:
|
|||
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("...")
|
||||
|
||||
|
@ -1240,7 +1265,7 @@ class TestTruncateExplanation:
|
|||
|
||||
line_count = 7
|
||||
line_len = 100
|
||||
expected_truncated_lines = 2
|
||||
expected_truncated_lines = 1
|
||||
pytester.makepyfile(
|
||||
r"""
|
||||
def test_many_lines():
|
||||
|
@ -1261,7 +1286,7 @@ class TestTruncateExplanation:
|
|||
"*+ 1*",
|
||||
"*+ 3*",
|
||||
"*+ 5*",
|
||||
"*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
|
||||
"*truncated (%d line hidden)*use*-vv*" % expected_truncated_lines,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue