Skip to content

Commit 67b93f8

Browse files
authored
bpo-38456: Use /bin/true in test_subprocess (GH-16736)
* bpo-38456: Use /bin/true in test_subprocess. Instead of sys.executable, "-c", "pass" or "import sys; sys.exit(0)" use /bin/true when it is available. On a reasonable machine this shaves up to two seconds wall time off the otherwise ~40sec execution on a --with-pydebug build. It should be more notable on many buildbots or overloaded slower I/O systems (CI, etc).
1 parent f3751ef commit 67b93f8

File tree

1 file changed

+59
-49
lines changed

1 file changed

+59
-49
lines changed

Lib/test/test_subprocess.py

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@
5454
# Ignore errors that indicate the command was not found
5555
NONEXISTING_ERRORS = (FileNotFoundError, NotADirectoryError, PermissionError)
5656

57+
ZERO_RETURN_CMD = (sys.executable, '-c', 'pass')
58+
59+
60+
def setUpModule():
61+
shell_true = shutil.which('true')
62+
if (os.access(shell_true, os.X_OK) and
63+
subprocess.run([shell_true]).returncode == 0):
64+
global ZERO_RETURN_CMD
65+
ZERO_RETURN_CMD = (shell_true,) # Faster than Python startup.
66+
5767

5868
class BaseTestCase(unittest.TestCase):
5969
def setUp(self):
@@ -98,7 +108,7 @@ def _execute_child(self, *args, **kwargs):
98108
class ProcessTestCase(BaseTestCase):
99109

100110
def test_io_buffered_by_default(self):
101-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
111+
p = subprocess.Popen(ZERO_RETURN_CMD,
102112
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
103113
stderr=subprocess.PIPE)
104114
try:
@@ -112,7 +122,7 @@ def test_io_buffered_by_default(self):
112122
p.wait()
113123

114124
def test_io_unbuffered_works(self):
115-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
125+
p = subprocess.Popen(ZERO_RETURN_CMD,
116126
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
117127
stderr=subprocess.PIPE, bufsize=0)
118128
try:
@@ -142,8 +152,7 @@ def test_call_timeout(self):
142152

143153
def test_check_call_zero(self):
144154
# check_call() function with zero return code
145-
rc = subprocess.check_call([sys.executable, "-c",
146-
"import sys; sys.exit(0)"])
155+
rc = subprocess.check_call(ZERO_RETURN_CMD)
147156
self.assertEqual(rc, 0)
148157

149158
def test_check_call_nonzero(self):
@@ -709,19 +718,19 @@ def test_invalid_env(self):
709718
newenv = os.environ.copy()
710719
newenv["FRUIT\0VEGETABLE"] = "cabbage"
711720
with self.assertRaises(ValueError):
712-
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)
721+
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)
713722

714723
# null character in the environment variable value
715724
newenv = os.environ.copy()
716725
newenv["FRUIT"] = "orange\0VEGETABLE=cabbage"
717726
with self.assertRaises(ValueError):
718-
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)
727+
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)
719728

720729
# equal character in the environment variable name
721730
newenv = os.environ.copy()
722731
newenv["FRUIT=ORANGE"] = "lemon"
723732
with self.assertRaises(ValueError):
724-
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)
733+
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)
725734

