Skip to content

Commit 4f0587f

Browse files
authored
[3.8] bpo-38456: Use /bin/true in test_subprocess (GH-16737)
* [3.8] bpo-38456: Use /bin/true in test_subprocess (GH-16736) 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).. (cherry picked from commit 67b93f8) * Handle when there is no 'true' command backport of 46113e0 by Pablo Galindo.
1 parent cc06217 commit 4f0587f

File tree

1 file changed

+51
-39
lines changed

1 file changed

+51
-39
lines changed

Lib/test/test_subprocess.py

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@
4545
# Ignore errors that indicate the command was not found
4646
NONEXISTING_ERRORS = (FileNotFoundError, NotADirectoryError, PermissionError)
4747

48+
ZERO_RETURN_CMD = (sys.executable, '-c', 'pass')
49+
50+
51+
def setUpModule():
52+
shell_true = shutil.which('true')
53+
if shell_true is None:
54+
return
55+
if (os.access(shell_true, os.X_OK) and
56+
subprocess.run([shell_true]).returncode == 0):
57+
global ZERO_RETURN_CMD
58+
ZERO_RETURN_CMD = (shell_true,) # Faster than Python startup.
59+
4860

4961
class BaseTestCase(unittest.TestCase):
5062
def setUp(self):
@@ -89,7 +101,7 @@ def _execute_child(self, *args, **kwargs):
89101
class ProcessTestCase(BaseTestCase):
90102

91103
def test_io_buffered_by_default(self):
92-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
104+
p = subprocess.Popen(ZERO_RETURN_CMD,
93105
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
94106
stderr=subprocess.PIPE)
95107
try:
@@ -103,7 +115,7 @@ def test_io_buffered_by_default(self):
103115
p.wait()
104116

105117
def test_io_unbuffered_works(self):
106-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
118+
p = subprocess.Popen(ZERO_RETURN_CMD,
107119
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
108120
stderr=subprocess.PIPE, bufsize=0)
109121
try:
@@ -133,8 +145,7 @@ def test_call_timeout(self):
133145

134146
def test_check_call_zero(self):
135147
# check_call() function with zero return code
136-
rc = subprocess.check_call([sys.executable, "-c",
137-
"import sys; sys.exit(0)"])
148+
rc = subprocess.check_call(ZERO_RETURN_CMD)
138149
self.assertEqual(rc, 0)
139150

140151
def test_check_call_nonzero(self):
@@ -700,19 +711,19 @@ def test_invalid_env(self):
700711
newenv = os.environ.copy()
701712
newenv["FRUIT\0VEGETABLE"] = "cabbage"
702713
with self.assertRaises(ValueError):
703-
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)
714+
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)
704715

705716
# null character in the environment variable value
706717
newenv = os.environ.copy()
707718
newenv["FRUIT"] = "orange\0VEGETABLE=cabbage"
708719
with self.assertRaises(ValueError):
709-
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)
720+
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)
710721

711722
# equal character in the environment variable name
712723
newenv = os.environ.copy()
713724
newenv["FRUIT=ORANGE"] = "lemon"
714725
with self.assertRaises(ValueError):
715-
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)
726+
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)
716727

717728
# equal character in the environment variable value
718729
newenv = os.environ.copy()
@@ -813,7 +824,7 @@ def test_communicate_pipe_fd_leak(self):
813824
options['stderr'] = subprocess.PIPE
814825
if not options:
815826
continue
816-
p = subprocess.Popen((sys.executable, "-c", "pass"), **options)
827+
p = subprocess.Popen(ZERO_RETURN_CMD, **options)
817828
p.communicate()
818829
if p.stdin is not None:
819830
self.assertTrue(p.stdin.closed)
@@ -952,7 +963,7 @@ def test_universal_newlines_communicate_input_none(self):
952963
#
953964
# We set stdout to PIPE because, as of this writing, a different
954965
# code path is tested when the number of pipes is zero or one.
955-
p = subprocess.Popen([sys.executable, "-c", "pass"],
966+
p = subprocess.Popen(ZERO_RETURN_CMD,
956967
stdin=subprocess.PIPE,
957968
stdout=subprocess.PIPE,
958969
universal_newlines=True)
@@ -1100,7 +1111,7 @@ def test_poll(self):
11001111
self.assertEqual(p.poll(), 0)
11011112

11021113
def test_wait(self):
1103-
p = subprocess.Popen([sys.executable, "-c", "pass"])
1114+
p = subprocess.Popen(ZERO_RETURN_CMD)
11041115
self.assertEqual(p.wait(), 0)
11051116
# Subsequent invocations should just return the returncode
11061117
self.assertEqual(p.wait(), 0)
@@ -1119,14 +1130,14 @@ def test_invalid_bufsize(self):
11191130
# an invalid type of the bufsize argument should raise
11201131
# TypeError.
11211132
with self.assertRaises(TypeError):
1122-
subprocess.Popen([sys.executable, "-c", "pass"], "orange")
1133+
subprocess.Popen(ZERO_RETURN_CMD, "orange")
11231134

11241135
def test_bufsize_is_none(self):
11251136
# bufsize=None should be the same as bufsize=0.
1126-
p = subprocess.Popen([sys.executable, "-c", "pass"], None)
1137+
p = subprocess.Popen(ZERO_RETURN_CMD, None)
11271138
self.assertEqual(p.wait(), 0)
11281139
# Again with keyword arg
1129-
p = subprocess.Popen([sys.executable, "-c", "pass"], bufsize=None)
1140+
p = subprocess.Popen(ZERO_RETURN_CMD, bufsize=None)
11301141
self.assertEqual(p.wait(), 0)
11311142

11321143
def _test_bufsize_equal_one(self, line, expected, universal_newlines):
@@ -1331,7 +1342,7 @@ def test_handles_closed_on_exception(self):
13311342

13321343
def test_communicate_epipe(self):
13331344
# Issue 10963: communicate() should hide EPIPE
1334-
p = subprocess.Popen([sys.executable, "-c", 'pass'],
1345+
p = subprocess.Popen(ZERO_RETURN_CMD,
13351346
stdin=subprocess.PIPE,
13361347
stdout=subprocess.PIPE,
13371348
stderr=subprocess.PIPE)
@@ -1342,7 +1353,7 @@ def test_communicate_epipe(self):
13421353

13431354
def test_communicate_epipe_only_stdin(self):
13441355
# Issue 10963: communicate() should hide EPIPE
1345-
p = subprocess.Popen([sys.executable, "-c", 'pass'],
1356+
p = subprocess.Popen(ZERO_RETURN_CMD,
13461357
stdin=subprocess.PIPE)
13471358
self.addCleanup(p.stdin.close)
13481359
p.wait()
@@ -1381,7 +1392,7 @@ def test_failed_child_execute_fd_leak(self):
13811392
fds_before_popen = os.listdir(fd_directory)
13821393
with self.assertRaises(PopenTestException):
13831394
PopenExecuteChildRaises(
1384-
[sys.executable, '-c', 'pass'], stdin=subprocess.PIPE,
1395+
ZERO_RETURN_CMD, stdin=subprocess.PIPE,
13851396
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
13861397

13871398
# NOTE: This test doesn't verify that the real _execute_child
@@ -1424,7 +1435,7 @@ def test_check(self):
14241435

14251436
def test_check_zero(self):
14261437
# check_returncode shouldn't raise when returncode is zero
1427-
cp = self.run_python("import sys; sys.exit(0)", check=True)
1438+
cp = subprocess.run(ZERO_RETURN_CMD, check=True)
14281439
self.assertEqual(cp.returncode, 0)
14291440

14301441
def test_timeout(self):
@@ -1824,7 +1835,7 @@ def raise_it():
18241835

18251836
with self.assertRaises(subprocess.SubprocessError):
18261837
self._TestExecuteChildPopen(
1827-
self, [sys.executable, "-c", "pass"],
1838+
self, ZERO_RETURN_CMD,
18281839
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
18291840
stderr=subprocess.PIPE, preexec_fn=raise_it)
18301841

@@ -2281,7 +2292,7 @@ def prepare():
22812292

22822293
try:
22832294
subprocess.call(
2284-
[sys.executable, "-c", "pass"],
2295+
ZERO_RETURN_CMD,
22852296
preexec_fn=prepare)
22862297
except ValueError as err:
22872298
# Pure Python implementations keeps the message
@@ -2324,29 +2335,30 @@ def test_undecodable_env(self):
23242335
self.assertEqual(stdout.decode('ascii'), ascii(encoded_value))
23252336

23262337
def test_bytes_program(self):
2327-
abs_program = os.fsencode(sys.executable)
2328-
path, program = os.path.split(sys.executable)
2338+
abs_program = os.fsencode(ZERO_RETURN_CMD[0])
2339+
args = list(ZERO_RETURN_CMD[1:])
2340+
path, program = os.path.split(ZERO_RETURN_CMD[0])
23292341
program = os.fsencode(program)
23302342

23312343
# absolute bytes path
2332-
exitcode = subprocess.call([abs_program, "-c", "pass"])
2344+
exitcode = subprocess.call([abs_program]+args)
23332345
self.assertEqual(exitcode, 0)
23342346

23352347
# absolute bytes path as a string
2336-
cmd = b"'" + abs_program + b"' -c pass"
2348+
cmd = b"'%s' %s" % (abs_program, " ".join(args).encode("utf-8"))
23372349
exitcode = subprocess.call(cmd, shell=True)
23382350
self.assertEqual(exitcode, 0)
23392351

23402352
# bytes program, unicode PATH
23412353
env = os.environ.copy()
23422354
env["PATH"] = path
2343-
exitcode = subprocess.call([program, "-c", "pass"], env=env)
2355+
exitcode = subprocess.call([program]+args, env=env)
23442356
self.assertEqual(exitcode, 0)
23452357

23462358
# bytes program, bytes PATH
23472359
envb = os.environb.copy()
23482360
envb[b"PATH"] = os.fsencode(path)
2349-
exitcode = subprocess.call([program, "-c", "pass"], env=envb)
2361+
exitcode = subprocess.call([program]+args, env=envb)
23502362
self.assertEqual(exitcode, 0)
23512363

23522364
def test_pipe_cloexec(self):
@@ -2574,7 +2586,7 @@ def test_pass_fds(self):
25742586
# pass_fds overrides close_fds with a warning.
25752587
with self.assertWarns(RuntimeWarning) as context:
25762588
self.assertFalse(subprocess.call(
2577-
[sys.executable, "-c", "import sys; sys.exit(0)"],
2589+
ZERO_RETURN_CMD,
25782590
close_fds=False, pass_fds=(fd, )))
25792591
self.assertIn('overriding close_fds', str(context.warning))
25802592

@@ -2636,19 +2648,19 @@ def test_pass_fds_redirected(self):
26362648

26372649
def test_stdout_stdin_are_single_inout_fd(self):
26382650
with io.open(os.devnull, "r+") as inout:
2639-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
2651+
p = subprocess.Popen(ZERO_RETURN_CMD,
26402652
stdout=inout, stdin=inout)
26412653
p.wait()
26422654

26432655
def test_stdout_stderr_are_single_inout_fd(self):
26442656
with io.open(os.devnull, "r+") as inout:
2645-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
2657+
p = subprocess.Popen(ZERO_RETURN_CMD,
26462658
stdout=inout, stderr=inout)
26472659
p.wait()
26482660

26492661
def test_stderr_stdin_are_single_inout_fd(self):
26502662
with io.open(os.devnull, "r+") as inout:
2651-
p = subprocess.Popen([sys.executable, "-c", "import sys; sys.exit(0)"],
2663+
p = subprocess.Popen(ZERO_RETURN_CMD,
26522664
stderr=inout, stdin=inout)
26532665
p.wait()
26542666

@@ -2836,7 +2848,7 @@ def __int__(self):
28362848
def test_communicate_BrokenPipeError_stdin_close(self):
28372849
# By not setting stdout or stderr or a timeout we force the fast path
28382850
# that just calls _stdin_write() internally due to our mock.
2839-
proc = subprocess.Popen([sys.executable, '-c', 'pass'])
2851+
proc = subprocess.Popen(ZERO_RETURN_CMD)
28402852
with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin:
28412853
mock_proc_stdin.close.side_effect = BrokenPipeError
28422854
proc.communicate() # Should swallow BrokenPipeError from close.
@@ -2845,7 +2857,7 @@ def test_communicate_BrokenPipeError_stdin_close(self):
28452857
def test_communicate_BrokenPipeError_stdin_write(self):
28462858
# By not setting stdout or stderr or a timeout we force the fast path
28472859
# that just calls _stdin_write() internally due to our mock.
2848-
proc = subprocess.Popen([sys.executable, '-c', 'pass'])
2860+
proc = subprocess.Popen(ZERO_RETURN_CMD)
28492861
with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin:
28502862
mock_proc_stdin.write.side_effect = BrokenPipeError
28512863
proc.communicate(b'stuff') # Should swallow the BrokenPipeError.
@@ -2884,7 +2896,7 @@ def test_communicate_BrokenPipeError_stdin_close_with_timeout(self):
28842896
'need _testcapi.W_STOPCODE')
28852897
def test_stopped(self):
28862898
"""Test wait() behavior when waitpid returns WIFSTOPPED; issue29335."""
2887-
args = [sys.executable, '-c', 'pass']
2899+
args = ZERO_RETURN_CMD
28882900
proc = subprocess.Popen(args)
28892901

28902902
# Wait until the real process completes to avoid zombie process
@@ -2914,7 +2926,7 @@ def test_startupinfo(self):
29142926
# Since Python is a console process, it won't be affected
29152927
# by wShowWindow, but the argument should be silently
29162928
# ignored
2917-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
2929+
subprocess.call(ZERO_RETURN_CMD,
29182930
startupinfo=startupinfo)
29192931

29202932
def test_startupinfo_keywords(self):
@@ -2930,7 +2942,7 @@ def test_startupinfo_keywords(self):
29302942
# Since Python is a console process, it won't be affected
29312943
# by wShowWindow, but the argument should be silently
29322944
# ignored
2933-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
2945+
subprocess.call(ZERO_RETURN_CMD,
29342946
startupinfo=startupinfo)
29352947

29362948
def test_startupinfo_copy(self):
@@ -2942,7 +2954,7 @@ def test_startupinfo_copy(self):
29422954
# Call Popen() twice with the same startupinfo object to make sure
29432955
# that it's not modified
29442956
for _ in range(2):
2945-
cmd = [sys.executable, "-c", "pass"]
2957+
cmd = ZERO_RETURN_CMD
29462958
with open(os.devnull, 'w') as null:
29472959
proc = subprocess.Popen(cmd,
29482960
stdout=null,
@@ -2982,7 +2994,7 @@ def test_issue31471(self):
29822994
class BadEnv(dict):
29832995
keys = None
29842996
with self.assertRaises(TypeError):
2985-
subprocess.Popen([sys.executable, "-c", "pass"], env=BadEnv())
2997+
subprocess.Popen(ZERO_RETURN_CMD, env=BadEnv())
29862998

29872999
def test_close_fds(self):
29883000
# close file descriptors
@@ -3043,13 +3055,13 @@ def test_close_fds_with_stdio(self):
30433055
def test_empty_attribute_list(self):
30443056
startupinfo = subprocess.STARTUPINFO()
30453057
startupinfo.lpAttributeList = {}
3046-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
3058+
subprocess.call(ZERO_RETURN_CMD,
30473059
startupinfo=startupinfo)
30483060

30493061
def test_empty_handle_list(self):
30503062
startupinfo = subprocess.STARTUPINFO()
30513063
startupinfo.lpAttributeList = {"handle_list": []}
3052-
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
3064+
subprocess.call(ZERO_RETURN_CMD,
30533065
startupinfo=startupinfo)
30543066

30553067
def test_shell_sequence(self):
@@ -3348,7 +3360,7 @@ def test_invalid_args(self):
33483360

33493361
def test_broken_pipe_cleanup(self):
33503362
"""Broken pipe error should not prevent wait() (Issue 21619)"""
3351-
proc = subprocess.Popen([sys.executable, '-c', 'pass'],
3363+
proc = subprocess.Popen(ZERO_RETURN_CMD,
33523364
stdin=subprocess.PIPE,
33533365
bufsize=support.PIPE_MAX_SIZE*2)
33543366
proc = proc.__enter__()

0 commit comments

Comments
 (0)