Skip to content

Commit e69090b

Browse files
Michael0x2agvanrossum
authored andcommitted
Extend tests to check if mypy re-checks specific files during incremental mode (#1811)
* Allow tests to check for specific file changes This commit adds an [stale ...] annotation which, when added to incremental tests, will make sure that only the files listed in the annotation are rechecked by mypy during the second run in incremental mode.
1 parent d86f5f6 commit e69090b

File tree

4 files changed

+89
-3
lines changed

4 files changed

+89
-3
lines changed

mypy/build.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ class BuildManager:
317317
errors: Used for reporting all errors
318318
options: Build options
319319
missing_modules: Set of modules that could not be imported encountered so far
320+
stale_modules: Set of modules that needed to be rechecked
320321
"""
321322

322323
def __init__(self, data_dir: str,
@@ -338,6 +339,7 @@ def __init__(self, data_dir: str,
338339
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
339340
self.type_checker = TypeChecker(self.errors, self.modules, options=options)
340341
self.missing_modules = set() # type: Set[str]
342+
self.stale_modules = set() # type: Set[str]
341343

342344
def all_imported_modules_in_file(self,
343345
file: MypyFile) -> List[Tuple[int, str, int]]:
@@ -1145,6 +1147,7 @@ def is_fresh(self) -> bool:
11451147
def mark_stale(self) -> None:
11461148
"""Throw away the cache data for this file, marking it as stale."""
11471149
self.meta = None
1150+
self.manager.stale_modules.add(self.id)
11481151

11491152
def check_blockers(self) -> None:
11501153
"""Raise CompileError if a blocking error is detected."""

mypy/test/data.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from os import remove, rmdir
77
import shutil
88

9-
from typing import Callable, List, Tuple
9+
from typing import Callable, List, Tuple, Set, Optional
1010

1111
from mypy.myunit import TestCase, SkipTestCaseException
1212

@@ -41,6 +41,7 @@ def parse_test_cases(
4141
i += 1
4242

4343
files = [] # type: List[Tuple[str, str]] # path and contents
44+
stale_modules = None # type: Optional[Set[str]] # module names
4445
while i < len(p) and p[i].id not in ['out', 'case']:
4546
if p[i].id == 'file':
4647
# Record an extra file needed for the test case.
@@ -57,6 +58,11 @@ def parse_test_cases(
5758
fnam = '__builtin__.py'
5859
files.append((os.path.join(base_path, fnam), f.read()))
5960
f.close()
61+
elif p[i].id == 'stale':
62+
if p[i].arg is None:
63+
stale_modules = set()
64+
else:
65+
stale_modules = {item.strip() for item in p[i].arg.split(',')}
6066
else:
6167
raise ValueError(
6268
'Invalid section header {} in {} at line {}'.format(
@@ -78,7 +84,8 @@ def parse_test_cases(
7884
expand_errors(input, tcout, 'main')
7985
lastline = p[i].line if i < len(p) else p[i - 1].line + 9999
8086
tc = DataDrivenTestCase(p[i0].arg, input, tcout, path,
81-
p[i0].line, lastline, perform, files)
87+
p[i0].line, lastline, perform,
88+
files, stale_modules)
8289
out.append(tc)
8390
if not ok:
8491
raise ValueError(
@@ -99,11 +106,12 @@ class DataDrivenTestCase(TestCase):
99106

100107
# (file path, file content) tuples
101108
files = None # type: List[Tuple[str, str]]
109+
expected_stale_modules = None # type: Optional[Set[str]]
102110

103111
clean_up = None # type: List[Tuple[bool, str]]
104112

105113
def __init__(self, name, input, output, file, line, lastline,
106-
perform, files):
114+
perform, files, expected_stale_modules):
107115
super().__init__(name)
108116
self.input = input
109117
self.output = output
@@ -112,6 +120,7 @@ def __init__(self, name, input, output, file, line, lastline,
112120
self.line = line
113121
self.perform = perform
114122
self.files = files
123+
self.expected_stale_modules = expected_stale_modules
115124

116125
def set_up(self) -> None:
117126
super().set_up()

mypy/test/testcheck.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import shutil
66
import sys
7+
import time
78

89
from typing import Tuple, List, Dict, Set
910

@@ -80,8 +81,10 @@ def run_test(self, testcase: DataDrivenTestCase) -> None:
8081
if incremental:
8182
# Incremental tests are run once with a cold cache, once with a warm cache.
8283
# Expect success on first run, errors from testcase.output (if any) on second run.
84+
# We briefly sleep to make sure file timestamps are distinct.
8385
self.clear_cache()
8486
self.run_test_once(testcase, 1)
87+
time.sleep(0.1)
8588
self.run_test_once(testcase, 2)
8689
elif optional:
8790
try:
@@ -145,6 +148,11 @@ def run_test_once(self, testcase: DataDrivenTestCase, incremental=0) -> None:
145148

146149
if incremental and res:
147150
self.verify_cache(module_name, program_name, a, res.manager)
151+
if testcase.expected_stale_modules is not None and incremental == 2:
152+
assert_string_arrays_equal(
153+
list(sorted(testcase.expected_stale_modules)),
154+
list(sorted(res.manager.stale_modules.difference({"__main__"}))),
155+
'Set of stale modules does not match expected set')
148156

149157
def verify_cache(self, module_name: str, program_name: str, a: List[str],
150158
manager: build.BuildManager) -> None:

test-data/unit/check-incremental.test

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
-- The first time it must pass.
44
-- Before it is run the second time, any *.py.next files are copied to *.py.
55
-- The second time it must produce the errors given in the [out] section, if any.
6+
--
7+
-- Any files that we expect to be stale and rechecked should be annotated in
8+
-- the [stale] annotation. The test suite will automatically assume that
9+
-- __main__ is stale so we can avoid constantly having to annotate it.
10+
-- The list of stale files can be in any arbitrary order, or can be left empty
11+
-- if no files should be stale.
612

713
[case testIncrementalEmpty]
814
[out]
@@ -15,6 +21,7 @@ def foo():
1521
[file m.py.next]
1622
def foo() -> None:
1723
pass
24+
[stale m]
1825
[out]
1926

2027
[case testIncrementalError]
@@ -25,7 +32,66 @@ def foo() -> None:
2532
[file m.py.next]
2633
def foo() -> None:
2734
bar()
35+
[stale m]
2836
[out]
2937
main:1: note: In module imported here:
3038
tmp/m.py: note: In function "foo":
3139
tmp/m.py:2: error: Name 'bar' is not defined
40+
41+
[case testIncrementalSimpleImportSequence]
42+
import mod1
43+
mod1.func1()
44+
45+
[file mod1.py]
46+
import mod2
47+
def func1() -> None: mod2.func2()
48+
49+
[file mod2.py]
50+
import mod3
51+
def func2() -> None: mod3.func3()
52+
53+
[file mod3.py]
54+
def func3() -> None: pass
55+
56+
[stale]
57+
[out]
58+
59+
60+
[case testIncrementalSimpleImportCascade]
61+
import mod1
62+
mod1.func1()
63+
64+
[file mod1.py]
65+
import mod2
66+
def func1() -> None: mod2.func2()
67+
68+
[file mod2.py]
69+
import mod3
70+
def func2() -> None: mod3.func3()
71+
72+
[file mod3.py]
73+
def func3() -> None: pass
74+
75+
[file mod3.py.next]
76+
def func3() -> int: return 2
77+
78+
[stale mod1, mod2, mod3]
79+
[out]
80+
81+
[case testIncrementalSimpleBranchingModules]
82+
import mod1
83+
import mod2
84+
85+
[file mod1.py]
86+
def func() -> None: pass
87+
88+
[file mod2.py]
89+
def func() -> None: pass
90+
91+
[file mod1.py.next]
92+
def func() -> int: return 1
93+
94+
[stale mod1]
95+
[out]
96+
97+

0 commit comments

Comments
 (0)