From e2821806371e2ca90ee2aae43fe6fcb623a80147 Mon Sep 17 00:00:00 2001 From: Michael Sullivan Date: Thu, 8 Mar 2018 14:55:33 -0800 Subject: [PATCH] Add more test facilities, fix a bug revealed by them --- mypy/dmypy_server.py | 12 ++- mypy/server/update.py | 10 ++ mypy/test/helpers.py | 17 +++- mypy/test/testcheck.py | 20 +--- mypy/test/testfinegrained.py | 23 ++++- test-data/unit/fine-grained-modules.test | 115 ++++++++++++++++++----- test-data/unit/fine-grained.test | 5 + 7 files changed, 152 insertions(+), 50 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 36c21abb4e5a..22d3a08958a1 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -273,9 +273,6 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict self.fscache = FileSystemCache(self.options.python_version) self.fswatcher = FileSystemWatcher(self.fscache) self.update_sources(sources) - if not self.options.use_fine_grained_cache: - # Stores the initial state of sources as a side effect. - self.fswatcher.find_changed() try: result = mypy.build.build(sources=sources, options=self.options, @@ -292,7 +289,6 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict graph = result.graph self.fine_grained_manager = FineGrainedBuildManager(manager, graph) self.previous_sources = sources - self.fscache.flush() # If we are using the fine-grained cache, build hasn't actually done # the typechecking on the updated files yet. @@ -310,6 +306,8 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict # Run an update changed = self.find_changed(sources) + + # Find anything that has had its dependency list change for state in self.fine_grained_manager.graph.values(): if not state.is_fresh(): assert state.path is not None @@ -317,7 +315,11 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict if changed: messages = self.fine_grained_manager.update(changed) - self.fscache.flush() + else: + # Stores the initial state of sources as a side effect. + self.fswatcher.find_changed() + + self.fscache.flush() status = 1 if messages else 0 self.previous_messages = messages[:] diff --git a/mypy/server/update.py b/mypy/server/update.py index 7b5a323a6cc1..c4f8d985e3f9 100644 --- a/mypy/server/update.py +++ b/mypy/server/update.py @@ -174,8 +174,14 @@ def __init__(self, # for the cache. self.manager.cache_enabled = False manager.saved_cache = {} + + # Some hints to the test suite about what is going on: # Active triggers during the last update self.triggered = [] # type: List[str] + # Modules passed to update during the last update + self.changed_modules = [] # type: List[Tuple[str, str]] + # Modules processed during the last update + self.updated_modules = [] # type: List[str] def update(self, changed_modules: List[Tuple[str, str]]) -> List[str]: """Update previous build result by processing changed modules. @@ -196,10 +202,13 @@ def update(self, changed_modules: List[Tuple[str, str]]) -> List[str]: """ assert changed_modules, 'No changed modules' + self.changed_modules = changed_modules + # Reset global caches for the new build. find_module_clear_caches() self.triggered = [] + self.updated_modules = [] changed_modules = dedupe_modules(changed_modules + self.stale) initial_set = {id for id, _ in changed_modules} self.manager.log_fine_grained('==== update %s ====' % ', '.join( @@ -256,6 +265,7 @@ def update_single(self, module: str, path: str) -> Tuple[List[str], - Whether there was a blocking error in the module """ self.manager.log_fine_grained('--- update single %r ---' % module) + self.updated_modules.append(module) # TODO: If new module brings in other modules, we parse some files multiple times. manager = self.manager diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 794499825a1d..4b2fa3a4f131 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -5,7 +5,7 @@ import time import shutil -from typing import List, Dict, Tuple, Callable, Any, Optional +from typing import List, Iterable, Dict, Tuple, Callable, Any, Optional from mypy import defaults from mypy.test.config import test_temp_dir @@ -98,6 +98,21 @@ def assert_string_arrays_equal(expected: List[str], actual: List[str], raise AssertionError(msg) +def assert_module_equivalence(name: str, + expected: Optional[Iterable[str]], actual: Iterable[str]) -> None: + if expected is not None: + expected_normalized = sorted(expected) + actual_normalized = sorted(set(actual).difference({"__main__"})) + assert_string_arrays_equal( + expected_normalized, + actual_normalized, + ('Actual modules ({}) do not match expected modules ({}) ' + 'for "[{} ...]"').format( + ', '.join(actual_normalized), + ', '.join(expected_normalized), + name)) + + def update_testcase_output(testcase: DataDrivenTestCase, output: List[str]) -> None: assert testcase.old_cwd is not None, "test was not properly set up" testcase_path = os.path.join(testcase.old_cwd, testcase.file) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 45303ce8b420..bf811a035a03 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -11,7 +11,7 @@ from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite from mypy.test.helpers import ( - assert_string_arrays_equal, normalize_error_messages, + assert_string_arrays_equal, normalize_error_messages, assert_module_equivalence, retry_on_error, update_testcase_output, parse_options, copy_and_fudge_mtime ) @@ -190,29 +190,15 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int = 0) self.verify_cache(module_data, a, res.manager) if incremental_step > 1: suffix = '' if incremental_step == 2 else str(incremental_step - 1) - self.check_module_equivalence( + assert_module_equivalence( 'rechecked' + suffix, testcase.expected_rechecked_modules.get(incremental_step - 1), res.manager.rechecked_modules) - self.check_module_equivalence( + assert_module_equivalence( 'stale' + suffix, testcase.expected_stale_modules.get(incremental_step - 1), res.manager.stale_modules) - def check_module_equivalence(self, name: str, - expected: Optional[Set[str]], actual: Set[str]) -> None: - if expected is not None: - expected_normalized = sorted(expected) - actual_normalized = sorted(actual.difference({"__main__"})) - assert_string_arrays_equal( - expected_normalized, - actual_normalized, - ('Actual modules ({}) do not match expected modules ({}) ' - 'for "[{} ...]"').format( - ', '.join(actual_normalized), - ', '.join(expected_normalized), - name)) - def verify_cache(self, module_data: List[Tuple[str, str, str]], a: List[str], manager: build.BuildManager) -> None: # There should be valid cache metadata for each module except diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index d261076c6c31..b8d4cf0becee 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -10,7 +10,7 @@ import os import re -from typing import List, Tuple, Optional, cast +from typing import List, Set, Tuple, Optional, cast from mypy import build from mypy.build import BuildManager, BuildSource, Graph @@ -21,7 +21,9 @@ from mypy.test.data import ( DataDrivenTestCase, DataSuite, UpdateFile, module_from_path ) -from mypy.test.helpers import assert_string_arrays_equal, parse_options, copy_and_fudge_mtime +from mypy.test.helpers import ( + assert_string_arrays_equal, parse_options, copy_and_fudge_mtime, assert_module_equivalence, +) from mypy.server.mergecheck import check_consistency from mypy.dmypy_server import Server from mypy.main import expand_dir @@ -96,6 +98,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: steps = testcase.find_steps() all_triggered = [] + for operations in steps: step += 1 for op in operations: @@ -108,10 +111,26 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: sources = self.parse_sources(main_src, step) new_messages = self.run_check(server, sources) + assert server.fine_grained_manager + + updated, changed = [], [] if server.fine_grained_manager: if CHECK_CONSISTENCY: check_consistency(server.fine_grained_manager) all_triggered.append(server.fine_grained_manager.triggered) + + updated = server.fine_grained_manager.updated_modules + changed = [mod for mod, file in server.fine_grained_manager.changed_modules] + + assert_module_equivalence( + 'stale' + str(step - 1), + testcase.expected_stale_modules.get(step - 1), + changed) + assert_module_equivalence( + 'rechecked' + str(step - 1), + testcase.expected_rechecked_modules.get(step - 1), + updated) + new_messages = normalize_messages(new_messages) a.append('==') diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index 41b2ba6203ae..ae1fae13f4e5 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -624,22 +624,6 @@ c.f(1) == a.py:2: error: Too many arguments for "f" -[case testRenameAndDeleteModule] -import a -[file a.py] -from b1 import f -f() -[file b1.py] -def f() -> None: pass -[file b2.py.2] -def f() -> None: pass -[delete b1.py.2] -[file a.py.2] -from b2 import f -f() -[out] -== - [case testDeleteFileWithinPackage] import a [file a.py] @@ -991,10 +975,10 @@ x = Foo() main:2: error: Too few arguments for "foo" of "Foo" -- This series of tests is designed to test adding a new module that --- doesn't appear in the cache, for cache mode. They aren't run only --- in cache mode, though, because they are still perfectly good --- regular tests. -[case testAddModuleAfterCache1] +-- doesn't appear in the cache, for cache mode. They are run in +-- cache mode oly because stale and rechecked differ heavily between +-- the modes. +[case testAddModuleAfterCache1-skip-nocache] # cmd: mypy main a.py # cmd2: mypy main a.py b.py # cmd3: mypy main a.py b.py @@ -1006,14 +990,22 @@ import b b.foo(0) [file b.py.2] def foo() -> None: pass + +[stale a, b] +[rechecked a, b] + [file b.py.3] def foo(x: int) -> None: pass + +[stale2 b] +[rechecked2 b] + [out] == a.py:2: error: Too many arguments for "foo" == -[case testAddModuleAfterCache2] +[case testAddModuleAfterCache2-skip-nocache] # cmd: mypy main a.py # cmd2: mypy main a.py b.py # cmd3: mypy main a.py b.py @@ -1024,30 +1016,51 @@ import b b.foo(0) [file b.py.2] def foo() -> None: pass + +[stale b] +[rechecked a, b] + [file b.py.3] def foo(x: int) -> None: pass + +[stale2 b] + [out] == a.py:2: error: Too many arguments for "foo" == -[case testAddModuleAfterCache3] +[case testAddModuleAfterCache3-skip-nocache] # cmd: mypy main a.py # cmd2: mypy main a.py b.py c.py d.py e.py f.py g.py # flags: --ignore-missing-imports --follow-imports=skip import a [file a.py] import b, c, d, e, f, g +b.foo(10) [file b.py.2] +def foo() -> None: pass [file c.py.2] [file d.py.2] [file e.py.2] [file f.py.2] [file g.py.2] + +-- No files should be stale or reprocessed in the first step since the large number +-- of missing files will force build to give up on cache loading. +[stale] + +[file b.py.3] +def foo(x: int) -> None: pass +[stale2 b] + [out] == +a.py:2: error: Too many arguments for "foo" +== -[case testAddModuleAfterCache4] + +[case testAddModuleAfterCache4-skip-nocache] # cmd: mypy main a.py # cmd2: mypy main a.py b.py # cmd3: mypy main a.py b.py @@ -1066,7 +1079,7 @@ def foo(x: int) -> None: pass b.py:2: error: Too many arguments for "foo" == -[case testAddModuleAfterCache5] +[case testAddModuleAfterCache5-skip-nocache] # cmd: mypy main a.py # cmd2: mypy main a.py b.py # cmd3: mypy main a.py b.py @@ -1080,14 +1093,20 @@ def foo() -> None: pass [file b.py.2] import a a.foo(10) + +[stale a, b] + [file a.py.3] def foo(x: int) -> None: pass + +[stale2 a] + [out] == b.py:2: error: Too many arguments for "foo" == -[case testAddModuleAfterCache6] +[case testAddModuleAfterCache6-skip-nocache] # cmd: mypy main a.py # cmd2: mypy main a.py b.py # cmd3: mypy main a.py b.py @@ -1096,18 +1115,64 @@ import a [file a.py] import b b.foo() + [file a.py.2] import b b.foo(0) [file b.py.2] def foo() -> None: pass + +[stale a, b] + [file b.py.3] def foo(x: int) -> None: pass + +[stale2 b] + [out] == a.py:2: error: Too many arguments for "foo" == +[case testRenameAndDeleteModuleAfterCache-skip-nocache] +import a +[file a.py] +from b1 import f +f() +[file b1.py] +def f() -> None: pass +[file b2.py.2] +def f() -> None: pass +[delete b1.py.2] +[file a.py.2] +from b2 import f +f() + +-- in no cache mode, there is no way to know about b1 yet +[stale a, b2] + +[out] +== + +[case testDeleteModuleAfterCache-skip-nocache] +import a +[file a.py] +from b import f +f() +[file b.py] +def f() -> None: pass +[delete b.py.2] + +-- in no cache mode, there is no way to know about b yet, +-- but a should still get triggered +[stale a] + +[out] +== +a.py:1: error: Cannot find module named 'b' +a.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + + [case testRefreshImportIfMypyElse1] import a [file a.py] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index ca9db254447b..a911dda66c93 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -15,6 +15,11 @@ -- -- == -- +-- +-- +-- Modules that are expected to be detected as changed can be checked with [stale ...] +-- while modules that are reprocessed by update (which can include cached files +-- that need to be loaded) can be checked with [rechecked ...] [case testReprocessFunction] import m