1"""Utilities for truncating assertion output.

2

3 Current default behaviour is to truncate assertion explanations at

4~8 terminal lines, unless running in "-vv" mode or running on CI.

5"""

6 from typing import List

7 from typing import Optional

8

9 from _pytest.assertion import util

10 from _pytest.nodes import Item

11

12

13 DEFAULT_MAX_LINES = 8

14 DEFAULT_MAX_CHARS = 8 * 80

15 USAGE_MSG = "use '-vv' to show"

16

17

18 def truncate_if_required(

19 explanation: List[str], item: Item, max_length: Optional[int] = None

20) -> List[str]:

21 """Truncate this assertion explanation if the given test item is eligible."""

22 if _should_truncate_item(item):

23 return _truncate_explanation(explanation)

24 return explanation

25

26

27 def _should_truncate_item(item: Item) -> bool:

28 """Whether or not this test item is eligible for truncation."""

29 verbose = item.config.option.verbose

30 return verbose < 2 and not util.running_on_ci()

31

32

33 def _truncate_explanation(

34 input_lines: List[str],

35 max_lines: Optional[int] = None,

36 max_chars: Optional[int] = None,

37) -> List[str]:

38 """Truncate given list of strings that makes up the assertion explanation.

39

40 Truncates to either 8 lines, or 640 characters - whichever the input reaches

41 first. The remaining lines will be replaced by a usage message.

42 """

43

44 if max_lines is None:

45 max_lines = DEFAULT_MAX_LINES

46 if max_chars is None:

47 max_chars = DEFAULT_MAX_CHARS

48

49 # Check if truncation required

50 input_char_count = len("".join(input_lines))

51 if len(input_lines) <= max_lines and input_char_count <= max_chars:

52 return input_lines

53

54 # Truncate first to max_lines, and then truncate to max_chars if max_chars

55 # is exceeded.

56 truncated_explanation = input_lines[:max_lines]

57 truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)

58

59 # Add ellipsis to final line

60 truncated_explanation[-1] = truncated_explanation[-1] + "..."

61

62 # Append useful message to explanation

63 truncated_line_count = len(input_lines) - len(truncated_explanation)

64 truncated_line_count += 1 # Account for the part-truncated final line

65 msg = "...Full output truncated"

66 if truncated_line_count == 1:

67 msg += f" ({truncated_line_count} line hidden)"

68 else:

69 msg += f" ({truncated_line_count} lines hidden)"

70 msg += f", {USAGE_MSG}"

71 truncated_explanation.extend(["", str(msg)])

72 return truncated_explanation

73

74

75 def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:

76 # Check if truncation required

77 if len("".join(input_lines)) <= max_chars:

78 return input_lines

79

80 # Find point at which input length exceeds total allowed length

81 iterated_char_count = 0

82 for iterated_index, input_line in enumerate(input_lines):

83 if iterated_char_count + len(input_line) > max_chars:

84 break

85 iterated_char_count += len(input_line)

86

87 # Create truncated explanation with modified final line

88 truncated_result = input_lines[:iterated_index]

89 final_line = input_lines[iterated_index]

90 if final_line:

91 final_line_truncate_point = max_chars - iterated_char_count

92 final_line = final_line[:final_line_truncate_point]

93 truncated_result.append(final_line)

94 return truncated_result