Skip to content

Commit 03f9521

Browse files
JukkaLgvanrossum
authored andcommitted
Add support for test cases with more than 2 incremental runs (#3347)
Use .2, .3 etc. as the suffixes for files in the second and later runs (instead of .next).
1 parent 05521e4 commit 03f9521

File tree

5 files changed

+301
-199
lines changed

5 files changed

+301
-199
lines changed

mypy/test/data.py

+46-30
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import shutil
99

1010
import pytest # type: ignore # no pytest in typeshed
11-
from typing import Callable, List, Tuple, Set, Optional, Iterator, Any
11+
from typing import Callable, List, Tuple, Set, Optional, Iterator, Any, Dict
1212

1313
from mypy.myunit import TestCase, SkipTestCaseException
1414

@@ -53,9 +53,9 @@ def parse_test_cases(
5353
files = [] # type: List[Tuple[str, str]] # path and contents
5454
output_files = [] # type: List[Tuple[str, str]] # path and contents for output files
5555
tcout = [] # type: List[str] # Regular output errors
56-
tcout2 = [] # type: List[str] # Output errors for incremental, second run
57-
stale_modules = None # type: Optional[Set[str]] # module names
58-
rechecked_modules = None # type: Optional[Set[str]] # module names
56+
tcout2 = {} # type: Dict[int, List[str]] # Output errors for incremental, runs 2+
57+
stale_modules = {} # type: Dict[int, Set[str]] # from run number to module names
58+
rechecked_modules = {} # type: Dict[ int, Set[str]] # from run number module names
5959
while i < len(p) and p[i].id != 'case':
6060
if p[i].id == 'file' or p[i].id == 'outfile':
6161
# Record an extra file needed for the test case.
@@ -78,43 +78,57 @@ def parse_test_cases(
7878
fnam = '__builtin__.pyi'
7979
with open(mpath) as f:
8080
files.append((join(base_path, fnam), f.read()))
81-
elif p[i].id == 'stale':
81+
elif re.match(r'stale[0-9]*$', p[i].id):
82+
if p[i].id == 'stale':
83+
passnum = 1
84+
else:
85+
passnum = int(p[i].id[len('stale'):])
86+
assert passnum > 0
8287
arg = p[i].arg
8388
if arg is None:
84-
stale_modules = set()
89+
stale_modules[passnum] = set()
90+
else:
91+
stale_modules[passnum] = {item.strip() for item in arg.split(',')}
92+
elif re.match(r'rechecked[0-9]*$', p[i].id):
93+
if p[i].id == 'rechecked':
94+
passnum = 1
8595
else:
86-
stale_modules = {item.strip() for item in arg.split(',')}
87-
elif p[i].id == 'rechecked':
96+
passnum = int(p[i].id[len('rechecked'):])
8897
arg = p[i].arg
8998
if arg is None:
90-
rechecked_modules = set()
99+
rechecked_modules[passnum] = set()
91100
else:
92-
rechecked_modules = {item.strip() for item in arg.split(',')}
101+
rechecked_modules[passnum] = {item.strip() for item in arg.split(',')}
93102
elif p[i].id == 'out' or p[i].id == 'out1':
94103
tcout = p[i].data
95104
if native_sep and os.path.sep == '\\':
96105
tcout = [fix_win_path(line) for line in tcout]
97106
ok = True
98-
elif p[i].id == 'out2':
99-
tcout2 = p[i].data
107+
elif re.match(r'out[0-9]*$', p[i].id):
108+
passnum = int(p[i].id[3:])
109+
assert passnum > 1
110+
output = p[i].data
100111
if native_sep and os.path.sep == '\\':
101-
tcout2 = [fix_win_path(line) for line in tcout2]
112+
output = [fix_win_path(line) for line in output]
113+
tcout2[passnum] = output
102114
ok = True
103115
else:
104116
raise ValueError(
105117
'Invalid section header {} in {} at line {}'.format(
106118
p[i].id, path, p[i].line))
107119
i += 1
108120

109-
if rechecked_modules is None:
110-
# If the set of rechecked modules isn't specified, make it the same as the set of
111-
# modules with a stale public interface.
112-
rechecked_modules = stale_modules
113-
if (stale_modules is not None
114-
and rechecked_modules is not None
115-
and not stale_modules.issubset(rechecked_modules)):
116-
raise ValueError(
117-
'Stale modules must be a subset of rechecked modules ({})'.format(path))
121+
for passnum in stale_modules.keys():
122+
if passnum not in rechecked_modules:
123+
# If the set of rechecked modules isn't specified, make it the same as the set
124+
# of modules with a stale public interface.
125+
rechecked_modules[passnum] = stale_modules[passnum]
126+
if (passnum in stale_modules
127+
and passnum in rechecked_modules
128+
and not stale_modules[passnum].issubset(rechecked_modules[passnum])):
129+
raise ValueError(
130+
('Stale modules after pass {} must be a subset of rechecked '
131+
'modules ({}:{})').format(passnum, path, p[i0].line))
118132

119133
if optional_out:
120134
ok = True
@@ -140,30 +154,32 @@ def parse_test_cases(
140154

141155
class DataDrivenTestCase(TestCase):
142156
input = None # type: List[str]
143-
output = None # type: List[str]
157+
output = None # type: List[str] # Output for the first pass
158+
output2 = None # type: Dict[int, List[str]] # Output for runs 2+, indexed by run number
144159

145160
file = ''
146161
line = 0
147162

148163
# (file path, file content) tuples
149164
files = None # type: List[Tuple[str, str]]
150-
expected_stale_modules = None # type: Optional[Set[str]]
165+
expected_stale_modules = None # type: Dict[int, Set[str]]
166+
expected_rechecked_modules = None # type: Dict[int, Set[str]]
151167

152168
clean_up = None # type: List[Tuple[bool, str]]
153169

154170
def __init__(self,
155171
name: str,
156172
input: List[str],
157173
output: List[str],
158-
output2: List[str],
174+
output2: Dict[int, List[str]],
159175
file: str,
160176
line: int,
161177
lastline: int,
162178
perform: Callable[['DataDrivenTestCase'], None],
163179
files: List[Tuple[str, str]],
164180
output_files: List[Tuple[str, str]],
165-
expected_stale_modules: Optional[Set[str]],
166-
expected_rechecked_modules: Optional[Set[str]],
181+
expected_stale_modules: Dict[int, Set[str]],
182+
expected_rechecked_modules: Dict[int, Set[str]],
167183
native_sep: bool = False,
168184
) -> None:
169185
super().__init__(name)
@@ -192,9 +208,9 @@ def set_up(self) -> None:
192208
f.write(content)
193209
self.clean_up.append((False, path))
194210
encountered_files.add(path)
195-
if path.endswith(".next"):
196-
# Make sure new files introduced in the second run are accounted for
197-
renamed_path = path[:-5]
211+
if re.search(r'\.[2-9]$', path):
212+
# Make sure new files introduced in the second and later runs are accounted for
213+
renamed_path = path[:-2]
198214
if renamed_path not in encountered_files:
199215
encountered_files.add(renamed_path)
200216
self.clean_up.append((False, renamed_path))

mypy/test/testcheck.py

+64-39
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,17 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
102102
# Expect success on first run, errors from testcase.output (if any) on second run.
103103
# We briefly sleep to make sure file timestamps are distinct.
104104
self.clear_cache()
105-
self.run_case_once(testcase, 1)
106-
self.run_case_once(testcase, 2)
105+
num_steps = max([2] + list(testcase.output2.keys()))
106+
# Check that there are no file changes beyond the last run (they would be ignored).
107+
for dn, dirs, files in os.walk(os.curdir):
108+
for file in files:
109+
m = re.search(r'\.([2-9])$', file)
110+
if m and int(m.group(1)) > num_steps:
111+
raise ValueError(
112+
'Output file {} exists though test case only has {} runs'.format(
113+
file, num_steps))
114+
for step in range(1, num_steps + 1):
115+
self.run_case_once(testcase, step)
107116
elif optional:
108117
experiments.STRICT_OPTIONAL = True
109118
self.run_case_once(testcase)
@@ -118,26 +127,26 @@ def clear_cache(self) -> None:
118127
if os.path.exists(dn):
119128
shutil.rmtree(dn)
120129

121-
def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> None:
130+
def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int = 0) -> None:
122131
find_module_clear_caches()
123132
original_program_text = '\n'.join(testcase.input)
124-
module_data = self.parse_module(original_program_text, incremental)
133+
module_data = self.parse_module(original_program_text, incremental_step)
125134

126-
if incremental:
127-
if incremental == 1:
135+
if incremental_step:
136+
if incremental_step == 1:
128137
# In run 1, copy program text to program file.
129138
for module_name, program_path, program_text in module_data:
130139
if module_name == '__main__':
131140
with open(program_path, 'w') as f:
132141
f.write(program_text)
133142
break
134-
elif incremental == 2:
135-
# In run 2, copy *.next files to * files.
143+
elif incremental_step > 1:
144+
# In runs 2+, copy *.[num] files to * files.
136145
for dn, dirs, files in os.walk(os.curdir):
137146
for file in files:
138-
if file.endswith('.next'):
147+
if file.endswith('.' + str(incremental_step)):
139148
full = os.path.join(dn, file)
140-
target = full[:-5]
149+
target = full[:-2]
141150
shutil.copy(full, target)
142151

143152
# In some systems, mtime has a resolution of 1 second which can cause
@@ -147,12 +156,12 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> N
147156
os.utime(target, times=(new_time, new_time))
148157

149158
# Parse options after moving files (in case mypy.ini is being moved).
150-
options = self.parse_options(original_program_text, testcase, incremental)
159+
options = self.parse_options(original_program_text, testcase, incremental_step)
151160
options.use_builtins_fixtures = True
152161
options.show_traceback = True
153162
if 'optional' in testcase.file:
154163
options.strict_optional = True
155-
if incremental:
164+
if incremental_step:
156165
options.incremental = True
157166
else:
158167
options.cache_dir = os.devnull # Dont waste time writing cache
@@ -161,7 +170,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> N
161170
for module_name, program_path, program_text in module_data:
162171
# Always set to none so we're forced to reread the module in incremental mode
163172
sources.append(BuildSource(program_path, module_name,
164-
None if incremental else program_text))
173+
None if incremental_step else program_text))
165174
res = None
166175
try:
167176
res = build.build(sources=sources,
@@ -173,42 +182,51 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> N
173182
a = normalize_error_messages(a)
174183

175184
# Make sure error messages match
176-
if incremental == 0:
177-
msg = 'Invalid type checker output ({}, line {})'
185+
if incremental_step == 0:
186+
# Not incremental
187+
msg = 'Unexpected type checker output ({}, line {})'
178188
output = testcase.output
179-
elif incremental == 1:
180-
msg = 'Invalid type checker output in incremental, run 1 ({}, line {})'
189+
elif incremental_step == 1:
190+
msg = 'Unexpected type checker output in incremental, run 1 ({}, line {})'
181191
output = testcase.output
182-
elif incremental == 2:
183-
msg = 'Invalid type checker output in incremental, run 2 ({}, line {})'
184-
output = testcase.output2
192+
elif incremental_step > 1:
193+
msg = ('Unexpected type checker output in incremental, run {}'.format(
194+
incremental_step) + ' ({}, line {})')
195+
output = testcase.output2.get(incremental_step, [])
185196
else:
186197
raise AssertionError()
187198

188199
if output != a and self.update_data:
189200
update_testcase_output(testcase, a)
190201
assert_string_arrays_equal(output, a, msg.format(testcase.file, testcase.line))
191202

192-
if incremental and res:
203+
if incremental_step and res:
193204
if options.follow_imports == 'normal' and testcase.output is None:
194205
self.verify_cache(module_data, a, res.manager)
195-
if incremental == 2:
206+
if incremental_step > 1:
207+
suffix = '' if incremental_step == 2 else str(incremental_step - 1)
196208
self.check_module_equivalence(
197-
'rechecked',
198-
testcase.expected_rechecked_modules,
209+
'rechecked' + suffix,
210+
testcase.expected_rechecked_modules.get(incremental_step - 1),
199211
res.manager.rechecked_modules)
200212
self.check_module_equivalence(
201-
'stale',
202-
testcase.expected_stale_modules,
213+
'stale' + suffix,
214+
testcase.expected_stale_modules.get(incremental_step - 1),
203215
res.manager.stale_modules)
204216

205217
def check_module_equivalence(self, name: str,
206218
expected: Optional[Set[str]], actual: Set[str]) -> None:
207219
if expected is not None:
220+
expected_normalized = sorted(expected)
221+
actual_normalized = sorted(actual.difference({"__main__"}))
208222
assert_string_arrays_equal(
209-
list(sorted(expected)),
210-
list(sorted(actual.difference({"__main__"}))),
211-
'Set of {} modules does not match expected set'.format(name))
223+
expected_normalized,
224+
actual_normalized,
225+
('Actual modules ({}) do not match expected modules ({}) '
226+
'for "[{} ...]"').format(
227+
', '.join(actual_normalized),
228+
', '.join(expected_normalized),
229+
name))
212230

213231
def verify_cache(self, module_data: List[Tuple[str, str, str]], a: List[str],
214232
manager: build.BuildManager) -> None:
@@ -268,7 +286,9 @@ def find_missing_cache_files(self, modules: Dict[str, str],
268286
missing[id] = path
269287
return set(missing.values())
270288

271-
def parse_module(self, program_text: str, incremental: int = 0) -> List[Tuple[str, str, str]]:
289+
def parse_module(self,
290+
program_text: str,
291+
incremental_step: int = 0) -> List[Tuple[str, str, str]]:
272292
"""Return the module and program names for a test case.
273293
274294
Normally, the unit tests will parse the default ('__main__')
@@ -278,15 +298,19 @@ def parse_module(self, program_text: str, incremental: int = 0) -> List[Tuple[st
278298
279299
# cmd: mypy -m foo.bar foo.baz
280300
301+
You can also use `# cmdN:` to have a different cmd for incremental
302+
step N (2, 3, ...).
303+
281304
Return a list of tuples (module name, file name, program text).
282305
"""
283306
m = re.search('# cmd: mypy -m ([a-zA-Z0-9_. ]+)$', program_text, flags=re.MULTILINE)
284-
m2 = re.search('# cmd2: mypy -m ([a-zA-Z0-9_. ]+)$', program_text, flags=re.MULTILINE)
285-
if m2 is not None and incremental == 2:
286-
# Optionally return a different command if in the second
287-
# stage of incremental mode, otherwise default to reusing
288-
# the original cmd.
289-
m = m2
307+
regex = '# cmd{}: mypy -m ([a-zA-Z0-9_. ]+)$'.format(incremental_step)
308+
alt_m = re.search(regex, program_text, flags=re.MULTILINE)
309+
if alt_m is not None and incremental_step > 1:
310+
# Optionally return a different command if in a later step
311+
# of incremental mode, otherwise default to reusing the
312+
# original cmd.
313+
m = alt_m
290314

291315
if m:
292316
# The test case wants to use a non-default main
@@ -304,11 +328,12 @@ def parse_module(self, program_text: str, incremental: int = 0) -> List[Tuple[st
304328
return [('__main__', 'main', program_text)]
305329

306330
def parse_options(self, program_text: str, testcase: DataDrivenTestCase,
307-
incremental: int) -> Options:
331+
incremental_step: int) -> Options:
308332
options = Options()
309333
flags = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE)
310-
if incremental == 2:
311-
flags2 = re.search('# flags2: (.*)$', program_text, flags=re.MULTILINE)
334+
if incremental_step > 1:
335+
flags2 = re.search('# flags{}: (.*)$'.format(incremental_step), program_text,
336+
flags=re.MULTILINE)
312337
if flags2:
313338
flags = flags2
314339

0 commit comments

Comments
 (0)