Skip to content

Commit 2c93219

Browse files
authored
Better support for incremental tests in parallel mode (#21234)
In parallel mode order of modules is unpredictable, so we ignore it. But we still check the order of messages _within_ the same module. This is a follow-up for #21119
1 parent f1ff74d commit 2c93219

File tree

3 files changed

+71
-22
lines changed

3 files changed

+71
-22
lines changed

mypy/test/helpers.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import shutil
99
import sys
1010
import time
11+
from collections import defaultdict
1112
from collections.abc import Callable, Iterable, Iterator
1213
from re import Pattern
1314
from typing import IO, Any
@@ -106,14 +107,64 @@ def render_diff_range(
106107
output.write("\n")
107108

108109

110+
def dump_original_errors(errors: list[str]) -> None:
111+
sys.stderr.write("Original errors:\n")
112+
for err in errors:
113+
sys.stderr.write(err + "\n")
114+
115+
116+
def module_order(errors: list[str]) -> list[str]:
117+
result = []
118+
seen = set()
119+
mods = []
120+
for e in errors:
121+
if ":" not in e:
122+
dump_original_errors(errors)
123+
pytest.fail(f"Only module scoped errors are supported, got {e}")
124+
mod, _ = e.split(":", maxsplit=1)
125+
mods.append(mod)
126+
for i, mod in enumerate(mods):
127+
if i > 0:
128+
if mod != mods[i - 1] and mod in seen:
129+
dump_original_errors(errors)
130+
pytest.fail(f"Each module must form a single block, {mod} appears split")
131+
if mod not in seen:
132+
result.append(mod)
133+
seen.add(mod)
134+
return result
135+
136+
137+
def match_module_order(actual: list[str], expected_order: list[str]) -> list[str]:
138+
actual_by_mod = defaultdict(list)
139+
actual_order = module_order(actual)
140+
if set(actual_order) != set(expected_order):
141+
# Different files, give up and show actual errors.
142+
return actual
143+
for a in actual:
144+
mod, _ = a.split(":", maxsplit=1)
145+
actual_by_mod[mod].append(a)
146+
result = []
147+
for mod in expected_order:
148+
result.extend(actual_by_mod[mod])
149+
return result
150+
151+
109152
def assert_string_arrays_equal(
110-
expected: list[str], actual: list[str], msg: str, *, traceback: bool = False
153+
expected: list[str],
154+
actual: list[str],
155+
msg: str,
156+
*,
157+
traceback: bool = False,
158+
ignore_modules_order: bool = False,
111159
) -> None:
112160
"""Assert that two string arrays are equal.
113161
114162
Display any differences in a human-readable form.
115163
"""
116164
actual = clean_up(actual)
165+
if ignore_modules_order:
166+
expected_module_order = module_order(expected)
167+
actual = match_module_order(actual, expected_module_order)
117168
if expected != actual:
118169
expected_ranges, actual_ranges = diff_ranges(expected, actual)
119170
sys.stderr.write("Expected:\n")

mypy/test/testcheck.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
8888
for step in range(1, num_steps + 1):
8989
idx = step - 2
9090
ops = steps[idx] if idx < len(steps) and idx >= 0 else []
91-
self.run_case_once(testcase, ops, step)
91+
self.run_case_once(testcase, ops, step, True)
9292
else:
9393
self.run_case_once(testcase)
9494

@@ -110,6 +110,7 @@ def run_case_once(
110110
testcase: DataDrivenTestCase,
111111
operations: list[FileOperation] | None = None,
112112
incremental_step: int = 0,
113+
is_incremental: bool = False,
113114
) -> None:
114115
if operations is None:
115116
operations = []
@@ -227,11 +228,18 @@ def run_case_once(
227228
if output != a and testcase.config.getoption("--update-data", False):
228229
update_testcase_output(testcase, a, incremental_step=incremental_step)
229230

231+
ignore_modules_order = False
230232
if options.num_workers > 0:
233+
ignore_modules_order = is_incremental
231234
# TypeVarIds are not stable in parallel checking, normalize.
232235
a = remove_typevar_ids(a)
233236
output = remove_typevar_ids(output)
234-
assert_string_arrays_equal(output, a, msg.format(testcase.file, testcase.line))
237+
assert_string_arrays_equal(
238+
output,
239+
a,
240+
msg.format(testcase.file, testcase.line),
241+
ignore_modules_order=ignore_modules_order,
242+
)
235243

236244
if res:
237245
if options.cache_dir != os.devnull:

test-data/unit/check-incremental.test

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,9 +1090,7 @@ import foo # type: ignore
10901090
[builtins fixtures/module.pyi]
10911091
[stale]
10921092

1093-
-- Order of processing of files is not stable in this test, skip it in parallel mode.
1094-
-- Note: do not use _no_parallel unless really needed!
1095-
[case testIncrementalWithSilentImportsAndIgnore_no_parallel]
1093+
[case testIncrementalWithSilentImportsAndIgnore]
10961094
# cmd: mypy -m main b
10971095
# cmd2: mypy -m main c c.submodule
10981096
# flags: --follow-imports=skip
@@ -1163,8 +1161,7 @@ class A:
11631161
[out1]
11641162
main:2: error: "A" has no attribute "bar"
11651163

1166-
-- Order of files unstable in parallel mode
1167-
[case testIncrementalChangedError_no_parallel]
1164+
[case testIncrementalChangedError]
11681165
import m
11691166
[file m.py]
11701167
import n
@@ -1326,8 +1323,7 @@ tmp/main.py:4: note: Revealed type is "builtins.str"
13261323
[out2]
13271324
tmp/main.py:4: note: Revealed type is "Any"
13281325

1329-
-- Order of files unstable in parallel mode
1330-
[case testIncrementalFixedBugCausesPropagation_no_parallel]
1326+
[case testIncrementalFixedBugCausesPropagation]
13311327
import mod1
13321328

13331329
[file mod1.py]
@@ -1367,8 +1363,7 @@ tmp/mod1.py:3: note: Revealed type is "builtins.int"
13671363
[out2]
13681364
tmp/mod1.py:3: note: Revealed type is "builtins.int"
13691365

1370-
-- Order of files unstable in parallel mode
1371-
[case testIncrementalIncidentalChangeWithBugCausesPropagation_no_parallel]
1366+
[case testIncrementalIncidentalChangeWithBugCausesPropagation]
13721367
import mod1
13731368

13741369
[file mod1.py]
@@ -1407,8 +1402,7 @@ tmp/mod1.py:3: note: Revealed type is "builtins.int"
14071402
tmp/mod3.py:6: error: Incompatible types in assignment (expression has type "str", variable has type "int")
14081403
tmp/mod1.py:3: note: Revealed type is "builtins.str"
14091404

1410-
-- Order of files unstable in parallel mode
1411-
[case testIncrementalIncidentalChangeWithBugFixCausesPropagation_no_parallel]
1405+
[case testIncrementalIncidentalChangeWithBugFixCausesPropagation]
14121406
import mod1
14131407

14141408
[file mod1.py]
@@ -1976,8 +1970,7 @@ main:2: error: Name "nonexisting" is not defined
19761970
[out2]
19771971
main:2: error: Name "nonexisting" is not defined
19781972

1979-
-- Order of files unstable in parallel mode
1980-
[case testIncrementalInnerClassAttrInMethodReveal_no_parallel]
1973+
[case testIncrementalInnerClassAttrInMethodReveal]
19811974
import crash
19821975
reveal_type(crash.C().a)
19831976
reveal_type(crash.D().a)
@@ -4089,8 +4082,7 @@ main:2: error: Argument 2 to "B" has incompatible type "str"; expected "int"
40894082
[out2]
40904083
[rechecked b]
40914084

4092-
-- Order of files unstable in parallel mode
4093-
[case testIncrementalDataclassesThreeFiles_no_parallel]
4085+
[case testIncrementalDataclassesThreeFiles]
40944086
from c import C
40954087
C('foo', 5, True)
40964088

@@ -5412,8 +5404,7 @@ tmp/c.py:2: note: Revealed type is "b.<subclass of "a.A" and "a.B">"
54125404
[out2]
54135405
tmp/c.py:2: note: Revealed type is "b.<subclass of "a.A" and "a.C">"
54145406

5415-
-- Order of files unstable in parallel mode
5416-
[case testIsInstanceAdHocIntersectionIncrementalIntersectionToUnreachable_no_parallel]
5407+
[case testIsInstanceAdHocIntersectionIncrementalIntersectionToUnreachable]
54175408
import c
54185409
[file a.py]
54195410
class A:
@@ -5447,8 +5438,7 @@ tmp/c.py:2: note: Revealed type is "a.<subclass of "a.A" and "a.B">"
54475438
tmp/b.py:2: error: Cannot determine type of "y"
54485439
tmp/c.py:2: note: Revealed type is "Any"
54495440

5450-
-- Order of files unstable in parallel mode
5451-
[case testIsInstanceAdHocIntersectionIncrementalUnreachaableToIntersection_no_parallel]
5441+
[case testIsInstanceAdHocIntersectionIncrementalUnreachaableToIntersection]
54525442
import c
54535443
[file a.py]
54545444
class A:

0 commit comments

Comments
 (0)