Skip to content

Commit 099500e

Browse files
authored
Unify test suites' module exclusion logic (#14575)
Individual test suites grew to have distinct approaches for selecting or excluding modules / symbols, with distinct ad-hoc exclude lists. Rather than rely on a common set of rules, test authors have been adding to those distinct lists (having to modify the test suite to support their test case). In this PR, we will consolidate this logic so we could have a common set of rules: - The "tested corpus" is what's asserted on. Everything else has a supporting role but does not contribute to what's being asserted on. - The "tested corpus" is - the `__main__` module - the `tested_modules`: modules provided through `[file ...]`, `[outfile ...]` or `[outfile-re ...]` - It follows that library code, whether imported from lib-stub/ or provided through `[builtins ...]` or `[typing ...]` will not be part of the "tested corpus". - At times we want `[file ...]` to also only have a supporting role and not be part of the tested corpus. In tests we used to have conventions like excluding modules starting with `_`. Instead, we'd have an explicit `[fixture ...]` block that just like `[file ...]` except it doesn't participate in the "tested corpus".
1 parent bed49ab commit 099500e

19 files changed

+173
-230
lines changed

mypy/build.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -705,8 +705,8 @@ def __init__(
705705
self.quickstart_state = read_quickstart_file(options, self.stdout)
706706
# Fine grained targets (module top levels and top level functions) processed by
707707
# the semantic analyzer, used only for testing. Currently used only by the new
708-
# semantic analyzer.
709-
self.processed_targets: list[str] = []
708+
# semantic analyzer. Tuple of module and target name.
709+
self.processed_targets: list[tuple[str, str]] = []
710710
# Missing stub packages encountered.
711711
self.missing_stub_packages: set[str] = set()
712712
# Cache for mypy ASTs that have completed semantic analysis

mypy/semanal_main.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def process_top_levels(graph: Graph, scc: list[str], patches: Patches) -> None:
218218
state = graph[next_id]
219219
assert state.tree is not None
220220
deferred, incomplete, progress = semantic_analyze_target(
221-
next_id, state, state.tree, None, final_iteration, patches
221+
next_id, next_id, state, state.tree, None, final_iteration, patches
222222
)
223223
all_deferred += deferred
224224
any_progress = any_progress or progress
@@ -289,7 +289,7 @@ def process_top_level_function(
289289
# OK, this is one last pass, now missing names will be reported.
290290
analyzer.incomplete_namespaces.discard(module)
291291
deferred, incomplete, progress = semantic_analyze_target(
292-
target, state, node, active_type, final_iteration, patches
292+
target, module, state, node, active_type, final_iteration, patches
293293
)
294294
if final_iteration:
295295
assert not deferred, "Must not defer during final iteration"
@@ -318,6 +318,7 @@ def get_all_leaf_targets(file: MypyFile) -> list[TargetInfo]:
318318

319319
def semantic_analyze_target(
320320
target: str,
321+
module: str,
321322
state: State,
322323
node: MypyFile | FuncDef | OverloadedFuncDef | Decorator,
323324
active_type: TypeInfo | None,
@@ -331,7 +332,7 @@ def semantic_analyze_target(
331332
- was some definition incomplete (need to run another pass)
332333
- were any new names defined (or placeholders replaced)
333334
"""
334-
state.manager.processed_targets.append(target)
335+
state.manager.processed_targets.append((module, target))
335336
tree = state.tree
336337
assert tree is not None
337338
analyzer = state.manager.semantic_analyzer

mypy/test/data.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ class DeleteFile(NamedTuple):
4141
FileOperation: _TypeAlias = Union[UpdateFile, DeleteFile]
4242

4343

44+
def _file_arg_to_module(filename: str) -> str:
45+
filename, _ = os.path.splitext(filename)
46+
parts = filename.split("/") # not os.sep since it comes from test data
47+
if parts[-1] == "__init__":
48+
parts.pop()
49+
return ".".join(parts)
50+
51+
4452
def parse_test_case(case: DataDrivenTestCase) -> None:
4553
"""Parse and prepare a single case from suite with test case descriptions.
4654
@@ -65,22 +73,26 @@ def parse_test_case(case: DataDrivenTestCase) -> None:
6573
rechecked_modules: dict[int, set[str]] = {} # from run number module names
6674
triggered: list[str] = [] # Active triggers (one line per incremental step)
6775
targets: dict[int, list[str]] = {} # Fine-grained targets (per fine-grained update)
76+
test_modules: list[str] = [] # Modules which are deemed "test" (vs "fixture")
6877

6978
# Process the parsed items. Each item has a header of form [id args],
7079
# optionally followed by lines of text.
7180
item = first_item = test_items[0]
81+
test_modules.append("__main__")
7282
for item in test_items[1:]:
73-
if item.id in {"file", "outfile", "outfile-re"}:
83+
if item.id in {"file", "fixture", "outfile", "outfile-re"}:
7484
# Record an extra file needed for the test case.
7585
assert item.arg is not None
7686
contents = expand_variables("\n".join(item.data))
77-
file_entry = (join(base_path, item.arg), contents)
78-
if item.id == "file":
79-
files.append(file_entry)
87+
path = join(base_path, item.arg)
88+
if item.id != "fixture":
89+
test_modules.append(_file_arg_to_module(item.arg))
90+
if item.id in {"file", "fixture"}:
91+
files.append((path, contents))
8092
elif item.id == "outfile-re":
81-
output_files.append((file_entry[0], re.compile(file_entry[1].rstrip(), re.S)))
82-
else:
83-
output_files.append(file_entry)
93+
output_files.append((path, re.compile(contents.rstrip(), re.S)))
94+
elif item.id == "outfile":
95+
output_files.append((path, contents))
8496
elif item.id == "builtins":
8597
# Use an alternative stub file for the builtins module.
8698
assert item.arg is not None
@@ -207,6 +219,7 @@ def parse_test_case(case: DataDrivenTestCase) -> None:
207219
case.triggered = triggered or []
208220
case.normalize_output = normalize_output
209221
case.expected_fine_grained_targets = targets
222+
case.test_modules = test_modules
210223

211224

212225
class DataDrivenTestCase(pytest.Item):
@@ -225,6 +238,8 @@ class DataDrivenTestCase(pytest.Item):
225238

226239
# (file path, file content) tuples
227240
files: list[tuple[str, str]]
241+
# Modules which is to be considered "test" rather than "fixture"
242+
test_modules: list[str]
228243
expected_stale_modules: dict[int, set[str]]
229244
expected_rechecked_modules: dict[int, set[str]]
230245
expected_fine_grained_targets: dict[int, list[str]]

mypy/test/testcheck.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from mypy.errors import CompileError
1212
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths
1313
from mypy.options import TYPE_VAR_TUPLE, UNPACK
14-
from mypy.semanal_main import core_modules
1514
from mypy.test.config import test_data_prefix, test_temp_dir
1615
from mypy.test.data import DataDrivenTestCase, DataSuite, FileOperation, module_from_path
1716
from mypy.test.helpers import (
@@ -188,12 +187,10 @@ def run_case_once(
188187
if incremental_step:
189188
name += str(incremental_step + 1)
190189
expected = testcase.expected_fine_grained_targets.get(incremental_step + 1)
191-
actual = res.manager.processed_targets
192-
# Skip the initial builtin cycle.
193190
actual = [
194-
t
195-
for t in actual
196-
if not any(t.startswith(mod) for mod in core_modules + ["mypy_extensions"])
191+
target
192+
for module, target in res.manager.processed_targets
193+
if module in testcase.test_modules
197194
]
198195
if expected is not None:
199196
assert_target_equivalence(name, expected, actual)

mypy/test/testdeps.py

+3-10
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,9 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
4141
a = ["Unknown compile error (likely syntax error in test case or fixture)"]
4242
else:
4343
deps: defaultdict[str, set[str]] = defaultdict(set)
44-
for module in files:
45-
if (
46-
module in dumped_modules
47-
or dump_all
48-
and module
49-
not in ("abc", "typing", "mypy_extensions", "typing_extensions", "enum")
50-
):
51-
new_deps = get_dependencies(
52-
files[module], type_map, options.python_version, options
53-
)
44+
for module, file in files.items():
45+
if (module in dumped_modules or dump_all) and (module in testcase.test_modules):
46+
new_deps = get_dependencies(file, type_map, options.python_version, options)
5447
for source in new_deps:
5548
deps[source].update(new_deps[source])
5649

mypy/test/testmerge.py

+13-33
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,6 @@
3636
AST = "AST"
3737

3838

39-
NOT_DUMPED_MODULES = (
40-
"builtins",
41-
"typing",
42-
"abc",
43-
"contextlib",
44-
"sys",
45-
"mypy_extensions",
46-
"typing_extensions",
47-
"enum",
48-
)
49-
50-
5139
class ASTMergeSuite(DataSuite):
5240
files = ["merge.test"]
5341

@@ -84,13 +72,13 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
8472
target_path = os.path.join(test_temp_dir, "target.py")
8573
shutil.copy(os.path.join(test_temp_dir, "target.py.next"), target_path)
8674

87-
a.extend(self.dump(fine_grained_manager, kind))
75+
a.extend(self.dump(fine_grained_manager, kind, testcase.test_modules))
8876
old_subexpr = get_subexpressions(result.manager.modules["target"])
8977

9078
a.append("==>")
9179

9280
new_file, new_types = self.build_increment(fine_grained_manager, "target", target_path)
93-
a.extend(self.dump(fine_grained_manager, kind))
81+
a.extend(self.dump(fine_grained_manager, kind, testcase.test_modules))
9482

9583
for expr in old_subexpr:
9684
if isinstance(expr, TypeVarExpr):
@@ -137,34 +125,32 @@ def build_increment(
137125
type_map = manager.graph[module_id].type_map()
138126
return module, type_map
139127

140-
def dump(self, manager: FineGrainedBuildManager, kind: str) -> list[str]:
141-
modules = manager.manager.modules
128+
def dump(
129+
self, manager: FineGrainedBuildManager, kind: str, test_modules: list[str]
130+
) -> list[str]:
131+
modules = {
132+
name: file for name, file in manager.manager.modules.items() if name in test_modules
133+
}
142134
if kind == AST:
143135
return self.dump_asts(modules)
144136
elif kind == TYPEINFO:
145137
return self.dump_typeinfos(modules)
146138
elif kind == SYMTABLE:
147139
return self.dump_symbol_tables(modules)
148140
elif kind == TYPES:
149-
return self.dump_types(manager)
141+
return self.dump_types(modules, manager)
150142
assert False, f"Invalid kind {kind}"
151143

152144
def dump_asts(self, modules: dict[str, MypyFile]) -> list[str]:
153145
a = []
154146
for m in sorted(modules):
155-
if m in NOT_DUMPED_MODULES:
156-
# We don't support incremental checking of changes to builtins, etc.
157-
continue
158147
s = modules[m].accept(self.str_conv)
159148
a.extend(s.splitlines())
160149
return a
161150

162151
def dump_symbol_tables(self, modules: dict[str, MypyFile]) -> list[str]:
163152
a = []
164153
for id in sorted(modules):
165-
if not is_dumped_module(id):
166-
# We don't support incremental checking of changes to builtins, etc.
167-
continue
168154
a.extend(self.dump_symbol_table(id, modules[id].names))
169155
return a
170156

@@ -197,8 +183,6 @@ def format_symbol_table_node(self, node: SymbolTableNode) -> str:
197183
def dump_typeinfos(self, modules: dict[str, MypyFile]) -> list[str]:
198184
a = []
199185
for id in sorted(modules):
200-
if not is_dumped_module(id):
201-
continue
202186
a.extend(self.dump_typeinfos_recursive(modules[id].names))
203187
return a
204188

@@ -217,13 +201,13 @@ def dump_typeinfo(self, info: TypeInfo) -> list[str]:
217201
s = info.dump(str_conv=self.str_conv, type_str_conv=self.type_str_conv)
218202
return s.splitlines()
219203

220-
def dump_types(self, manager: FineGrainedBuildManager) -> list[str]:
204+
def dump_types(
205+
self, modules: dict[str, MypyFile], manager: FineGrainedBuildManager
206+
) -> list[str]:
221207
a = []
222208
# To make the results repeatable, we try to generate unique and
223209
# deterministic sort keys.
224-
for module_id in sorted(manager.manager.modules):
225-
if not is_dumped_module(module_id):
226-
continue
210+
for module_id in sorted(modules):
227211
all_types = manager.manager.all_types
228212
# Compute a module type map from the global type map
229213
tree = manager.graph[module_id].tree
@@ -242,7 +226,3 @@ def dump_types(self, manager: FineGrainedBuildManager) -> list[str]:
242226

243227
def format_type(self, typ: Type) -> str:
244228
return typ.accept(self.type_str_conv)
245-
246-
247-
def is_dumped_module(id: str) -> bool:
248-
return id not in NOT_DUMPED_MODULES and (not id.startswith("_") or id == "__main__")

mypy/test/testsemanal.py

+16-38
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import os.path
65
import sys
76
from typing import Dict
87

@@ -77,27 +76,9 @@ def test_semanal(testcase: DataDrivenTestCase) -> None:
7776
raise CompileError(a)
7877
# Include string representations of the source files in the actual
7978
# output.
80-
for fnam in sorted(result.files.keys()):
81-
f = result.files[fnam]
82-
# Omit the builtins module and files with a special marker in the
83-
# path.
84-
# TODO the test is not reliable
85-
if (
86-
not f.path.endswith(
87-
(
88-
os.sep + "builtins.pyi",
89-
"typing.pyi",
90-
"mypy_extensions.pyi",
91-
"typing_extensions.pyi",
92-
"abc.pyi",
93-
"collections.pyi",
94-
"sys.pyi",
95-
)
96-
)
97-
and not os.path.basename(f.path).startswith("_")
98-
and not os.path.splitext(os.path.basename(f.path))[0].endswith("_")
99-
):
100-
a += str(f).split("\n")
79+
for module in sorted(result.files.keys()):
80+
if module in testcase.test_modules:
81+
a += str(result.files[module]).split("\n")
10182
except CompileError as e:
10283
a = e.messages
10384
if testcase.normalize_output:
@@ -164,10 +145,10 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
164145
a = result.errors
165146
if a:
166147
raise CompileError(a)
167-
for f in sorted(result.files.keys()):
168-
if f not in ("builtins", "typing", "abc"):
169-
a.append(f"{f}:")
170-
for s in str(result.files[f].names).split("\n"):
148+
for module in sorted(result.files.keys()):
149+
if module in testcase.test_modules:
150+
a.append(f"{module}:")
151+
for s in str(result.files[module].names).split("\n"):
171152
a.append(" " + s)
172153
except CompileError as e:
173154
a = e.messages
@@ -199,11 +180,13 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
199180

200181
# Collect all TypeInfos in top-level modules.
201182
typeinfos = TypeInfoMap()
202-
for f in result.files.values():
203-
for n in f.names.values():
204-
if isinstance(n.node, TypeInfo):
205-
assert n.fullname
206-
typeinfos[n.fullname] = n.node
183+
for module, file in result.files.items():
184+
if module in testcase.test_modules:
185+
for n in file.names.values():
186+
if isinstance(n.node, TypeInfo):
187+
assert n.fullname
188+
if any(n.fullname.startswith(m + ".") for m in testcase.test_modules):
189+
typeinfos[n.fullname] = n.node
207190

208191
# The output is the symbol table converted into a string.
209192
a = str(typeinfos).split("\n")
@@ -220,12 +203,7 @@ class TypeInfoMap(Dict[str, TypeInfo]):
220203
def __str__(self) -> str:
221204
a: list[str] = ["TypeInfoMap("]
222205
for x, y in sorted(self.items()):
223-
if (
224-
not x.startswith("builtins.")
225-
and not x.startswith("typing.")
226-
and not x.startswith("abc.")
227-
):
228-
ti = ("\n" + " ").join(str(y).split("\n"))
229-
a.append(f" {x} : {ti}")
206+
ti = ("\n" + " ").join(str(y).split("\n"))
207+
a.append(f" {x} : {ti}")
230208
a[-1] += ")"
231209
return "\n".join(a)

mypy/test/testtransform.py

+4-23
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from __future__ import annotations
44

5-
import os.path
6-
75
from mypy import build
86
from mypy.errors import CompileError
97
from mypy.modulefinder import BuildSource
@@ -50,29 +48,12 @@ def test_transform(testcase: DataDrivenTestCase) -> None:
5048
raise CompileError(a)
5149
# Include string representations of the source files in the actual
5250
# output.
53-
for fnam in sorted(result.files.keys()):
54-
f = result.files[fnam]
55-
56-
# Omit the builtins module and files with a special marker in the
57-
# path.
58-
# TODO the test is not reliable
59-
if (
60-
not f.path.endswith(
61-
(
62-
os.sep + "builtins.pyi",
63-
"typing_extensions.pyi",
64-
"typing.pyi",
65-
"abc.pyi",
66-
"sys.pyi",
67-
)
68-
)
69-
and not os.path.basename(f.path).startswith("_")
70-
and not os.path.splitext(os.path.basename(f.path))[0].endswith("_")
71-
):
51+
for module in sorted(result.files.keys()):
52+
if module in testcase.test_modules:
7253
t = TypeAssertTransformVisitor()
7354
t.test_only = True
74-
f = t.mypyfile(f)
75-
a += str(f).split("\n")
55+
file = t.mypyfile(result.files[module])
56+
a += str(file).split("\n")
7657
except CompileError as e:
7758
a = e.messages
7859
if testcase.normalize_output:

0 commit comments

Comments
 (0)