mirror of https://github.com/python/cpython
Issue 17457: extend test discovery to support namespace packages
This commit is contained in:
parent
8933521b3d
commit
e28bb15054
|
@ -61,8 +61,9 @@ class TestLoader(object):
|
||||||
def loadTestsFromTestCase(self, testCaseClass):
|
def loadTestsFromTestCase(self, testCaseClass):
|
||||||
"""Return a suite of all tests cases contained in testCaseClass"""
|
"""Return a suite of all tests cases contained in testCaseClass"""
|
||||||
if issubclass(testCaseClass, suite.TestSuite):
|
if issubclass(testCaseClass, suite.TestSuite):
|
||||||
raise TypeError("Test cases should not be derived from TestSuite." \
|
raise TypeError("Test cases should not be derived from "
|
||||||
" Maybe you meant to derive from TestCase?")
|
"TestSuite. Maybe you meant to derive from "
|
||||||
|
"TestCase?")
|
||||||
testCaseNames = self.getTestCaseNames(testCaseClass)
|
testCaseNames = self.getTestCaseNames(testCaseClass)
|
||||||
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
|
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
|
||||||
testCaseNames = ['runTest']
|
testCaseNames = ['runTest']
|
||||||
|
@ -200,6 +201,8 @@ class TestLoader(object):
|
||||||
self._top_level_dir = top_level_dir
|
self._top_level_dir = top_level_dir
|
||||||
|
|
||||||
is_not_importable = False
|
is_not_importable = False
|
||||||
|
is_namespace = False
|
||||||
|
tests = []
|
||||||
if os.path.isdir(os.path.abspath(start_dir)):
|
if os.path.isdir(os.path.abspath(start_dir)):
|
||||||
start_dir = os.path.abspath(start_dir)
|
start_dir = os.path.abspath(start_dir)
|
||||||
if start_dir != top_level_dir:
|
if start_dir != top_level_dir:
|
||||||
|
@ -213,14 +216,51 @@ class TestLoader(object):
|
||||||
else:
|
else:
|
||||||
the_module = sys.modules[start_dir]
|
the_module = sys.modules[start_dir]
|
||||||
top_part = start_dir.split('.')[0]
|
top_part = start_dir.split('.')[0]
|
||||||
start_dir = os.path.abspath(os.path.dirname((the_module.__file__)))
|
try:
|
||||||
|
start_dir = os.path.abspath(
|
||||||
|
os.path.dirname((the_module.__file__)))
|
||||||
|
except AttributeError:
|
||||||
|
# look for namespace packages
|
||||||
|
try:
|
||||||
|
spec = the_module.__spec__
|
||||||
|
except AttributeError:
|
||||||
|
spec = None
|
||||||
|
|
||||||
|
if spec and spec.loader is None:
|
||||||
|
if spec.submodule_search_locations is not None:
|
||||||
|
is_namespace = True
|
||||||
|
|
||||||
|
for path in the_module.__path__:
|
||||||
|
if (not set_implicit_top and
|
||||||
|
not path.startswith(top_level_dir)):
|
||||||
|
continue
|
||||||
|
self._top_level_dir = \
|
||||||
|
(path.split(the_module.__name__
|
||||||
|
.replace(".", os.path.sep))[0])
|
||||||
|
tests.extend(self._find_tests(path,
|
||||||
|
pattern,
|
||||||
|
namespace=True))
|
||||||
|
elif the_module.__name__ in sys.builtin_module_names:
|
||||||
|
# builtin module
|
||||||
|
raise TypeError('Can not use builtin modules '
|
||||||
|
'as dotted module names') from None
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'don\'t know how to discover from {!r}'
|
||||||
|
.format(the_module)) from None
|
||||||
|
|
||||||
if set_implicit_top:
|
if set_implicit_top:
|
||||||
self._top_level_dir = self._get_directory_containing_module(top_part)
|
if not is_namespace:
|
||||||
|
self._top_level_dir = \
|
||||||
|
self._get_directory_containing_module(top_part)
|
||||||
|
sys.path.remove(top_level_dir)
|
||||||
|
else:
|
||||||
sys.path.remove(top_level_dir)
|
sys.path.remove(top_level_dir)
|
||||||
|
|
||||||
if is_not_importable:
|
if is_not_importable:
|
||||||
raise ImportError('Start directory is not importable: %r' % start_dir)
|
raise ImportError('Start directory is not importable: %r' % start_dir)
|
||||||
|
|
||||||
|
if not is_namespace:
|
||||||
tests = list(self._find_tests(start_dir, pattern))
|
tests = list(self._find_tests(start_dir, pattern))
|
||||||
return self.suiteClass(tests)
|
return self.suiteClass(tests)
|
||||||
|
|
||||||
|
@ -254,7 +294,7 @@ class TestLoader(object):
|
||||||
# override this method to use alternative matching strategy
|
# override this method to use alternative matching strategy
|
||||||
return fnmatch(path, pattern)
|
return fnmatch(path, pattern)
|
||||||
|
|
||||||
def _find_tests(self, start_dir, pattern):
|
def _find_tests(self, start_dir, pattern, namespace=False):
|
||||||
"""Used by discovery. Yields test suites it loads."""
|
"""Used by discovery. Yields test suites it loads."""
|
||||||
paths = sorted(os.listdir(start_dir))
|
paths = sorted(os.listdir(start_dir))
|
||||||
|
|
||||||
|
@ -287,7 +327,8 @@ class TestLoader(object):
|
||||||
raise ImportError(msg % (mod_name, module_dir, expected_dir))
|
raise ImportError(msg % (mod_name, module_dir, expected_dir))
|
||||||
yield self.loadTestsFromModule(module)
|
yield self.loadTestsFromModule(module)
|
||||||
elif os.path.isdir(full_path):
|
elif os.path.isdir(full_path):
|
||||||
if not os.path.isfile(os.path.join(full_path, '__init__.py')):
|
if (not namespace and
|
||||||
|
not os.path.isfile(os.path.join(full_path, '__init__.py'))):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
load_tests = None
|
load_tests = None
|
||||||
|
@ -304,7 +345,8 @@ class TestLoader(object):
|
||||||
# tests loaded from package file
|
# tests loaded from package file
|
||||||
yield tests
|
yield tests
|
||||||
# recurse into the package
|
# recurse into the package
|
||||||
yield from self._find_tests(full_path, pattern)
|
yield from self._find_tests(full_path, pattern,
|
||||||
|
namespace=namespace)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
yield load_tests(self, tests, pattern)
|
yield load_tests(self, tests, pattern)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
|
import builtins
|
||||||
from test import support
|
from test import support
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -173,7 +175,7 @@ class TestDiscovery(unittest.TestCase):
|
||||||
self.addCleanup(restore_isdir)
|
self.addCleanup(restore_isdir)
|
||||||
|
|
||||||
_find_tests_args = []
|
_find_tests_args = []
|
||||||
def _find_tests(start_dir, pattern):
|
def _find_tests(start_dir, pattern, namespace=None):
|
||||||
_find_tests_args.append((start_dir, pattern))
|
_find_tests_args.append((start_dir, pattern))
|
||||||
return ['tests']
|
return ['tests']
|
||||||
loader._find_tests = _find_tests
|
loader._find_tests = _find_tests
|
||||||
|
@ -436,7 +438,7 @@ class TestDiscovery(unittest.TestCase):
|
||||||
expectedPath = os.path.abspath(os.path.dirname(unittest.test.__file__))
|
expectedPath = os.path.abspath(os.path.dirname(unittest.test.__file__))
|
||||||
|
|
||||||
self.wasRun = False
|
self.wasRun = False
|
||||||
def _find_tests(start_dir, pattern):
|
def _find_tests(start_dir, pattern, namespace=None):
|
||||||
self.wasRun = True
|
self.wasRun = True
|
||||||
self.assertEqual(start_dir, expectedPath)
|
self.assertEqual(start_dir, expectedPath)
|
||||||
return tests
|
return tests
|
||||||
|
@ -446,5 +448,79 @@ class TestDiscovery(unittest.TestCase):
|
||||||
self.assertEqual(suite._tests, tests)
|
self.assertEqual(suite._tests, tests)
|
||||||
|
|
||||||
|
|
||||||
|
def test_discovery_from_dotted_path_builtin_modules(self):
|
||||||
|
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
|
||||||
|
listdir = os.listdir
|
||||||
|
os.listdir = lambda _: ['test_this_does_not_exist.py']
|
||||||
|
isfile = os.path.isfile
|
||||||
|
isdir = os.path.isdir
|
||||||
|
os.path.isdir = lambda _: False
|
||||||
|
orig_sys_path = sys.path[:]
|
||||||
|
def restore():
|
||||||
|
os.path.isfile = isfile
|
||||||
|
os.path.isdir = isdir
|
||||||
|
os.listdir = listdir
|
||||||
|
sys.path[:] = orig_sys_path
|
||||||
|
self.addCleanup(restore)
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
loader.discover('sys')
|
||||||
|
self.assertEqual(str(cm.exception),
|
||||||
|
'Can not use builtin modules '
|
||||||
|
'as dotted module names')
|
||||||
|
|
||||||
|
def test_discovery_from_dotted_namespace_packages(self):
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
|
||||||
|
orig_import = __import__
|
||||||
|
package = types.ModuleType('package')
|
||||||
|
package.__path__ = ['/a', '/b']
|
||||||
|
package.__spec__ = types.SimpleNamespace(
|
||||||
|
loader=None,
|
||||||
|
submodule_search_locations=['/a', '/b']
|
||||||
|
)
|
||||||
|
|
||||||
|
def _import(packagename, *args, **kwargs):
|
||||||
|
sys.modules[packagename] = package
|
||||||
|
return package
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
builtins.__import__ = orig_import
|
||||||
|
self.addCleanup(cleanup)
|
||||||
|
builtins.__import__ = _import
|
||||||
|
|
||||||
|
_find_tests_args = []
|
||||||
|
def _find_tests(start_dir, pattern, namespace=None):
|
||||||
|
_find_tests_args.append((start_dir, pattern))
|
||||||
|
return ['%s/tests' % start_dir]
|
||||||
|
|
||||||
|
loader._find_tests = _find_tests
|
||||||
|
loader.suiteClass = list
|
||||||
|
suite = loader.discover('package')
|
||||||
|
self.assertEqual(suite, ['/a/tests', '/b/tests'])
|
||||||
|
|
||||||
|
def test_discovery_failed_discovery(self):
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
package = types.ModuleType('package')
|
||||||
|
orig_import = __import__
|
||||||
|
|
||||||
|
def _import(packagename, *args, **kwargs):
|
||||||
|
sys.modules[packagename] = package
|
||||||
|
return package
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
builtins.__import__ = orig_import
|
||||||
|
self.addCleanup(cleanup)
|
||||||
|
builtins.__import__ = _import
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
loader.discover('package')
|
||||||
|
self.assertEqual(str(cm.exception),
|
||||||
|
'don\'t know how to discover from {!r}'
|
||||||
|
.format(package))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -479,6 +479,9 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #17457: unittest test discovery now works with namespace packages.
|
||||||
|
Patch by Claudiu Popa.
|
||||||
|
|
||||||
- Issue #18235: Fix the sysconfig variables LDSHARED and BLDSHARED under AIX.
|
- Issue #18235: Fix the sysconfig variables LDSHARED and BLDSHARED under AIX.
|
||||||
Patch by David Edelsohn.
|
Patch by David Edelsohn.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!wing
|
||||||
|
#!version=5.0
|
||||||
|
##################################################################
|
||||||
|
# Wing IDE project file #
|
||||||
|
##################################################################
|
||||||
|
[project attributes]
|
||||||
|
proj.directory-list = [{'dirloc': loc('..'),
|
||||||
|
'excludes': [u'.hg',
|
||||||
|
u'Lib/unittest/__pycache__',
|
||||||
|
u'Lib/unittest/test/__pycache__',
|
||||||
|
u'Lib/__pycache__',
|
||||||
|
u'build',
|
||||||
|
u'Doc/build'],
|
||||||
|
'filter': '*',
|
||||||
|
'include_hidden': False,
|
||||||
|
'recursive': True,
|
||||||
|
'watch_for_changes': True}]
|
||||||
|
proj.file-type = 'shared'
|
Loading…
Reference in New Issue