Skip to content

Commit 89a8e67

Browse files
elazargmsullivan
authored andcommitted
unify DataDrivenTestCase classes (#5013)
1 parent 532f3fb commit 89a8e67

File tree

1 file changed

+52
-80
lines changed

1 file changed

+52
-80
lines changed

mypy/test/data.py

Lines changed: 52 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,16 @@
2828
FileOperation = Union[UpdateFile, DeleteFile]
2929

3030

31-
def parse_test_cases(
32-
path: str,
33-
base_path: str = '.',
34-
optional_out: bool = False,
35-
native_sep: bool = False) -> List['DataDrivenTestCase']:
36-
"""Parse a file with test case descriptions.
37-
38-
Return an array of test cases.
31+
def parse_test_cases(parent: 'DataSuiteCollector', suite: 'DataSuite',
32+
path: str) -> Iterator['DataDrivenTestCase']:
33+
"""Parse a single file from suite with test case descriptions.
3934
4035
NB: this function and DataDrivenTestCase were shared between the
4136
myunit and pytest codepaths -- if something looks redundant,
4237
that's likely the reason.
4338
"""
44-
if native_sep:
39+
base_path = suite.base_path
40+
if suite.native_sep:
4541
join = os.path.join
4642
else:
4743
join = posixpath.join # type: ignore
@@ -51,7 +47,6 @@ def parse_test_cases(
5147
for i in range(len(lst)):
5248
lst[i] = lst[i].rstrip('\n')
5349
p = parse_test_data(lst, path)
54-
out = [] # type: List[DataDrivenTestCase]
5550

5651
# Process the parsed items. Each item has a header of form [id args],
5752
# optionally followed by lines of text.
@@ -143,7 +138,7 @@ def parse_test_cases(
143138
assert passnum > 1
144139
output = p[i].data
145140
output = [expand_variables(line) for line in output]
146-
if native_sep and os.path.sep == '\\':
141+
if suite.native_sep and os.path.sep == '\\':
147142
output = [fix_win_path(line) for line in output]
148143
tcout2[passnum] = output
149144
ok = True
@@ -167,7 +162,7 @@ def parse_test_cases(
167162
('Stale modules after pass {} must be a subset of rechecked '
168163
'modules ({}:{})').format(passnum, path, p[i0].line))
169164

170-
if optional_out:
165+
if suite.optional_out:
171166
ok = True
172167

173168
if ok:
@@ -178,24 +173,24 @@ def parse_test_cases(
178173
lastline = p[i].line if i < len(p) else p[i - 1].line + 9999
179174
arg0 = p[i0].arg
180175
assert arg0 is not None
181-
tc = DataDrivenTestCase(arg0, input, tcout, tcout2, path,
182-
p[i0].line, lastline,
183-
files, output_files, stale_modules,
184-
rechecked_modules, deleted_paths, native_sep,
185-
triggered)
186-
out.append(tc)
176+
case_name = add_test_name_suffix(arg0, suite.test_name_suffix)
177+
skip = arg0.endswith('-skip')
178+
if skip:
179+
case_name = case_name[:-len('-skip')]
180+
yield DataDrivenTestCase(case_name, parent, skip, input, tcout, tcout2, path,
181+
p[i0].line, lastline,
182+
files, output_files, stale_modules,
183+
rechecked_modules, deleted_paths, suite.native_sep,
184+
triggered)
187185
if not ok:
188186
raise ValueError(
189187
'{}, line {}: Error in test case description'.format(
190188
path, p[i0].line))
191189

192-
return out
193-
194190

195-
class DataDrivenTestCase:
196-
"""Holds parsed data and handles directory setup and teardown for MypyDataCase."""
191+
class DataDrivenTestCase(pytest.Item): # type: ignore # inheriting from Any
192+
"""Holds parsed data-driven test cases, and handles directory setup and teardown."""
197193

198-
# TODO: rename to ParsedTestCase or merge with MypyDataCase (yet avoid multiple inheritance)
199194
# TODO: only create files on setup, not during parsing
200195

201196
input = None # type: List[str]
@@ -215,6 +210,8 @@ class DataDrivenTestCase:
215210

216211
def __init__(self,
217212
name: str,
213+
parent: 'DataSuiteCollector',
214+
skip: bool,
218215
input: List[str],
219216
output: List[str],
220217
output2: Dict[int, List[str]],
@@ -229,7 +226,9 @@ def __init__(self,
229226
native_sep: bool = False,
230227
triggered: Optional[List[str]] = None,
231228
) -> None:
232-
self.name = name
229+
230+
super().__init__(name, parent)
231+
self.skip = skip
233232
self.old_cwd = None # type: Optional[str]
234233
self.tmpdir = None # type: Optional[tempfile.TemporaryDirectory[str]]
235234
self.input = input
@@ -246,6 +245,14 @@ def __init__(self,
246245
self.native_sep = native_sep
247246
self.triggered = triggered or []
248247

248+
def runtest(self) -> None:
249+
if self.skip:
250+
pytest.skip()
251+
suite = self.parent.obj()
252+
suite.update_data = self.config.getoption('--update-data', False)
253+
suite.setup()
254+
suite.run_case(self)
255+
249256
def setup(self) -> None:
250257
self.old_cwd = os.getcwd()
251258
self.tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-')
@@ -343,6 +350,22 @@ def teardown(self) -> None:
343350
self.old_cwd = None
344351
self.tmpdir = None
345352

353+
def reportinfo(self) -> Tuple[str, int, str]:
354+
return self.file, self.line, self.name
355+
356+
def repr_failure(self, excinfo: Any) -> str:
357+
if excinfo.errisinstance(SystemExit):
358+
# We assume that before doing exit() (which raises SystemExit) we've printed
359+
# enough context about what happened so that a stack trace is not useful.
360+
# In particular, uncaught exceptions during semantic analysis or type checking
361+
# call exit() and they already print out a stack trace.
362+
excrepr = excinfo.exconly()
363+
else:
364+
self.parent._prunetraceback(excinfo)
365+
excrepr = excinfo.getrepr(style='short')
366+
367+
return "data: {}:{}:\n{}".format(self.file, self.line, excrepr)
368+
346369
def find_steps(self) -> List[List[FileOperation]]:
347370
"""Return a list of descriptions of file operations for each incremental step.
348371
@@ -569,7 +592,7 @@ def fix_cobertura_filename(line: str) -> str:
569592

570593

571594
# This function name is special to pytest. See
572-
# http://doc.pytest.org/en/latest/writing_plugins.html#initialization-command-line-and-configuration-hooks
595+
# https://docs.pytest.org/en/latest/reference.html#initialization-hooks
573596
def pytest_addoption(parser: Any) -> None:
574597
group = parser.getgroup('mypy')
575598
group.addoption('--update-data', action='store_true', default=False,
@@ -589,26 +612,20 @@ def pytest_pycollect_makeitem(collector: Any, name: str,
589612
# Only classes derived from DataSuite contain test cases, not the DataSuite class itself
590613
if issubclass(obj, DataSuite) and obj is not DataSuite:
591614
# Non-None result means this obj is a test case.
592-
# The collect method of the returned MypyDataSuite instance will be called later,
615+
# The collect method of the returned DataSuiteCollector instance will be called later,
593616
# with self.obj being obj.
594-
return MypyDataSuite(name, parent=collector)
617+
return DataSuiteCollector(name, parent=collector)
595618
return None
596619

597620

598-
class MypyDataSuite(pytest.Class): # type: ignore # inheriting from Any
621+
class DataSuiteCollector(pytest.Class): # type: ignore # inheriting from Any
599622
def collect(self) -> Iterator[pytest.Item]: # type: ignore
600623
"""Called by pytest on each of the object returned from pytest_pycollect_makeitem"""
601624

602625
# obj is the object for which pytest_pycollect_makeitem returned self.
603626
suite = self.obj # type: DataSuite
604627
for f in suite.files:
605-
for case in parse_test_cases(os.path.join(suite.data_prefix, f),
606-
base_path=suite.base_path,
607-
optional_out=suite.optional_out,
608-
native_sep=suite.native_sep):
609-
if suite.filter(case):
610-
case.name = add_test_name_suffix(case.name, suite.test_name_suffix)
611-
yield MypyDataCase(case.name, self, case)
628+
yield from parse_test_cases(self, suite, os.path.join(suite.data_prefix, f))
612629

613630

614631
def add_test_name_suffix(name: str, suffix: str) -> str:
@@ -637,47 +654,6 @@ def has_stable_flags(testcase: DataDrivenTestCase) -> bool:
637654
return True
638655

639656

640-
class MypyDataCase(pytest.Item): # type: ignore # inheriting from Any
641-
def __init__(self, name: str, parent: MypyDataSuite, case: DataDrivenTestCase) -> None:
642-
self.skip = False
643-
if name.endswith('-skip'):
644-
self.skip = True
645-
name = name[:-len('-skip')]
646-
647-
super().__init__(name, parent)
648-
self.case = case
649-
650-
def runtest(self) -> None:
651-
if self.skip:
652-
pytest.skip()
653-
suite = self.parent.obj()
654-
suite.update_data = self.config.getoption('--update-data', False)
655-
suite.setup()
656-
suite.run_case(self.case)
657-
658-
def setup(self) -> None:
659-
self.case.setup()
660-
661-
def teardown(self) -> None:
662-
self.case.teardown()
663-
664-
def reportinfo(self) -> Tuple[str, int, str]:
665-
return self.case.file, self.case.line, self.case.name
666-
667-
def repr_failure(self, excinfo: Any) -> str:
668-
if excinfo.errisinstance(SystemExit):
669-
# We assume that before doing exit() (which raises SystemExit) we've printed
670-
# enough context about what happened so that a stack trace is not useful.
671-
# In particular, uncaught exceptions during semantic analysis or type checking
672-
# call exit() and they already print out a stack trace.
673-
excrepr = excinfo.exconly()
674-
else:
675-
self.parent._prunetraceback(excinfo)
676-
excrepr = excinfo.getrepr(style='short')
677-
678-
return "data: {}:{}:\n{}".format(self.case.file, self.case.line, excrepr)
679-
680-
681657
class DataSuite:
682658
# option fields - class variables
683659
files = None # type: List[str]
@@ -699,7 +675,3 @@ def setup(self) -> None:
699675
@abstractmethod
700676
def run_case(self, testcase: DataDrivenTestCase) -> None:
701677
raise NotImplementedError
702-
703-
@classmethod
704-
def filter(cls, testcase: DataDrivenTestCase) -> bool:
705-
return True

0 commit comments

Comments
 (0)