summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_subprocess.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/test/test_subprocess.py')
-rw-r--r--Lib/test/test_subprocess.py98
1 files changed, 83 insertions, 15 deletions
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index 319bc0d..5eeea54 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -1561,21 +1561,6 @@ class ProcessTestCase(BaseTestCase):
self.assertIsInstance(subprocess.Popen[bytes], types.GenericAlias)
self.assertIsInstance(subprocess.CompletedProcess[str], types.GenericAlias)
- @unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
- "vfork() not enabled by configure.")
- @mock.patch("subprocess._fork_exec")
- def test__use_vfork(self, mock_fork_exec):
- self.assertTrue(subprocess._USE_VFORK) # The default value regardless.
- mock_fork_exec.side_effect = RuntimeError("just testing args")
- with self.assertRaises(RuntimeError):
- subprocess.run([sys.executable, "-c", "pass"])
- mock_fork_exec.assert_called_once()
- self.assertTrue(mock_fork_exec.call_args.args[-1])
- with mock.patch.object(subprocess, '_USE_VFORK', False):
- with self.assertRaises(RuntimeError):
- subprocess.run([sys.executable, "-c", "pass"])
- self.assertFalse(mock_fork_exec.call_args_list[-1].args[-1])
-
class RunFuncTestCase(BaseTestCase):
def run_python(self, code, **kwargs):
@@ -3360,6 +3345,89 @@ class POSIXProcessTestCase(BaseTestCase):
self.assertEqual(out, b'')
self.assertIn(b"preexec_fn not supported at interpreter shutdown", err)
+ @unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
+ "vfork() not enabled by configure.")
+ @mock.patch("subprocess._fork_exec")
+ def test__use_vfork(self, mock_fork_exec):
+ self.assertTrue(subprocess._USE_VFORK) # The default value regardless.
+ mock_fork_exec.side_effect = RuntimeError("just testing args")
+ with self.assertRaises(RuntimeError):
+ subprocess.run([sys.executable, "-c", "pass"])
+ mock_fork_exec.assert_called_once()
+ # NOTE: These assertions are *ugly* as they require the last arg
+ # to remain the have_vfork boolean. We really need to refactor away
+ # from the giant "wall of args" internal C extension API.
+ self.assertTrue(mock_fork_exec.call_args.args[-1])
+ with mock.patch.object(subprocess, '_USE_VFORK', False):
+ with self.assertRaises(RuntimeError):
+ subprocess.run([sys.executable, "-c", "pass"])
+ self.assertFalse(mock_fork_exec.call_args_list[-1].args[-1])
+
+ @unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
+ "vfork() not enabled by configure.")
+ @unittest.skipIf(sys.platform != "linux", "Linux only, requires strace.")
+ def test_vfork_used_when_expected(self):
+ # This is a performance regression test to ensure we default to using
+ # vfork() when possible.
+ strace_binary = "/usr/bin/strace"
+ # The only system calls we are interested in.
+ strace_filter = "--trace=clone,clone2,clone3,fork,vfork,exit,exit_group"
+ true_binary = "/bin/true"
+ strace_command = [strace_binary, strace_filter]
+
+ try:
+ does_strace_work_process = subprocess.run(
+ strace_command + [true_binary],
+ stderr=subprocess.PIPE,
+ stdout=subprocess.DEVNULL,
+ )
+ rc = does_strace_work_process.returncode
+ stderr = does_strace_work_process.stderr
+ except OSError:
+ rc = -1
+ stderr = ""
+ if rc or (b"+++ exited with 0 +++" not in stderr):
+ self.skipTest("strace not found or not working as expected.")
+
+ with self.subTest(name="default_is_vfork"):
+ vfork_result = assert_python_ok(
+ "-c",
+ textwrap.dedent(f"""\
+ import subprocess
+ subprocess.check_call([{true_binary!r}])"""),
+ __run_using_command=strace_command,
+ )
+ # Match both vfork() and clone(..., flags=...|CLONE_VFORK|...)
+ self.assertRegex(vfork_result.err, br"(?i)vfork")
+ # Do NOT check that fork() or other clones did not happen.
+ # If the OS denys the vfork it'll fallback to plain fork().
+
+ # Test that each individual thing that would disable the use of vfork
+ # actually disables it.
+ for sub_name, preamble, sp_kwarg, expect_permission_error in (
+ ("!use_vfork", "subprocess._USE_VFORK = False", "", False),
+ ("preexec", "", "preexec_fn=lambda: None", False),
+ ("setgid", "", f"group={os.getgid()}", True),
+ ("setuid", "", f"user={os.getuid()}", True),
+ ("setgroups", "", "extra_groups=[]", True),
+ ):
+ with self.subTest(name=sub_name):
+ non_vfork_result = assert_python_ok(
+ "-c",
+ textwrap.dedent(f"""\
+ import subprocess
+ {preamble}
+ try:
+ subprocess.check_call(
+ [{true_binary!r}], **dict({sp_kwarg}))
+ except PermissionError:
+ if not {expect_permission_error}:
+ raise"""),
+ __run_using_command=strace_command,
+ )
+ # Ensure neither vfork() or clone(..., flags=...|CLONE_VFORK|...).
+ self.assertNotRegex(non_vfork_result.err, br"(?i)vfork")
+
@unittest.skipUnless(mswindows, "Windows specific tests")
class Win32ProcessTestCase(BaseTestCase):