Skip to content

Commit 3fc3823

Browse files
authored
[mypyc] Support multi-step testing for test_run (#7877)
This isn't actually useful right now but is needed to test incremental compilation.
1 parent 0c8566f commit 3fc3823

File tree

3 files changed

+110
-22
lines changed

3 files changed

+110
-22
lines changed

mypyc/build.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,13 @@ def write_file(path: str, contents: str) -> None:
292292
with open(path, 'w', encoding='utf-8') as f:
293293
f.write(contents)
294294

295+
# Fudge the mtime forward because otherwise when two builds happen close
296+
# together (like in a test) setuptools might not realize the source is newer
297+
# than the new artifact.
298+
# XXX: This is bad though.
299+
new_mtime = os.stat(path).st_mtime + 1
300+
os.utime(path, times=(new_mtime, new_mtime))
301+
295302

296303
def construct_groups(
297304
sources: List[BuildSource],

mypyc/test-data/run-multimodule.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,15 @@ Traceback (most recent call last):
233233
File "driver.py", line 6, in <module>
234234
other.fail2()
235235
File "other.py", line 3, in fail2
236+
x[2] = 2
236237
IndexError: list assignment index out of range
237238
Traceback (most recent call last):
238239
File "driver.py", line 12, in <module>
239240
native.fail()
240241
File "native.py", line 4, in fail
241242
fail2()
242243
File "other.py", line 3, in fail2
244+
x[2] = 2
243245
IndexError: list assignment index out of range
244246

245247
[case testMultiModuleCycle]
@@ -532,3 +534,24 @@ x = 10
532534
[file driver.py]
533535
from native import foo
534536
foo()
537+
538+
[case testTrivialIncremental]
539+
# separate: [(["other.py", "other_b.py"], "stuff")]
540+
from other import x
541+
from other_b import z
542+
y = x + z
543+
[file other.py]
544+
x = 1
545+
[file other.py.2]
546+
x = 2
547+
[file other_b.py]
548+
z = 1
549+
550+
[file driver.py]
551+
from native import y
552+
print(y)
553+
[out]
554+
2
555+
[out2]
556+
3
557+
[rechecked other]

mypyc/test/test_run.py

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
"""Test cases for building an C extension and running it."""
22

3+
import ast
34
import glob
45
import os.path
56
import platform
7+
import re
68
import subprocess
79
import contextlib
810
import shutil
911
import sys
1012
from typing import Any, Iterator, List, cast
1113

1214
from mypy import build
13-
from mypy.test.data import DataDrivenTestCase
15+
from mypy.test.data import DataDrivenTestCase, UpdateFile
1416
from mypy.test.config import test_temp_dir
1517
from mypy.errors import CompileError
1618
from mypy.options import Options
19+
from mypy.test.helpers import copy_and_fudge_mtime, assert_module_equivalence
1720

1821
from mypyc import emitmodule
1922
from mypyc.options import CompilerOptions
@@ -109,14 +112,45 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
109112
# by chdiring into tmp/
110113
with use_custom_builtins(os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase), (
111114
chdir_manager('tmp')):
112-
os.mkdir(WORKDIR)
113115
self.run_case_inner(testcase)
114116

115117
def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
116-
bench = testcase.config.getoption('--bench', False) and 'Benchmark' in testcase.name
118+
os.mkdir(WORKDIR)
117119

118120
text = '\n'.join(testcase.input)
119121

122+
with open('native.py', 'w', encoding='utf-8') as f:
123+
f.write(text)
124+
with open('interpreted.py', 'w', encoding='utf-8') as f:
125+
f.write(text)
126+
127+
shutil.copyfile(TESTUTIL_PATH, 'testutil.py')
128+
129+
step = 1
130+
self.run_case_step(testcase, step)
131+
132+
steps = testcase.find_steps()
133+
if steps == [[]]:
134+
steps = []
135+
136+
for operations in steps:
137+
step += 1
138+
with chdir_manager('..'):
139+
for op in operations:
140+
if isinstance(op, UpdateFile):
141+
# Modify/create file
142+
copy_and_fudge_mtime(op.source_path, op.target_path)
143+
else:
144+
# Delete file
145+
try:
146+
os.remove(op.path)
147+
except FileNotFoundError:
148+
pass
149+
self.run_case_step(testcase, step)
150+
151+
def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) -> None:
152+
bench = testcase.config.getoption('--bench', False) and 'Benchmark' in testcase.name
153+
120154
options = Options()
121155
options.use_builtins_fixtures = True
122156
options.show_traceback = True
@@ -128,43 +162,39 @@ def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
128162
options.python_version = max(sys.version_info[:2], (3, 6))
129163
options.export_types = True
130164
options.preserve_asts = True
165+
options.incremental = False
131166

132167
# Avoid checking modules/packages named 'unchecked', to provide a way
133168
# to test interacting with code we don't have types for.
134169
options.per_module_options['unchecked.*'] = {'follow_imports': 'error'}
135170

136-
source_path = 'native.py'
137-
with open(source_path, 'w', encoding='utf-8') as f:
138-
f.write(text)
139-
with open('interpreted.py', 'w', encoding='utf-8') as f:
140-
f.write(text)
141-
142-
shutil.copyfile(TESTUTIL_PATH, 'testutil.py')
143-
144-
source = build.BuildSource(source_path, 'native', text)
171+
source = build.BuildSource('native.py', 'native', None)
145172
sources = [source]
146173
module_names = ['native']
147-
module_paths = [os.path.abspath('native.py')]
174+
module_paths = ['native.py']
148175

149176
# Hard code another module name to compile in the same compilation unit.
150177
to_delete = []
151178
for fn, text in testcase.files:
152179
fn = os.path.relpath(fn, test_temp_dir)
153180

154-
if os.path.basename(fn).startswith('other'):
181+
if os.path.basename(fn).startswith('other') and fn.endswith('.py'):
155182
name = os.path.basename(fn).split('.')[0]
156183
module_names.append(name)
157-
sources.append(build.BuildSource(fn, name, text))
184+
sources.append(build.BuildSource(fn, name, None))
158185
to_delete.append(fn)
159-
module_paths.append(os.path.abspath(fn))
186+
module_paths.append(fn)
160187

161188
shutil.copyfile(fn,
162189
os.path.join(os.path.dirname(fn), name + '_interpreted.py'))
163190

164191
for source in sources:
165192
options.per_module_options.setdefault(source.module, {})['mypyc'] = True
166193

167-
groups = construct_groups(sources, self.separate, len(module_names) > 1)
194+
separate = (self.get_separate('\n'.join(testcase.input), incremental_step) if self.separate
195+
else False)
196+
197+
groups = construct_groups(sources, separate, len(module_names) > 1)
168198

169199
try:
170200
result = emitmodule.parse_and_typecheck(
@@ -193,7 +223,7 @@ def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
193223
setup_file = os.path.abspath(os.path.join(WORKDIR, 'setup.py'))
194224
# We pass the C file information to the build script via setup.py unfortunately
195225
with open(setup_file, 'w', encoding='utf-8') as f:
196-
f.write(setup_format.format(module_paths, self.separate, cfiles, self.multi_file))
226+
f.write(setup_format.format(module_paths, separate, cfiles, self.multi_file))
197227

198228
if not run_setup(setup_file, ['build_ext', '--inplace']):
199229
if testcase.config.getoption('--mypyc-showc'):
@@ -204,9 +234,6 @@ def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
204234
suffix = 'pyd' if sys.platform == 'win32' else 'so'
205235
assert glob.glob('native.*.{}'.format(suffix))
206236

207-
for p in to_delete:
208-
os.remove(p)
209-
210237
driver_path = 'driver.py'
211238
env = os.environ.copy()
212239
env['MYPYC_RUN_BENCH'] = '1' if bench else '0'
@@ -240,10 +267,41 @@ def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
240267
print('Test output:')
241268
print(output)
242269
else:
243-
assert_test_output(testcase, outlines, 'Invalid output')
270+
if incremental_step == 1:
271+
msg = 'Invalid output'
272+
expected = testcase.output
273+
else:
274+
msg = 'Invalid output (step {})'.format(incremental_step)
275+
expected = testcase.output2.get(incremental_step, [])
276+
277+
assert_test_output(testcase, outlines, msg, expected)
278+
279+
if incremental_step > 1 and options.incremental:
280+
suffix = '' if incremental_step == 2 else str(incremental_step - 1)
281+
expected_rechecked = testcase.expected_rechecked_modules.get(incremental_step - 1)
282+
if expected_rechecked is not None:
283+
assert_module_equivalence(
284+
'rechecked' + suffix,
285+
expected_rechecked, result.manager.rechecked_modules)
286+
expected_stale = testcase.expected_stale_modules.get(incremental_step - 1)
287+
if expected_stale is not None:
288+
assert_module_equivalence(
289+
'stale' + suffix,
290+
expected_stale, result.manager.stale_modules)
244291

245292
assert proc.returncode == 0
246293

294+
def get_separate(self, program_text: str,
295+
incremental_step: int) -> Any:
296+
template = r'# separate{}: (\[.*\])$'
297+
m = re.search(template.format(incremental_step), program_text, flags=re.MULTILINE)
298+
if not m:
299+
m = re.search(template.format(''), program_text, flags=re.MULTILINE)
300+
if m:
301+
return ast.literal_eval(m.group(1))
302+
else:
303+
return True
304+
247305

248306
# Run the main multi-module tests in multi-file compliation mode
249307
class TestRunMultiFile(TestRun):

0 commit comments

Comments
 (0)