Skip to content

Commit d77310a

Browse files
authored
Better diffs in tests (#16112)
It's annoying that one line change causes everything else to show up as a diff. Just use difflib instead. I also highlight the changed lines. We can't use FancyFormatter because it doesn't work well with pytest.
1 parent 2c2d126 commit d77310a

File tree

1 file changed

+68
-60
lines changed

1 file changed

+68
-60
lines changed

mypy/test/helpers.py

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import difflib
45
import os
56
import pathlib
67
import re
@@ -43,64 +44,81 @@ def run_mypy(args: list[str]) -> None:
4344
pytest.fail(msg="Sample check failed", pytrace=False)
4445

4546

46-
def assert_string_arrays_equal(expected: list[str], actual: list[str], msg: str) -> None:
47-
"""Assert that two string arrays are equal.
47+
def diff_ranges(
48+
left: list[str], right: list[str]
49+
) -> tuple[list[tuple[int, int]], list[tuple[int, int]]]:
50+
seq = difflib.SequenceMatcher(None, left, right)
51+
# note last triple is a dummy, so don't need to worry
52+
blocks = seq.get_matching_blocks()
4853

49-
Display any differences in a human-readable form.
50-
"""
51-
actual = clean_up(actual)
52-
if actual != expected:
53-
num_skip_start = num_skipped_prefix_lines(expected, actual)
54-
num_skip_end = num_skipped_suffix_lines(expected, actual)
54+
i = 0
55+
j = 0
56+
left_ranges = []
57+
right_ranges = []
58+
for block in blocks:
59+
# mismatched range
60+
left_ranges.append((i, block.a))
61+
right_ranges.append((j, block.b))
5562

56-
sys.stderr.write("Expected:\n")
63+
i = block.a + block.size
64+
j = block.b + block.size
5765

58-
# If omit some lines at the beginning, indicate it by displaying a line
59-
# with '...'.
60-
if num_skip_start > 0:
61-
sys.stderr.write(" ...\n")
66+
# matched range
67+
left_ranges.append((block.a, i))
68+
right_ranges.append((block.b, j))
69+
return left_ranges, right_ranges
6270

63-
# Keep track of the first different line.
64-
first_diff = -1
6571

66-
# Display only this many first characters of identical lines.
67-
width = 75
72+
def render_diff_range(
73+
ranges: list[tuple[int, int]], content: list[str], colour: str | None = None
74+
) -> None:
75+
for i, line_range in enumerate(ranges):
76+
is_matching = i % 2 == 1
77+
lines = content[line_range[0] : line_range[1]]
78+
for j, line in enumerate(lines):
79+
if (
80+
is_matching
81+
# elide the middle of matching blocks
82+
and j >= 3
83+
and j < len(lines) - 3
84+
):
85+
if j == 3:
86+
sys.stderr.write(" ...\n")
87+
continue
6888

69-
for i in range(num_skip_start, len(expected) - num_skip_end):
70-
if i >= len(actual) or expected[i] != actual[i]:
71-
if first_diff < 0:
72-
first_diff = i
73-
sys.stderr.write(f" {expected[i]:<45} (diff)")
74-
else:
75-
e = expected[i]
76-
sys.stderr.write(" " + e[:width])
77-
if len(e) > width:
78-
sys.stderr.write("...")
79-
sys.stderr.write("\n")
80-
if num_skip_end > 0:
81-
sys.stderr.write(" ...\n")
89+
if not is_matching and colour:
90+
sys.stderr.write(colour)
8291

83-
sys.stderr.write("Actual:\n")
92+
sys.stderr.write(" " + line)
8493

85-
if num_skip_start > 0:
86-
sys.stderr.write(" ...\n")
94+
if not is_matching:
95+
if colour:
96+
sys.stderr.write("\033[0m")
97+
sys.stderr.write(" (diff)")
8798

88-
for j in range(num_skip_start, len(actual) - num_skip_end):
89-
if j >= len(expected) or expected[j] != actual[j]:
90-
sys.stderr.write(f" {actual[j]:<45} (diff)")
91-
else:
92-
a = actual[j]
93-
sys.stderr.write(" " + a[:width])
94-
if len(a) > width:
95-
sys.stderr.write("...")
9699
sys.stderr.write("\n")
97-
if not actual:
98-
sys.stderr.write(" (empty)\n")
99-
if num_skip_end > 0:
100-
sys.stderr.write(" ...\n")
101100

102-
sys.stderr.write("\n")
103101

102+
def assert_string_arrays_equal(expected: list[str], actual: list[str], msg: str) -> None:
103+
"""Assert that two string arrays are equal.
104+
105+
Display any differences in a human-readable form.
106+
"""
107+
actual = clean_up(actual)
108+
if expected != actual:
109+
expected_ranges, actual_ranges = diff_ranges(expected, actual)
110+
sys.stderr.write("Expected:\n")
111+
red = "\033[31m" if sys.platform != "win32" else None
112+
render_diff_range(expected_ranges, expected, colour=red)
113+
sys.stderr.write("Actual:\n")
114+
green = "\033[32m" if sys.platform != "win32" else None
115+
render_diff_range(actual_ranges, actual, colour=green)
116+
117+
sys.stderr.write("\n")
118+
first_diff = next(
119+
(i for i, (a, b) in enumerate(zip(expected, actual)) if a != b),
120+
max(len(expected), len(actual)),
121+
)
104122
if 0 <= first_diff < len(actual) and (
105123
len(expected[first_diff]) >= MIN_LINE_LENGTH_FOR_ALIGNMENT
106124
or len(actual[first_diff]) >= MIN_LINE_LENGTH_FOR_ALIGNMENT
@@ -109,6 +127,10 @@ def assert_string_arrays_equal(expected: list[str], actual: list[str], msg: str)
109127
# long lines.
110128
show_align_message(expected[first_diff], actual[first_diff])
111129

130+
sys.stderr.write(
131+
"Update the test output using --update-data -n0 "
132+
"(you can additionally use the -k selector to update only specific tests)"
133+
)
112134
pytest.fail(msg, pytrace=False)
113135

114136

@@ -226,20 +248,6 @@ def local_sys_path_set() -> Iterator[None]:
226248
sys.path = old_sys_path
227249

228250

229-
def num_skipped_prefix_lines(a1: list[str], a2: list[str]) -> int:
230-
num_eq = 0
231-
while num_eq < min(len(a1), len(a2)) and a1[num_eq] == a2[num_eq]:
232-
num_eq += 1
233-
return max(0, num_eq - 4)
234-
235-
236-
def num_skipped_suffix_lines(a1: list[str], a2: list[str]) -> int:
237-
num_eq = 0
238-
while num_eq < min(len(a1), len(a2)) and a1[-num_eq - 1] == a2[-num_eq - 1]:
239-
num_eq += 1
240-
return max(0, num_eq - 4)
241-
242-
243251
def testfile_pyversion(path: str) -> tuple[int, int]:
244252
if path.endswith("python312.test"):
245253
return 3, 12

0 commit comments

Comments
 (0)