-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Add support for test cases with more than 2 incremental runs #3347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
22fd4e9
cf1b070
3e9cdab
9c52d92
a755347
7c3d7e4
63a7194
af40038
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,8 +102,17 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: | |
| # Expect success on first run, errors from testcase.output (if any) on second run. | ||
| # We briefly sleep to make sure file timestamps are distinct. | ||
| self.clear_cache() | ||
| self.run_case_once(testcase, 1) | ||
| self.run_case_once(testcase, 2) | ||
| num_steps = max([2] + list(testcase.output2.keys())) | ||
| # Check that there are no file changes beyond the last run (they would be ignored). | ||
| for dn, dirs, files in os.walk(os.curdir): | ||
| for file in files: | ||
| m = re.search(r'\.([2-9])$', file) | ||
| if m and int(m.group(1)) > num_steps: | ||
| raise ValueError( | ||
| 'Output file {} exists though test case only has {} runs'.format( | ||
| file, num_steps)) | ||
| for step in range(1, num_steps + 1): | ||
| self.run_case_once(testcase, step) | ||
| elif optional: | ||
| experiments.STRICT_OPTIONAL = True | ||
| self.run_case_once(testcase) | ||
|
|
@@ -118,26 +127,26 @@ def clear_cache(self) -> None: | |
| if os.path.exists(dn): | ||
| shutil.rmtree(dn) | ||
|
|
||
| def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> None: | ||
| def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int = 0) -> None: | ||
| find_module_clear_caches() | ||
| original_program_text = '\n'.join(testcase.input) | ||
| module_data = self.parse_module(original_program_text, incremental) | ||
| module_data = self.parse_module(original_program_text, incremental_step) | ||
|
|
||
| if incremental: | ||
| if incremental == 1: | ||
| if incremental_step: | ||
| if incremental_step == 1: | ||
| # In run 1, copy program text to program file. | ||
| for module_name, program_path, program_text in module_data: | ||
| if module_name == '__main__': | ||
| with open(program_path, 'w') as f: | ||
| f.write(program_text) | ||
| break | ||
| elif incremental == 2: | ||
| # In run 2, copy *.next files to * files. | ||
| elif incremental_step > 1: | ||
| # In runs 2+, copy *.[num] files to * files. | ||
| for dn, dirs, files in os.walk(os.curdir): | ||
| for file in files: | ||
| if file.endswith('.next'): | ||
| if file.endswith('.' + str(incremental_step)): | ||
| full = os.path.join(dn, file) | ||
| target = full[:-5] | ||
| target = full[:-2] | ||
| shutil.copy(full, target) | ||
|
|
||
| # 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 | |
| os.utime(target, times=(new_time, new_time)) | ||
|
|
||
| # Parse options after moving files (in case mypy.ini is being moved). | ||
| options = self.parse_options(original_program_text, testcase, incremental) | ||
| options = self.parse_options(original_program_text, testcase, incremental_step) | ||
| options.use_builtins_fixtures = True | ||
| options.show_traceback = True | ||
| if 'optional' in testcase.file: | ||
| options.strict_optional = True | ||
| if incremental: | ||
| if incremental_step: | ||
| options.incremental = True | ||
| else: | ||
| 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 | |
| for module_name, program_path, program_text in module_data: | ||
| # Always set to none so we're forced to reread the module in incremental mode | ||
| sources.append(BuildSource(program_path, module_name, | ||
| None if incremental else program_text)) | ||
| None if incremental_step else program_text)) | ||
| res = None | ||
| try: | ||
| res = build.build(sources=sources, | ||
|
|
@@ -173,42 +182,51 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental: int = 0) -> N | |
| a = normalize_error_messages(a) | ||
|
|
||
| # Make sure error messages match | ||
| if incremental == 0: | ||
| msg = 'Invalid type checker output ({}, line {})' | ||
| if incremental_step == 0: | ||
| # Not incremental | ||
| msg = 'Unexpected type checker output ({}, line {})' | ||
| output = testcase.output | ||
| elif incremental == 1: | ||
| msg = 'Invalid type checker output in incremental, run 1 ({}, line {})' | ||
| elif incremental_step == 1: | ||
| msg = 'Unexpected type checker output in incremental, run 1 ({}, line {})' | ||
| output = testcase.output | ||
| elif incremental == 2: | ||
| msg = 'Invalid type checker output in incremental, run 2 ({}, line {})' | ||
| output = testcase.output2 | ||
| elif incremental_step > 1: | ||
| msg = ('Unexpected type checker output in incremental, run {}'.format( | ||
| incremental_step) + ' ({}, line {})') | ||
| output = testcase.output2.get(incremental_step, []) | ||
| else: | ||
| raise AssertionError() | ||
|
|
||
| if output != a and self.update_data: | ||
| update_testcase_output(testcase, a) | ||
| assert_string_arrays_equal(output, a, msg.format(testcase.file, testcase.line)) | ||
|
|
||
| if incremental and res: | ||
| if incremental_step and res: | ||
| if options.follow_imports == 'normal' and testcase.output is None: | ||
| self.verify_cache(module_data, a, res.manager) | ||
| if incremental == 2: | ||
| if incremental_step > 1: | ||
| suffix = '' if incremental_step == 2 else str(incremental_step - 1) | ||
| self.check_module_equivalence( | ||
| 'rechecked', | ||
| testcase.expected_rechecked_modules, | ||
| 'rechecked' + suffix, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding the suffix makes for a slightly awkward error message, "Set of rechecked1 modules does not match expected set". Especially since this refers to the difference between rechecked[1] and rechecked[2]. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll update the message to be less confusing. |
||
| testcase.expected_rechecked_modules.get(incremental_step - 1), | ||
| res.manager.rechecked_modules) | ||
| self.check_module_equivalence( | ||
| 'stale', | ||
| testcase.expected_stale_modules, | ||
| '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( | ||
| list(sorted(expected)), | ||
| list(sorted(actual.difference({"__main__"}))), | ||
| 'Set of {} modules does not match expected set'.format(name)) | ||
| 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: | ||
|
|
@@ -268,7 +286,9 @@ def find_missing_cache_files(self, modules: Dict[str, str], | |
| missing[id] = path | ||
| return set(missing.values()) | ||
|
|
||
| def parse_module(self, program_text: str, incremental: int = 0) -> List[Tuple[str, str, str]]: | ||
| def parse_module(self, | ||
| program_text: str, | ||
| incremental_step: int = 0) -> List[Tuple[str, str, str]]: | ||
| """Return the module and program names for a test case. | ||
|
|
||
| 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 | |
|
|
||
| # cmd: mypy -m foo.bar foo.baz | ||
|
|
||
| You can also use `# cmdN:` to have a different cmd for incremental | ||
| step N (2, 3, ...). | ||
|
|
||
| Return a list of tuples (module name, file name, program text). | ||
| """ | ||
| m = re.search('# cmd: mypy -m ([a-zA-Z0-9_. ]+)$', program_text, flags=re.MULTILINE) | ||
| m2 = re.search('# cmd2: mypy -m ([a-zA-Z0-9_. ]+)$', program_text, flags=re.MULTILINE) | ||
| if m2 is not None and incremental == 2: | ||
| # Optionally return a different command if in the second | ||
| # stage of incremental mode, otherwise default to reusing | ||
| # the original cmd. | ||
| m = m2 | ||
| regex = '# cmd{}: mypy -m ([a-zA-Z0-9_. ]+)$'.format(incremental_step) | ||
| alt_m = re.search(regex, program_text, flags=re.MULTILINE) | ||
| if alt_m is not None and incremental_step > 1: | ||
| # Optionally return a different command if in a later step | ||
| # of incremental mode, otherwise default to reusing the | ||
| # original cmd. | ||
| m = alt_m | ||
|
|
||
| if m: | ||
| # 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 | |
| return [('__main__', 'main', program_text)] | ||
|
|
||
| def parse_options(self, program_text: str, testcase: DataDrivenTestCase, | ||
| incremental: int) -> Options: | ||
| incremental_step: int) -> Options: | ||
| options = Options() | ||
| flags = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE) | ||
| if incremental == 2: | ||
| flags2 = re.search('# flags2: (.*)$', program_text, flags=re.MULTILINE) | ||
| if incremental_step > 1: | ||
| flags2 = re.search('# flags{}: (.*)$'.format(incremental_step), program_text, | ||
| flags=re.MULTILINE) | ||
| if flags2: | ||
| flags = flags2 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see, this is why we can't have 10 or more passes...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update to allow more than 9 passes.