Skip to content

Commit

Permalink
pythongh-112334: Regression test that vfork is used when expected.
Browse files Browse the repository at this point in the history
  • Loading branch information
gpshead committed Dec 4, 2023
1 parent 304a1b3 commit 86a5548
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 0 deletions.
15 changes: 15 additions & 0 deletions Lib/test/support/script_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,28 @@ def fail(self, cmd_line):
# Executing the interpreter in a subprocess
@support.requires_subprocess()
def run_python_until_end(*args, **env_vars):
"""Used to implement assert_python_*.
*args are the command line flags to pass to the python interpreter.
**env_vars keyword arguments are environment variables to set on the process.
If _run_using_command= is supplied, it must be a list of
command line arguments to prepend to the command line used.
Useful when you want to run another command that should launch the
python interpreter via its own arguments. ["/bin/echo", "--"] for
example could print the unquoted python command line instead of
run it.
"""
env_required = interpreter_requires_environment()
run_using_command = env_vars.pop('_run_using_command', None)
cwd = env_vars.pop('__cwd', None)
if '__isolated' in env_vars:
isolated = env_vars.pop('__isolated')
else:
isolated = not env_vars and not env_required
cmd_line = [sys.executable, '-X', 'faulthandler']
if run_using_command:
cmd_line = run_using_command + cmd_line
if isolated:
# isolated mode: ignore Python environment variables, ignore user
# site-packages, and don't add the current directory to sys.path
Expand Down
62 changes: 62 additions & 0 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1570,12 +1570,74 @@ def test__use_vfork(self, mock_fork_exec):
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=execve,clone,clone3,fork,vfork,exit,exit_group"
true_binary = "/bin/true"
strace_command = [strace_binary, strace_filter]

does_strace_work_process = subprocess.run(
strace_command + [true_binary],
stderr=subprocess.PIPE,
stdout=subprocess.DEVNULL,
)
if (does_strace_work_process.returncode != 0 or
b"+++ exited with 0 +++" not in does_strace_work_process.stderr):
self.skipTest("strace not found or is not working as expected.")

with self.subTest(name="default_is_vfork"):
vfork_result = assert_python_ok(
"-c",
f"""if True:
import subprocess
subprocess.check_call([{true_binary!r}])
""",
_run_using_command=strace_command,
)
self.assertRegex(vfork_result.err, br"(?m)^vfork[(]")
self.assertNotRegex(vfork_result.err, br"(?m)^(fork|clone)")

# 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,
)
self.assertNotRegex(non_vfork_result.err, br"(?m)^vfork[(]")
self.assertRegex(non_vfork_result.err, br"(?m)^(fork|clone)")


class RunFuncTestCase(BaseTestCase):
def run_python(self, code, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Adds a regression test to verify that ``vfork()`` is used when expected by
:mod:`subprocess` on vfork enabled POSIX systems (Linux).

0 comments on commit 86a5548

Please sign in to comment.