20
20
from .runtests import RunTests
21
21
from .single import PROGRESS_MIN_TIME
22
22
from .utils import (
23
- StrPath , TestName ,
23
+ StrPath , StrJSON , TestName , MS_WINDOWS ,
24
24
format_duration , print_warning )
25
25
from .worker import create_worker_process , USE_PROCESS_GROUP
26
26
27
- if sys . platform == 'win32' :
27
+ if MS_WINDOWS :
28
28
import locale
29
+ import msvcrt
30
+
29
31
30
32
31
33
# Display the running tests if nothing happened last N seconds
@@ -153,10 +155,11 @@ def mp_result_error(
153
155
) -> MultiprocessResult :
154
156
return MultiprocessResult (test_result , stdout , err_msg )
155
157
156
- def _run_process (self , runtests : RunTests , output_file : TextIO ,
158
+ def _run_process (self , runtests : RunTests , output_fd : int , json_fd : int ,
157
159
tmp_dir : StrPath | None = None ) -> int :
158
160
try :
159
- popen = create_worker_process (runtests , output_file , tmp_dir )
161
+ popen = create_worker_process (runtests , output_fd , json_fd ,
162
+ tmp_dir )
160
163
161
164
self ._killed = False
162
165
self ._popen = popen
@@ -208,7 +211,7 @@ def _run_process(self, runtests: RunTests, output_file: TextIO,
208
211
def _runtest (self , test_name : TestName ) -> MultiprocessResult :
209
212
self .current_test_name = test_name
210
213
211
- if sys . platform == 'win32' :
214
+ if MS_WINDOWS :
212
215
# gh-95027: When stdout is not a TTY, Python uses the ANSI code
213
216
# page for the sys.stdout encoding. If the main process runs in a
214
217
# terminal, sys.stdout uses WindowsConsoleIO with UTF-8 encoding.
@@ -221,14 +224,25 @@ def _runtest(self, test_name: TestName) -> MultiprocessResult:
221
224
match_tests = self .runtests .get_match_tests (test_name )
222
225
else :
223
226
match_tests = None
224
- kwargs = {}
225
- if match_tests :
226
- kwargs ['match_tests' ] = match_tests
227
- worker_runtests = self .runtests .copy (tests = tests , ** kwargs )
227
+ err_msg = None
228
228
229
229
# gh-94026: Write stdout+stderr to a tempfile as workaround for
230
230
# non-blocking pipes on Emscripten with NodeJS.
231
- with tempfile .TemporaryFile ('w+' , encoding = encoding ) as stdout_file :
231
+ with (tempfile .TemporaryFile ('w+' , encoding = encoding ) as stdout_file ,
232
+ tempfile .TemporaryFile ('w+' , encoding = 'utf8' ) as json_file ):
233
+ stdout_fd = stdout_file .fileno ()
234
+ json_fd = json_file .fileno ()
235
+ if MS_WINDOWS :
236
+ json_fd = msvcrt .get_osfhandle (json_fd )
237
+
238
+ kwargs = {}
239
+ if match_tests :
240
+ kwargs ['match_tests' ] = match_tests
241
+ worker_runtests = self .runtests .copy (
242
+ tests = tests ,
243
+ json_fd = json_fd ,
244
+ ** kwargs )
245
+
232
246
# gh-93353: Check for leaked temporary files in the parent process,
233
247
# since the deletion of temporary files can happen late during
234
248
# Python finalization: too late for libregrtest.
@@ -239,12 +253,14 @@ def _runtest(self, test_name: TestName) -> MultiprocessResult:
239
253
tmp_dir = tempfile .mkdtemp (prefix = "test_python_" )
240
254
tmp_dir = os .path .abspath (tmp_dir )
241
255
try :
242
- retcode = self ._run_process (worker_runtests , stdout_file , tmp_dir )
256
+ retcode = self ._run_process (worker_runtests ,
257
+ stdout_fd , json_fd , tmp_dir )
243
258
finally :
244
259
tmp_files = os .listdir (tmp_dir )
245
260
os_helper .rmtree (tmp_dir )
246
261
else :
247
- retcode = self ._run_process (worker_runtests , stdout_file )
262
+ retcode = self ._run_process (worker_runtests ,
263
+ stdout_fd , json_fd )
248
264
tmp_files = ()
249
265
stdout_file .seek (0 )
250
266
@@ -257,23 +273,27 @@ def _runtest(self, test_name: TestName) -> MultiprocessResult:
257
273
result = TestResult (test_name , state = State .MULTIPROCESSING_ERROR )
258
274
return self .mp_result_error (result , err_msg = err_msg )
259
275
276
+ try :
277
+ # deserialize run_tests_worker() output
278
+ json_file .seek (0 )
279
+ worker_json : StrJSON = json_file .read ()
280
+ if worker_json :
281
+ result = TestResult .from_json (worker_json )
282
+ else :
283
+ err_msg = f"empty JSON"
284
+ except Exception as exc :
285
+ # gh-101634: Catch UnicodeDecodeError if stdout cannot be
286
+ # decoded from encoding
287
+ err_msg = f"Fail to read or parser worker process JSON: { exc } "
288
+ result = TestResult (test_name , state = State .MULTIPROCESSING_ERROR )
289
+ return self .mp_result_error (result , stdout , err_msg = err_msg )
290
+
260
291
if retcode is None :
261
292
result = TestResult (test_name , state = State .TIMEOUT )
262
293
return self .mp_result_error (result , stdout )
263
294
264
- err_msg = None
265
295
if retcode != 0 :
266
296
err_msg = "Exit code %s" % retcode
267
- else :
268
- stdout , _ , worker_json = stdout .rpartition ("\n " )
269
- stdout = stdout .rstrip ()
270
- if not worker_json :
271
- err_msg = "Failed to parse worker stdout"
272
- else :
273
- try :
274
- result = TestResult .from_json (worker_json )
275
- except Exception as exc :
276
- err_msg = "Failed to parse worker JSON: %s" % exc
277
297
278
298
if err_msg :
279
299
result = TestResult (test_name , state = State .MULTIPROCESSING_ERROR )
0 commit comments