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