Skip to content

Commit 977eeec

Browse files
authored
Add test cases that delete a file during incremental checking (#3461)
* Add test cases that delete a file during incremental checking * Add retries on Windows to avoid flakiness on AppVeyor * Fix test case failure on AppVeyor
1 parent d4864a9 commit 977eeec

File tree

4 files changed

+86
-8
lines changed

4 files changed

+86
-8
lines changed

mypy/test/data.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def parse_test_cases(
5454
output_files = [] # type: List[Tuple[str, str]] # path and contents for output files
5555
tcout = [] # type: List[str] # Regular output errors
5656
tcout2 = {} # type: Dict[int, List[str]] # Output errors for incremental, runs 2+
57+
deleted_paths = {} # type: Dict[int, Set[str]] # from run number of paths
5758
stale_modules = {} # type: Dict[int, Set[str]] # from run number to module names
5859
rechecked_modules = {} # type: Dict[ int, Set[str]] # from run number module names
5960
while i < len(p) and p[i].id != 'case':
@@ -99,6 +100,16 @@ def parse_test_cases(
99100
rechecked_modules[passnum] = set()
100101
else:
101102
rechecked_modules[passnum] = {item.strip() for item in arg.split(',')}
103+
elif p[i].id == 'delete':
104+
# File to delete during a multi-step test case
105+
arg = p[i].arg
106+
assert arg is not None
107+
m = re.match(r'(.*)\.([0-9]+)$', arg)
108+
assert m, 'Invalid delete section: {}'.format(arg)
109+
num = int(m.group(2))
110+
assert num >= 2, "Can't delete during step {}".format(num)
111+
full = join(base_path, m.group(1))
112+
deleted_paths.setdefault(num, set()).add(full)
102113
elif p[i].id == 'out' or p[i].id == 'out1':
103114
tcout = p[i].data
104115
if native_sep and os.path.sep == '\\':
@@ -142,7 +153,7 @@ def parse_test_cases(
142153
tc = DataDrivenTestCase(p[i0].arg, input, tcout, tcout2, path,
143154
p[i0].line, lastline, perform,
144155
files, output_files, stale_modules,
145-
rechecked_modules, native_sep)
156+
rechecked_modules, deleted_paths, native_sep)
146157
out.append(tc)
147158
if not ok:
148159
raise ValueError(
@@ -180,6 +191,7 @@ def __init__(self,
180191
output_files: List[Tuple[str, str]],
181192
expected_stale_modules: Dict[int, Set[str]],
182193
expected_rechecked_modules: Dict[int, Set[str]],
194+
deleted_paths: Dict[int, Set[str]],
183195
native_sep: bool = False,
184196
) -> None:
185197
super().__init__(name)
@@ -194,24 +206,30 @@ def __init__(self,
194206
self.output_files = output_files
195207
self.expected_stale_modules = expected_stale_modules
196208
self.expected_rechecked_modules = expected_rechecked_modules
209+
self.deleted_paths = deleted_paths
197210
self.native_sep = native_sep
198211

199212
def set_up(self) -> None:
200213
super().set_up()
201214
encountered_files = set()
202215
self.clean_up = []
216+
all_deleted = [] # type: List[str]
217+
for paths in self.deleted_paths.values():
218+
all_deleted += paths
203219
for path, content in self.files:
204220
dir = os.path.dirname(path)
205221
for d in self.add_dirs(dir):
206222
self.clean_up.append((True, d))
207223
with open(path, 'w') as f:
208224
f.write(content)
209-
self.clean_up.append((False, path))
225+
if path not in all_deleted:
226+
# TODO: Don't assume that deleted files don't get reintroduced.
227+
self.clean_up.append((False, path))
210228
encountered_files.add(path)
211229
if re.search(r'\.[2-9]$', path):
212230
# Make sure new files introduced in the second and later runs are accounted for
213231
renamed_path = path[:-2]
214-
if renamed_path not in encountered_files:
232+
if renamed_path not in encountered_files and renamed_path not in all_deleted:
215233
encountered_files.add(renamed_path)
216234
self.clean_up.append((False, renamed_path))
217235
for path, _ in self.output_files:

mypy/test/helpers.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import sys
2-
import re
31
import os
2+
import re
3+
import sys
4+
import time
45

5-
from typing import List, Dict, Tuple
6+
from typing import List, Dict, Tuple, Callable, Any
67

78
from mypy import defaults
89
from mypy.myunit import AssertionFailure
@@ -283,3 +284,26 @@ def normalize_error_messages(messages: List[str]) -> List[str]:
283284
for m in messages:
284285
a.append(m.replace(os.sep, '/'))
285286
return a
287+
288+
289+
def retry_on_error(func: Callable[[], Any], max_wait: float = 1.0) -> None:
290+
"""Retry callback with exponential backoff when it raises OSError.
291+
292+
If the function still generates an error after max_wait seconds, propagate
293+
the exception.
294+
295+
This can be effective against random file system operation failures on
296+
Windows.
297+
"""
298+
t0 = time.time()
299+
wait_time = 0.01
300+
while True:
301+
try:
302+
func()
303+
return
304+
except OSError:
305+
wait_time = min(wait_time * 2, t0 + max_wait - time.time())
306+
if wait_time <= 0.01:
307+
# Done enough waiting, the error seems persistent.
308+
raise
309+
time.sleep(wait_time)

mypy/test/testcheck.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from mypy.test.data import parse_test_cases, DataDrivenTestCase, DataSuite
1818
from mypy.test.helpers import (
1919
assert_string_arrays_equal, normalize_error_messages,
20-
testcase_pyversion, update_testcase_output,
20+
retry_on_error, testcase_pyversion, update_testcase_output,
2121
)
2222
from mypy.errors import CompileError
2323
from mypy.options import Options
@@ -147,13 +147,18 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int = 0)
147147
if file.endswith('.' + str(incremental_step)):
148148
full = os.path.join(dn, file)
149149
target = full[:-2]
150-
shutil.copy(full, target)
150+
# Use retries to work around potential flakiness on Windows (AppVeyor).
151+
retry_on_error(lambda: shutil.copy(full, target))
151152

152153
# In some systems, mtime has a resolution of 1 second which can cause
153154
# annoying-to-debug issues when a file has the same size after a
154155
# change. We manually set the mtime to circumvent this.
155156
new_time = os.stat(target).st_mtime + 1
156157
os.utime(target, times=(new_time, new_time))
158+
# Delete files scheduled to be deleted in [delete <path>.num] sections.
159+
for path in testcase.deleted_paths.get(incremental_step, set()):
160+
# Use retries to work around potential flakiness on Windows (AppVeyor).
161+
retry_on_error(lambda: os.remove(path))
157162

158163
# Parse options after moving files (in case mypy.ini is being moved).
159164
options = self.parse_options(original_program_text, testcase, incremental_step)

test-data/unit/check-incremental.test

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
-- Before the tests are run again, in step N any *.py.N files are copied to
44
-- *.py.
55
--
6+
-- You can add an empty section like `[delete mod.py.2]` to delete `mod.py`
7+
-- before the second run.
8+
--
69
-- Errors expected in the first run should be in the `[out1]` section, and
710
-- errors expected in the second run should be in the `[out2]` section, and so on.
811
-- If a section is omitted, it is expected there are no errors on that run.
@@ -1947,6 +1950,34 @@ main:5: error: Revealed type is 'builtins.int'
19471950
-- TODO: Add another test for metaclass in import cycle (reversed from the above test).
19481951
-- This currently doesn't work.
19491952

1953+
[case testDeleteFile]
1954+
import n
1955+
[file n.py]
1956+
import m
1957+
[file m.py]
1958+
x = 1
1959+
[delete m.py.2]
1960+
[rechecked n]
1961+
[stale]
1962+
[out2]
1963+
tmp/n.py:1: error: Cannot find module named 'm'
1964+
tmp/n.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
1965+
1966+
[case testDeleteFileWithinCycle]
1967+
import a
1968+
[file a.py]
1969+
import b
1970+
[file b.py]
1971+
import c
1972+
[file c.py]
1973+
import a
1974+
[file a.py.2]
1975+
import c
1976+
[delete b.py.2]
1977+
[rechecked a, c]
1978+
[stale a]
1979+
[out2]
1980+
19501981
[case testThreePassesBasic]
19511982
import m
19521983
[file m.py]

0 commit comments

Comments
 (0)