blob: 2caa7511ed76e24bcfce855b9ddf0ba6ecb3d445 [file] [log] [blame]
#!/usr/bin/env python3
"""Mypy test runner."""
if False:
import typing
if True:
# When this is run as a script, `typing` is not available yet.
import sys
from os.path import join, isdir
def get_versions(): # type: () -> typing.List[str]
major = sys.version_info[0]
minor = sys.version_info[1]
if major == 2:
return ['2.7']
else:
# generates list of python versions to use.
# For Python2, this is only [2.7].
# Otherwise, it is [3.4, 3.3, 3.2, 3.1, 3.0].
return ['%d.%d' % (major, i) for i in range(minor, -1, -1)]
sys.path[0:0] = [v for v in [join('lib-typing', v) for v in get_versions()] if isdir(v)]
# Now `typing` is available.
from typing import Dict, List, Optional, Set, Iterable
from mypy.waiter import Waiter, LazySubprocess
from mypy import util
from mypy.test.config import test_data_prefix
from mypy.test.testpythoneval import python_eval_files, python_34_eval_files
import itertools
import os
import re
# Ideally, all tests would be `discover`able so that they can be driven
# (and parallelized) by an external test driver.
class Driver:
def __init__(self, whitelist: List[str], blacklist: List[str],
arglist: List[str], verbosity: int, parallel_limit: int,
xfail: List[str], coverage: bool) -> None:
self.whitelist = whitelist
self.blacklist = blacklist
self.arglist = arglist
self.verbosity = verbosity
self.waiter = Waiter(verbosity=verbosity, limit=parallel_limit, xfail=xfail)
self.versions = get_versions()
self.cwd = os.getcwd()
self.mypy = os.path.join(self.cwd, 'scripts', 'mypy')
self.env = dict(os.environ)
self.coverage = coverage
def prepend_path(self, name: str, paths: List[str]) -> None:
old_val = self.env.get(name)
paths = [p for p in paths if isdir(p)]
if not paths:
return
if old_val is not None:
new_val = ':'.join(itertools.chain(paths, [old_val]))
else:
new_val = ':'.join(paths)
self.env[name] = new_val
def allow(self, name: str) -> bool:
if any(f in name for f in self.whitelist):
if not any(f in name for f in self.blacklist):
if self.verbosity >= 2:
print('SELECT #%d %s' % (len(self.waiter.queue), name))
return True
if self.verbosity >= 3:
print('OMIT %s' % name)
return False
def add_mypy_cmd(self, name: str, mypy_args: List[str], cwd: Optional[str] = None) -> None:
full_name = 'check %s' % name
if not self.allow(full_name):
return
args = [sys.executable, self.mypy] + mypy_args
args.append('--show-traceback')
self.waiter.add(LazySubprocess(full_name, args, cwd=cwd, env=self.env))
def add_mypy(self, name: str, *args: str, cwd: Optional[str] = None) -> None:
self.add_mypy_cmd(name, list(args), cwd=cwd)
def add_mypy_modules(self, name: str, modules: Iterable[str],
cwd: Optional[str] = None) -> None:
args = list(itertools.chain(*(['-m', mod] for mod in modules)))
self.add_mypy_cmd(name, args, cwd=cwd)
def add_mypy_package(self, name: str, packagename: str, *flags: str) -> None:
self.add_mypy_cmd(name, ['-p', packagename] + list(flags))
def add_mypy_string(self, name: str, *args: str, cwd: Optional[str] = None) -> None:
self.add_mypy_cmd(name, ['-c'] + list(args), cwd=cwd)
def add_pytest(self, name: str, pytest_args: List[str], coverage: bool = False) -> None:
full_name = 'pytest %s' % name
if not self.allow(full_name):
return
if coverage and self.coverage:
args = [sys.executable, '-m', 'pytest', '--cov=mypy'] + pytest_args
else:
args = [sys.executable, '-m', 'pytest'] + pytest_args
self.waiter.add(LazySubprocess(full_name, args, env=self.env))
def add_python(self, name: str, *args: str, cwd: Optional[str] = None) -> None:
name = 'run %s' % name
if not self.allow(name):
return
largs = list(args)
largs[0:0] = [sys.executable]
env = self.env
self.waiter.add(LazySubprocess(name, largs, cwd=cwd, env=env))
def add_python_mod(self, name: str, *args: str, cwd: Optional[str] = None,
coverage: bool = False) -> None:
name = 'run %s' % name
if not self.allow(name):
return
largs = list(args)
if coverage and self.coverage:
largs[0:0] = ['coverage', 'run', '-m']
else:
largs[0:0] = [sys.executable, '-m']
env = self.env
self.waiter.add(LazySubprocess(name, largs, cwd=cwd, env=env))
def add_python_string(self, name: str, *args: str, cwd: Optional[str] = None) -> None:
name = 'run %s' % name
if not self.allow(name):
return
largs = list(args)
largs[0:0] = [sys.executable, '-c']
env = self.env
self.waiter.add(LazySubprocess(name, largs, cwd=cwd, env=env))
def add_python2(self, name: str, *args: str, cwd: Optional[str] = None) -> None:
name = 'run2 %s' % name
if not self.allow(name):
return
largs = list(args)
python2 = util.try_find_python2_interpreter()
assert python2, "Couldn't find a Python 2.7 interpreter"
largs[0:0] = [python2]
env = self.env
self.waiter.add(LazySubprocess(name, largs, cwd=cwd, env=env))
def add_flake8(self, cwd: Optional[str] = None) -> None:
name = 'lint'
if not self.allow(name):
return
largs = ['flake8', '-j{}'.format(self.waiter.limit)]
env = self.env
self.waiter.add(LazySubprocess(name, largs, cwd=cwd, env=env))
def list_tasks(self) -> None:
for id, task in enumerate(self.waiter.queue):
print('{id}:{task}'.format(id=id, task=task.name))
def add_basic(driver: Driver) -> None:
if False:
driver.add_mypy('file setup.py', 'setup.py')
driver.add_mypy('file runtests.py', 'runtests.py')
driver.add_mypy('legacy entry script', 'scripts/mypy')
driver.add_mypy('legacy myunit script', 'scripts/myunit')
# needs typed_ast installed:
driver.add_mypy('fast-parse', '--fast-parse', 'test-data/samples/hello.py')
def add_selftypecheck(driver: Driver) -> None:
driver.add_mypy_package('package mypy', 'mypy', '--fast-parser',
'--config-file', 'mypy_self_check.ini')
driver.add_mypy_package('package mypy', 'mypy', '--fast-parser',
'--config-file', 'mypy_strict_optional.ini')
def find_files(base: str, prefix: str = '', suffix: str = '') -> List[str]:
return [join(root, f)
for root, dirs, files in os.walk(base)
for f in files
if f.startswith(prefix) and f.endswith(suffix)]
def file_to_module(file: str) -> str:
rv = os.path.splitext(file)[0].replace(os.sep, '.')
if rv.endswith('.__init__'):
rv = rv[:-len('.__init__')]
return rv
def add_imports(driver: Driver) -> None:
# Make sure each module can be imported originally.
# There is currently a bug in mypy where a module can pass typecheck
# because of *implicit* imports from other modules.
for f in find_files('mypy', suffix='.py'):
mod = file_to_module(f)
if not mod.endswith('.__main__'):
driver.add_python_string('import %s' % mod, 'import %s' % mod)
PYTEST_FILES = [os.path.join('mypy', 'test', '{}.py'.format(name)) for name in [
'testcheck', 'testextensions',
]]
def add_pytest(driver: Driver) -> None:
for f in PYTEST_FILES:
driver.add_pytest(f, [f] + driver.arglist, True)
def add_myunit(driver: Driver) -> None:
for f in find_files('mypy', prefix='test', suffix='.py'):
mod = file_to_module(f)
if mod in ('mypy.test.testpythoneval', 'mypy.test.testcmdline'):
# Run Python evaluation integration tests and command-line
# parsing tests separately since they are much slower than
# proper unit tests.
pass
elif f in PYTEST_FILES:
# This module has been converted to pytest; don't try to use myunit.
pass
else:
driver.add_python_mod('unit-test %s' % mod, 'mypy.myunit', '-m', mod,
*driver.arglist, coverage=True)
def add_pythoneval(driver: Driver) -> None:
cases = set()
case_re = re.compile(r'^\[case ([^\]]+)\]$')
for file in python_eval_files + python_34_eval_files:
with open(os.path.join(test_data_prefix, file), 'r') as f:
for line in f:
m = case_re.match(line)
if m:
case_name = m.group(1)
assert case_name[:4] == 'test'
cases.add(case_name[4:5])
for prefix in sorted(cases):
driver.add_python_mod(
'eval-test-' + prefix,
'mypy.myunit',
'-m',
'mypy.test.testpythoneval',
'test_testpythoneval_PythonEvaluationSuite.test' + prefix + '*',
*driver.arglist,
coverage=True
)
def add_cmdline(driver: Driver) -> None:
driver.add_python_mod('cmdline-test', 'mypy.myunit',
'-m', 'mypy.test.testcmdline', *driver.arglist,
coverage=True)
def add_stubs(driver: Driver) -> None:
# We only test each module in the one version mypy prefers to find.
# TODO: test stubs for other versions, especially Python 2 stubs.
modules = set() # type: Set[str]
modules.add('typing')
# TODO: This should also test Python 2, and pass pyversion accordingly.
for version in ["2and3", "3", "3.3", "3.4", "3.5"]:
for stub_type in ['builtins', 'stdlib', 'third_party']:
stubdir = join('typeshed', stub_type, version)
for f in find_files(stubdir, suffix='.pyi'):
module = file_to_module(f[len(stubdir) + 1:])
modules.add(module)
driver.add_mypy_modules('stubs', sorted(modules))
def add_stdlibsamples(driver: Driver) -> None:
seen = set() # type: Set[str]
for version in driver.versions:
stdlibsamples_dir = join(driver.cwd, 'test-data', 'stdlib-samples', version)
modules = [] # type: List[str]
for f in find_files(stdlibsamples_dir, prefix='test_', suffix='.py'):
module = file_to_module(f[len(stdlibsamples_dir) + 1:])
if module not in seen:
seen.add(module)
modules.append(module)
if modules:
driver.add_mypy_modules('stdlibsamples (%s)' % (version,), modules,
cwd=stdlibsamples_dir)
def add_samples(driver: Driver) -> None:
for f in find_files(os.path.join('test-data', 'samples'), suffix='.py'):
driver.add_mypy('file %s' % f, f, '--fast-parser')
def usage(status: int) -> None:
print('Usage: %s [-h | -v | -q | [-x] FILTER | -a ARG] ... [-- FILTER ...]' % sys.argv[0])
print()
print('Run mypy tests. If given no arguments, run all tests.')
print()
print('Examples:')
print(' %s unit-test (run unit tests only)' % sys.argv[0])
print(' %s unit-test -a "*tuple*"' % sys.argv[0])
print(' (run all unit tests with "tuple" in test name)')
print()
print('Options:')
print(' -h, --help show this help')
print(' -v, --verbose increase driver verbosity')
print(' -q, --quiet decrease driver verbosity')
print(' -jN run N tasks at once (default: one per CPU)')
print(' -a, --argument ARG pass an argument to myunit tasks')
print(' (-v: verbose; glob pattern: filter by test name)')
print(' -l, --list list included tasks (after filtering) and exit')
print(' FILTER include tasks matching FILTER')
print(' -x, --exclude FILTER exclude tasks matching FILTER')
print(' -c, --coverage calculate code coverage while running tests')
print(' -- treat all remaining arguments as positional')
sys.exit(status)
def sanity() -> None:
paths = os.getenv('PYTHONPATH')
if paths is None:
return
failed = False
for p in paths.split(os.pathsep):
if not os.path.isabs(p):
print('Relative PYTHONPATH entry %r' % p)
failed = True
if failed:
print('Please use absolute so that chdir() tests can work.')
print('Cowardly refusing to continue.')
sys.exit(1)
def main() -> None:
sanity()
verbosity = 0
parallel_limit = 0
whitelist = [] # type: List[str]
blacklist = [] # type: List[str]
arglist = [] # type: List[str]
list_only = False
coverage = False
allow_opts = True
curlist = whitelist
for a in sys.argv[1:]:
if curlist is not arglist and allow_opts and a.startswith('-'):
if curlist is not whitelist:
break
if a == '--':
allow_opts = False
elif a == '-v' or a == '--verbose':
verbosity += 1
elif a == '-q' or a == '--quiet':
verbosity -= 1
elif a.startswith('-j'):
try:
parallel_limit = int(a[2:])
except ValueError:
usage(1)
elif a == '-x' or a == '--exclude':
curlist = blacklist
elif a == '-a' or a == '--argument':
curlist = arglist
elif a == '-l' or a == '--list':
list_only = True
elif a == '-c' or a == '--coverage':
coverage = True
elif a == '-h' or a == '--help':
usage(0)
else:
usage(1)
else:
curlist.append(a)
curlist = whitelist
if curlist is blacklist:
sys.exit('-x must be followed by a filter')
if curlist is arglist:
sys.exit('-a must be followed by an argument')
# empty string is a substring of all names
if not whitelist:
whitelist.append('')
driver = Driver(whitelist=whitelist, blacklist=blacklist, arglist=arglist,
verbosity=verbosity, parallel_limit=parallel_limit, xfail=[], coverage=coverage)
driver.prepend_path('PATH', [join(driver.cwd, 'scripts')])
driver.prepend_path('MYPYPATH', [driver.cwd])
driver.prepend_path('PYTHONPATH', [driver.cwd])
driver.prepend_path('PYTHONPATH', [join(driver.cwd, 'lib-typing', v) for v in driver.versions])
add_pythoneval(driver)
add_cmdline(driver)
add_basic(driver)
add_selftypecheck(driver)
add_pytest(driver)
add_myunit(driver)
add_imports(driver)
add_stubs(driver)
add_stdlibsamples(driver)
add_samples(driver)
driver.add_flake8()
if list_only:
driver.list_tasks()
return
exit_code = driver.waiter.run()
if verbosity >= 1:
times = driver.waiter.times2 if verbosity >= 2 else driver.waiter.times1
times_sortable = ((t, tp) for (tp, t) in times.items())
for total_time, test_type in sorted(times_sortable, reverse=True):
print('total time in %s: %f' % (test_type, total_time))
sys.exit(exit_code)
if __name__ == '__main__':
main()