summaryrefslogtreecommitdiffstats
path: root/Lib/test/_test_multiprocessing.py
diff options
context:
space:
mode:
authorGregory P. Smith <greg@krypto.org>2023-07-06 22:46:50 (GMT)
committerGitHub <noreply@github.com>2023-07-06 22:46:50 (GMT)
commitc60df361ce2d734148d503f4a711e67c110fe223 (patch)
tree19092e94465dcef4b6ee032c8bc939eaaf20f727 /Lib/test/_test_multiprocessing.py
parent76fac7bce55302a8e9a524d72f5384fd89e6dfde (diff)
downloadcpython-c60df361ce2d734148d503f4a711e67c110fe223.zip
cpython-c60df361ce2d734148d503f4a711e67c110fe223.tar.gz
cpython-c60df361ce2d734148d503f4a711e67c110fe223.tar.bz2
gh-90876: Restore the ability to import multiprocessing when `sys.executable` is `None` (#106464)
Prevent `multiprocessing.spawn` from failing to *import* in environments where `sys.executable` is `None`. This regressed in 3.11 with the addition of support for path-like objects in multiprocessing. Adds a test decorator to have tests only run when part of test_multiprocessing_spawn to `_test_multiprocessing.py` so we can start to avoid re-running the same not-global-state specific test in all 3 modes when there is no need.
Diffstat (limited to 'Lib/test/_test_multiprocessing.py')
-rw-r--r--Lib/test/_test_multiprocessing.py82
1 files changed, 76 insertions, 6 deletions
diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py
index c101fe9..c1f9487 100644
--- a/Lib/test/_test_multiprocessing.py
+++ b/Lib/test/_test_multiprocessing.py
@@ -13,6 +13,7 @@ import sys
import os
import gc
import errno
+import functools
import signal
import array
import socket
@@ -31,6 +32,7 @@ from test import support
from test.support import hashlib_helper
from test.support import import_helper
from test.support import os_helper
+from test.support import script_helper
from test.support import socket_helper
from test.support import threading_helper
from test.support import warnings_helper
@@ -171,6 +173,59 @@ def check_enough_semaphores():
"to run the test (required: %d)." % nsems_min)
+def only_run_in_spawn_testsuite(reason):
+ """Returns a decorator: raises SkipTest when SM != spawn at test time.
+
+ This can be useful to save overall Python test suite execution time.
+ "spawn" is the universal mode available on all platforms so this limits the
+ decorated test to only execute within test_multiprocessing_spawn.
+
+ This would not be necessary if we refactored our test suite to split things
+ into other test files when they are not start method specific to be rerun
+ under all start methods.
+ """
+
+ def decorator(test_item):
+
+ @functools.wraps(test_item)
+ def spawn_check_wrapper(*args, **kwargs):
+ if (start_method := multiprocessing.get_start_method()) != "spawn":
+ raise unittest.SkipTest(f"{start_method=}, not 'spawn'; {reason}")
+ return test_item(*args, **kwargs)
+
+ return spawn_check_wrapper
+
+ return decorator
+
+
+class TestInternalDecorators(unittest.TestCase):
+ """Logic within a test suite that could errantly skip tests? Test it!"""
+
+ @unittest.skipIf(sys.platform == "win32", "test requires that fork exists.")
+ def test_only_run_in_spawn_testsuite(self):
+ if multiprocessing.get_start_method() != "spawn":
+ raise unittest.SkipTest("only run in test_multiprocessing_spawn.")
+
+ try:
+ @only_run_in_spawn_testsuite("testing this decorator")
+ def return_four_if_spawn():
+ return 4
+ except Exception as err:
+ self.fail(f"expected decorated `def` not to raise; caught {err}")
+
+ orig_start_method = multiprocessing.get_start_method(allow_none=True)
+ try:
+ multiprocessing.set_start_method("spawn", force=True)
+ self.assertEqual(return_four_if_spawn(), 4)
+ multiprocessing.set_start_method("fork", force=True)
+ with self.assertRaises(unittest.SkipTest) as ctx:
+ return_four_if_spawn()
+ self.assertIn("testing this decorator", str(ctx.exception))
+ self.assertIn("start_method=", str(ctx.exception))
+ finally:
+ multiprocessing.set_start_method(orig_start_method, force=True)
+
+
#
# Creates a wrapper for a function which records the time it takes to finish
#
@@ -5815,6 +5870,7 @@ class TestSyncManagerTypes(unittest.TestCase):
class TestNamedResource(unittest.TestCase):
+ @only_run_in_spawn_testsuite("spawn specific test.")
def test_global_named_resource_spawn(self):
#
# gh-90549: Check that global named resources in main module
@@ -5825,22 +5881,18 @@ class TestNamedResource(unittest.TestCase):
with open(testfn, 'w', encoding='utf-8') as f:
f.write(textwrap.dedent('''\
import multiprocessing as mp
-
ctx = mp.get_context('spawn')
-
global_resource = ctx.Semaphore()
-
def submain(): pass
-
if __name__ == '__main__':
p = ctx.Process(target=submain)
p.start()
p.join()
'''))
- rc, out, err = test.support.script_helper.assert_python_ok(testfn)
+ rc, out, err = script_helper.assert_python_ok(testfn)
# on error, err = 'UserWarning: resource_tracker: There appear to
# be 1 leaked semaphore objects to clean up at shutdown'
- self.assertEqual(err, b'')
+ self.assertFalse(err, msg=err.decode('utf-8'))
class MiscTestCase(unittest.TestCase):
@@ -5849,6 +5901,24 @@ class MiscTestCase(unittest.TestCase):
support.check__all__(self, multiprocessing, extra=multiprocessing.__all__,
not_exported=['SUBDEBUG', 'SUBWARNING'])
+ @only_run_in_spawn_testsuite("avoids redundant testing.")
+ def test_spawn_sys_executable_none_allows_import(self):
+ # Regression test for a bug introduced in
+ # https://github.com/python/cpython/issues/90876 that caused an
+ # ImportError in multiprocessing when sys.executable was None.
+ # This can be true in embedded environments.
+ rc, out, err = script_helper.assert_python_ok(
+ "-c",
+ """if 1:
+ import sys
+ sys.executable = None
+ assert "multiprocessing" not in sys.modules, "already imported!"
+ import multiprocessing
+ import multiprocessing.spawn # This should not fail\n""",
+ )
+ self.assertEqual(rc, 0)
+ self.assertFalse(err, msg=err.decode('utf-8'))
+
#
# Mixins