55import signal
66import subprocess
77import sys
8+ import tempfile
89import threading
910import time
1011import traceback
11- from typing import NamedTuple , NoReturn , Literal , Any
12+ from typing import NamedTuple , NoReturn , Literal , Any , TextIO
1213
1314from test import support
1415from test .support import os_helper
@@ -51,7 +52,7 @@ def parse_worker_args(worker_args) -> tuple[Namespace, str]:
5152 return (ns , test_name )
5253
5354
54- def run_test_in_subprocess (testname : str , ns : Namespace ) -> subprocess .Popen :
55+ def run_test_in_subprocess (testname : str , ns : Namespace , stdout_fh : TextIO ) -> subprocess .Popen :
5556 ns_dict = vars (ns )
5657 worker_args = (ns_dict , testname )
5758 worker_args = json .dumps (worker_args )
@@ -67,18 +68,17 @@ def run_test_in_subprocess(testname: str, ns: Namespace) -> subprocess.Popen:
6768 # Running the child from the same working directory as regrtest's original
6869 # invocation ensures that TEMPDIR for the child is the same when
6970 # sysconfig.is_python_build() is true. See issue 15300.
70- kw = {}
71+ kw = dict (
72+ stdout = stdout_fh ,
73+ # bpo-45410: Write stderr into stdout to keep messages order
74+ stderr = stdout_fh ,
75+ text = True ,
76+ close_fds = (os .name != 'nt' ),
77+ cwd = os_helper .SAVEDCWD ,
78+ )
7179 if USE_PROCESS_GROUP :
7280 kw ['start_new_session' ] = True
73- return subprocess .Popen (cmd ,
74- stdout = subprocess .PIPE ,
75- # bpo-45410: Write stderr into stdout to keep
76- # messages order
77- stderr = subprocess .STDOUT ,
78- universal_newlines = True ,
79- close_fds = (os .name != 'nt' ),
80- cwd = os_helper .SAVEDCWD ,
81- ** kw )
81+ return subprocess .Popen (cmd , ** kw )
8282
8383
8484def run_tests_worker (ns : Namespace , test_name : str ) -> NoReturn :
@@ -204,12 +204,12 @@ def mp_result_error(
204204 test_result .duration_sec = time .monotonic () - self .start_time
205205 return MultiprocessResult (test_result , stdout , err_msg )
206206
207- def _run_process (self , test_name : str ) -> tuple [ int , str , str ] :
207+ def _run_process (self , test_name : str , stdout_fh : TextIO ) -> int :
208208 self .start_time = time .monotonic ()
209209
210210 self .current_test_name = test_name
211211 try :
212- popen = run_test_in_subprocess (test_name , self .ns )
212+ popen = run_test_in_subprocess (test_name , self .ns , stdout_fh )
213213
214214 self ._killed = False
215215 self ._popen = popen
@@ -226,10 +226,10 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
226226 raise ExitThread
227227
228228 try :
229- # bpo-45410: stderr is written into stdout
230- stdout , _ = popen .communicate (timeout = self .timeout )
231- retcode = popen .returncode
229+ # gh-94026: stdout+stderr are written to tempfile
230+ retcode = popen .wait (timeout = self .timeout )
232231 assert retcode is not None
232+ return retcode
233233 except subprocess .TimeoutExpired :
234234 if self ._stopped :
235235 # kill() has been called: communicate() fails on reading
@@ -244,17 +244,12 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
244244 # bpo-38207: Don't attempt to call communicate() again: on it
245245 # can hang until all child processes using stdout
246246 # pipes completes.
247- stdout = ''
248247 except OSError :
249248 if self ._stopped :
250249 # kill() has been called: communicate() fails
251250 # on reading closed stdout
252251 raise ExitThread
253252 raise
254- else :
255- stdout = stdout .strip ()
256-
257- return (retcode , stdout )
258253 except :
259254 self ._kill ()
260255 raise
@@ -264,7 +259,17 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
264259 self .current_test_name = None
265260
266261 def _runtest (self , test_name : str ) -> MultiprocessResult :
267- retcode , stdout = self ._run_process (test_name )
262+ # gh-94026: Write stdout+stderr to a tempfile as workaround for
263+ # non-blocking pipes on Emscripten with NodeJS.
264+ with tempfile .TemporaryFile (
265+ 'w+' , encoding = sys .stdout .encoding
266+ ) as stdout_fh :
267+ # gh-93353: Check for leaked temporary files in the parent process,
268+ # since the deletion of temporary files can happen late during
269+ # Python finalization: too late for libregrtest.
270+ retcode = self ._run_process (test_name , stdout_fh )
271+ stdout_fh .seek (0 )
272+ stdout = stdout_fh .read ().strip ()
268273
269274 if retcode is None :
270275 return self .mp_result_error (Timeout (test_name ), stdout )
@@ -311,9 +316,6 @@ def run(self) -> None:
311316 def _wait_completed (self ) -> None :
312317 popen = self ._popen
313318
314- # stdout must be closed to ensure that communicate() does not hang
315- popen .stdout .close ()
316-
317319 try :
318320 popen .wait (JOIN_TIMEOUT )
319321 except (subprocess .TimeoutExpired , OSError ) as exc :
0 commit comments