Skip to content

Commit dc52e86

Browse files
elazargJukkaL
authored andcommitted
Refactor test discovery (#3973)
Test discovery logic is duplicated over all the different test suites. This PR refactors this logic to the plugin method collect(), leaving only the choice of configuration in the classes. This refactoring should enable further optimizations in the discovery of specific tests. The argument perform to parse_test_cases() is (already) unused, so it is removed. Same goes for include_path. The core of the changes can be tested by diffing the result of `py.test --collect-only -n0` with the main branch. The only differences should be the execution time and the name of the suite PythonCmdlineSuite which is called PythonEvaluationSuite on master. TestCase is split into BaseTestCase to avoid sharing of fields and methods that's not really shared with myunit tests, such as run(). I believe this change need not last long.
1 parent 38acf4b commit dc52e86

19 files changed

+244
-366
lines changed

mypy/myunit/__init__.py

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import importlib
22
import os
33
import sys
4-
import re
54
import tempfile
65
import time
76
import traceback
87

9-
from typing import List, Tuple, Any, Callable, Union, cast, Optional
10-
from types import TracebackType
8+
from typing import List, Tuple, Any, Callable, Union, cast, Optional, Iterable
9+
from types import TracebackType, MethodType
1110

1211

1312
# TODO remove global state
@@ -105,30 +104,23 @@ def fail() -> None:
105104
raise AssertionFailure()
106105

107106

108-
class TestCase:
109-
def __init__(self, name: str, suite: 'Optional[Suite]' = None,
110-
func: Optional[Callable[[], None]] = None) -> None:
111-
self.func = func
107+
class BaseTestCase:
108+
"""Common base class for _MyUnitTestCase and DataDrivenTestCase.
109+
110+
Handles temporary folder creation and deletion.
111+
"""
112+
def __init__(self, name: str) -> None:
112113
self.name = name
113-
self.suite = suite
114114
self.old_cwd = None # type: Optional[str]
115115
self.tmpdir = None # type: Optional[tempfile.TemporaryDirectory[str]]
116116

117-
def run(self) -> None:
118-
if self.func:
119-
self.func()
120-
121-
def set_up(self) -> None:
117+
def setup(self) -> None:
122118
self.old_cwd = os.getcwd()
123119
self.tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-')
124120
os.chdir(self.tmpdir.name)
125121
os.mkdir('tmp')
126-
if self.suite:
127-
self.suite.set_up()
128122

129-
def tear_down(self) -> None:
130-
if self.suite:
131-
self.suite.tear_down()
123+
def teardown(self) -> None:
132124
assert self.old_cwd is not None and self.tmpdir is not None, \
133125
"test was not properly set up"
134126
os.chdir(self.old_cwd)
@@ -140,35 +132,51 @@ def tear_down(self) -> None:
140132
self.tmpdir = None
141133

142134

135+
class _MyUnitTestCase(BaseTestCase):
136+
"""A concrete, myunit-specific test case, a wrapper around a method to run."""
137+
138+
def __init__(self, name: str, suite: 'Suite', run: Callable[[], None]) -> None:
139+
super().__init__(name)
140+
self.run = run
141+
self.suite = suite
142+
143+
def setup(self) -> None:
144+
super().setup()
145+
self.suite.setup()
146+
147+
def teardown(self) -> None:
148+
self.suite.teardown() # No-op
149+
super().teardown()
150+
151+
143152
class Suite:
144-
def __init__(self) -> None:
145-
self.prefix = typename(type(self)) + '.'
146-
# Each test case is either a TestCase object or (str, function).
147-
self._test_cases = [] # type: List[Any]
148-
self.init()
153+
"""Abstract class for myunit test suites - node in the tree whose leaves are _MyUnitTestCases.
149154
150-
def set_up(self) -> None:
151-
pass
155+
The children `cases` are looked up during __init__, looking for attributes named test_*
156+
they are either no-arg methods or of a pair (name, Suite).
157+
"""
152158

153-
def tear_down(self) -> None:
154-
pass
159+
cases = None # type: Iterable[Union[_MyUnitTestCase, Tuple[str, Suite]]]
155160

156-
def init(self) -> None:
161+
def __init__(self) -> None:
162+
self.prefix = typename(type(self)) + '.'
163+
self.cases = []
157164
for m in dir(self):
158-
if m.startswith('test'):
165+
if m.startswith('test_'):
159166
t = getattr(self, m)
160167
if isinstance(t, Suite):
161-
self.add_test((m + '.', t))
168+
self.cases.append((m + '.', t))
162169
else:
163-
self.add_test(TestCase(m, self, getattr(self, m)))
170+
assert isinstance(t, MethodType)
171+
self.cases.append(_MyUnitTestCase(m, self, t))
164172

165-
def add_test(self, test: Union[TestCase,
166-
Tuple[str, Callable[[], None]],
167-
Tuple[str, 'Suite']]) -> None:
168-
self._test_cases.append(test)
173+
def setup(self) -> None:
174+
"""Set up fixtures"""
175+
pass
169176

170-
def cases(self) -> List[Any]:
171-
return self._test_cases[:]
177+
def teardown(self) -> None:
178+
# This method is not overridden in practice
179+
pass
172180

173181
def skip(self) -> None:
174182
raise SkipTestCaseException()
@@ -250,10 +258,11 @@ def main(args: Optional[List[str]] = None) -> None:
250258
sys.exit(1)
251259

252260

253-
def run_test_recursive(test: Any, num_total: int, num_fail: int, num_skip: int,
261+
def run_test_recursive(test: Union[_MyUnitTestCase, Tuple[str, Suite], ListSuite],
262+
num_total: int, num_fail: int, num_skip: int,
254263
prefix: str, depth: int) -> Tuple[int, int, int]:
255-
"""The first argument may be TestCase, Suite or (str, Suite)."""
256-
if isinstance(test, TestCase):
264+
"""The first argument may be _MyUnitTestCase, Suite or (str, Suite)."""
265+
if isinstance(test, _MyUnitTestCase):
257266
name = prefix + test.name
258267
for pattern in patterns:
259268
if match_pattern(name, pattern):
@@ -275,7 +284,7 @@ def run_test_recursive(test: Any, num_total: int, num_fail: int, num_skip: int,
275284
suite = test
276285
suite_prefix = test.prefix
277286

278-
for stest in suite.cases():
287+
for stest in suite.cases:
279288
new_prefix = prefix
280289
if depth > 0:
281290
new_prefix = prefix + suite_prefix
@@ -284,22 +293,22 @@ def run_test_recursive(test: Any, num_total: int, num_fail: int, num_skip: int,
284293
return num_total, num_fail, num_skip
285294

286295

287-
def run_single_test(name: str, test: Any) -> Tuple[bool, bool]:
296+
def run_single_test(name: str, test: _MyUnitTestCase) -> Tuple[bool, bool]:
288297
if is_verbose:
289298
sys.stderr.write(name)
290299
sys.stderr.flush()
291300

292301
time0 = time.time()
293-
test.set_up() # FIX: check exceptions
294-
exc_traceback = None # type: Any
302+
test.setup() # FIX: check exceptions
303+
exc_traceback = None # type: Optional[TracebackType]
295304
try:
296305
test.run()
297306
except BaseException as e:
298307
if isinstance(e, KeyboardInterrupt):
299308
raise
300309
exc_type, exc_value, exc_traceback = sys.exc_info()
301310
finally:
302-
test.tear_down()
311+
test.teardown()
303312
times.append((time.time() - time0, name))
304313

305314
if exc_traceback:

mypy/test/config.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import os
21
import os.path
32

4-
import typing
5-
6-
73
this_file_dir = os.path.dirname(os.path.realpath(__file__))
84
PREFIX = os.path.dirname(os.path.dirname(this_file_dir))
95

0 commit comments

Comments
 (0)