726735
# equal character in the environment variable value
727736
newenv = os.environ.copy()
@@ -822,7 +831,7 @@ def test_communicate_pipe_fd_leak(self):
822831
options['stderr'] = subprocess.PIPE
823832
if not options:
824833
continue
825-
p = subprocess.Popen((sys.executable, "-c", "pass"), **options)
834+
p = subprocess.Popen(ZERO_RETURN_CMD, **options)
826835
p.communicate()
827836
if p.stdin is not None:
828837
self.assertTrue(p.stdin.closed)
@@ -961,7 +970,7 @@ def test_universal_newlines_communicate_input_none(self):
961970
#
962971
# We set stdout to PIPE because, as of this writing, a different
963972
# code path is tested when the number of pipes is zero or one.
964-
p = subprocess.Popen([sys.executable, "-c", "pass"],
973+
p = subprocess.Popen(ZERO_RETURN_CMD,
965974
stdin=subprocess.PIPE,
966975
stdout=subprocess.PIPE,
967976
universal_newlines=True)
@@ -1109,7 +1118,7 @@ def test_poll(self):
11091118
self.assertEqual(p.poll(), 0)
11101119

11111120
def test_wait(self):
1112-
p = subprocess.Popen([sys.executable, "-c", "pass"])
1121+
p = subprocess.Popen(ZERO_RETURN_CMD)
11131122
self.assertEqual(p.wait(), 0)
11141123
# Subsequent invocations should just return the returncode
11151124
self.assertEqual(p.wait(), 0)
@@ -1128,14 +1137,14 @@ def test_invalid_bufsize(self):
11281137
# an invalid type of the bufsize argument should raise
11291138
# TypeError.
11301139
with self.assertRaises(TypeError):
1131-
subprocess.Popen([sys.executable, "-c", "pass"], "orange")
1140+
subprocess.Popen(ZERO_RETURN_CMD, "orange")
11321141

11331142
def test_bufsize_is_none(self):
11341143
# bufsize=None should be the same as bufsize=0.
1135-
p = subprocess.Popen([sys.executable, "-c", "pass"], None)
1144+
p = subprocess.Popen(ZERO_RETURN_CMD, None)
11361145
self.assertEqual(p.wait(), 0)
11371146
# Again with keyword arg
1138-
p = subprocess.Popen([sys.executable, "-c", "pass"], bufsize=None)
1147+
p = subprocess.Popen(ZERO_RETURN_CMD, bufsize=None)
11391148
self.assertEqual(p.wait(), 0)
11401149

11411150
def _test_bufsize_equal_one(self, line, expected, universal_newlines):
@@ -1340,7 +1349,7 @@ def test_handles_closed_on_exception(self):
13401349

13411350
def test_communicate_epipe(self):
13421351
# Issue 10963: communicate() should hide EPIPE
1343-
p = subprocess.Popen([sys.executable, "-c", 'pass'],
1352+
p = subprocess.Popen(ZERO_RETURN_CMD,
13441353
stdin=subprocess.PIPE,
13451354
stdout=subprocess.PIPE,
13461355
stderr=subprocess.PIPE)
@@ -1351,7 +1360,7 @@ def test_communicate_epipe(self):
13511360

13521361
def test_communicate_epipe_only_stdin(self):
13531362
# Issue 10963: communicate() should hide EPIPE
1354-
p = subprocess.Popen([sys.executable, "-c", 'pass'],
1363+
p = subprocess.Popen(ZERO_RETURN_CMD,
13551364
stdin=subprocess.PIPE)
13561365
self.addCleanup(p.stdin.close)
13571366
p.wait()
@@ -1390,7 +1399,7 @@ def test_failed_child_execute_fd_leak(self):
13901399
fds_before_popen = os.listdir(fd_directory)
13911400
with self.assertRaises(PopenTestException):
13921401
PopenExecuteChildRaises(
1393-
[sys.executable, '-c', 'pass'], stdin=subprocess.PIPE,
1402+
ZERO_RETURN_CMD, stdin=subprocess.PIPE,
13941403
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
13951404

13961405
# NOTE: This test doesn't verify that the real _execute_child
@@ -1433,7 +1442,7 @@ def test_check(self):
14331442

14341443
def test_check_zero(self):
14351444
# check_returncode shouldn't raise when returncode is zero
1436-
cp = self.run_python("import sys; sys.exit(0)", check=True)
1445+
cp = subprocess.run(ZERO_RETURN_CMD, check=True)
14371446
self.assertEqual(cp.returncode, 0)
14381447

14391448
def test_timeout(self):
@@ -1791,16 +1800,16 @@ def test_user(self):
17911800
self.assertEqual(child_user, user_uid)
17921801

17931802
with self.assertRaises(ValueError):
1794-
subprocess.check_call([sys.executable, "-c", "pass"], user=-1)
1803+
subprocess.check_call(ZERO_RETURN_CMD, user=-1)
17951804

17961805
if pwd is None:
17971806
with self.assertRaises(ValueError):
1798-
subprocess.check_call([sys.executable, "-c", "pass"], user=name_uid)
1807+
subprocess.check_call(ZERO_RETURN_CMD, user=name_uid)
17991808

18001809
@unittest.skipIf(hasattr(os, 'setreuid'), 'setreuid() available on platform')
18011810
def test_user_error(self):
18021811
with self.assertRaises(ValueError):
1803-
subprocess.check_call([sys.executable, "-c", "pass"], user=65535)
1812+
subprocess.check_call(ZERO_RETURN_CMD, user=65535)
18041813

18051814
@unittest.skipUnless(hasattr(os, 'setregid'), 'no setregid() on platform')
18061815
def test_group(self):
@@ -1834,16 +1843,16 @@ def test_group(self):
18341843

18351844
# make sure we bomb on negative values
18361845
with self.assertRaises(ValueError):
1837-
subprocess.check_call([sys.executable, "-c", "pass"], group=-1)
1846+
subprocess.check_call(ZERO_RETURN_CMD, group=-1)
18381847

18391848
if grp is None:
18401849
with self.assertRaises(ValueError):
1841-
subprocess.check_call([sys.executable, "-c", "pass"], group=name_group)
1850+
subprocess.check_call(ZERO_RETURN_CMD, group=name_group)
18421851

18431852
@unittest.skipIf(hasattr(os, 'setregid'), 'setregid() available on platform')
18441853
def test_group_error(self):
18451854
with self.assertRaises(ValueError):
1846-
subprocess.check_call([sys.executable, "-c", "pass"], group=65535)
1855+
subprocess.check_call(ZERO_RETURN_CMD, group=65535)
18471856

18481857
@unittest.skipUnless(hasattr(os, 'setgroups'), 'no setgroups() on platform')
18491858
def test_extra_groups(self):
@@ -1882,17 +1891,17 @@ def test_extra_groups(self):
18821891

18831892
# make sure we bomb on negative values
18841893
with self.assertRaises(ValueError):
1885-
subprocess.check_call([sys.executable, "-c", "pass"], extra_groups=[-1])
1894+
subprocess.check_call(ZERO_RETURN_CMD, extra_groups=[-1])
18861895

18871896
if grp is None:
18881897
with self.assertRaises(ValueError):
1889-
subprocess.check_call([sys.executable, "-c", "pass"],
1898+
subprocess.check_call(ZERO_RETURN_CMD,
18901899
extra_groups=[name_group])
18911900

18921901
@unittest.skipIf(hasattr(os, 'setgroups'), 'setgroups() available on platform')
18931902
def test_extra_groups_error(self):
18941903
with self.assertRaises(ValueError):
1895-
subprocess.check_call([sys.executable, "-c", "pass"], extra_groups=[])
1904+
subprocess.check_call(ZERO_RETURN_CMD, extra_groups=[])
18961905

18971906
@unittest.skipIf(mswindows or not hasattr(os, 'umask'),
18981907
'POSIX umask() is not available.')
@@ -1904,7 +1913,7 @@ def test_umask(self):
19041913
# We set an unusual umask in the child so as a unique mode
19051914
# for us to test the child's touched file for.
19061915
subprocess.check_call(
1907-
[sys.executable, "-c", f"open({name!r}, 'w')"], # touch
1916+
[sys.executable, "-c", f"open({name!r}, 'w').close()"],
19081917
umask=0o053)
19091918
# Ignore execute permissions entirely in our test,
19101919
# filesystems could be mounted to ignore or force that.
@@ -2007,7 +2016,7 @@ def raise_it():
20072016

20082017
with self.assertRaises(subprocess.SubprocessError):
20092018
self._TestExecuteChildPopen(
2010-
self, [sys.executable, "-c", "pass"],
2019+
self, ZERO_RETURN_CMD,
20112020
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
20122021
stderr=subprocess.PIPE, preexec_fn=raise_it)
20132022

@@ -2464,7 +2473,7 @@ def prepare():
24642473

24652474
try:
24662475
subprocess.call(
2467-
[sys.executable, "-c", "pass"],
2476+
ZERO_RETURN_CMD,
24682477
preexec_fn=prepare)
24692478
except ValueError as err:
24702479
# Pure Python implementations keeps the message
@@ -2507,29 +2516,30 @@ def test_undecodable_env(self):
25072516
self.assertEqual(stdout.decode('ascii'), ascii(encoded_value))
25082517

25092518
def test_bytes_program(self):
2510-
abs_program = os.fsencode(sys.executable)
2511-
path, program = os.path.split(sys.executable)
2519+
abs_program = os.fsencode(ZERO_RETURN_CMD[0])
2520+
args = list(ZERO_RETURN_CMD[1:])
2521+
path, program = os.path.split(ZERO_RETURN_CMD[0])
25122522
program = os.fsencode(program)
25132523

25142524
# absolute bytes path
2515-
exitcode = subprocess.call([abs_program, "-c", "pass"])
2525+
exitcode = subprocess.call([abs_program]+args)
25162526
self.assertEqual(exitcode, 0)
25172527

25182528
# absolute bytes path as a string
2519-
cmd = b"'" + abs_program + b"' -c pass"
2529+
cmd = b"'%s' %s" % (abs_program, " ".join(args).encode("utf-8"))
25202530
exitcode = subprocess.call(cmd, shell=True)
25212531
self.assertEqual(exitcode, 0)
25222532

25232533
# bytes program, unicode PATH
25242534
env = os.environ.copy()
25252535
env["PATH"] = path
2526-
exitcode = subprocess.call([program, "-c", "pass"], env=env)
2536+
exitcode = subprocess.call([program]+args, env=env)
25272537
self.assertEqual(exitcode, 0)
25282538

25292539
# bytes program, bytes PATH
25302540
envb = os.environb.copy()
25312541
envb[b"PATH"] = os.fsencode(path)
2532-
exitcode = subprocess.call([program, "-c", "pass"], env=envb)
2542+
exitcode = subprocess.call([program]+args, env=envb)
25332543
self.assertEqual(exitcode, 0)
25342544

25352545
def test_pipe_cloexec(self):
@@ -2757,7 +2767,7 @@ def test_pass_fds(self):
27572767
# pass_fds overrides close_fds with a warning.
27582768
with self.assertWarns(RuntimeWarning) as context:
27592769
self.assertFalse(subprocess.call(
2760-
[sys.executable, "-c", "import sys; sys.exit(0)"],
2770+
ZERO_RETURN_CMD,
27612771
close_fds=False, pass_fds=(fd, )))
27622772
self.assertIn('overriding close_fds', str(context.warning))
27632773

@@ -2819,19 +2829,19 @@ def test_pass_fds_redirected(self):
28192829

28202830
def test_stdout_stdin_are_single_inout_fd(self):
28212831
with io.open(os.devnull, "r+") as inout:
2822-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
2832+
p = subprocess.Popen(ZERO_RETURN_CMD,
28232833
stdout=inout, stdin=inout)
28242834
p.wait()
28252835

28262836
def test_stdout_stderr_are_single_inout_fd(self):
28272837
with io.open(os.devnull, "r+") as inout:
2828-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
2838+
p = subprocess.Popen(ZERO_RETURN_CMD,
28292839
stdout=inout, stderr=inout)
28302840
p.wait()
28312841

28322842
def test_stderr_stdin_are_single_inout_fd(self):
28332843
with io.open(os.devnull, "r+") as inout:
2834-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
2844+
p = subprocess.Popen(ZERO_RETURN_CMD,
28352845
stderr=inout, stdin=inout)
28362846
p.wait()
28372847

@@ -3031,7 +3041,7 @@ def __int__(self):
30313041
def test_communicate_BrokenPipeError_stdin_close(self):
30323042
# By not setting stdout or stderr or a timeout we force the fast path
30333043
# that just calls _stdin_write() internally due to our mock.
3034-
proc = subprocess.Popen([sys.executable, '-c', 'pass'])
3044+
proc = subprocess.Popen(ZERO_RETURN_CMD)
30353045
with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin:
30363046
mock_proc_stdin.close.side_effect = BrokenPipeError
30373047
proc.communicate() # Should swallow BrokenPipeError from close.
@@ -3040,7 +3050,7 @@ def test_communicate_BrokenPipeError_stdin_close(self):
30403050
def test_communicate_BrokenPipeError_stdin_write(self):
30413051
# By not setting stdout or stderr or a timeout we force the fast path
30423052
# that just calls _stdin_write() internally due to our mock.
3043-
proc = subprocess.Popen([sys.executable, '-c', 'pass'])
3053+
proc = subprocess.Popen(ZERO_RETURN_CMD)
30443054
with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin:
30453055
mock_proc_stdin.write.side_effect = BrokenPipeError
30463056
proc.communicate(b'stuff') # Should swallow the BrokenPipeError.
@@ -3079,7 +3089,7 @@ def test_communicate_BrokenPipeError_stdin_close_with_timeout(self):
30793089
'need _testcapi.W_STOPCODE')
30803090
def test_stopped(self):
30813091
"""Test wait() behavior when waitpid returns WIFSTOPPED; issue29335."""
3082-
args = [sys.executable, '-c', 'pass']
3092+
args = ZERO_RETURN_CMD
30833093
proc = subprocess.Popen(args)
30843094

30853095
# Wait until the real process completes to avoid zombie process
@@ -3109,7 +3119,7 @@ def test_startupinfo(self):
31093119
# Since Python is a console process, it won't be affected
31103120
# by wShowWindow, but the argument should be silently
31113121
# ignored
3112-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
3122+
subprocess.call(ZERO_RETURN_CMD,
31133123
startupinfo=startupinfo)
31143124

31153125
def test_startupinfo_keywords(self):
@@ -3125,7 +3135,7 @@ def test_startupinfo_keywords(self):
31253135
# Since Python is a console process, it won't be affected
31263136
# by wShowWindow, but the argument should be silently
31273137
# ignored
3128-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
3138+
subprocess.call(ZERO_RETURN_CMD,
31293139
startupinfo=startupinfo)
31303140

31313141
def test_startupinfo_copy(self):
@@ -3137,7 +3147,7 @@ def test_startupinfo_copy(self):
31373147
# Call Popen() twice with the same startupinfo object to make sure
31383148
# that it's not modified
31393149
for _ in range(2):
3140-
cmd = [sys.executable, "-c", "pass"]
3150+
cmd = ZERO_RETURN_CMD
31413151
with open(os.devnull, 'w') as null:
31423152
proc = subprocess.Popen(cmd,
31433153
stdout=null,
@@ -3177,7 +3187,7 @@ def test_issue31471(self):
31773187
class BadEnv(dict):
31783188
keys = None
31793189
with self.assertRaises(TypeError):
3180-
subprocess.Popen([sys.executable, "-c", "pass"], env=BadEnv())
3190+
subprocess.Popen(ZERO_RETURN_CMD, env=BadEnv())
31813191

31823192
def test_close_fds(self):
31833193
# close file descriptors
@@ -3238,13 +3248,13 @@ def test_close_fds_with_stdio(self):
32383248
def test_empty_attribute_list(self):
32393249
startupinfo = subprocess.STARTUPINFO()
32403250
startupinfo.lpAttributeList = {}
3241-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
3251+
subprocess.call(ZERO_RETURN_CMD,
32423252
startupinfo=startupinfo)
32433253

32443254
def test_empty_handle_list(self):
32453255
startupinfo = subprocess.STARTUPINFO()
32463256
startupinfo.lpAttributeList = {"handle_list": []}
3247-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
3257+
subprocess.call(ZERO_RETURN_CMD,
32483258
startupinfo=startupinfo)
32493259

32503260
def test_shell_sequence(self):
@@ -3543,7 +3553,7 @@ def test_invalid_args(self):
35433553

35443554
def test_broken_pipe_cleanup(self):
35453555
"""Broken pipe error should not prevent wait() (Issue 21619)"""
3546-
proc = subprocess.Popen([sys.executable, '-c', 'pass'],
3556+
proc = subprocess.Popen(ZERO_RETURN_CMD,
35473557
stdin=subprocess.PIPE,
35483558
bufsize=support.PIPE_MAX_SIZE*2)
35493559
proc = proc.__enter__()

0 commit comments

Comments
 (0)