2023-09-02 13:09:36 -03:00
|
|
|
import dataclasses
|
2016-03-22 22:04:32 -03:00
|
|
|
import faulthandler
|
2015-09-29 18:15:38 -03:00
|
|
|
import json
|
2022-06-14 08:43:02 -03:00
|
|
|
import os.path
|
2015-09-29 22:05:43 -03:00
|
|
|
import queue
|
2019-10-18 10:49:08 -03:00
|
|
|
import signal
|
2019-04-26 03:40:25 -03:00
|
|
|
import subprocess
|
2015-09-29 18:15:38 -03:00
|
|
|
import sys
|
2022-06-16 16:48:26 -03:00
|
|
|
import tempfile
|
2017-09-07 13:56:24 -03:00
|
|
|
import threading
|
2015-09-29 19:33:29 -03:00
|
|
|
import time
|
2019-04-26 03:40:25 -03:00
|
|
|
import traceback
|
2022-06-29 05:05:16 -03:00
|
|
|
from typing import NamedTuple, NoReturn, Literal, Any, TextIO
|
2021-07-22 15:25:58 -03:00
|
|
|
|
2015-09-29 18:15:38 -03:00
|
|
|
from test import support
|
2020-06-30 10:46:06 -03:00
|
|
|
from test.support import os_helper
|
2023-09-02 13:09:36 -03:00
|
|
|
from test.support import TestStats
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
from test.libregrtest.cmdline import Namespace
|
|
|
|
from test.libregrtest.main import Regrtest
|
2016-03-23 08:14:10 -03:00
|
|
|
from test.libregrtest.runtest import (
|
2023-09-03 18:37:15 -03:00
|
|
|
runtest, TestResult, State, PROGRESS_MIN_TIME,
|
|
|
|
MatchTests, RunTests)
|
2015-09-29 21:17:28 -03:00
|
|
|
from test.libregrtest.setup import setup_tests
|
2019-10-01 07:29:36 -03:00
|
|
|
from test.libregrtest.utils import format_duration, print_warning
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2022-10-21 11:21:36 -03:00
|
|
|
if sys.platform == 'win32':
|
|
|
|
import locale
|
|
|
|
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2015-09-29 22:05:43 -03:00
|
|
|
# Display the running tests if nothing happened last N seconds
|
2015-11-04 04:03:53 -04:00
|
|
|
PROGRESS_UPDATE = 30.0 # seconds
|
2019-09-17 05:08:19 -03:00
|
|
|
assert PROGRESS_UPDATE >= PROGRESS_MIN_TIME
|
2015-09-29 22:05:43 -03:00
|
|
|
|
2019-10-08 13:45:43 -03:00
|
|
|
# Kill the main process after 5 minutes. It is supposed to write an update
|
|
|
|
# every PROGRESS_UPDATE seconds. Tolerate 5 minutes for Python slowest
|
|
|
|
# buildbot workers.
|
|
|
|
MAIN_PROCESS_TIMEOUT = 5 * 60.0
|
|
|
|
assert MAIN_PROCESS_TIMEOUT >= PROGRESS_UPDATE
|
|
|
|
|
2019-05-13 22:47:32 -03:00
|
|
|
# Time to wait until a worker completes: should be immediate
|
|
|
|
JOIN_TIMEOUT = 30.0 # seconds
|
|
|
|
|
2019-10-18 10:49:08 -03:00
|
|
|
USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg"))
|
|
|
|
|
2016-03-24 08:04:15 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
@dataclasses.dataclass(slots=True)
|
|
|
|
class WorkerJob:
|
|
|
|
test_name: str
|
|
|
|
namespace: Namespace
|
|
|
|
rerun: bool = False
|
|
|
|
match_tests: MatchTests | None = None
|
2015-09-29 19:33:29 -03:00
|
|
|
|
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
class _EncodeWorkerJob(json.JSONEncoder):
|
|
|
|
def default(self, o: Any) -> dict[str, Any]:
|
|
|
|
match o:
|
|
|
|
case WorkerJob():
|
|
|
|
result = dataclasses.asdict(o)
|
|
|
|
result["__worker_job__"] = True
|
|
|
|
return result
|
|
|
|
case Namespace():
|
|
|
|
result = vars(o)
|
|
|
|
result["__namespace__"] = True
|
|
|
|
return result
|
|
|
|
case _:
|
|
|
|
return super().default(o)
|
|
|
|
|
|
|
|
|
|
|
|
def _decode_worker_job(d: dict[str, Any]) -> WorkerJob | dict[str, Any]:
|
|
|
|
if "__worker_job__" in d:
|
|
|
|
d.pop('__worker_job__')
|
|
|
|
return WorkerJob(**d)
|
|
|
|
if "__namespace__" in d:
|
|
|
|
d.pop('__namespace__')
|
|
|
|
return Namespace(**d)
|
|
|
|
else:
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_worker_args(worker_json: str) -> tuple[Namespace, str]:
|
|
|
|
return json.loads(worker_json,
|
|
|
|
object_hook=_decode_worker_job)
|
2019-05-14 10:49:16 -03:00
|
|
|
|
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
def run_test_in_subprocess(worker_job: WorkerJob,
|
|
|
|
output_file: TextIO,
|
|
|
|
tmp_dir: str | None = None) -> subprocess.Popen:
|
|
|
|
ns = worker_job.namespace
|
|
|
|
python = ns.python
|
|
|
|
worker_args = json.dumps(worker_job, cls=_EncodeWorkerJob)
|
|
|
|
|
|
|
|
if python is not None:
|
|
|
|
executable = python
|
2022-05-02 19:51:34 -03:00
|
|
|
else:
|
|
|
|
executable = [sys.executable]
|
|
|
|
cmd = [*executable, *support.args_from_interpreter_flags(),
|
2016-09-21 12:12:50 -03:00
|
|
|
'-u', # Unbuffered stdout and stderr
|
2015-09-29 19:33:29 -03:00
|
|
|
'-m', 'test.regrtest',
|
2018-09-07 12:20:42 -03:00
|
|
|
'--worker-args', worker_args]
|
2015-09-29 19:33:29 -03:00
|
|
|
|
2022-06-14 08:43:02 -03:00
|
|
|
env = dict(os.environ)
|
2022-06-14 13:04:53 -03:00
|
|
|
if tmp_dir is not None:
|
|
|
|
env['TMPDIR'] = tmp_dir
|
|
|
|
env['TEMP'] = tmp_dir
|
|
|
|
env['TMP'] = tmp_dir
|
2022-06-14 08:43:02 -03:00
|
|
|
|
2015-09-29 18:15:38 -03:00
|
|
|
# Running the child from the same working directory as regrtest's original
|
|
|
|
# invocation ensures that TEMPDIR for the child is the same when
|
|
|
|
# sysconfig.is_python_build() is true. See issue 15300.
|
2022-06-29 05:05:16 -03:00
|
|
|
kw = dict(
|
|
|
|
env=env,
|
2023-09-03 18:37:15 -03:00
|
|
|
stdout=output_file,
|
2022-06-29 05:05:16 -03:00
|
|
|
# bpo-45410: Write stderr into stdout to keep messages order
|
2023-09-03 18:37:15 -03:00
|
|
|
stderr=output_file,
|
2022-06-29 05:05:16 -03:00
|
|
|
text=True,
|
|
|
|
close_fds=(os.name != 'nt'),
|
|
|
|
cwd=os_helper.SAVEDCWD,
|
|
|
|
)
|
2019-10-18 10:49:08 -03:00
|
|
|
if USE_PROCESS_GROUP:
|
|
|
|
kw['start_new_session'] = True
|
2022-06-29 05:05:16 -03:00
|
|
|
return subprocess.Popen(cmd, **kw)
|
2015-09-29 18:15:38 -03:00
|
|
|
|
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
def run_tests_worker(worker_json: str) -> NoReturn:
|
|
|
|
worker_job = _parse_worker_args(worker_json)
|
|
|
|
ns = worker_job.namespace
|
|
|
|
test_name = worker_job.test_name
|
|
|
|
rerun = worker_job.rerun
|
|
|
|
match_tests = worker_job.match_tests
|
|
|
|
|
2015-09-29 21:17:28 -03:00
|
|
|
setup_tests(ns)
|
2015-09-29 20:32:39 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
if rerun:
|
|
|
|
if match_tests:
|
|
|
|
matching = "matching: " + ", ".join(match_tests)
|
|
|
|
print(f"Re-running {test_name} in verbose mode ({matching})", flush=True)
|
|
|
|
else:
|
|
|
|
print(f"Re-running {test_name} in verbose mode", flush=True)
|
|
|
|
ns.verbose = True
|
2019-05-14 10:49:16 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
if match_tests is not None:
|
|
|
|
ns.match_tests = match_tests
|
|
|
|
|
|
|
|
result = runtest(ns, test_name)
|
2015-09-29 18:15:38 -03:00
|
|
|
print() # Force a newline (just in case)
|
2019-05-14 10:49:16 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
# Serialize TestResult as dict in JSON
|
|
|
|
print(json.dumps(result, cls=EncodeTestResult), flush=True)
|
2015-09-29 18:15:38 -03:00
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
# We do not use a generator so multiple threads can call next().
|
|
|
|
class MultiprocessIterator:
|
|
|
|
|
|
|
|
"""A thread-safe iterator over tests for multiprocess mode."""
|
|
|
|
|
2019-05-13 14:17:54 -03:00
|
|
|
def __init__(self, tests_iter):
|
2015-09-29 18:15:38 -03:00
|
|
|
self.lock = threading.Lock()
|
2019-05-13 14:17:54 -03:00
|
|
|
self.tests_iter = tests_iter
|
2015-09-29 18:15:38 -03:00
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __next__(self):
|
|
|
|
with self.lock:
|
2019-05-13 14:17:54 -03:00
|
|
|
if self.tests_iter is None:
|
|
|
|
raise StopIteration
|
|
|
|
return next(self.tests_iter)
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
with self.lock:
|
|
|
|
self.tests_iter = None
|
2015-09-29 18:15:38 -03:00
|
|
|
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
class MultiprocessResult(NamedTuple):
|
|
|
|
result: TestResult
|
2021-10-08 12:14:37 -03:00
|
|
|
# bpo-45410: stderr is written into stdout to keep messages order
|
2023-09-02 13:09:36 -03:00
|
|
|
worker_stdout: str | None = None
|
|
|
|
err_msg: str | None = None
|
2021-07-22 15:25:58 -03:00
|
|
|
|
|
|
|
|
|
|
|
ExcStr = str
|
|
|
|
QueueOutput = tuple[Literal[False], MultiprocessResult] | tuple[Literal[True], ExcStr]
|
|
|
|
|
2019-04-25 23:08:53 -03:00
|
|
|
|
2019-05-13 22:47:32 -03:00
|
|
|
class ExitThread(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2019-10-01 07:29:36 -03:00
|
|
|
class TestWorkerProcess(threading.Thread):
|
2021-07-22 15:25:58 -03:00
|
|
|
def __init__(self, worker_id: int, runner: "MultiprocessTestRunner") -> None:
|
2015-09-29 18:15:38 -03:00
|
|
|
super().__init__()
|
2019-10-01 07:29:36 -03:00
|
|
|
self.worker_id = worker_id
|
2023-09-03 18:37:15 -03:00
|
|
|
self.runtests = runner.runtests
|
2019-10-03 11:15:16 -03:00
|
|
|
self.pending = runner.pending
|
|
|
|
self.output = runner.output
|
|
|
|
self.ns = runner.ns
|
|
|
|
self.timeout = runner.worker_timeout
|
|
|
|
self.regrtest = runner.regrtest
|
2023-09-03 18:37:15 -03:00
|
|
|
self.rerun = runner.rerun
|
2019-04-25 23:08:53 -03:00
|
|
|
self.current_test_name = None
|
2015-09-29 19:33:29 -03:00
|
|
|
self.start_time = None
|
2019-04-26 03:40:25 -03:00
|
|
|
self._popen = None
|
2019-05-13 22:47:32 -03:00
|
|
|
self._killed = False
|
2019-10-01 07:29:36 -03:00
|
|
|
self._stopped = False
|
2019-05-13 22:47:32 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def __repr__(self) -> str:
|
2019-10-01 07:29:36 -03:00
|
|
|
info = [f'TestWorkerProcess #{self.worker_id}']
|
2019-05-13 22:47:32 -03:00
|
|
|
if self.is_alive():
|
2019-10-02 08:35:11 -03:00
|
|
|
info.append("running")
|
2019-10-01 07:29:36 -03:00
|
|
|
else:
|
|
|
|
info.append('stopped')
|
|
|
|
test = self.current_test_name
|
2019-05-13 22:47:32 -03:00
|
|
|
if test:
|
|
|
|
info.append(f'test={test}')
|
|
|
|
popen = self._popen
|
2019-10-02 08:35:11 -03:00
|
|
|
if popen is not None:
|
|
|
|
dt = time.monotonic() - self.start_time
|
|
|
|
info.extend((f'pid={self._popen.pid}',
|
|
|
|
f'time={format_duration(dt)}'))
|
2019-05-13 22:47:32 -03:00
|
|
|
return '<%s>' % ' '.join(info)
|
2015-09-29 19:33:29 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def _kill(self) -> None:
|
2019-08-21 06:59:20 -03:00
|
|
|
popen = self._popen
|
2019-10-01 07:29:36 -03:00
|
|
|
if popen is None:
|
|
|
|
return
|
2019-08-21 06:59:20 -03:00
|
|
|
|
2019-10-02 08:35:11 -03:00
|
|
|
if self._killed:
|
|
|
|
return
|
|
|
|
self._killed = True
|
|
|
|
|
2019-10-18 10:49:08 -03:00
|
|
|
if USE_PROCESS_GROUP:
|
|
|
|
what = f"{self} process group"
|
|
|
|
else:
|
|
|
|
what = f"{self}"
|
|
|
|
|
|
|
|
print(f"Kill {what}", file=sys.stderr, flush=True)
|
2019-08-21 06:59:20 -03:00
|
|
|
try:
|
2019-10-18 10:49:08 -03:00
|
|
|
if USE_PROCESS_GROUP:
|
|
|
|
os.killpg(popen.pid, signal.SIGKILL)
|
|
|
|
else:
|
|
|
|
popen.kill()
|
2019-10-16 19:29:12 -03:00
|
|
|
except ProcessLookupError:
|
2019-10-18 10:49:08 -03:00
|
|
|
# popen.kill(): the process completed, the TestWorkerProcess thread
|
|
|
|
# read its exit status, but Popen.send_signal() read the returncode
|
|
|
|
# just before Popen.wait() set returncode.
|
2019-10-16 19:29:12 -03:00
|
|
|
pass
|
2019-08-21 06:59:20 -03:00
|
|
|
except OSError as exc:
|
2019-10-18 10:49:08 -03:00
|
|
|
print_warning(f"Failed to kill {what}: {exc!r}")
|
2019-08-21 06:59:20 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def stop(self) -> None:
|
2019-10-01 07:29:36 -03:00
|
|
|
# Method called from a different thread to stop this thread
|
|
|
|
self._stopped = True
|
|
|
|
self._kill()
|
2019-08-14 09:18:51 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def mp_result_error(
|
|
|
|
self,
|
|
|
|
test_result: TestResult,
|
2023-09-02 13:09:36 -03:00
|
|
|
stdout: str | None = None,
|
2021-07-22 15:25:58 -03:00
|
|
|
err_msg=None
|
|
|
|
) -> MultiprocessResult:
|
2021-10-08 12:14:37 -03:00
|
|
|
return MultiprocessResult(test_result, stdout, err_msg)
|
2021-07-22 15:25:58 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
def _run_process(self, worker_job, output_file: TextIO,
|
|
|
|
tmp_dir: str | None = None) -> int:
|
|
|
|
self.current_test_name = worker_job.test_name
|
2019-10-01 07:29:36 -03:00
|
|
|
try:
|
2023-09-03 18:37:15 -03:00
|
|
|
popen = run_test_in_subprocess(worker_job, output_file, tmp_dir)
|
2019-10-02 08:35:11 -03:00
|
|
|
|
2019-10-01 07:29:36 -03:00
|
|
|
self._killed = False
|
2019-10-02 08:35:11 -03:00
|
|
|
self._popen = popen
|
2019-10-01 07:29:36 -03:00
|
|
|
except:
|
|
|
|
self.current_test_name = None
|
|
|
|
raise
|
|
|
|
|
|
|
|
try:
|
|
|
|
if self._stopped:
|
|
|
|
# If kill() has been called before self._popen is set,
|
|
|
|
# self._popen is still running. Call again kill()
|
|
|
|
# to ensure that the process is killed.
|
|
|
|
self._kill()
|
|
|
|
raise ExitThread
|
|
|
|
|
2019-08-21 06:59:20 -03:00
|
|
|
try:
|
2022-06-29 05:05:16 -03:00
|
|
|
# gh-94026: stdout+stderr are written to tempfile
|
|
|
|
retcode = popen.wait(timeout=self.timeout)
|
2019-10-08 13:45:43 -03:00
|
|
|
assert retcode is not None
|
2022-06-29 05:05:16 -03:00
|
|
|
return retcode
|
2019-10-01 07:29:36 -03:00
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
if self._stopped:
|
2021-10-08 12:14:37 -03:00
|
|
|
# kill() has been called: communicate() fails on reading
|
|
|
|
# closed stdout
|
2019-10-01 07:29:36 -03:00
|
|
|
raise ExitThread
|
|
|
|
|
2019-10-08 13:45:43 -03:00
|
|
|
# On timeout, kill the process
|
|
|
|
self._kill()
|
|
|
|
|
|
|
|
# None means TIMEOUT for the caller
|
|
|
|
retcode = None
|
|
|
|
# bpo-38207: Don't attempt to call communicate() again: on it
|
2021-10-08 12:14:37 -03:00
|
|
|
# can hang until all child processes using stdout
|
2019-10-08 13:45:43 -03:00
|
|
|
# pipes completes.
|
2019-10-01 07:29:36 -03:00
|
|
|
except OSError:
|
|
|
|
if self._stopped:
|
|
|
|
# kill() has been called: communicate() fails
|
2021-10-08 12:14:37 -03:00
|
|
|
# on reading closed stdout
|
2019-10-01 07:29:36 -03:00
|
|
|
raise ExitThread
|
|
|
|
raise
|
|
|
|
except:
|
|
|
|
self._kill()
|
|
|
|
raise
|
2015-09-29 19:33:29 -03:00
|
|
|
finally:
|
2019-10-01 07:29:36 -03:00
|
|
|
self._wait_completed()
|
2019-04-26 03:40:25 -03:00
|
|
|
self._popen = None
|
2019-10-01 07:29:36 -03:00
|
|
|
self.current_test_name = None
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def _runtest(self, test_name: str) -> MultiprocessResult:
|
2022-10-21 11:21:36 -03:00
|
|
|
if sys.platform == 'win32':
|
|
|
|
# gh-95027: When stdout is not a TTY, Python uses the ANSI code
|
|
|
|
# page for the sys.stdout encoding. If the main process runs in a
|
|
|
|
# terminal, sys.stdout uses WindowsConsoleIO with UTF-8 encoding.
|
|
|
|
encoding = locale.getencoding()
|
|
|
|
else:
|
|
|
|
encoding = sys.stdout.encoding
|
2023-06-27 23:26:52 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
match_tests = self.runtests.get_match_tests(test_name)
|
|
|
|
|
2022-06-29 05:05:16 -03:00
|
|
|
# gh-94026: Write stdout+stderr to a tempfile as workaround for
|
|
|
|
# non-blocking pipes on Emscripten with NodeJS.
|
2023-09-03 18:37:15 -03:00
|
|
|
with tempfile.TemporaryFile('w+', encoding=encoding) as stdout_file:
|
|
|
|
worker_job = WorkerJob(test_name,
|
|
|
|
namespace=self.ns,
|
|
|
|
rerun=self.rerun,
|
|
|
|
match_tests=match_tests)
|
2022-06-14 13:04:53 -03:00
|
|
|
# gh-93353: Check for leaked temporary files in the parent process,
|
|
|
|
# since the deletion of temporary files can happen late during
|
|
|
|
# Python finalization: too late for libregrtest.
|
2022-06-29 05:05:16 -03:00
|
|
|
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 = os.path.abspath(tmp_dir)
|
|
|
|
try:
|
2023-09-03 18:37:15 -03:00
|
|
|
retcode = self._run_process(worker_job, stdout_file, tmp_dir)
|
2022-06-29 05:05:16 -03:00
|
|
|
finally:
|
|
|
|
tmp_files = os.listdir(tmp_dir)
|
|
|
|
os_helper.rmtree(tmp_dir)
|
|
|
|
else:
|
2023-09-03 18:37:15 -03:00
|
|
|
retcode = self._run_process(worker_job, stdout_file)
|
2022-06-29 05:05:16 -03:00
|
|
|
tmp_files = ()
|
2023-09-03 18:37:15 -03:00
|
|
|
stdout_file.seek(0)
|
2023-06-27 23:26:52 -03:00
|
|
|
|
|
|
|
try:
|
2023-09-03 18:37:15 -03:00
|
|
|
stdout = stdout_file.read().strip()
|
2023-06-27 23:26:52 -03:00
|
|
|
except Exception as exc:
|
|
|
|
# gh-101634: Catch UnicodeDecodeError if stdout cannot be
|
|
|
|
# decoded from encoding
|
|
|
|
err_msg = f"Cannot read process stdout: {exc}"
|
2023-09-02 13:09:36 -03:00
|
|
|
result = TestResult(test_name, state=State.MULTIPROCESSING_ERROR)
|
|
|
|
return self.mp_result_error(result, err_msg=err_msg)
|
2019-10-01 07:29:36 -03:00
|
|
|
|
2019-10-08 13:45:43 -03:00
|
|
|
if retcode is None:
|
2023-09-02 13:09:36 -03:00
|
|
|
result = TestResult(test_name, state=State.TIMEOUT)
|
|
|
|
return self.mp_result_error(result, stdout)
|
2015-09-29 19:33:29 -03:00
|
|
|
|
2019-04-26 03:40:25 -03:00
|
|
|
err_msg = None
|
2015-09-29 19:33:29 -03:00
|
|
|
if retcode != 0:
|
2019-04-26 03:40:25 -03:00
|
|
|
err_msg = "Exit code %s" % retcode
|
|
|
|
else:
|
2023-09-02 13:09:36 -03:00
|
|
|
stdout, _, worker_json = stdout.rpartition("\n")
|
2019-04-26 03:40:25 -03:00
|
|
|
stdout = stdout.rstrip()
|
2023-09-02 13:09:36 -03:00
|
|
|
if not worker_json:
|
2019-04-26 03:40:25 -03:00
|
|
|
err_msg = "Failed to parse worker stdout"
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
# deserialize run_tests_worker() output
|
2023-09-02 13:09:36 -03:00
|
|
|
result = json.loads(worker_json,
|
|
|
|
object_hook=decode_test_result)
|
2019-04-26 03:40:25 -03:00
|
|
|
except Exception as exc:
|
|
|
|
err_msg = "Failed to parse worker JSON: %s" % exc
|
|
|
|
|
2023-09-02 13:09:36 -03:00
|
|
|
if err_msg:
|
|
|
|
result = TestResult(test_name, state=State.MULTIPROCESSING_ERROR)
|
|
|
|
return self.mp_result_error(result, stdout, err_msg)
|
2015-09-29 19:33:29 -03:00
|
|
|
|
2022-06-14 08:43:02 -03:00
|
|
|
if tmp_files:
|
|
|
|
msg = (f'\n\n'
|
2022-06-14 13:04:53 -03:00
|
|
|
f'Warning -- {test_name} leaked temporary files '
|
|
|
|
f'({len(tmp_files)}): {", ".join(sorted(tmp_files))}')
|
2022-06-14 08:43:02 -03:00
|
|
|
stdout += msg
|
2023-09-02 13:09:36 -03:00
|
|
|
result.set_env_changed()
|
2022-06-14 08:43:02 -03:00
|
|
|
|
2023-09-02 13:09:36 -03:00
|
|
|
return MultiprocessResult(result, stdout)
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def run(self) -> None:
|
2023-09-03 18:37:15 -03:00
|
|
|
fail_fast = self.ns.failfast
|
|
|
|
fail_env_changed = self.ns.fail_env_changed
|
2019-10-01 07:29:36 -03:00
|
|
|
while not self._stopped:
|
2019-04-26 03:40:25 -03:00
|
|
|
try:
|
|
|
|
try:
|
|
|
|
test_name = next(self.pending)
|
|
|
|
except StopIteration:
|
|
|
|
break
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2023-09-02 13:09:36 -03:00
|
|
|
self.start_time = time.monotonic()
|
2019-04-26 03:40:25 -03:00
|
|
|
mp_result = self._runtest(test_name)
|
2023-09-02 13:09:36 -03:00
|
|
|
mp_result.result.duration = time.monotonic() - self.start_time
|
2019-04-26 03:40:25 -03:00
|
|
|
self.output.put((False, mp_result))
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
if mp_result.result.must_stop(fail_fast, fail_env_changed):
|
2019-04-26 03:40:25 -03:00
|
|
|
break
|
2019-05-13 22:47:32 -03:00
|
|
|
except ExitThread:
|
|
|
|
break
|
2019-04-26 03:40:25 -03:00
|
|
|
except BaseException:
|
|
|
|
self.output.put((True, traceback.format_exc()))
|
|
|
|
break
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def _wait_completed(self) -> None:
|
2019-10-01 07:29:36 -03:00
|
|
|
popen = self._popen
|
|
|
|
|
|
|
|
try:
|
|
|
|
popen.wait(JOIN_TIMEOUT)
|
|
|
|
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
|
|
print_warning(f"Failed to wait for {self} completion "
|
|
|
|
f"(timeout={format_duration(JOIN_TIMEOUT)}): "
|
|
|
|
f"{exc!r}")
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def wait_stopped(self, start_time: float) -> None:
|
2019-10-08 13:45:43 -03:00
|
|
|
# bpo-38207: MultiprocessTestRunner.stop_workers() called self.stop()
|
|
|
|
# which killed the process. Sometimes, killing the process from the
|
|
|
|
# main thread does not interrupt popen.communicate() in
|
|
|
|
# TestWorkerProcess thread. This loop with a timeout is a workaround
|
|
|
|
# for that.
|
|
|
|
#
|
|
|
|
# Moreover, if this method fails to join the thread, it is likely
|
|
|
|
# that Python will hang at exit while calling threading._shutdown()
|
|
|
|
# which tries again to join the blocked thread. Regrtest.main()
|
|
|
|
# uses EXIT_TIMEOUT to workaround this second bug.
|
2019-10-01 07:29:36 -03:00
|
|
|
while True:
|
|
|
|
# Write a message every second
|
|
|
|
self.join(1.0)
|
|
|
|
if not self.is_alive():
|
|
|
|
break
|
|
|
|
dt = time.monotonic() - start_time
|
2019-10-03 11:15:16 -03:00
|
|
|
self.regrtest.log(f"Waiting for {self} thread "
|
|
|
|
f"for {format_duration(dt)}")
|
2019-10-01 07:29:36 -03:00
|
|
|
if dt > JOIN_TIMEOUT:
|
|
|
|
print_warning(f"Failed to join {self} in {format_duration(dt)}")
|
|
|
|
break
|
|
|
|
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def get_running(workers: list[TestWorkerProcess]) -> list[TestWorkerProcess]:
|
2019-04-26 03:40:25 -03:00
|
|
|
running = []
|
2015-09-29 18:15:38 -03:00
|
|
|
for worker in workers:
|
2019-04-26 03:40:25 -03:00
|
|
|
current_test_name = worker.current_test_name
|
|
|
|
if not current_test_name:
|
|
|
|
continue
|
|
|
|
dt = time.monotonic() - worker.start_time
|
|
|
|
if dt >= PROGRESS_MIN_TIME:
|
|
|
|
text = '%s (%s)' % (current_test_name, format_duration(dt))
|
|
|
|
running.append(text)
|
|
|
|
return running
|
|
|
|
|
|
|
|
|
2019-10-01 07:29:36 -03:00
|
|
|
class MultiprocessTestRunner:
|
2023-09-03 18:37:15 -03:00
|
|
|
def __init__(self, regrtest: Regrtest, runtests: RunTests) -> None:
|
|
|
|
ns = regrtest.ns
|
|
|
|
timeout = ns.timeout
|
|
|
|
|
2019-04-26 03:40:25 -03:00
|
|
|
self.regrtest = regrtest
|
2023-09-03 18:37:15 -03:00
|
|
|
self.runtests = runtests
|
|
|
|
self.rerun = runtests.rerun
|
2019-10-03 11:15:16 -03:00
|
|
|
self.log = self.regrtest.log
|
2023-09-03 18:37:15 -03:00
|
|
|
self.ns = ns
|
2021-07-22 15:25:58 -03:00
|
|
|
self.output: queue.Queue[QueueOutput] = queue.Queue()
|
2023-09-03 18:37:15 -03:00
|
|
|
tests_iter = runtests.iter_tests()
|
|
|
|
self.pending = MultiprocessIterator(tests_iter)
|
|
|
|
if timeout is not None:
|
2020-04-14 13:29:44 -03:00
|
|
|
# Rely on faulthandler to kill a worker process. This timouet is
|
|
|
|
# when faulthandler fails to kill a worker process. Give a maximum
|
|
|
|
# of 5 minutes to faulthandler to kill the worker.
|
2023-09-03 18:37:15 -03:00
|
|
|
self.worker_timeout = min(timeout * 1.5, timeout + 5 * 60)
|
2019-04-26 03:40:25 -03:00
|
|
|
else:
|
2019-08-14 09:18:51 -03:00
|
|
|
self.worker_timeout = None
|
2019-04-26 03:40:25 -03:00
|
|
|
self.workers = None
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def start_workers(self) -> None:
|
2023-09-03 18:37:15 -03:00
|
|
|
use_mp = self.ns.use_mp
|
|
|
|
timeout = self.ns.timeout
|
2019-10-03 11:15:16 -03:00
|
|
|
self.workers = [TestWorkerProcess(index, self)
|
2023-09-03 18:37:15 -03:00
|
|
|
for index in range(1, use_mp + 1)]
|
2020-04-14 13:29:44 -03:00
|
|
|
msg = f"Run tests in parallel using {len(self.workers)} child processes"
|
2023-09-03 18:37:15 -03:00
|
|
|
if timeout:
|
2020-04-14 13:29:44 -03:00
|
|
|
msg += (" (timeout: %s, worker timeout: %s)"
|
2023-09-03 18:37:15 -03:00
|
|
|
% (format_duration(timeout),
|
2020-04-14 13:29:44 -03:00
|
|
|
format_duration(self.worker_timeout)))
|
|
|
|
self.log(msg)
|
2019-04-26 03:40:25 -03:00
|
|
|
for worker in self.workers:
|
|
|
|
worker.start()
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def stop_workers(self) -> None:
|
2019-05-13 22:47:32 -03:00
|
|
|
start_time = time.monotonic()
|
2019-04-26 03:40:25 -03:00
|
|
|
for worker in self.workers:
|
2019-10-01 07:29:36 -03:00
|
|
|
worker.stop()
|
2019-04-26 03:40:25 -03:00
|
|
|
for worker in self.workers:
|
2019-10-01 07:29:36 -03:00
|
|
|
worker.wait_stopped(start_time)
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def _get_result(self) -> QueueOutput | None:
|
2023-09-03 18:37:15 -03:00
|
|
|
pgo = self.ns.pgo
|
2019-09-17 05:08:19 -03:00
|
|
|
use_faulthandler = (self.ns.timeout is not None)
|
|
|
|
timeout = PROGRESS_UPDATE
|
2022-01-10 23:03:09 -04:00
|
|
|
|
|
|
|
# bpo-46205: check the status of workers every iteration to avoid
|
|
|
|
# waiting forever on an empty queue.
|
|
|
|
while any(worker.is_alive() for worker in self.workers):
|
2019-09-17 05:08:19 -03:00
|
|
|
if use_faulthandler:
|
2019-10-08 13:45:43 -03:00
|
|
|
faulthandler.dump_traceback_later(MAIN_PROCESS_TIMEOUT,
|
|
|
|
exit=True)
|
2016-03-22 22:04:32 -03:00
|
|
|
|
2019-04-26 03:40:25 -03:00
|
|
|
# wait for a thread
|
2015-09-29 22:05:43 -03:00
|
|
|
try:
|
2019-04-26 03:40:25 -03:00
|
|
|
return self.output.get(timeout=timeout)
|
2015-09-29 22:05:43 -03:00
|
|
|
except queue.Empty:
|
2019-04-26 03:40:25 -03:00
|
|
|
pass
|
|
|
|
|
|
|
|
# display progress
|
|
|
|
running = get_running(self.workers)
|
2023-09-03 18:37:15 -03:00
|
|
|
if running and not pgo:
|
2019-10-03 11:15:16 -03:00
|
|
|
self.log('running: %s' % ', '.join(running))
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2022-01-10 23:03:09 -04:00
|
|
|
# all worker threads are done: consume pending results
|
|
|
|
try:
|
|
|
|
return self.output.get(timeout=0)
|
|
|
|
except queue.Empty:
|
|
|
|
return None
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def display_result(self, mp_result: MultiprocessResult) -> None:
|
2019-04-26 03:40:25 -03:00
|
|
|
result = mp_result.result
|
2023-09-03 18:37:15 -03:00
|
|
|
pgo = self.ns.pgo
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
text = str(result)
|
2023-09-02 13:09:36 -03:00
|
|
|
if mp_result.err_msg:
|
|
|
|
# MULTIPROCESSING_ERROR
|
|
|
|
text += ' (%s)' % mp_result.err_msg
|
2023-09-03 18:37:15 -03:00
|
|
|
elif (result.duration >= PROGRESS_MIN_TIME and not pgo):
|
2023-09-02 13:09:36 -03:00
|
|
|
text += ' (%s)' % format_duration(result.duration)
|
2019-04-26 03:40:25 -03:00
|
|
|
running = get_running(self.workers)
|
2023-09-03 18:37:15 -03:00
|
|
|
if running and not pgo:
|
2019-04-26 03:40:25 -03:00
|
|
|
text += ' -- running: %s' % ', '.join(running)
|
|
|
|
self.regrtest.display_progress(self.test_index, text)
|
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def _process_result(self, item: QueueOutput) -> bool:
|
|
|
|
"""Returns True if test runner must stop."""
|
2023-09-03 18:37:15 -03:00
|
|
|
rerun = self.runtests.rerun
|
2019-04-26 03:40:25 -03:00
|
|
|
if item[0]:
|
|
|
|
# Thread got an exception
|
|
|
|
format_exc = item[1]
|
2019-10-03 11:15:16 -03:00
|
|
|
print_warning(f"regrtest worker thread failed: {format_exc}")
|
2023-09-02 13:09:36 -03:00
|
|
|
result = TestResult("<regrtest worker>", state=State.MULTIPROCESSING_ERROR)
|
2023-09-03 18:37:15 -03:00
|
|
|
self.regrtest.accumulate_result(result, rerun=rerun)
|
|
|
|
return result
|
2019-04-26 03:40:25 -03:00
|
|
|
|
|
|
|
self.test_index += 1
|
|
|
|
mp_result = item[1]
|
2023-09-03 18:37:15 -03:00
|
|
|
result = mp_result.result
|
|
|
|
self.regrtest.accumulate_result(result, rerun=rerun)
|
2019-04-26 03:40:25 -03:00
|
|
|
self.display_result(mp_result)
|
|
|
|
|
2023-09-02 13:09:36 -03:00
|
|
|
if mp_result.worker_stdout:
|
|
|
|
print(mp_result.worker_stdout, flush=True)
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
return result
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def run_tests(self) -> None:
|
2023-09-03 18:37:15 -03:00
|
|
|
fail_fast = self.ns.failfast
|
|
|
|
fail_env_changed = self.ns.fail_env_changed
|
|
|
|
timeout = self.ns.timeout
|
|
|
|
|
2019-04-26 03:40:25 -03:00
|
|
|
self.start_workers()
|
|
|
|
|
|
|
|
self.test_index = 0
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
item = self._get_result()
|
|
|
|
if item is None:
|
|
|
|
break
|
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
result = self._process_result(item)
|
|
|
|
if result.must_stop(fail_fast, fail_env_changed):
|
2019-04-26 03:40:25 -03:00
|
|
|
break
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print()
|
|
|
|
self.regrtest.interrupted = True
|
|
|
|
finally:
|
2023-09-03 18:37:15 -03:00
|
|
|
if timeout is not None:
|
2019-04-26 03:40:25 -03:00
|
|
|
faulthandler.cancel_dump_traceback_later()
|
|
|
|
|
2019-10-01 07:29:36 -03:00
|
|
|
# Always ensure that all worker processes are no longer
|
|
|
|
# worker when we exit this function
|
|
|
|
self.pending.stop()
|
|
|
|
self.stop_workers()
|
2019-04-26 03:40:25 -03:00
|
|
|
|
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
def run_tests_multiprocess(regrtest: Regrtest, runtests: RunTests) -> None:
|
|
|
|
MultiprocessTestRunner(regrtest, runtests).run_tests()
|
2021-07-22 15:25:58 -03:00
|
|
|
|
|
|
|
|
|
|
|
class EncodeTestResult(json.JSONEncoder):
|
|
|
|
"""Encode a TestResult (sub)class object into a JSON dict."""
|
|
|
|
|
|
|
|
def default(self, o: Any) -> dict[str, Any]:
|
|
|
|
if isinstance(o, TestResult):
|
2023-09-02 13:09:36 -03:00
|
|
|
result = dataclasses.asdict(o)
|
2021-07-22 15:25:58 -03:00
|
|
|
result["__test_result__"] = o.__class__.__name__
|
|
|
|
return result
|
|
|
|
|
|
|
|
return super().default(o)
|
|
|
|
|
|
|
|
|
2023-09-03 18:37:15 -03:00
|
|
|
def decode_test_result(d: dict[str, Any]) -> TestResult | dict[str, Any]:
|
2021-07-22 15:25:58 -03:00
|
|
|
"""Decode a TestResult (sub)class object from a JSON dict."""
|
|
|
|
|
|
|
|
if "__test_result__" not in d:
|
|
|
|
return d
|
|
|
|
|
2023-09-02 13:09:36 -03:00
|
|
|
d.pop('__test_result__')
|
|
|
|
if d['stats'] is not None:
|
|
|
|
d['stats'] = TestStats(**d['stats'])
|
|
|
|
return TestResult(**d)
|