Repository: pytest-dev/pytest
File: src/_pytest/assertion/truncate.py (Python)

Code before:
    # 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 necessary
    if max_lines > 0:
        truncated_explanation = input_lines[:max_lines]
    else:
        truncated_explanation = input_lines
    # We reevaluate the need to truncate chars following removal of some lines
    need_to_truncate_char = (
        70 < tolerable_max_chars < sum(len(e) for e in truncated_explanation)
    )
    if need_to_truncate_char:
        truncated_explanation = _truncate_by_char_count(
            truncated_explanation, max_chars
        )

    if truncated_explanation == input_lines:
        # No truncation happened, so we do not need to add any explanations
        return truncated_explanation

    truncated_explanation[-1] = truncated_explanation[-1] + "..."
    truncated_line_count = (
        len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char)
    )
    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]:
    # Find point at which input length exceeds total allowed length
    iterated_char_count = 0
    for iterated_index, input_line in enumerate(input_lines):
        if iterated_char_count + len(input_line) > max_chars:
            break
        iterated_char_count += len(input_line)

    # Create truncated explanation with modified final line
    truncated_result = input_lines[:iterated_index]
    final_line = input_lines[iterated_index]
    if final_line:
        final_line_truncate_point = max_chars - iterated_char_count
        final_line = final_line[:final_line_truncate_point]

Review comment (refactor):
Am I correct that a minimal fix for the issue would be

```diff
diff --git i/src/_pytest/assertion/truncate.py w/src/_pytest/assertion/truncate.py
index 5820e6e8a..fbe18fd3d 100644
--- i/src/_pytest/assertion/truncate.py
+++ w/src/_pytest/assertion/truncate.py
@@ -105,12 +105,12 @@ def _truncate_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] = "..."
+    if truncated_char:
+        # It's possible that we did not remove any char from this line
+        truncated_line_count += 1
     return [
         *truncated_explanation,
         "",
```

If so, I think it would be good to do the minimal fix in its own commit, just to separate the logic fix from the refactoring (which should have no logical changes), that's a bit easier to review.

Code after:
    # 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
    if max_lines == 0 or len(input_lines) <= max_lines + 2:
        if max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars:
            return input_lines
        truncated_explanation = input_lines
    else:
        # Truncate first to max_lines, and then truncate to max_chars if necessary
        truncated_explanation = input_lines[:max_lines]
    # We reevaluate the need to truncate chars following removal of some lines
    need_to_truncate_char = (
        max_chars > 0
        and sum(len(e) for e in truncated_explanation) > tolerable_max_chars
    )
    if need_to_truncate_char:
        truncated_explanation = _truncate_by_char_count(
            truncated_explanation, max_chars
        )
    # Something was truncated, adding '...' at the end to show that
    truncated_explanation[-1] += "..."
    truncated_line_count = (
        len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char)
    )
    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]:
    # Find point at which input length exceeds total allowed length
    iterated_char_count = 0
    for iterated_index, input_line in enumerate(input_lines):
        if iterated_char_count + len(input_line) > max_chars:
            break
        iterated_char_count += len(input_line)

    # Create truncated explanation with modified final line
    truncated_result = input_lines[:iterated_index]
    final_line = input_lines[iterated_index]
    if final_line:
        final_line_truncate_point = max_chars - iterated_char_count
        final_line = final_line[:final_line_truncate_point]
    truncated_result.append(final_line)
    return truncated_result