Skip to content

Commit 0b2c2f0

Browse files
committed
Rework testfinegrained to use dmypy_server
1 parent d3e7a10 commit 0b2c2f0

File tree

5 files changed

+103
-83
lines changed

5 files changed

+103
-83
lines changed

mypy/dmypy_server.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import mypy.build
2121
import mypy.errors
2222
import mypy.main
23-
import mypy.server.update
23+
from mypy.server.update import FineGrainedBuildManager
2424
from mypy.dmypy_util import STATUS_FILE, receive
2525
from mypy.gclogger import GcLogger
2626
from mypy.fscache import FileSystemCache
@@ -109,12 +109,14 @@ class Server:
109109
# NOTE: the instance is constructed in the parent process but
110110
# serve() is called in the grandchild (by daemonize()).
111111

112-
def __init__(self, options: Options) -> None:
112+
def __init__(self, options: Options, alt_lib_path: Optional[str] = None) -> None:
113113
"""Initialize the server with the desired mypy flags."""
114114
self.saved_cache = {} # type: mypy.build.SavedCache
115-
self.fine_grained_initialized = False
116115
self.fine_grained = options.fine_grained_incremental
117116
self.options = options
117+
self.alt_lib_path = alt_lib_path
118+
self.fine_grained_manager = None # type: Optional[FineGrainedBuildManager]
119+
118120
if os.path.isfile(STATUS_FILE):
119121
os.unlink(STATUS_FILE)
120122
if self.fine_grained:
@@ -224,15 +226,13 @@ def cmd_recheck(self) -> Dict[str, object]:
224226
# Needed by tests.
225227
last_manager = None # type: Optional[mypy.build.BuildManager]
226228

