Skip to content

Commit 676f2d6

Browse files
committed
Split mypy.myunit package into modules
Part of python#908
1 parent e7af203 commit 676f2d6

File tree

9 files changed

+336
-397
lines changed

9 files changed

+336
-397
lines changed

mypy/myunit/__init__.py

Lines changed: 1 addition & 395 deletions
Original file line numberDiff line numberDiff line change
@@ -1,395 +1 @@
1-
import importlib
2-
import os
3-
import sys
4-
import re
5-
import tempfile
6-
import time
7-
import traceback
8-
9-
from typing import List, Tuple, Any, Callable, Union, cast
10-
11-
12-
"""
13-
Public interfaces to this module:
14-
- python -m mypy.myunit and scripts/myunit.
15-
- TestCase can be subclassed:
16-
- TestCase.set_up can be overridden.
17-
- TestCase.tear_down can be overridden.
18-
- TestCase.run can be overridden.
19-
- Suite can be subclassed:
20-
- Suite.cases can be overridden.
21-
- Suite.test* functions will be collected otherwise.
22-
- Suite.set_up can be overridden.
23-
- SkipTestCaseException can be raised within a test to cause a skip.
24-
- AssertionFailure can be raised with a message that will be printed.
25-
- UPDATE_TESTCASES and APPEND_TESTCASES are mutable globals.
26-
- assert_equal can be used to fail.
27-
- assert_true can be used to fail.
28-
- assert_false can be used to fail.
29-
30-
All other APIs are private, in particular:
31-
- Suite being able to contain other Suite is not public.
32-
- All the details of how tests are actually collected and run.
33-
"""
34-
35-
36-
# TODO remove global state
37-
is_verbose = False
38-
is_quiet = False
39-
patterns = [] # type: List[str]
40-
times = [] # type: List[Tuple[float, str]]
41-
APPEND_TESTCASES = ''
42-
UPDATE_TESTCASES = False
43-
44-
45-
class AssertionFailure(Exception):
46-
"""Exception used to signal skipped test cases."""
47-
def __init__(self, s: str = None) -> None:
48-
if s:
49-
super().__init__(s)
50-
else:
51-
super().__init__()
52-
53-
54-
class SkipTestCaseException(Exception): pass
55-
56-
57-
def assert_true(b: bool, msg: str = None) -> None:
58-
if not b:
59-
raise AssertionFailure(msg)
60-
61-
62-
def assert_false(b: bool, msg: str = None) -> None:
63-
if b:
64-
raise AssertionFailure(msg)
65-
66-
67-
def good_repr(obj: object) -> str:
68-
if isinstance(obj, str):
69-
if obj.count('\n') > 1:
70-
bits = ["'''\\"]
71-
for line in obj.split('\n'):
72-
# force repr to use ' not ", then cut it off
73-
bits.append(repr('"' + line)[2:-1])
74-
bits[-1] += "'''"
75-
return '\n'.join(bits)
76-
return repr(obj)
77-
78-
79-
def assert_equal(a: object, b: object, fmt: str = '{} != {}') -> None:
80-
if a != b:
81-
raise AssertionFailure(fmt.format(good_repr(a), good_repr(b)))
82-
83-
84-
def assert_not_equal(a: object, b: object, fmt: str = '{} == {}') -> None:
85-
if a == b:
86-
raise AssertionFailure(fmt.format(good_repr(a), good_repr(b)))
87-
88-
89-
def assert_raises(typ: type, *rest: Any) -> None:
90-
"""Usage: assert_raises(exception class[, message], function[, args])
91-
92-
Call function with the given arguments and expect an exception of the given
93-
type.
94-
95-
TODO use overloads for better type checking
96-
"""
97-
# Parse arguments.
98-
msg = None # type: str
99-
if isinstance(rest[0], str) or rest[0] is None:
100-
msg = rest[0]
101-
rest = rest[1:]
102-
f = rest[0]
103-
args = [] # type: List[Any]
104-
if len(rest) > 1:
105-
args = rest[1]
106-
assert len(rest) <= 2
107-
108-
# Perform call and verify the exception.
109-
try:
110-
f(*args)
111-
except Exception as e:
112-
assert_type(typ, e)
113-
if msg:
114-
assert_equal(e.args[0], msg, 'Invalid message {}, expected {}')
115-
else:
116-
raise AssertionFailure('No exception raised')
117-
118-
119-
def assert_type(typ: type, value: object) -> None:
120-
if type(value) != typ:
121-
raise AssertionFailure('Invalid type {}, expected {}'.format(
122-
typename(type(value)), typename(typ)))
123-
124-
125-
def fail() -> None:
126-
raise AssertionFailure()
127-
128-
129-
class TestCase:
130-
def __init__(self, name: str, suite: 'Suite' = None,
131-
func: Callable[[], None] = None) -> None:
132-
self.func = func
133-
self.name = name
134-
self.suite = suite
135-
self.old_cwd = None # type: str
136-
self.tmpdir = None # type: tempfile.TemporaryDirectory
137-
138-
def run(self) -> None:
139-
if self.func:
140-
self.func()
141-
142-
def set_up(self) -> None:
143-
self.old_cwd = os.getcwd()
144-
self.tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-',
145-
dir=os.path.abspath('tmp-test-dirs'))
146-
os.chdir(self.tmpdir.name)
147-
os.mkdir('tmp')
148-
if self.suite:
149-
self.suite.set_up()
150-
151-
def tear_down(self) -> None:
152-
if self.suite:
153-
self.suite.tear_down()
154-
os.chdir(self.old_cwd)
155-
self.tmpdir.cleanup()
156-
self.old_cwd = None
157-
self.tmpdir = None
158-
159-
160-
TestUnion = Union[TestCase, Tuple[str, 'Suite']]
161-
162-
163-
class Suite:
164-
def __init__(self) -> None:
165-
self.prefix = typename(type(self)) + '.'
166-
self._test_cases = [] # type: List[TestUnion]
167-
self.init()
168-
169-
def set_up(self) -> None:
170-
pass
171-
172-
def tear_down(self) -> None:
173-
pass
174-
175-
def init(self) -> None:
176-
for m in dir(self):
177-
if m.startswith('test'):
178-
t = getattr(self, m)
179-
if isinstance(t, Suite):
180-
self.add_test((m + '.', t))
181-
else:
182-
assert callable(t)
183-
self.add_test(TestCase(m, self, t))
184-
185-
def add_test(self, test: TestUnion) -> None:
186-
self._test_cases.append(test)
187-
188-
def cases(self) -> List[TestUnion]:
189-
return self._test_cases[:]
190-
191-
def skip(self) -> None:
192-
raise SkipTestCaseException()
193-
194-
195-
def add_suites_from_module(suites: List[Suite], mod_name: str) -> None:
196-
mod = importlib.import_module(mod_name)
197-
got_suite = False
198-
for suite in mod.__dict__.values():
199-
if isinstance(suite, type) and issubclass(suite, Suite) and suite is not Suite:
200-
got_suite = True
201-
suites.append(cast(Callable[[], Suite], suite)())
202-
if not got_suite:
203-
# Sanity check in case e.g. it uses unittest instead of a myunit.
204-
# The codecs tests do since they need to be python2-compatible.
205-
sys.exit('Test module %s had no test!' % mod_name)
206-
207-
208-
class ListSuite(Suite):
209-
def __init__(self, suites: List[Suite]) -> None:
210-
for suite in suites:
211-
mod_name = type(suite).__module__.replace('.', '_')
212-
mod_name = mod_name.replace('mypy_', '')
213-
mod_name = mod_name.replace('test_', '')
214-
mod_name = mod_name.strip('_').replace('__', '_')
215-
type_name = type(suite).__name__
216-
name = 'test_%s_%s' % (mod_name, type_name)
217-
setattr(self, name, suite)
218-
super().__init__()
219-
220-
221-
def main(args: List[str] = None) -> None:
222-
global patterns, is_verbose, is_quiet
223-
global APPEND_TESTCASES, UPDATE_TESTCASES
224-
if not args:
225-
args = sys.argv[1:]
226-
is_verbose = False
227-
is_quiet = False
228-
suites = [] # type: List[Suite]
229-
patterns = []
230-
i = 0
231-
while i < len(args):
232-
a = args[i]
233-
if a == '-v':
234-
is_verbose = True
235-
elif a == '-q':
236-
is_quiet = True
237-
elif a == '-u':
238-
APPEND_TESTCASES = '.new'
239-
UPDATE_TESTCASES = True
240-
elif a == '-i':
241-
APPEND_TESTCASES = ''
242-
UPDATE_TESTCASES = True
243-
elif a == '-m':
244-
i += 1
245-
if i == len(args):
246-
sys.exit('-m requires an argument')
247-
add_suites_from_module(suites, args[i])
248-
elif not a.startswith('-'):
249-
patterns.append(a)
250-
else:
251-
sys.exit('Usage: python -m mypy.myunit [-v] [-q] [-u | -i]'
252-
+ ' -m test.module [-m test.module ...] [filter ...]')
253-
i += 1
254-
if len(patterns) == 0:
255-
patterns.append('*')
256-
if not suites:
257-
sys.exit('At least one -m argument is required')
258-
259-
t = ListSuite(suites)
260-
num_total, num_fail, num_skip = run_test_recursive(t, 0, 0, 0, '', 0)
261-
262-
skip_msg = ''
263-
if num_skip > 0:
264-
skip_msg = ', {} skipped'.format(num_skip)
265-
266-
if num_fail == 0:
267-
if not is_quiet:
268-
print('%d test cases run%s, all passed.' % (num_total, skip_msg))
269-
print('*** OK ***')
270-
else:
271-
sys.stderr.write('%d/%d test cases failed%s.\n' % (num_fail,
272-
num_total,
273-
skip_msg))
274-
sys.stderr.write('*** FAILURE ***\n')
275-
sys.exit(1)
276-
277-
278-
def run_test_recursive(test: Union[Suite, TestUnion],
279-
num_total: int, num_fail: int, num_skip: int,
280-
prefix: str, depth: int) -> Tuple[int, int, int]:
281-
if isinstance(test, TestCase):
282-
name = prefix + test.name
283-
for pattern in patterns:
284-
if match_pattern(name, pattern):
285-
match = True
286-
break
287-
else:
288-
match = False
289-
if match:
290-
is_fail, is_skip = run_single_test(name, test)
291-
if is_fail: num_fail += 1
292-
if is_skip: num_skip += 1
293-
num_total += 1
294-
else:
295-
suite = None # type: Suite
296-
suite_prefix = ''
297-
if isinstance(test, tuple):
298-
suite_prefix, suite = cast(Tuple[str, Suite], test)
299-
else:
300-
suite = cast(Suite, test)
301-
suite_prefix = suite.prefix
302-
303-
for stest in suite.cases():
304-
new_prefix = prefix
305-
if depth > 0:
306-
new_prefix = prefix + suite_prefix
307-
num_total, num_fail, num_skip = run_test_recursive(
308-
stest, num_total, num_fail, num_skip, new_prefix, depth + 1)
309-
return num_total, num_fail, num_skip
310-
311-
312-
def run_single_test(name: str, test: TestCase) -> Tuple[bool, bool]:
313-
if is_verbose:
314-
sys.stderr.write(name)
315-
sys.stderr.flush()
316-
317-
time0 = time.time()
318-
test.set_up() # FIX: check exceptions
319-
exc_traceback = None # type: Any
320-
try:
321-
test.run()
322-
except Exception:
323-
exc_type, exc_value, exc_traceback = sys.exc_info()
324-
test.tear_down() # FIX: check exceptions
325-
times.append((time.time() - time0, name))
326-
327-
if exc_traceback:
328-
if isinstance(exc_value, SkipTestCaseException):
329-
if is_verbose:
330-
sys.stderr.write(' (skipped)\n')
331-
return False, True
332-
else:
333-
handle_failure(name, exc_type, exc_value, exc_traceback)
334-
return True, False
335-
elif is_verbose:
336-
sys.stderr.write('\n')
337-
338-
return False, False
339-
340-
341-
def handle_failure(name, exc_type, exc_value, exc_traceback) -> None:
342-
# Report failed test case.
343-
if is_verbose:
344-
sys.stderr.write('\n\n')
345-
msg = ''
346-
if exc_value.args and exc_value.args[0]:
347-
msg = ': ' + str(exc_value)
348-
else:
349-
msg = ''
350-
sys.stderr.write('Traceback (most recent call last):\n')
351-
tb = traceback.format_tb(exc_traceback)
352-
tb = clean_traceback(tb)
353-
for s in tb:
354-
sys.stderr.write(s)
355-
exception = typename(exc_type)
356-
sys.stderr.write('{}{}\n\n'.format(exception, msg))
357-
sys.stderr.write('{} failed\n\n'.format(name))
358-
359-
360-
def typename(t: type) -> str:
361-
if '.' in str(t):
362-
return str(t).split('.')[-1].rstrip("'>")
363-
else:
364-
return str(t)[8:-2]
365-
366-
367-
def match_pattern(s: str, p: str) -> bool:
368-
if len(p) == 0:
369-
return len(s) == 0
370-
elif p[0] == '*':
371-
if len(p) == 1:
372-
return True
373-
else:
374-
for i in range(len(s) + 1):
375-
if match_pattern(s[i:], p[1:]):
376-
return True
377-
return False
378-
elif len(s) == 0:
379-
return False
380-
else:
381-
return s[0] == p[0] and match_pattern(s[1:], p[1:])
382-
383-
384-
def clean_traceback(tb: List[str]) -> List[str]:
385-
# Remove clutter from the traceback.
386-
start = 0
387-
for i, s in enumerate(tb):
388-
if '\n test.run()\n' in s or '\n self.func()\n' in s:
389-
start = i + 1
390-
tb = tb[start:]
391-
for f in ['assert_equal', 'assert_not_equal', 'assert_type',
392-
'assert_raises', 'assert_true']:
393-
if tb != [] and ', in {}\n'.format(f) in tb[-1]:
394-
tb = tb[:-1]
395-
return tb
1+
# This page intentionally left blank

0 commit comments

Comments
 (0)