5
5
import signal
6
6
import subprocess
7
7
import sys
8
+ import tempfile
8
9
import threading
9
10
import time
10
11
import traceback
11
- from typing import NamedTuple , NoReturn , Literal , Any
12
+ from typing import NamedTuple , NoReturn , Literal , Any , TextIO
12
13
13
14
from test import support
14
15
from test .support import os_helper
@@ -51,7 +52,7 @@ def parse_worker_args(worker_args) -> tuple[Namespace, str]:
51
52
return (ns , test_name )
52
53
53
54
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 :
55
56
ns_dict = vars (ns )
56
57
worker_args = (ns_dict , testname )
57
58
worker_args = json .dumps (worker_args )
@@ -67,18 +68,17 @@ def run_test_in_subprocess(testname: str, ns: Namespace) -> subprocess.Popen:
67
68
# Running the child from the same working directory as regrtest's original
68
69
# invocation ensures that TEMPDIR for the child is the same when
69
70
# 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
+ )
71
79
if USE_PROCESS_GROUP :
72
80
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 )
82
82
83
83
84
84
def run_tests_worker (ns : Namespace , test_name : str ) -> NoReturn :
@@ -204,12 +204,12 @@ def mp_result_error(
204
204
test_result .duration_sec = time .monotonic () - self .start_time
205
205
return MultiprocessResult (test_result , stdout , err_msg )
206
206
207
- def _run_process (self , test_name : str ) -> tuple [ int , str , str ] :
207
+ def _run_process (self , test_name : str , stdout_fh : TextIO ) -> int :
208
208
self .start_time = time .monotonic ()
209
209
210
210
self .current_test_name = test_name
211
211
try :
212
- popen = run_test_in_subprocess (test_name , self .ns )
212
+ popen = run_test_in_subprocess (test_name , self .ns , stdout_fh )
213
213
214
214
self ._killed = False
215
215
self ._popen = popen
@@ -226,10 +226,10 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
226
226
raise ExitThread
227
227
228
228
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 )
232
231
assert retcode is not None
232
+ return retcode
233
233
except subprocess .TimeoutExpired :
234
234
if self ._stopped :
235
235
# kill() has been called: communicate() fails on reading
@@ -244,17 +244,12 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
244
244
# bpo-38207: Don't attempt to call communicate() again: on it
245
245
# can hang until all child processes using stdout
246
246
# pipes completes.
247
- stdout = ''
248
247
except OSError :
249
248
if self ._stopped :
250
249
# kill() has been called: communicate() fails
251
250
# on reading closed stdout
252
251
raise ExitThread
253
252
raise
254
- else :
255
- stdout = stdout .strip ()
256
-
257
- return (retcode , stdout )
258
253
except :
259
254
self ._kill ()
260
255
raise
@@ -264,7 +259,17 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
264
259
self .current_test_name = None
265
260
266
261
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 ()
268
273
269
274
if retcode is None :
270
275
return self .mp_result_error (Timeout (test_name ), stdout )
@@ -311,9 +316,6 @@ def run(self) -> None:
311
316
def _wait_completed (self ) -> None :
312
317
popen = self ._popen
313
318
314
- # stdout must be closed to ensure that communicate() does not hang
315
- popen .stdout .close ()
316
-
317
319
try :
318
320
popen .wait (JOIN_TIMEOUT )
319
321
except (subprocess .TimeoutExpired , OSError ) as exc :
0 commit comments