Skip to content

[3.11] gh-108927: Fix removing testing modules from sys.modules (GH-108952) #112712

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

Merged
merged 1 commit into from
Dec 4, 2023
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
18 changes: 13 additions & 5 deletions Lib/test/libregrtest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def run_tests_sequentially(self, runtests):
else:
tracer = None

save_modules = sys.modules.keys()
save_modules = set(sys.modules)

jobs = runtests.get_jobs()
if jobs is not None:
Expand All @@ -330,10 +330,18 @@ def run_tests_sequentially(self, runtests):

result = self.run_test(test_name, runtests, tracer)

# Unload the newly imported modules (best effort finalization)
for module in sys.modules.keys():
if module not in save_modules and module.startswith("test."):
support.unload(module)
# Unload the newly imported test modules (best effort finalization)
new_modules = [module for module in sys.modules
if module not in save_modules and
module.startswith(("test.", "test_"))]
for module in new_modules:
sys.modules.pop(module, None)
# Remove the attribute of the parent module.
parent, _, name = module.rpartition('.')
try:
delattr(sys.modules[parent], name)
except (KeyError, AttributeError):
pass

if result.must_stop(self.fail_fast, self.fail_env_changed):
break
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/libregrtest/single.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ def _load_run_test(result: TestResult, runtests: RunTests) -> None:
# Load the test module and run the tests.
test_name = result.test_name
module_name = abs_module_name(test_name, runtests.test_dir)

# Remove the module from sys.module to reload it if it was already imported
sys.modules.pop(module_name, None)

test_mod = importlib.import_module(module_name)

if hasattr(test_mod, "test_main"):
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/regrtestdata/import_from_tests/test_regrtest_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sys
import unittest
import test_regrtest_b.util

class Test(unittest.TestCase):
def test(self):
test_regrtest_b.util # does not fail
self.assertIn('test_regrtest_a', sys.modules)
self.assertIs(sys.modules['test_regrtest_b'], test_regrtest_b)
self.assertIs(sys.modules['test_regrtest_b.util'], test_regrtest_b.util)
self.assertNotIn('test_regrtest_c', sys.modules)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import sys
import unittest

class Test(unittest.TestCase):
def test(self):
self.assertNotIn('test_regrtest_a', sys.modules)
self.assertIn('test_regrtest_b', sys.modules)
self.assertNotIn('test_regrtest_b.util', sys.modules)
self.assertNotIn('test_regrtest_c', sys.modules)
Empty file.
11 changes: 11 additions & 0 deletions Lib/test/regrtestdata/import_from_tests/test_regrtest_c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sys
import unittest
import test_regrtest_b.util

class Test(unittest.TestCase):
def test(self):
test_regrtest_b.util # does not fail
self.assertNotIn('test_regrtest_a', sys.modules)
self.assertIs(sys.modules['test_regrtest_b'], test_regrtest_b)
self.assertIs(sys.modules['test_regrtest_b.util'], test_regrtest_b.util)
self.assertIn('test_regrtest_c', sys.modules)
19 changes: 19 additions & 0 deletions Lib/test/test_regrtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,25 @@ def test_dev_mode(self):
self.check_executed_tests(output, tests,
stats=len(tests), parallel=True)

def test_unload_tests(self):
# Test that unloading test modules does not break tests
# that import from other tests.
# The test execution order matters for this test.
# Both test_regrtest_a and test_regrtest_c which are executed before
# and after test_regrtest_b import a submodule from the test_regrtest_b
# package and use it in testing. test_regrtest_b itself does not import
# that submodule.
# Previously test_regrtest_c failed because test_regrtest_b.util in
# sys.modules was left after test_regrtest_a (making the import
# statement no-op), but new test_regrtest_b without the util attribute
# was imported for test_regrtest_b.
testdir = os.path.join(os.path.dirname(__file__),
'regrtestdata', 'import_from_tests')
tests = [f'test_regrtest_{name}' for name in ('a', 'b', 'c')]
args = ['-Wd', '-E', '-bb', '-m', 'test', '--testdir=%s' % testdir, *tests]
output = self.run_python(args)
self.check_executed_tests(output, tests, stats=3)

def check_add_python_opts(self, option):
# --fast-ci and --slow-ci add "-u -W default -bb -E" options to Python
code = textwrap.dedent(r"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed order dependence in running tests in the same process
when a test that has submodules (e.g. test_importlib) follows a test that
imports its submodule (e.g. test_importlib.util) and precedes a test
(e.g. test_unittest or test_compileall) that uses that submodule.