gh-94026: Buffer regrtest worker stdout in temporary file (GH-94253)

Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Christian Heimes 2022-06-29 10:05:16 +02:00 committed by GitHub
parent 5150cbcd68
commit 199ba23324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 38 additions and 39 deletions

View File

@ -9,7 +9,7 @@
import threading import threading
import time import time
import traceback import traceback
from typing import NamedTuple, NoReturn, Literal, Any from typing import NamedTuple, NoReturn, Literal, Any, TextIO
from test import support from test import support
from test.support import os_helper from test.support import os_helper
@ -53,7 +53,7 @@ def parse_worker_args(worker_args) -> tuple[Namespace, str]:
return (ns, test_name) return (ns, test_name)
def run_test_in_subprocess(testname: str, ns: Namespace, tmp_dir: str) -> subprocess.Popen: def run_test_in_subprocess(testname: str, ns: Namespace, tmp_dir: str, stdout_fh: TextIO) -> subprocess.Popen:
ns_dict = vars(ns) ns_dict = vars(ns)
worker_args = (ns_dict, testname) worker_args = (ns_dict, testname)
worker_args = json.dumps(worker_args) worker_args = json.dumps(worker_args)
@ -75,18 +75,18 @@ def run_test_in_subprocess(testname: str, ns: Namespace, tmp_dir: str) -> subpro
# Running the child from the same working directory as regrtest's original # Running the child from the same working directory as regrtest's original
# invocation ensures that TEMPDIR for the child is the same when # invocation ensures that TEMPDIR for the child is the same when
# sysconfig.is_python_build() is true. See issue 15300. # sysconfig.is_python_build() is true. See issue 15300.
kw = {'env': env} kw = dict(
if USE_PROCESS_GROUP: env=env,
kw['start_new_session'] = True stdout=stdout_fh,
return subprocess.Popen(cmd, # bpo-45410: Write stderr into stdout to keep messages order
stdout=subprocess.PIPE, stderr=stdout_fh,
# bpo-45410: Write stderr into stdout to keep text=True,
# messages order
stderr=subprocess.STDOUT,
universal_newlines=True,
close_fds=(os.name != 'nt'), close_fds=(os.name != 'nt'),
cwd=os_helper.SAVEDCWD, cwd=os_helper.SAVEDCWD,
**kw) )
if USE_PROCESS_GROUP:
kw['start_new_session'] = True
return subprocess.Popen(cmd, **kw)
def run_tests_worker(ns: Namespace, test_name: str) -> NoReturn: def run_tests_worker(ns: Namespace, test_name: str) -> NoReturn:
@ -212,12 +212,12 @@ def mp_result_error(
test_result.duration_sec = time.monotonic() - self.start_time test_result.duration_sec = time.monotonic() - self.start_time
return MultiprocessResult(test_result, stdout, err_msg) return MultiprocessResult(test_result, stdout, err_msg)
def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]: def _run_process(self, test_name: str, tmp_dir: str, stdout_fh: TextIO) -> int:
self.start_time = time.monotonic() self.start_time = time.monotonic()
self.current_test_name = test_name self.current_test_name = test_name
try: try:
popen = run_test_in_subprocess(test_name, self.ns, tmp_dir) popen = run_test_in_subprocess(test_name, self.ns, tmp_dir, stdout_fh)
self._killed = False self._killed = False
self._popen = popen self._popen = popen
@ -234,10 +234,10 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
raise ExitThread raise ExitThread
try: try:
# bpo-45410: stderr is written into stdout # gh-94026: stdout+stderr are written to tempfile
stdout, _ = popen.communicate(timeout=self.timeout) retcode = popen.wait(timeout=self.timeout)
retcode = popen.returncode
assert retcode is not None assert retcode is not None
return retcode
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
if self._stopped: if self._stopped:
# kill() has been called: communicate() fails on reading # 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]:
# bpo-38207: Don't attempt to call communicate() again: on it # bpo-38207: Don't attempt to call communicate() again: on it
# can hang until all child processes using stdout # can hang until all child processes using stdout
# pipes completes. # pipes completes.
stdout = ''
except OSError: except OSError:
if self._stopped: if self._stopped:
# kill() has been called: communicate() fails # kill() has been called: communicate() fails
# on reading closed stdout # on reading closed stdout
raise ExitThread raise ExitThread
raise raise
else:
stdout = stdout.strip()
return (retcode, stdout)
except: except:
self._kill() self._kill()
raise raise
@ -272,23 +267,30 @@ def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
self.current_test_name = None self.current_test_name = None
def _runtest(self, test_name: str) -> MultiprocessResult: def _runtest(self, test_name: str) -> MultiprocessResult:
# Don't check for leaked temporary files and directories if Python is # gh-94026: Write stdout+stderr to a tempfile as workaround for
# run on WASI. WASI don't pass environment variables like TMPDIR to # non-blocking pipes on Emscripten with NodeJS.
# worker processes. with tempfile.TemporaryFile(
if not support.is_wasi: 'w+', encoding=sys.stdout.encoding
) as stdout_fh:
# gh-93353: Check for leaked temporary files in the parent process, # gh-93353: Check for leaked temporary files in the parent process,
# since the deletion of temporary files can happen late during # since the deletion of temporary files can happen late during
# Python finalization: too late for libregrtest. # Python finalization: too late for libregrtest.
if not support.is_wasi:
# Don't check for leaked temporary files and directories if Python is
# run on WASI. WASI don't pass environment variables like TMPDIR to
# worker processes.
tmp_dir = tempfile.mkdtemp(prefix="test_python_") tmp_dir = tempfile.mkdtemp(prefix="test_python_")
tmp_dir = os.path.abspath(tmp_dir) tmp_dir = os.path.abspath(tmp_dir)
try: try:
retcode, stdout = self._run_process(test_name, tmp_dir) retcode = self._run_process(test_name, tmp_dir, stdout_fh)
finally: finally:
tmp_files = os.listdir(tmp_dir) tmp_files = os.listdir(tmp_dir)
os_helper.rmtree(tmp_dir) os_helper.rmtree(tmp_dir)
else: else:
retcode, stdout = self._run_process(test_name, None) retcode = self._run_process(test_name, None, stdout_fh)
tmp_files = () tmp_files = ()
stdout_fh.seek(0)
stdout = stdout_fh.read().strip()
if retcode is None: if retcode is None:
return self.mp_result_error(Timeout(test_name), stdout) return self.mp_result_error(Timeout(test_name), stdout)
@ -343,9 +345,6 @@ def run(self) -> None:
def _wait_completed(self) -> None: def _wait_completed(self) -> None:
popen = self._popen popen = self._popen
# stdout must be closed to ensure that communicate() does not hang
popen.stdout.close()
try: try:
popen.wait(JOIN_TIMEOUT) popen.wait(JOIN_TIMEOUT)
except (subprocess.TimeoutExpired, OSError) as exc: except (subprocess.TimeoutExpired, OSError) as exc: