@@ -102,8 +102,17 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
102
102
# Expect success on first run, errors from testcase.output (if any) on second run.
103
103
# We briefly sleep to make sure file timestamps are distinct.
104
104
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 )
107
116
elif optional :
108
117
experiments .STRICT_OPTIONAL = True
109
118
self .run_case_once (testcase )
@@ -118,26 +127,26 @@ def clear_cache(self) -> None:
118
127
if os .path .exists (dn ):
119
128
shutil .rmtree (dn )
120
129
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 :
122
131
find_module_clear_caches ()
123
132
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 )
125
134
126
- if incremental :
127
- if incremental == 1 :
135
+ if incremental_step :
136
+ if incremental_step == 1 :
128
137
# In run 1, copy program text to program file.
129
138
for module_name , program_path , program_text in module_data :
130
139
if module_name == '__main__' :
131
140
with open (program_path , 'w' ) as f :
132
141
f .write (program_text )
133
142
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.
136
145
for dn , dirs , files in os .walk (os .curdir ):
137
146
for file in files :
138
- if file .endswith ('.next' ):
147
+ if file .endswith ('.' + str ( incremental_step ) ):
139
148
full = os .path .join (dn , file )
140
- target = full [:- 5 ]
149
+ target = full [:- 2 ]
141
150
shutil .copy (full , target )
142
151
143
152
# 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
147
156
os .utime (target , times = (new_time , new_time ))
148
157
149
158
# 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 )
151
160
options .use_builtins_fixtures = True
152
161
options .show_traceback = True
153
162
if 'optional' in testcase .file :
154
163
options .strict_optional = True
155
- if incremental :
164
+ if incremental_step :
156
165
options .incremental = True
157
166
else :
158
167
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
161
170
for module_name , program_path , program_text in module_data :
162
171
# Always set to none so we're forced to reread the module in incremental mode
163
172
sources .append (BuildSource (program_path , module_name ,
164
- None if incremental else program_text ))
173
+ None if incremental_step else program_text ))
165
174
res = None
166
175
try :
167
176
res = build .build (sources = sources ,
@@ -173,42 +182,51 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> N
173
182
a = normalize_error_messages (a )
174
183
175
184
# 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 {})'
178
188
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 {})'
181
191
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 , [])
185
196
else :
186
197
raise AssertionError ()
187
198
188
199
if output != a and self .update_data :
189
200
update_testcase_output (testcase , a )
190
201
assert_string_arrays_equal (output , a , msg .format (testcase .file , testcase .line ))
191
202
192
- if incremental and res :
203
+ if incremental_step and res :
193
204
if options .follow_imports == 'normal' and testcase .output is None :
194
205
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 )
196
208
self .check_module_equivalence (
197
- 'rechecked' ,
198
- testcase .expected_rechecked_modules ,
209
+ 'rechecked' + suffix ,
210
+ testcase .expected_rechecked_modules . get ( incremental_step - 1 ) ,
199
211
res .manager .rechecked_modules )
200
212
self .check_module_equivalence (
201
- 'stale' ,
202
- testcase .expected_stale_modules ,
213
+ 'stale' + suffix ,
214
+ testcase .expected_stale_modules . get ( incremental_step - 1 ) ,
203
215
res .manager .stale_modules )
204
216
205
217
def check_module_equivalence (self , name : str ,
206
218
expected : Optional [Set [str ]], actual : Set [str ]) -> None :
207
219
if expected is not None :
220
+ expected_normalized = sorted (expected )
221
+ actual_normalized = sorted (actual .difference ({"__main__" }))
208
222
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 ))
212
230
213
231
def verify_cache (self , module_data : List [Tuple [str , str , str ]], a : List [str ],
214
232
manager : build .BuildManager ) -> None :
@@ -268,7 +286,9 @@ def find_missing_cache_files(self, modules: Dict[str, str],
268
286
missing [id ] = path
269
287
return set (missing .values ())
270
288
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 ]]:
272
292
"""Return the module and program names for a test case.
273
293
274
294
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
278
298
279
299
# cmd: mypy -m foo.bar foo.baz
280
300
301
+ You can also use `# cmdN:` to have a different cmd for incremental
302
+ step N (2, 3, ...).
303
+
281
304
Return a list of tuples (module name, file name, program text).
282
305
"""
283
306
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
290
314
291
315
if m :
292
316
# 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
304
328
return [('__main__' , 'main' , program_text )]
305
329
306
330
def parse_options (self , program_text : str , testcase : DataDrivenTestCase ,
307
- incremental : int ) -> Options :
331
+ incremental_step : int ) -> Options :
308
332
options = Options ()
309
333
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 )
312
337
if flags2 :
313
338
flags = flags2
314
339
0 commit comments