mirror of
https://github.com/python/cpython.git
synced 2025-08-30 21:48:47 +00:00
- Issue #16662: load_tests() is now unconditionally run when it is present in
a package's __init__.py. TestLoader.loadTestsFromModule() still accepts use_load_tests, but it is deprecated and ignored. A new keyword-only attribute `pattern` is added and documented. Patch given by Robert Collins, tweaked by Barry Warsaw.
This commit is contained in:
parent
238f5aa6a5
commit
d78742a260
5 changed files with 472 additions and 61 deletions
|
@ -1561,7 +1561,7 @@ Loading and running tests
|
||||||
:class:`testCaseClass`.
|
:class:`testCaseClass`.
|
||||||
|
|
||||||
|
|
||||||
.. method:: loadTestsFromModule(module)
|
.. method:: loadTestsFromModule(module, pattern=None)
|
||||||
|
|
||||||
Return a suite of all tests cases contained in the given module. This
|
Return a suite of all tests cases contained in the given module. This
|
||||||
method searches *module* for classes derived from :class:`TestCase` and
|
method searches *module* for classes derived from :class:`TestCase` and
|
||||||
|
@ -1578,11 +1578,18 @@ Loading and running tests
|
||||||
|
|
||||||
If a module provides a ``load_tests`` function it will be called to
|
If a module provides a ``load_tests`` function it will be called to
|
||||||
load the tests. This allows modules to customize test loading.
|
load the tests. This allows modules to customize test loading.
|
||||||
This is the `load_tests protocol`_.
|
This is the `load_tests protocol`_. The *pattern* argument is passed as
|
||||||
|
the third argument to ``load_tests``.
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
Support for ``load_tests`` added.
|
Support for ``load_tests`` added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.5
|
||||||
|
The undocumented and unofficial *use_load_tests* default argument is
|
||||||
|
deprecated and ignored, although it is still accepted for backward
|
||||||
|
compatibility. The method also now accepts a keyword-only argument
|
||||||
|
*pattern* which is passed to ``load_tests`` as the third argument.
|
||||||
|
|
||||||
|
|
||||||
.. method:: loadTestsFromName(name, module=None)
|
.. method:: loadTestsFromName(name, module=None)
|
||||||
|
|
||||||
|
@ -1634,18 +1641,18 @@ Loading and running tests
|
||||||
the start directory is not the top level directory then the top level
|
the start directory is not the top level directory then the top level
|
||||||
directory must be specified separately.
|
directory must be specified separately.
|
||||||
|
|
||||||
If importing a module fails, for example due to a syntax error, then this
|
If importing a module fails, for example due to a syntax error, then
|
||||||
will be recorded as a single error and discovery will continue. If the
|
this will be recorded as a single error and discovery will continue. If
|
||||||
import failure is due to :exc:`SkipTest` being raised, it will be recorded
|
the import failure is due to :exc:`SkipTest` being raised, it will be
|
||||||
as a skip instead of an error.
|
recorded as a skip instead of an error.
|
||||||
|
|
||||||
If a test package name (directory with :file:`__init__.py`) matches the
|
If a package (a directory containing a file named :file:`__init__.py`) is
|
||||||
pattern then the package will be checked for a ``load_tests``
|
found, the package will be checked for a ``load_tests`` function. If this
|
||||||
function. If this exists then it will be called with *loader*, *tests*,
|
exists then it will be called with *loader*, *tests*, *pattern*.
|
||||||
*pattern*.
|
|
||||||
|
|
||||||
If load_tests exists then discovery does *not* recurse into the package,
|
If ``load_tests`` exists then discovery does *not* recurse into the
|
||||||
``load_tests`` is responsible for loading all tests in the package.
|
package, ``load_tests`` is responsible for loading all tests in the
|
||||||
|
package.
|
||||||
|
|
||||||
The pattern is deliberately not stored as a loader attribute so that
|
The pattern is deliberately not stored as a loader attribute so that
|
||||||
packages can continue discovery themselves. *top_level_dir* is stored so
|
packages can continue discovery themselves. *top_level_dir* is stored so
|
||||||
|
@ -1664,6 +1671,11 @@ Loading and running tests
|
||||||
the same even if the underlying file system's ordering is not
|
the same even if the underlying file system's ordering is not
|
||||||
dependent on file name.
|
dependent on file name.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.5
|
||||||
|
Found packages are now checked for ``load_tests`` regardless of
|
||||||
|
whether their path matches *pattern*, because it is impossible for
|
||||||
|
a package name to match the default pattern.
|
||||||
|
|
||||||
|
|
||||||
The following attributes of a :class:`TestLoader` can be configured either by
|
The following attributes of a :class:`TestLoader` can be configured either by
|
||||||
subclassing or assignment on an instance:
|
subclassing or assignment on an instance:
|
||||||
|
@ -2032,7 +2044,10 @@ test runs or test discovery by implementing a function called ``load_tests``.
|
||||||
If a test module defines ``load_tests`` it will be called by
|
If a test module defines ``load_tests`` it will be called by
|
||||||
:meth:`TestLoader.loadTestsFromModule` with the following arguments::
|
:meth:`TestLoader.loadTestsFromModule` with the following arguments::
|
||||||
|
|
||||||
load_tests(loader, standard_tests, None)
|
load_tests(loader, standard_tests, pattern)
|
||||||
|
|
||||||
|
where *pattern* is passed straight through from ``loadTestsFromModule``. It
|
||||||
|
defaults to ``None``.
|
||||||
|
|
||||||
It should return a :class:`TestSuite`.
|
It should return a :class:`TestSuite`.
|
||||||
|
|
||||||
|
@ -2054,21 +2069,12 @@ A typical ``load_tests`` function that loads tests from a specific set of
|
||||||
suite.addTests(tests)
|
suite.addTests(tests)
|
||||||
return suite
|
return suite
|
||||||
|
|
||||||
If discovery is started, either from the command line or by calling
|
If discovery is started in a directory containing a package, either from the
|
||||||
:meth:`TestLoader.discover`, with a pattern that matches a package
|
command line or by calling :meth:`TestLoader.discover`, then the package
|
||||||
name then the package :file:`__init__.py` will be checked for ``load_tests``.
|
:file:`__init__.py` will be checked for ``load_tests``. If that function does
|
||||||
|
not exist, discovery will recurse into the package as though it were just
|
||||||
.. note::
|
another directory. Otherwise, discovery of the package's tests will be left up
|
||||||
|
to ``load_tests`` which is called with the following arguments::
|
||||||
The default pattern is ``'test*.py'``. This matches all Python files
|
|
||||||
that start with ``'test'`` but *won't* match any test directories.
|
|
||||||
|
|
||||||
A pattern like ``'test*'`` will match test packages as well as
|
|
||||||
modules.
|
|
||||||
|
|
||||||
If the package :file:`__init__.py` defines ``load_tests`` then it will be
|
|
||||||
called and discovery not continued into the package. ``load_tests``
|
|
||||||
is called with the following arguments::
|
|
||||||
|
|
||||||
load_tests(loader, standard_tests, pattern)
|
load_tests(loader, standard_tests, pattern)
|
||||||
|
|
||||||
|
@ -2087,6 +2093,11 @@ continue (and potentially modify) test discovery. A 'do nothing'
|
||||||
standard_tests.addTests(package_tests)
|
standard_tests.addTests(package_tests)
|
||||||
return standard_tests
|
return standard_tests
|
||||||
|
|
||||||
|
.. versionchanged:: 3.5
|
||||||
|
Discovery no longer checks package names for matching *pattern* due to the
|
||||||
|
impossibility of package names matching the default pattern.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Class and Module Fixtures
|
Class and Module Fixtures
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
|
@ -6,6 +6,7 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
import functools
|
import functools
|
||||||
|
import warnings
|
||||||
|
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
|
@ -70,8 +71,27 @@ class TestLoader(object):
|
||||||
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
|
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
|
||||||
return loaded_suite
|
return loaded_suite
|
||||||
|
|
||||||
def loadTestsFromModule(self, module, use_load_tests=True):
|
# XXX After Python 3.5, remove backward compatibility hacks for
|
||||||
|
# use_load_tests deprecation via *args and **kws. See issue 16662.
|
||||||
|
def loadTestsFromModule(self, module, *args, pattern=None, **kws):
|
||||||
"""Return a suite of all tests cases contained in the given module"""
|
"""Return a suite of all tests cases contained in the given module"""
|
||||||
|
# This method used to take an undocumented and unofficial
|
||||||
|
# use_load_tests argument. For backward compatibility, we still
|
||||||
|
# accept the argument (which can also be the first position) but we
|
||||||
|
# ignore it and issue a deprecation warning if it's present.
|
||||||
|
if len(args) == 1 or 'use_load_tests' in kws:
|
||||||
|
warnings.warn('use_load_tests is deprecated and ignored',
|
||||||
|
DeprecationWarning)
|
||||||
|
kws.pop('use_load_tests', None)
|
||||||
|
if len(args) > 1:
|
||||||
|
raise TypeError('loadTestsFromModule() takes 1 positional argument but {} were given'.format(len(args)))
|
||||||
|
if len(kws) != 0:
|
||||||
|
# Since the keyword arguments are unsorted (see PEP 468), just
|
||||||
|
# pick the alphabetically sorted first argument to complain about,
|
||||||
|
# if multiple were given. At least the error message will be
|
||||||
|
# predictable.
|
||||||
|
complaint = sorted(kws)[0]
|
||||||
|
raise TypeError("loadTestsFromModule() got an unexpected keyword argument '{}'".format(complaint))
|
||||||
tests = []
|
tests = []
|
||||||
for name in dir(module):
|
for name in dir(module):
|
||||||
obj = getattr(module, name)
|
obj = getattr(module, name)
|
||||||
|
@ -80,9 +100,9 @@ class TestLoader(object):
|
||||||
|
|
||||||
load_tests = getattr(module, 'load_tests', None)
|
load_tests = getattr(module, 'load_tests', None)
|
||||||
tests = self.suiteClass(tests)
|
tests = self.suiteClass(tests)
|
||||||
if use_load_tests and load_tests is not None:
|
if load_tests is not None:
|
||||||
try:
|
try:
|
||||||
return load_tests(self, tests, None)
|
return load_tests(self, tests, pattern)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _make_failed_load_tests(module.__name__, e,
|
return _make_failed_load_tests(module.__name__, e,
|
||||||
self.suiteClass)
|
self.suiteClass)
|
||||||
|
@ -325,7 +345,7 @@ class TestLoader(object):
|
||||||
msg = ("%r module incorrectly imported from %r. Expected %r. "
|
msg = ("%r module incorrectly imported from %r. Expected %r. "
|
||||||
"Is this module globally installed?")
|
"Is this module globally installed?")
|
||||||
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, pattern=pattern)
|
||||||
elif os.path.isdir(full_path):
|
elif os.path.isdir(full_path):
|
||||||
if (not namespace and
|
if (not namespace and
|
||||||
not os.path.isfile(os.path.join(full_path, '__init__.py'))):
|
not os.path.isfile(os.path.join(full_path, '__init__.py'))):
|
||||||
|
@ -333,26 +353,27 @@ class TestLoader(object):
|
||||||
|
|
||||||
load_tests = None
|
load_tests = None
|
||||||
tests = None
|
tests = None
|
||||||
if fnmatch(path, pattern):
|
name = self._get_name_from_path(full_path)
|
||||||
# only check load_tests if the package directory itself matches the filter
|
try:
|
||||||
name = self._get_name_from_path(full_path)
|
|
||||||
package = self._get_module_from_name(name)
|
package = self._get_module_from_name(name)
|
||||||
|
except case.SkipTest as e:
|
||||||
|
yield _make_skipped_test(name, e, self.suiteClass)
|
||||||
|
except:
|
||||||
|
yield _make_failed_import_test(name, self.suiteClass)
|
||||||
|
else:
|
||||||
load_tests = getattr(package, 'load_tests', None)
|
load_tests = getattr(package, 'load_tests', None)
|
||||||
tests = self.loadTestsFromModule(package, use_load_tests=False)
|
tests = self.loadTestsFromModule(package, pattern=pattern)
|
||||||
|
|
||||||
if load_tests is None:
|
|
||||||
if tests is not None:
|
if tests is not None:
|
||||||
# tests loaded from package file
|
# tests loaded from package file
|
||||||
yield tests
|
yield tests
|
||||||
|
|
||||||
|
if load_tests is not None:
|
||||||
|
# loadTestsFromModule(package) has load_tests for us.
|
||||||
|
continue
|
||||||
# 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)
|
namespace=namespace)
|
||||||
else:
|
|
||||||
try:
|
|
||||||
yield load_tests(self, tests, pattern)
|
|
||||||
except Exception as e:
|
|
||||||
yield _make_failed_load_tests(package.__name__, e,
|
|
||||||
self.suiteClass)
|
|
||||||
|
|
||||||
defaultTestLoader = TestLoader()
|
defaultTestLoader = TestLoader()
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,13 @@ class TestDiscovery(unittest.TestCase):
|
||||||
self.addCleanup(restore_isfile)
|
self.addCleanup(restore_isfile)
|
||||||
|
|
||||||
loader._get_module_from_name = lambda path: path + ' module'
|
loader._get_module_from_name = lambda path: path + ' module'
|
||||||
loader.loadTestsFromModule = lambda module: module + ' tests'
|
orig_load_tests = loader.loadTestsFromModule
|
||||||
|
def loadTestsFromModule(module, pattern=None):
|
||||||
|
# This is where load_tests is called.
|
||||||
|
base = orig_load_tests(module, pattern=pattern)
|
||||||
|
return base + [module + ' tests']
|
||||||
|
loader.loadTestsFromModule = loadTestsFromModule
|
||||||
|
loader.suiteClass = lambda thing: thing
|
||||||
|
|
||||||
top_level = os.path.abspath('/foo')
|
top_level = os.path.abspath('/foo')
|
||||||
loader._top_level_dir = top_level
|
loader._top_level_dir = top_level
|
||||||
|
@ -76,9 +82,9 @@ class TestDiscovery(unittest.TestCase):
|
||||||
|
|
||||||
# The test suites found should be sorted alphabetically for reliable
|
# The test suites found should be sorted alphabetically for reliable
|
||||||
# execution order.
|
# execution order.
|
||||||
expected = [name + ' module tests' for name in
|
expected = [[name + ' module tests'] for name in
|
||||||
('test1', 'test2')]
|
('test1', 'test2', 'test_dir')]
|
||||||
expected.extend([('test_dir.%s' % name) + ' module tests' for name in
|
expected.extend([[('test_dir.%s' % name) + ' module tests'] for name in
|
||||||
('test3', 'test4')])
|
('test3', 'test4')])
|
||||||
self.assertEqual(suite, expected)
|
self.assertEqual(suite, expected)
|
||||||
|
|
||||||
|
@ -116,34 +122,204 @@ class TestDiscovery(unittest.TestCase):
|
||||||
if os.path.basename(path) == 'test_directory':
|
if os.path.basename(path) == 'test_directory':
|
||||||
def load_tests(loader, tests, pattern):
|
def load_tests(loader, tests, pattern):
|
||||||
self.load_tests_args.append((loader, tests, pattern))
|
self.load_tests_args.append((loader, tests, pattern))
|
||||||
return 'load_tests'
|
return [self.path + ' load_tests']
|
||||||
self.load_tests = load_tests
|
self.load_tests = load_tests
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.path == other.path
|
return self.path == other.path
|
||||||
|
|
||||||
loader._get_module_from_name = lambda name: Module(name)
|
loader._get_module_from_name = lambda name: Module(name)
|
||||||
def loadTestsFromModule(module, use_load_tests):
|
orig_load_tests = loader.loadTestsFromModule
|
||||||
if use_load_tests:
|
def loadTestsFromModule(module, pattern=None):
|
||||||
raise self.failureException('use_load_tests should be False for packages')
|
# This is where load_tests is called.
|
||||||
return module.path + ' module tests'
|
base = orig_load_tests(module, pattern=pattern)
|
||||||
|
return base + [module.path + ' module tests']
|
||||||
loader.loadTestsFromModule = loadTestsFromModule
|
loader.loadTestsFromModule = loadTestsFromModule
|
||||||
|
loader.suiteClass = lambda thing: thing
|
||||||
|
|
||||||
loader._top_level_dir = '/foo'
|
loader._top_level_dir = '/foo'
|
||||||
# this time no '.py' on the pattern so that it can match
|
# this time no '.py' on the pattern so that it can match
|
||||||
# a test package
|
# a test package
|
||||||
suite = list(loader._find_tests('/foo', 'test*'))
|
suite = list(loader._find_tests('/foo', 'test*'))
|
||||||
|
|
||||||
# We should have loaded tests from the test_directory package by calling load_tests
|
# We should have loaded tests from the a_directory and test_directory2
|
||||||
# and directly from the test_directory2 package
|
# directly and via load_tests for the test_directory package, which
|
||||||
|
# still calls the baseline module loader.
|
||||||
self.assertEqual(suite,
|
self.assertEqual(suite,
|
||||||
['load_tests', 'test_directory2' + ' module tests'])
|
[['a_directory module tests'],
|
||||||
|
['test_directory load_tests',
|
||||||
|
'test_directory module tests'],
|
||||||
|
['test_directory2 module tests']])
|
||||||
|
|
||||||
|
|
||||||
# The test module paths should be sorted for reliable execution order
|
# The test module paths should be sorted for reliable execution order
|
||||||
self.assertEqual(Module.paths, ['test_directory', 'test_directory2'])
|
self.assertEqual(Module.paths,
|
||||||
|
['a_directory', 'test_directory', 'test_directory2'])
|
||||||
|
|
||||||
|
# load_tests should have been called once with loader, tests and pattern
|
||||||
|
# (but there are no tests in our stub module itself, so thats [] at the
|
||||||
|
# time of call.
|
||||||
|
self.assertEqual(Module.load_tests_args,
|
||||||
|
[(loader, [], 'test*')])
|
||||||
|
|
||||||
|
def test_find_tests_default_calls_package_load_tests(self):
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
|
||||||
|
original_listdir = os.listdir
|
||||||
|
def restore_listdir():
|
||||||
|
os.listdir = original_listdir
|
||||||
|
original_isfile = os.path.isfile
|
||||||
|
def restore_isfile():
|
||||||
|
os.path.isfile = original_isfile
|
||||||
|
original_isdir = os.path.isdir
|
||||||
|
def restore_isdir():
|
||||||
|
os.path.isdir = original_isdir
|
||||||
|
|
||||||
|
directories = ['a_directory', 'test_directory', 'test_directory2']
|
||||||
|
path_lists = [directories, [], [], []]
|
||||||
|
os.listdir = lambda path: path_lists.pop(0)
|
||||||
|
self.addCleanup(restore_listdir)
|
||||||
|
|
||||||
|
os.path.isdir = lambda path: True
|
||||||
|
self.addCleanup(restore_isdir)
|
||||||
|
|
||||||
|
os.path.isfile = lambda path: os.path.basename(path) not in directories
|
||||||
|
self.addCleanup(restore_isfile)
|
||||||
|
|
||||||
|
class Module(object):
|
||||||
|
paths = []
|
||||||
|
load_tests_args = []
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self.paths.append(path)
|
||||||
|
if os.path.basename(path) == 'test_directory':
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.load_tests_args.append((loader, tests, pattern))
|
||||||
|
return [self.path + ' load_tests']
|
||||||
|
self.load_tests = load_tests
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.path == other.path
|
||||||
|
|
||||||
|
loader._get_module_from_name = lambda name: Module(name)
|
||||||
|
orig_load_tests = loader.loadTestsFromModule
|
||||||
|
def loadTestsFromModule(module, pattern=None):
|
||||||
|
# This is where load_tests is called.
|
||||||
|
base = orig_load_tests(module, pattern=pattern)
|
||||||
|
return base + [module.path + ' module tests']
|
||||||
|
loader.loadTestsFromModule = loadTestsFromModule
|
||||||
|
loader.suiteClass = lambda thing: thing
|
||||||
|
|
||||||
|
loader._top_level_dir = '/foo'
|
||||||
|
# this time no '.py' on the pattern so that it can match
|
||||||
|
# a test package
|
||||||
|
suite = list(loader._find_tests('/foo', 'test*.py'))
|
||||||
|
|
||||||
|
# We should have loaded tests from the a_directory and test_directory2
|
||||||
|
# directly and via load_tests for the test_directory package, which
|
||||||
|
# still calls the baseline module loader.
|
||||||
|
self.assertEqual(suite,
|
||||||
|
[['a_directory module tests'],
|
||||||
|
['test_directory load_tests',
|
||||||
|
'test_directory module tests'],
|
||||||
|
['test_directory2 module tests']])
|
||||||
|
# The test module paths should be sorted for reliable execution order
|
||||||
|
self.assertEqual(Module.paths,
|
||||||
|
['a_directory', 'test_directory', 'test_directory2'])
|
||||||
|
|
||||||
|
|
||||||
# load_tests should have been called once with loader, tests and pattern
|
# load_tests should have been called once with loader, tests and pattern
|
||||||
self.assertEqual(Module.load_tests_args,
|
self.assertEqual(Module.load_tests_args,
|
||||||
[(loader, 'test_directory' + ' module tests', 'test*')])
|
[(loader, [], 'test*.py')])
|
||||||
|
|
||||||
|
def test_find_tests_customise_via_package_pattern(self):
|
||||||
|
# This test uses the example 'do-nothing' load_tests from
|
||||||
|
# https://docs.python.org/3/library/unittest.html#load-tests-protocol
|
||||||
|
# to make sure that that actually works.
|
||||||
|
# Housekeeping
|
||||||
|
original_listdir = os.listdir
|
||||||
|
def restore_listdir():
|
||||||
|
os.listdir = original_listdir
|
||||||
|
self.addCleanup(restore_listdir)
|
||||||
|
original_isfile = os.path.isfile
|
||||||
|
def restore_isfile():
|
||||||
|
os.path.isfile = original_isfile
|
||||||
|
self.addCleanup(restore_isfile)
|
||||||
|
original_isdir = os.path.isdir
|
||||||
|
def restore_isdir():
|
||||||
|
os.path.isdir = original_isdir
|
||||||
|
self.addCleanup(restore_isdir)
|
||||||
|
self.addCleanup(sys.path.remove, '/foo')
|
||||||
|
|
||||||
|
# Test data: we expect the following:
|
||||||
|
# a listdir to find our package, and a isfile and isdir check on it.
|
||||||
|
# a module-from-name call to turn that into a module
|
||||||
|
# followed by load_tests.
|
||||||
|
# then our load_tests will call discover() which is messy
|
||||||
|
# but that finally chains into find_tests again for the child dir -
|
||||||
|
# which is why we don't have a infinite loop.
|
||||||
|
# We expect to see:
|
||||||
|
# the module load tests for both package and plain module called,
|
||||||
|
# and the plain module result nested by the package module load_tests
|
||||||
|
# indicating that it was processed and could have been mutated.
|
||||||
|
vfs = {'/foo': ['my_package'],
|
||||||
|
'/foo/my_package': ['__init__.py', 'test_module.py']}
|
||||||
|
def list_dir(path):
|
||||||
|
return list(vfs[path])
|
||||||
|
os.listdir = list_dir
|
||||||
|
os.path.isdir = lambda path: not path.endswith('.py')
|
||||||
|
os.path.isfile = lambda path: path.endswith('.py')
|
||||||
|
|
||||||
|
class Module(object):
|
||||||
|
paths = []
|
||||||
|
load_tests_args = []
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self.paths.append(path)
|
||||||
|
if path.endswith('test_module'):
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.load_tests_args.append((loader, tests, pattern))
|
||||||
|
return [self.path + ' load_tests']
|
||||||
|
else:
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.load_tests_args.append((loader, tests, pattern))
|
||||||
|
# top level directory cached on loader instance
|
||||||
|
__file__ = '/foo/my_package/__init__.py'
|
||||||
|
this_dir = os.path.dirname(__file__)
|
||||||
|
pkg_tests = loader.discover(
|
||||||
|
start_dir=this_dir, pattern=pattern)
|
||||||
|
return [self.path + ' load_tests', tests
|
||||||
|
] + pkg_tests
|
||||||
|
self.load_tests = load_tests
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.path == other.path
|
||||||
|
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
loader._get_module_from_name = lambda name: Module(name)
|
||||||
|
loader.suiteClass = lambda thing: thing
|
||||||
|
|
||||||
|
loader._top_level_dir = '/foo'
|
||||||
|
# this time no '.py' on the pattern so that it can match
|
||||||
|
# a test package
|
||||||
|
suite = list(loader._find_tests('/foo', 'test*.py'))
|
||||||
|
|
||||||
|
# We should have loaded tests from both my_package and
|
||||||
|
# my_pacakge.test_module, and also run the load_tests hook in both.
|
||||||
|
# (normally this would be nested TestSuites.)
|
||||||
|
self.assertEqual(suite,
|
||||||
|
[['my_package load_tests', [],
|
||||||
|
['my_package.test_module load_tests']]])
|
||||||
|
# Parents before children.
|
||||||
|
self.assertEqual(Module.paths,
|
||||||
|
['my_package', 'my_package.test_module'])
|
||||||
|
|
||||||
|
# load_tests should have been called twice with loader, tests and pattern
|
||||||
|
self.assertEqual(Module.load_tests_args,
|
||||||
|
[(loader, [], 'test*.py'),
|
||||||
|
(loader, [], 'test*.py')])
|
||||||
|
|
||||||
def test_discover(self):
|
def test_discover(self):
|
||||||
loader = unittest.TestLoader()
|
loader = unittest.TestLoader()
|
||||||
|
@ -203,6 +379,17 @@ class TestDiscovery(unittest.TestCase):
|
||||||
sys.path[:] = orig_sys_path
|
sys.path[:] = orig_sys_path
|
||||||
self.addCleanup(restore)
|
self.addCleanup(restore)
|
||||||
|
|
||||||
|
def setup_import_issue_package_tests(self, vfs):
|
||||||
|
self.addCleanup(setattr, os, 'listdir', os.listdir)
|
||||||
|
self.addCleanup(setattr, os.path, 'isfile', os.path.isfile)
|
||||||
|
self.addCleanup(setattr, os.path, 'isdir', os.path.isdir)
|
||||||
|
self.addCleanup(sys.path.__setitem__, slice(None), list(sys.path))
|
||||||
|
def list_dir(path):
|
||||||
|
return list(vfs[path])
|
||||||
|
os.listdir = list_dir
|
||||||
|
os.path.isdir = lambda path: not path.endswith('.py')
|
||||||
|
os.path.isfile = lambda path: path.endswith('.py')
|
||||||
|
|
||||||
def test_discover_with_modules_that_fail_to_import(self):
|
def test_discover_with_modules_that_fail_to_import(self):
|
||||||
loader = unittest.TestLoader()
|
loader = unittest.TestLoader()
|
||||||
|
|
||||||
|
@ -216,6 +403,25 @@ class TestDiscovery(unittest.TestCase):
|
||||||
with self.assertRaises(ImportError):
|
with self.assertRaises(ImportError):
|
||||||
test.test_this_does_not_exist()
|
test.test_this_does_not_exist()
|
||||||
|
|
||||||
|
def test_discover_with_init_modules_that_fail_to_import(self):
|
||||||
|
vfs = {'/foo': ['my_package'],
|
||||||
|
'/foo/my_package': ['__init__.py', 'test_module.py']}
|
||||||
|
self.setup_import_issue_package_tests(vfs)
|
||||||
|
import_calls = []
|
||||||
|
def _get_module_from_name(name):
|
||||||
|
import_calls.append(name)
|
||||||
|
raise ImportError("Cannot import Name")
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
loader._get_module_from_name = _get_module_from_name
|
||||||
|
suite = loader.discover('/foo')
|
||||||
|
|
||||||
|
self.assertIn('/foo', sys.path)
|
||||||
|
self.assertEqual(suite.countTestCases(), 1)
|
||||||
|
test = list(list(suite)[0])[0] # extract test from suite
|
||||||
|
with self.assertRaises(ImportError):
|
||||||
|
test.my_package()
|
||||||
|
self.assertEqual(import_calls, ['my_package'])
|
||||||
|
|
||||||
def test_discover_with_module_that_raises_SkipTest_on_import(self):
|
def test_discover_with_module_that_raises_SkipTest_on_import(self):
|
||||||
loader = unittest.TestLoader()
|
loader = unittest.TestLoader()
|
||||||
|
|
||||||
|
@ -232,6 +438,26 @@ class TestDiscovery(unittest.TestCase):
|
||||||
suite.run(result)
|
suite.run(result)
|
||||||
self.assertEqual(len(result.skipped), 1)
|
self.assertEqual(len(result.skipped), 1)
|
||||||
|
|
||||||
|
def test_discover_with_init_module_that_raises_SkipTest_on_import(self):
|
||||||
|
vfs = {'/foo': ['my_package'],
|
||||||
|
'/foo/my_package': ['__init__.py', 'test_module.py']}
|
||||||
|
self.setup_import_issue_package_tests(vfs)
|
||||||
|
import_calls = []
|
||||||
|
def _get_module_from_name(name):
|
||||||
|
import_calls.append(name)
|
||||||
|
raise unittest.SkipTest('skipperoo')
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
loader._get_module_from_name = _get_module_from_name
|
||||||
|
suite = loader.discover('/foo')
|
||||||
|
|
||||||
|
self.assertIn('/foo', sys.path)
|
||||||
|
self.assertEqual(suite.countTestCases(), 1)
|
||||||
|
result = unittest.TestResult()
|
||||||
|
suite.run(result)
|
||||||
|
self.assertEqual(len(result.skipped), 1)
|
||||||
|
self.assertEqual(result.testsRun, 1)
|
||||||
|
self.assertEqual(import_calls, ['my_package'])
|
||||||
|
|
||||||
def test_command_line_handling_parseArgs(self):
|
def test_command_line_handling_parseArgs(self):
|
||||||
program = TestableTestProgram()
|
program = TestableTestProgram()
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,26 @@
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
import warnings
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
# Decorator used in the deprecation tests to reset the warning registry for
|
||||||
|
# test isolation and reproducibility.
|
||||||
|
def warningregistry(func):
|
||||||
|
def wrapper(*args, **kws):
|
||||||
|
missing = object()
|
||||||
|
saved = getattr(warnings, '__warningregistry__', missing).copy()
|
||||||
|
try:
|
||||||
|
return func(*args, **kws)
|
||||||
|
finally:
|
||||||
|
if saved is missing:
|
||||||
|
try:
|
||||||
|
del warnings.__warningregistry__
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
warnings.__warningregistry__ = saved
|
||||||
|
|
||||||
|
|
||||||
class Test_TestLoader(unittest.TestCase):
|
class Test_TestLoader(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -150,6 +167,7 @@ class Test_TestLoader(unittest.TestCase):
|
||||||
|
|
||||||
# Check that loadTestsFromModule honors (or not) a module
|
# Check that loadTestsFromModule honors (or not) a module
|
||||||
# with a load_tests function.
|
# with a load_tests function.
|
||||||
|
@warningregistry
|
||||||
def test_loadTestsFromModule__load_tests(self):
|
def test_loadTestsFromModule__load_tests(self):
|
||||||
m = types.ModuleType('m')
|
m = types.ModuleType('m')
|
||||||
class MyTestCase(unittest.TestCase):
|
class MyTestCase(unittest.TestCase):
|
||||||
|
@ -168,10 +186,139 @@ class Test_TestLoader(unittest.TestCase):
|
||||||
suite = loader.loadTestsFromModule(m)
|
suite = loader.loadTestsFromModule(m)
|
||||||
self.assertIsInstance(suite, unittest.TestSuite)
|
self.assertIsInstance(suite, unittest.TestSuite)
|
||||||
self.assertEqual(load_tests_args, [loader, suite, None])
|
self.assertEqual(load_tests_args, [loader, suite, None])
|
||||||
|
# With Python 3.5, the undocumented and unofficial use_load_tests is
|
||||||
|
# ignored (and deprecated).
|
||||||
|
load_tests_args = []
|
||||||
|
with warnings.catch_warnings(record=False):
|
||||||
|
warnings.simplefilter('never')
|
||||||
|
suite = loader.loadTestsFromModule(m, use_load_tests=False)
|
||||||
|
self.assertEqual(load_tests_args, [loader, suite, None])
|
||||||
|
|
||||||
|
@warningregistry
|
||||||
|
def test_loadTestsFromModule__use_load_tests_deprecated_positional(self):
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter('always')
|
||||||
|
m = types.ModuleType('m')
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
m.testcase_1 = MyTestCase
|
||||||
|
|
||||||
|
load_tests_args = []
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.assertIsInstance(tests, unittest.TestSuite)
|
||||||
|
load_tests_args.extend((loader, tests, pattern))
|
||||||
|
return tests
|
||||||
|
m.load_tests = load_tests
|
||||||
|
# The method still works.
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
# use_load_tests=True as a positional argument.
|
||||||
|
suite = loader.loadTestsFromModule(m, False)
|
||||||
|
self.assertIsInstance(suite, unittest.TestSuite)
|
||||||
|
# load_tests was still called because use_load_tests is deprecated
|
||||||
|
# and ignored.
|
||||||
|
self.assertEqual(load_tests_args, [loader, suite, None])
|
||||||
|
# We got a warning.
|
||||||
|
self.assertIs(w[-1].category, DeprecationWarning)
|
||||||
|
self.assertEqual(str(w[-1].message),
|
||||||
|
'use_load_tests is deprecated and ignored')
|
||||||
|
|
||||||
|
@warningregistry
|
||||||
|
def test_loadTestsFromModule__use_load_tests_deprecated_keyword(self):
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter('always')
|
||||||
|
m = types.ModuleType('m')
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
m.testcase_1 = MyTestCase
|
||||||
|
|
||||||
|
load_tests_args = []
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.assertIsInstance(tests, unittest.TestSuite)
|
||||||
|
load_tests_args.extend((loader, tests, pattern))
|
||||||
|
return tests
|
||||||
|
m.load_tests = load_tests
|
||||||
|
# The method still works.
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
suite = loader.loadTestsFromModule(m, use_load_tests=False)
|
||||||
|
self.assertIsInstance(suite, unittest.TestSuite)
|
||||||
|
# load_tests was still called because use_load_tests is deprecated
|
||||||
|
# and ignored.
|
||||||
|
self.assertEqual(load_tests_args, [loader, suite, None])
|
||||||
|
# We got a warning.
|
||||||
|
self.assertIs(w[-1].category, DeprecationWarning)
|
||||||
|
self.assertEqual(str(w[-1].message),
|
||||||
|
'use_load_tests is deprecated and ignored')
|
||||||
|
|
||||||
|
def test_loadTestsFromModule__too_many_positional_args(self):
|
||||||
|
m = types.ModuleType('m')
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
m.testcase_1 = MyTestCase
|
||||||
|
|
||||||
load_tests_args = []
|
load_tests_args = []
|
||||||
suite = loader.loadTestsFromModule(m, use_load_tests=False)
|
def load_tests(loader, tests, pattern):
|
||||||
self.assertEqual(load_tests_args, [])
|
self.assertIsInstance(tests, unittest.TestSuite)
|
||||||
|
load_tests_args.extend((loader, tests, pattern))
|
||||||
|
return tests
|
||||||
|
m.load_tests = load_tests
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
loader.loadTestsFromModule(m, False, 'testme.*')
|
||||||
|
self.assertEqual(type(cm.exception), TypeError)
|
||||||
|
# The error message names the first bad argument alphabetically,
|
||||||
|
# however use_load_tests (which sorts first) is ignored.
|
||||||
|
self.assertEqual(
|
||||||
|
str(cm.exception),
|
||||||
|
'loadTestsFromModule() takes 1 positional argument but 2 were given')
|
||||||
|
|
||||||
|
@warningregistry
|
||||||
|
def test_loadTestsFromModule__use_load_tests_other_bad_keyword(self):
|
||||||
|
m = types.ModuleType('m')
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
m.testcase_1 = MyTestCase
|
||||||
|
|
||||||
|
load_tests_args = []
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.assertIsInstance(tests, unittest.TestSuite)
|
||||||
|
load_tests_args.extend((loader, tests, pattern))
|
||||||
|
return tests
|
||||||
|
m.load_tests = load_tests
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter('never')
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
loader.loadTestsFromModule(
|
||||||
|
m, use_load_tests=False, very_bad=True, worse=False)
|
||||||
|
self.assertEqual(type(cm.exception), TypeError)
|
||||||
|
# The error message names the first bad argument alphabetically,
|
||||||
|
# however use_load_tests (which sorts first) is ignored.
|
||||||
|
self.assertEqual(
|
||||||
|
str(cm.exception),
|
||||||
|
"loadTestsFromModule() got an unexpected keyword argument 'very_bad'")
|
||||||
|
|
||||||
|
def test_loadTestsFromModule__pattern(self):
|
||||||
|
m = types.ModuleType('m')
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
m.testcase_1 = MyTestCase
|
||||||
|
|
||||||
|
load_tests_args = []
|
||||||
|
def load_tests(loader, tests, pattern):
|
||||||
|
self.assertIsInstance(tests, unittest.TestSuite)
|
||||||
|
load_tests_args.extend((loader, tests, pattern))
|
||||||
|
return tests
|
||||||
|
m.load_tests = load_tests
|
||||||
|
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
suite = loader.loadTestsFromModule(m, pattern='testme.*')
|
||||||
|
self.assertIsInstance(suite, unittest.TestSuite)
|
||||||
|
self.assertEqual(load_tests_args, [loader, suite, 'testme.*'])
|
||||||
|
|
||||||
def test_loadTestsFromModule__faulty_load_tests(self):
|
def test_loadTestsFromModule__faulty_load_tests(self):
|
||||||
m = types.ModuleType('m')
|
m = types.ModuleType('m')
|
||||||
|
|
|
@ -132,6 +132,12 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #16662: load_tests() is now unconditionally run when it is present in
|
||||||
|
a package's __init__.py. TestLoader.loadTestsFromModule() still accepts
|
||||||
|
use_load_tests, but it is deprecated and ignored. A new keyword-only
|
||||||
|
attribute `pattern` is added and documented. Patch given by Robert Collins,
|
||||||
|
tweaked by Barry Warsaw.
|
||||||
|
|
||||||
- Issue #22226: First letter no longer is stripped from the "status" key in
|
- Issue #22226: First letter no longer is stripped from the "status" key in
|
||||||
the result of Treeview.heading().
|
the result of Treeview.heading().
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue