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
|
Pedro Algarvio
|
||||||
Petter Strandmark
|
Petter Strandmark
|
||||||
Philipp Loose
|
Philipp Loose
|
||||||
|
Pierre Sassoulas
|
||||||
Pieter Mulder
|
Pieter Mulder
|
||||||
Piotr Banaszkiewicz
|
Piotr Banaszkiewicz
|
||||||
Piotr Helm
|
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.
|
"""Truncate given list of strings that makes up the assertion explanation.
|
||||||
|
|
||||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
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:
|
if max_lines is None:
|
||||||
max_lines = DEFAULT_MAX_LINES
|
max_lines = DEFAULT_MAX_LINES
|
||||||
if max_chars is None:
|
if max_chars is None:
|
||||||
|
@ -48,35 +48,56 @@ def _truncate_explanation(
|
||||||
|
|
||||||
# Check if truncation required
|
# Check if truncation required
|
||||||
input_char_count = len("".join(input_lines))
|
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
|
return input_lines
|
||||||
|
# Truncate first to max_lines, and then truncate to max_chars if necessary
|
||||||
# Truncate first to max_lines, and then truncate to max_chars if max_chars
|
|
||||||
# is exceeded.
|
|
||||||
truncated_explanation = input_lines[:max_lines]
|
truncated_explanation = input_lines[:max_lines]
|
||||||
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
|
truncated_char = True
|
||||||
|
# We reevaluate the need to truncate chars following removal of some lines
|
||||||
# Add ellipsis to final line
|
if len("".join(truncated_explanation)) > tolerable_max_chars:
|
||||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
truncated_explanation = _truncate_by_char_count(
|
||||||
|
truncated_explanation, max_chars
|
||||||
# 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)"
|
|
||||||
else:
|
else:
|
||||||
msg += f" ({truncated_line_count} lines hidden)"
|
truncated_char = False
|
||||||
msg += f", {USAGE_MSG}"
|
|
||||||
truncated_explanation.extend(["", str(msg)])
|
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||||
return 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]:
|
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
|
# Find point at which input length exceeds total allowed length
|
||||||
iterated_char_count = 0
|
iterated_char_count = 0
|
||||||
for iterated_index, input_line in enumerate(input_lines):
|
for iterated_index, input_line in enumerate(input_lines):
|
||||||
|
|
|
@ -807,9 +807,9 @@ class TestAssert_reprcompare_dataclass:
|
||||||
"E ['field_b']",
|
"E ['field_b']",
|
||||||
"E ",
|
"E ",
|
||||||
"E Drill down into differing attribute field_b:",
|
"E Drill down into differing attribute field_b:",
|
||||||
"E field_b: 'b' != 'c'...",
|
"E field_b: 'b' != 'c'",
|
||||||
"E ",
|
"E - c",
|
||||||
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
|
"E + b",
|
||||||
],
|
],
|
||||||
consecutive=True,
|
consecutive=True,
|
||||||
)
|
)
|
||||||
|
@ -827,7 +827,7 @@ class TestAssert_reprcompare_dataclass:
|
||||||
"E Drill down into differing attribute g:",
|
"E Drill down into differing attribute g:",
|
||||||
"E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
|
"E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
|
||||||
"E ",
|
"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,
|
consecutive=True,
|
||||||
)
|
)
|
||||||
|
@ -1188,30 +1188,55 @@ class TestTruncateExplanation:
|
||||||
def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
|
def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
|
||||||
expl = ["" for x in range(50)]
|
expl = ["" for x in range(50)]
|
||||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
|
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
|
||||||
|
assert len(result) != len(expl)
|
||||||
assert result != expl
|
assert result != expl
|
||||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||||
assert "Full output truncated" in result[-1]
|
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]
|
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||||
assert last_line_before_trunc_msg.endswith("...")
|
assert last_line_before_trunc_msg.endswith("...")
|
||||||
|
|
||||||
def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
|
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)
|
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||||
assert result != expl
|
assert result != expl
|
||||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||||
assert "Full output truncated" in result[-1]
|
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]
|
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||||
assert last_line_before_trunc_msg.endswith("...")
|
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:
|
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)
|
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||||
assert result != expl
|
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 "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]
|
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||||
assert last_line_before_trunc_msg.endswith("...")
|
assert last_line_before_trunc_msg.endswith("...")
|
||||||
|
|
||||||
|
@ -1240,7 +1265,7 @@ class TestTruncateExplanation:
|
||||||
|
|
||||||
line_count = 7
|
line_count = 7
|
||||||
line_len = 100
|
line_len = 100
|
||||||
expected_truncated_lines = 2
|
expected_truncated_lines = 1
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
r"""
|
r"""
|
||||||
def test_many_lines():
|
def test_many_lines():
|
||||||
|
@ -1261,7 +1286,7 @@ class TestTruncateExplanation:
|
||||||
"*+ 1*",
|
"*+ 1*",
|
||||||
"*+ 3*",
|
"*+ 3*",
|
||||||
"*+ 5*",
|
"*+ 5*",
|
||||||
"*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
|
"*truncated (%d line hidden)*use*-vv*" % expected_truncated_lines,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue