99import threading
1010import time
1111import traceback
12- from typing import NamedTuple ,NoReturn ,Literal ,Any
12+ from typing import NamedTuple ,NoReturn ,Literal ,Any , TextIO
1313
1414from test import support
1515from test .support import os_helper
@@ -53,7 +53,7 @@ def parse_worker_args(worker_args) -> tuple[Namespace, str]:
5353return (ns ,test_name )
5454
5555
56- def run_test_in_subprocess (testname :str ,ns :Namespace ,tmp_dir :str )-> subprocess .Popen :
56+ def run_test_in_subprocess (testname :str ,ns :Namespace ,tmp_dir :str , stdout_fh : TextIO )-> subprocess .Popen :
5757ns_dict = vars (ns )
5858worker_args = (ns_dict ,testname )
5959worker_args = json .dumps (worker_args )
@@ -75,18 +75,18 @@ def run_test_in_subprocess(testname: str, ns: Namespace, tmp_dir: str) -> subpro
7575# Running the child from the same working directory as regrtest's original
7676# invocation ensures that TEMPDIR for the child is the same when
7777# sysconfig.is_python_build() is true. See issue 15300.
78- kw = {'env' :env }
78+ kw = dict (
79+ env = env ,
80+ stdout = stdout_fh ,
81+ # bpo-45410: Write stderr into stdout to keep messages order
82+ stderr = stdout_fh ,
83+ text = True ,
84+ close_fds = (os .name != 'nt' ),
85+ cwd = os_helper .SAVEDCWD ,
86+ )
7987if USE_PROCESS_GROUP :
8088kw ['start_new_session' ]= True
81- return subprocess .Popen (cmd ,
82- stdout = subprocess .PIPE ,
83- # bpo-45410: Write stderr into stdout to keep
84- # messages order
85- stderr = subprocess .STDOUT ,
86- universal_newlines = True ,
87- close_fds = (os .name != 'nt' ),
88- cwd = os_helper .SAVEDCWD ,
89- ** kw )
89+ return subprocess .Popen (cmd ,** kw )
9090
9191
9292def run_tests_worker (ns :Namespace ,test_name :str )-> NoReturn :
@@ -212,12 +212,12 @@ def mp_result_error(
212212test_result .duration_sec = time .monotonic ()- self .start_time
213213return MultiprocessResult (test_result ,stdout ,err_msg )
214214
215- def _run_process (self ,test_name :str ,tmp_dir :str )-> tuple [ int , str , str ] :
215+ def _run_process (self ,test_name :str ,tmp_dir :str , stdout_fh : TextIO )-> int :
216216self .start_time = time .monotonic ()
217217
218218self .current_test_name = test_name
219219try :
220- popen = run_test_in_subprocess (test_name ,self .ns ,tmp_dir )
220+ popen = run_test_in_subprocess (test_name ,self .ns ,tmp_dir , stdout_fh )
221221
222222self ._killed = False
223223self ._popen = popen
@@ -234,10 +234,10 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
234234raise ExitThread
235235
236236try :
237- # bpo-45410: stderr is written into stdout
238- stdout ,_ = popen .communicate (timeout = self .timeout )
239- retcode = popen .returncode
237+ # gh-94026: stdout+stderr are written to tempfile
238+ retcode = popen .wait (timeout = self .timeout )
240239assert retcode is not None
240+ return retcode
241241except subprocess .TimeoutExpired :
242242if self ._stopped :
243243# kill() has been called: communicate() fails on reading
@@ -252,17 +252,12 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
252252# bpo-38207: Don't attempt to call communicate() again: on it
253253# can hang until all child processes using stdout
254254# pipes completes.
255- stdout = ''
256255except OSError :
257256if self ._stopped :
258257# kill() has been called: communicate() fails
259258# on reading closed stdout
260259raise ExitThread
261260raise
262- else :
263- stdout = stdout .strip ()
264-
265- return (retcode ,stdout )
266261except :
267262self ._kill ()
268263raise
@@ -272,23 +267,30 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
272267self .current_test_name = None
273268
274269def _runtest (self ,test_name :str )-> MultiprocessResult :
275- # Don't check for leaked temporary files and directories if Python is
276- # run on WASI. WASI don't pass environment variables like TMPDIR to
277- # worker processes.
278- if not support .is_wasi :
270+ # gh-94026: Write stdout+stderr to a tempfile as workaround for
271+ # non-blocking pipes on Emscripten with NodeJS.
272+ with tempfile .TemporaryFile (
273+ 'w+' ,encoding = sys .stdout .encoding
274+ )as stdout_fh :
279275# gh-93353: Check for leaked temporary files in the parent process,
280276# since the deletion of temporary files can happen late during
281277# Python finalization: too late for libregrtest.
282- tmp_dir = tempfile .mkdtemp (prefix = "test_python_" )
283- tmp_dir = os .path .abspath (tmp_dir )
284- try :
285- retcode ,stdout = self ._run_process (test_name ,tmp_dir )
286- finally :
287- tmp_files = os .listdir (tmp_dir )
288- os_helper .rmtree (tmp_dir )
289- else :
290- retcode ,stdout = self ._run_process (test_name ,None )
291- tmp_files = ()
278+ if not support .is_wasi :
279+ # Don't check for leaked temporary files and directories if Python is
280+ # run on WASI. WASI don't pass environment variables like TMPDIR to
281+ # worker processes.
282+ tmp_dir = tempfile .mkdtemp (prefix = "test_python_" )
283+ tmp_dir = os .path .abspath (tmp_dir )
284+ try :
285+ retcode = self ._run_process (test_name ,tmp_dir ,stdout_fh )
286+ finally :
287+ tmp_files = os .listdir (tmp_dir )
288+ os_helper .rmtree (tmp_dir )
289+ else :
290+ retcode = self ._run_process (test_name ,None ,stdout_fh )
291+ tmp_files = ()
292+ stdout_fh .seek (0 )
293+ stdout = stdout_fh .read ().strip ()
292294
293295if retcode is None :
294296return self .mp_result_error (Timeout (test_name ),stdout )
@@ -343,9 +345,6 @@ def run(self) -> None:
343345def _wait_completed (self )-> None :
344346popen = self ._popen
345347
346- # stdout must be closed to ensure that communicate() does not hang
347- popen .stdout .close ()
348-
349348try :
350349popen .wait (JOIN_TIMEOUT )
351350except (subprocess .TimeoutExpired ,OSError )as exc :