227-
def check(self, sources: List[mypy.build.BuildSource],
228-
alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
229+
def check(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
229230
if self.fine_grained:
230231
return self.check_fine_grained(sources)
231232
else:
232-
return self.check_default(sources, alt_lib_path)
233+
return self.check_default(sources)
233234

234-
def check_default(self, sources: List[mypy.build.BuildSource],
235-
alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
235+
def check_default(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
236236
"""Check using the default (per-file) incremental mode."""
237237
self.last_manager = None
238238
blockers = False
@@ -241,7 +241,7 @@ def check_default(self, sources: List[mypy.build.BuildSource],
241241
# saved_cache is mutated in place.
242242
res = mypy.build.build(sources, self.options,
243243
saved_cache=self.saved_cache,
244-
alt_lib_path=alt_lib_path)
244+
alt_lib_path=self.alt_lib_path)
245245
msgs = res.errors
246246
self.last_manager = res.manager # type: Optional[mypy.build.BuildManager]
247247
except mypy.errors.CompileError as err:
@@ -264,7 +264,7 @@ def check_default(self, sources: List[mypy.build.BuildSource],
264264

265265
def check_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
266266
"""Check using fine-grained incremental mode."""
267-
if not self.fine_grained_initialized:
267+
if not self.fine_grained_manager:
268268
return self.initialize_fine_grained(sources)
269269
else:
270270
return self.fine_grained_increment(sources)
@@ -277,9 +277,9 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
277277
# Stores the initial state of sources as a side effect.
278278
self.fswatcher.find_changed()
279279
try:
280-
# TODO: alt_lib_path
281280
result = mypy.build.build(sources=sources,
282-
options=self.options)
281+
options=self.options,
282+
alt_lib_path=self.alt_lib_path)
283283
except mypy.errors.CompileError as e:
284284
output = ''.join(s + '\n' for s in e.messages)
285285
if e.use_stdout:
@@ -290,8 +290,7 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
290290
messages = result.errors
291291
manager = result.manager
292292
graph = result.graph
293-
self.fine_grained_manager = mypy.server.update.FineGrainedBuildManager(manager, graph)
294-
self.fine_grained_initialized = True
293+
self.fine_grained_manager = FineGrainedBuildManager(manager, graph)
295294
self.previous_sources = sources
296295
self.fscache.flush()
297296

@@ -320,6 +319,8 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
320319
return {'out': ''.join(s + '\n' for s in messages), 'err': '', 'status': status}
321320

322321
def fine_grained_increment(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
322+
assert self.fine_grained_manager is not None
323+
323324
t0 = time.time()
324325
self.update_sources(sources)
325326
changed = self.find_changed(sources)

mypy/test/testdmypy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,14 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int) ->
119119
if incremental_step == 1:
120120
if 'fine-grained' in testcase.file:
121121
options.fine_grained_incremental = True
122-
self.server = dmypy_server.Server(options)
122+
self.server = dmypy_server.Server(options, alt_lib_path=test_temp_dir)
123123

124124
assert self.server is not None # Set in step 1 and survives into next steps
125125
sources = []
126126
for module_name, program_path, program_text in module_data:
127127
# Always set to none so we're forced to reread the module in incremental mode
128128
sources.append(build.BuildSource(program_path, module_name, None))
129-
response = self.server.check(sources, alt_lib_path=test_temp_dir)
129+
response = self.server.check(sources)
130130
a = (response['out'] or response['err']).splitlines()
131131
a = normalize_error_messages(a)
132132

mypy/test/testfinegrained.py

Lines changed: 80 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,27 @@
1111
import re
1212
import shutil
1313

14-
from typing import List, Tuple, Dict, Optional, Set
14+
from typing import List, Tuple, Optional, cast
1515

1616
from mypy import build
1717
from mypy.build import BuildManager, BuildSource, Graph
18-
from mypy.errors import Errors, CompileError
19-
from mypy.nodes import Node, MypyFile, SymbolTable, SymbolTableNode, TypeInfo, Expression
18+
from mypy.errors import CompileError
2019
from mypy.options import Options
21-
from mypy.server.astmerge import merge_asts
22-
from mypy.server.subexpr import get_subexpressions
2320
from mypy.server.update import FineGrainedBuildManager
24-
from mypy.strconv import StrConv, indent
25-
from mypy.test.config import test_temp_dir, test_data_prefix
21+
from mypy.test.config import test_temp_dir
2622
from mypy.test.data import (
27-
parse_test_cases, DataDrivenTestCase, DataSuite, UpdateFile, module_from_path
23+
DataDrivenTestCase, DataSuite, UpdateFile, module_from_path
2824
)
2925
from mypy.test.helpers import assert_string_arrays_equal, parse_options
30-
from mypy.test.testtypegen import ignore_node
31-
from mypy.types import TypeStrVisitor, Type
32-
from mypy.util import short_type
3326
from mypy.server.mergecheck import check_consistency
27+
from mypy.dmypy_server import Server
28+
from mypy.main import expand_dir
3429

3530
import pytest # type: ignore # no pytest in typeshed
3631

32+
# TODO: This entire thing is a weird semi-duplication of testdmypy.
33+
# One of them should be eliminated and its remaining useful features
34+
# merged into the other.
3735

3836
# Set to True to perform (somewhat expensive) checks for duplicate AST nodes after merge
3937
CHECK_CONSISTENCY = False
@@ -75,52 +73,56 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
7573
return
7674

7775
main_src = '\n'.join(testcase.input)
76+
main_path = os.path.join(test_temp_dir, 'main')
77+
with open(main_path, 'w') as f:
78+
f.write(main_src)
79+
80+
server = Server(self.get_options(main_src, testcase, build_cache=False),
81+
alt_lib_path=test_temp_dir)
82+
7883
step = 1
79-
sources_override = self.parse_sources(main_src, step)
80-
messages, manager, graph = self.build(main_src, testcase, sources_override,
81-
build_cache=self.use_cache,
82-
enable_cache=self.use_cache)
84+
sources = self.parse_sources(main_src, step)
85+
if self.use_cache:
86+
messages = self.build(self.get_options(main_src, testcase, build_cache=True), sources)
87+
else:
88+
messages = self.run_check(server, sources)
89+
8390
a = []
8491
if messages:
8592
a.extend(normalize_messages(messages))
8693

87-
fine_grained_manager = None
88-
if not self.use_cache:
89-
fine_grained_manager = FineGrainedBuildManager(manager, graph)
94+
if server.fine_grained_manager:
9095
if CHECK_CONSISTENCY:
91-
check_consistency(fine_grained_manager)
96+
check_consistency(server.fine_grained_manager)
9297

9398
steps = testcase.find_steps()
9499
all_triggered = []
95100
for operations in steps:
96101
step += 1
97-
modules = []
98102
for op in operations:
99103
if isinstance(op, UpdateFile):
100104
# Modify/create file
105+
106+
# In some systems, mtime has a resolution of 1 second which can cause
107+
# annoying-to-debug issues when a file has the same size after a
108+
# change. We manually set the mtime to circumvent this.
109+
new_time = None
110+
if os.path.isfile(op.target_path):
111+
new_time = os.stat(op.target_path).st_mtime + 1
112+
101113
shutil.copy(op.source_path, op.target_path)
102-
modules.append((op.module, op.target_path))
114+
if new_time:
115+
os.utime(op.target_path, times=(new_time, new_time))
103116
else:
104117
# Delete file
105118
os.remove(op.path)
106-
modules.append((op.module, op.path))
107-
sources_override = self.parse_sources(main_src, step)
108-
if sources_override is not None:
109-
modules = [(module, path)
110-
for module, path in sources_override
111-
if any(m == module for m, _ in modules)]
112-
113-
# If this is the second iteration and we are using a
114-
# cache, now we need to set it up
115-
if fine_grained_manager is None:
116-
messages, manager, graph = self.build(main_src, testcase, sources_override,
117-
build_cache=False, enable_cache=True)
118-
fine_grained_manager = FineGrainedBuildManager(manager, graph)
119-
120-
new_messages = fine_grained_manager.update(modules)
121-
if CHECK_CONSISTENCY:
122-
check_consistency(fine_grained_manager)
123-
all_triggered.append(fine_grained_manager.triggered)
119+
sources = self.parse_sources(main_src, step)
120+
new_messages = self.run_check(server, sources)
121+
122+
if server.fine_grained_manager:
123+
if CHECK_CONSISTENCY:
124+
check_consistency(server.fine_grained_manager)
125+
all_triggered.append(server.fine_grained_manager.triggered)
124126
new_messages = normalize_messages(new_messages)
125127

126128
a.append('==')
@@ -141,39 +143,39 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
141143
'Invalid active triggers ({}, line {})'.format(testcase.file,
142144
testcase.line))
143145

144-
def build(self,
145-
source: str,
146-
testcase: DataDrivenTestCase,
147-
sources_override: Optional[List[Tuple[str, str]]],
148-
build_cache: bool,
149-
enable_cache: bool) -> Tuple[List[str], BuildManager, Graph]:
146+
def get_options(self,
147+
source: str,
148+
testcase: DataDrivenTestCase,
149+
build_cache: bool) -> Options:
150150
# This handles things like '# flags: --foo'.
151151
options = parse_options(source, testcase, incremental_step=1)
152152
options.incremental = True
153153
options.use_builtins_fixtures = True
154154
options.show_traceback = True
155155
options.fine_grained_incremental = not build_cache
156-
options.use_fine_grained_cache = enable_cache and not build_cache
157-
options.cache_fine_grained = enable_cache
156+
options.use_fine_grained_cache = self.use_cache and not build_cache
157+
options.cache_fine_grained = self.use_cache
158158
options.local_partial_types = True
159+
if options.follow_imports == 'normal':
160+
options.follow_imports = 'error'
159161

160-
main_path = os.path.join(test_temp_dir, 'main')
161-
with open(main_path, 'w') as f:
162-
f.write(source)
163-
if sources_override is not None:
164-
sources = [BuildSource(path, module, None)
165-
for module, path in sources_override]
166-
else:
167-
sources = [BuildSource(main_path, None, None)]
162+
return options
163+
164+
def run_check(self, server: Server, sources: List[BuildSource]) -> List[str]:
165+
response = server.check(sources)
166+
out = cast(str, response['out'] or response['err'])
167+
return out.splitlines()
168+
169+
def build(self,
170+
options: Options,
171+
sources: List[BuildSource]) -> List[str]:
168172
try:
169173
result = build.build(sources=sources,
170174
options=options,
171175
alt_lib_path=test_temp_dir)
172176
except CompileError as e:
173-
# TODO: We need a manager and a graph in this case as well
174-
assert False, str('\n'.join(e.messages))
175-
return e.messages, None, None
176-
return result.errors, result.manager, result.graph
177+
return e.messages
178+
return result.errors
177179

178180
def format_triggered(self, triggered: List[List[str]]) -> List[str]:
179181
result = []
@@ -185,11 +187,22 @@ def format_triggered(self, triggered: List[List[str]]) -> List[str]:
185187
return result
186188

187189
def parse_sources(self, program_text: str,
188-
incremental_step: int) -> Optional[List[Tuple[str, str]]]:
189-
"""Return target (module, path) tuples for a test case, if not using the defaults.
190+
incremental_step: int) -> List[BuildSource]:
191+
"""Return target BuildSources for a test case.
192+
193+
Normally, the unit tests will check all files included in the test
194+
case. This differs from how testcheck works by default, as dmypy
195+
doesn't currently support following imports.
196+
197+
You can override this behavior and instruct the tests to check
198+
multiple modules by using a comment like this in the test case
199+
input:
200+
201+
# cmd: main a.py
202+
203+
You can also use `# cmdN:` to have a different cmd for incremental
204+
step N (2, 3, ...).
190205
191-
These are defined through a comment like '# cmd: main a.py' in the test case
192-
description.
193206
"""
194207
m = re.search('# cmd: mypy ([a-zA-Z0-9_./ ]+)$', program_text, flags=re.MULTILINE)
195208
regex = '# cmd{}: mypy ([a-zA-Z0-9_./ ]+)$'.format(incremental_step)
@@ -209,9 +222,11 @@ def parse_sources(self, program_text: str,
209222
module = module_from_path(path)
210223
if module == 'main':
211224
module = '__main__'
212-
result.append((module, path))
225+
result.append(BuildSource(path, module, None))
213226
return result
214-
return None
227+
else:
228+
base = BuildSource(os.path.join(test_temp_dir, 'main'), '__main__', None)
229+
return [base] + expand_dir(test_temp_dir)
215230

216231

217232
def normalize_messages(messages: List[str]) -> List[str]:

test-data/unit/fine-grained-modules.test

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ main:1: error: Cannot find module named 'p.q'
282282
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
283283
==
284284

285-
[case testDeletionOfSubmoduleTriggersImportFrom1-skip-nocache]
285+
-- TODO: Fix this bug. It is a real bug that was been papered over
286+
-- by the test harness.
287+
[case testDeletionOfSubmoduleTriggersImportFrom1-skip-nocache-skip]
286288
-- Different cache/no-cache tests because:
287289
-- missing module error message mismatch
288290
from p import q

test-data/unit/fine-grained.test

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,9 +1458,11 @@ class C: pass
14581458
class D(C):
14591459
1()
14601460
class E(D): pass
1461+
# Something needs to change
14611462

14621463
[file b.py.2]
14631464
import a
1465+
# Something needs to change
14641466

14651467
[triggered]
14661468
2: a, a
@@ -1775,7 +1777,7 @@ p = Point(dict(x=42, y=1337))
17751777
[file a.py.2]
17761778
from mypy_extensions import TypedDict
17771779
Point = TypedDict('Point', {'x': int, 'y': int})
1778-
p = Point(dict(x=42, y=1337))
1780+
p = Point(dict(x=42, y=1337)) # dummy change
17791781
[out]
17801782
==
17811783

0 commit comments

Comments
 (0)