9
9
import threading
10
10
import time
11
11
import traceback
12
- from typing import NamedTuple , NoReturn , Literal , Any
12
+ from typing import NamedTuple , NoReturn , Literal , Any , TextIO
13
13
14
14
from test import support
15
15
from test .support import os_helper
@@ -53,7 +53,7 @@ def parse_worker_args(worker_args) -> tuple[Namespace, str]:
53
53
return (ns , test_name )
54
54
55
55
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 :
57
57
ns_dict = vars (ns )
58
58
worker_args = (ns_dict , testname )
59
59
worker_args = json .dumps (worker_args )
@@ -75,18 +75,18 @@ def run_test_in_subprocess(testname: str, ns: Namespace, tmp_dir: str) -> subpro
75
75
# Running the child from the same working directory as regrtest's original
76
76
# invocation ensures that TEMPDIR for the child is the same when
77
77
# 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
+ )
79
87
if USE_PROCESS_GROUP :
80
88
kw ['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 )
90
90
91
91
92
92
def run_tests_worker (ns : Namespace , test_name : str ) -> NoReturn :
@@ -212,12 +212,12 @@ def mp_result_error(
212
212
test_result .duration_sec = time .monotonic () - self .start_time
213
213
return MultiprocessResult (test_result , stdout , err_msg )
214
214
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 :
216
216
self .start_time = time .monotonic ()
217
217
218
218
self .current_test_name = test_name
219
219
try :
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 )
221
221
222
222
self ._killed = False
223
223
self ._popen = popen
@@ -234,10 +234,10 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
234
234
raise ExitThread
235
235
236
236
try :
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 )
240
239
assert retcode is not None
240
+ return retcode
241
241
except subprocess .TimeoutExpired :
242
242
if self ._stopped :
243
243
# 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]:
252
252
# bpo-38207: Don't attempt to call communicate() again: on it
253
253
# can hang until all child processes using stdout
254
254
# pipes completes.
255
- stdout = ''
256
255
except OSError :
257
256
if self ._stopped :
258
257
# kill() has been called: communicate() fails
259
258
# on reading closed stdout
260
259
raise ExitThread
261
260
raise
262
- else :
263
- stdout = stdout .strip ()
264
-
265
- return (retcode , stdout )
266
261
except :
267
262
self ._kill ()
268
263
raise
@@ -272,23 +267,30 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
272
267
self .current_test_name = None
273
268
274
269
def _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 :
279
275
# gh-93353: Check for leaked temporary files in the parent process,
280
276
# since the deletion of temporary files can happen late during
281
277
# 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 ()
292
294
293
295
if retcode is None :
294
296
return self .mp_result_error (Timeout (test_name ), stdout )
@@ -343,9 +345,6 @@ def run(self) -> None:
343
345
def _wait_completed (self ) -> None :
344
346
popen = self ._popen
345
347
346
- # stdout must be closed to ensure that communicate() does not hang
347
- popen .stdout .close ()
348
-
349
348
try :
350
349
popen .wait (JOIN_TIMEOUT )
351
350
except (subprocess .TimeoutExpired , OSError ) as exc :
0 commit comments