Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-59215: unittest: _top_level_dir is incorrectly persisted #15242

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1880,8 +1880,8 @@ Loading and running tests
Python identifiers) will be loaded.

All test modules must be importable from the top level of the project. If
the start directory is not the top level directory then the top level
directory must be specified separately.
the start directory is not the top level directory then *top_level_dir*
must be specified separately.

If importing a module fails, for example due to a syntax error, then
this will be recorded as a single error and discovery will continue. If
Expand All @@ -1901,9 +1901,11 @@ Loading and running tests
package.

The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. *top_level_dir* is stored so
``load_tests`` does not need to pass this argument in to
``loader.discover()``.
packages can continue discovery themselves.

*top_level_dir* is stored internally, and used as a default to any
nested calls to ``discover()``. That is, if a package's ``load_tests``
calls ``loader.discover()``, it does not need to pass this argument.

*start_dir* can be a dotted module name as well as a directory.

Expand All @@ -1930,6 +1932,9 @@ Loading and running tests
*start_dir* can not be a :term:`namespace packages <namespace package>`.
It has been broken since Python 3.7 and Python 3.11 officially remove it.

.. versionchanged:: 3.13
*top_level_dir* is only stored for the duration of *discover* call.


The following attributes of a :class:`TestLoader` can be configured either by
subclassing or assignment on an instance:
Expand Down
26 changes: 25 additions & 1 deletion Lib/test/test_unittest/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,34 @@ def _find_tests(start_dir, pattern):
top_level_dir = os.path.abspath('/foo/bar')
start_dir = os.path.abspath('/foo/bar/baz')
self.assertEqual(suite, "['tests']")
self.assertEqual(loader._top_level_dir, top_level_dir)
self.assertEqual(loader._top_level_dir, os.path.abspath('/foo'))
self.assertEqual(_find_tests_args, [(start_dir, 'pattern')])
self.assertIn(top_level_dir, sys.path)

def test_discover_should_not_persist_top_level_dir_between_calls(self):
original_isfile = os.path.isfile
original_isdir = os.path.isdir
original_sys_path = sys.path[:]
def restore():
os.path.isfile = original_isfile
os.path.isdir = original_isdir
sys.path[:] = original_sys_path
self.addCleanup(restore)

os.path.isfile = lambda path: True
os.path.isdir = lambda path: True
loader = unittest.TestLoader()
loader.suiteClass = str
dir = '/foo/bar'
top_level_dir = '/foo'

loader.discover(dir, top_level_dir=top_level_dir)
self.assertEqual(loader._top_level_dir, None)

loader._top_level_dir = dir2 = '/previous/dir'
loader.discover(dir, top_level_dir=top_level_dir)
self.assertEqual(loader._top_level_dir, dir2)

def test_discover_start_dir_is_package_calls_package_load_tests(self):
# This test verifies that the package load_tests in a package is indeed
# invoked when the start_dir is a package (and not the top level).
Expand Down
2 changes: 2 additions & 0 deletions Lib/unittest/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
Paths are sorted before being imported to ensure reproducible execution
order even on filesystems with non-alphabetical ordering like ext3/4.
"""
original_top_level_dir = self._top_level_dir
set_implicit_top = False
if top_level_dir is None and self._top_level_dir is not None:
# make top_level_dir optional if called from load_tests in a package
Expand Down Expand Up @@ -307,6 +308,7 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
raise ImportError('Start directory is not importable: %r' % start_dir)

tests = list(self._find_tests(start_dir, pattern))
self._top_level_dir = original_top_level_dir
return self.suiteClass(tests)

def _get_directory_containing_module(self, module_name):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:meth:`unittest.TestLoader.discover` now saves the original value of
``unittest.TestLoader._top_level_dir`` and restores it at the end of the
call.
Loading