Skip to content

Commit e08b70f

Browse files
gh-108927: Fix removing testing modules from sys.modules (GH-108952)
It breaks import machinery if the test module has submodules used in other tests.
1 parent c718ab9 commit e08b70f

File tree

8 files changed

+67
-9
lines changed

8 files changed

+67
-9
lines changed

Lib/test/libregrtest/main.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def run_tests_sequentially(self, runtests) -> None:
311311
else:
312312
tracer = None
313313

314-
save_modules = sys.modules.keys()
314+
save_modules = set(sys.modules)
315315

316316
jobs = runtests.get_jobs()
317317
if jobs is not None:
@@ -335,10 +335,18 @@ def run_tests_sequentially(self, runtests) -> None:
335335

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

338-
# Unload the newly imported modules (best effort finalization)
339-
for module in sys.modules.keys():
340-
if module not in save_modules and module.startswith("test."):
341-
support.unload(module)
338+
# Unload the newly imported test modules (best effort finalization)
339+
new_modules = [module for module in sys.modules
340+
if module not in save_modules and
341+
module.startswith(("test.", "test_"))]
342+
for module in new_modules:
343+
sys.modules.pop(module, None)
344+
# Remove the attribute of the parent module.
345+
parent, _, name = module.rpartition('.')
346+
try:
347+
delattr(sys.modules[parent], name)
348+
except (KeyError, AttributeError):
349+
pass
342350

343351
if result.must_stop(self.fail_fast, self.fail_env_changed):
344352
break

Lib/test/libregrtest/single.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,6 @@ def _load_run_test(result: TestResult, runtests: RunTests) -> None:
122122
# Load the test module and run the tests.
123123
test_name = result.test_name
124124
module_name = abs_module_name(test_name, runtests.test_dir)
125-
126-
# Remove the module from sys.module to reload it if it was already imported
127-
sys.modules.pop(module_name, None)
128-
129125
test_mod = importlib.import_module(module_name)
130126

131127
if hasattr(test_mod, "test_main"):
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sys
2+
import unittest
3+
import test_regrtest_b.util
4+
5+
class Test(unittest.TestCase):
6+
def test(self):
7+
test_regrtest_b.util # does not fail
8+
self.assertIn('test_regrtest_a', sys.modules)
9+
self.assertIs(sys.modules['test_regrtest_b'], test_regrtest_b)
10+
self.assertIs(sys.modules['test_regrtest_b.util'], test_regrtest_b.util)
11+
self.assertNotIn('test_regrtest_c', sys.modules)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import sys
2+
import unittest
3+
4+
class Test(unittest.TestCase):
5+
def test(self):
6+
self.assertNotIn('test_regrtest_a', sys.modules)
7+
self.assertIn('test_regrtest_b', sys.modules)
8+
self.assertNotIn('test_regrtest_b.util', sys.modules)
9+
self.assertNotIn('test_regrtest_c', sys.modules)

Lib/test/regrtestdata/import_from_tests/test_regrtest_b/util.py

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sys
2+
import unittest
3+
import test_regrtest_b.util
4+
5+
class Test(unittest.TestCase):
6+
def test(self):
7+
test_regrtest_b.util # does not fail
8+
self.assertNotIn('test_regrtest_a', sys.modules)
9+
self.assertIs(sys.modules['test_regrtest_b'], test_regrtest_b)
10+
self.assertIs(sys.modules['test_regrtest_b.util'], test_regrtest_b.util)
11+
self.assertIn('test_regrtest_c', sys.modules)

Lib/test/test_regrtest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,25 @@ def test_dev_mode(self):
20312031
self.check_executed_tests(output, tests,
20322032
stats=len(tests), parallel=True)
20332033

2034+
def test_unload_tests(self):
2035+
# Test that unloading test modules does not break tests
2036+
# that import from other tests.
2037+
# The test execution order matters for this test.
2038+
# Both test_regrtest_a and test_regrtest_c which are executed before
2039+
# and after test_regrtest_b import a submodule from the test_regrtest_b
2040+
# package and use it in testing. test_regrtest_b itself does not import
2041+
# that submodule.
2042+
# Previously test_regrtest_c failed because test_regrtest_b.util in
2043+
# sys.modules was left after test_regrtest_a (making the import
2044+
# statement no-op), but new test_regrtest_b without the util attribute
2045+
# was imported for test_regrtest_b.
2046+
testdir = os.path.join(os.path.dirname(__file__),
2047+
'regrtestdata', 'import_from_tests')
2048+
tests = [f'test_regrtest_{name}' for name in ('a', 'b', 'c')]
2049+
args = ['-Wd', '-E', '-bb', '-m', 'test', '--testdir=%s' % testdir, *tests]
2050+
output = self.run_python(args)
2051+
self.check_executed_tests(output, tests, stats=3)
2052+
20342053
def check_add_python_opts(self, option):
20352054
# --fast-ci and --slow-ci add "-u -W default -bb -E" options to Python
20362055
code = textwrap.dedent(r"""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed order dependence in running tests in the same process
2+
when a test that has submodules (e.g. test_importlib) follows a test that
3+
imports its submodule (e.g. test_importlib.util) and precedes a test
4+
(e.g. test_unittest or test_compileall) that uses that submodule.

0 commit comments

Comments
 (0)