2023-09-02 13:09:36 -03:00
|
|
|
import dataclasses
|
2016-03-22 22:04:32 -03:00
|
|
|
import faulthandler
|
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
|
2023-09-10 20:11:22 -03:00
|
|
|
from typing import Literal, 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
|
2015-09-29 18:15:38 -03:00
|
|
|
|
2023-09-10 22:46:26 -03:00
|
|
|
from test.libregrtest.logger import Logger
|
2021-07-22 15:25:58 -03:00
|
|
|
from test.libregrtest.main import Regrtest
|
2023-09-10 21:07:18 -03:00
|
|
|
from test.libregrtest.result import TestResult, State
|
2023-09-09 23:30:43 -03:00
|
|
|
from test.libregrtest.results import TestResults
|
2023-09-10 21:07:18 -03:00
|
|
|
from test.libregrtest.runtests import RunTests
|
|
|
|
from test.libregrtest.single import PROGRESS_MIN_TIME
|
2023-09-10 20:11:22 -03:00
|
|
|
from test.libregrtest.utils import (
|
2023-09-10 21:07:18 -03:00
|
|
|
StrPath, TestName,
|
|
|
|
format_duration, print_warning)
|
2023-09-10 20:11:22 -03:00
|
|
|
from test.libregrtest.worker import create_worker_process, USE_PROCESS_GROUP
|
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
|
|
|
|
|
2015-09-29 18:15:38 -03:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
2023-09-08 22:03:39 -03:00
|
|
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
|
|
class MultiprocessResult:
|
2021-07-22 15:25:58 -03:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2023-09-09 21:24:38 -03:00
|
|
|
class WorkerThread(threading.Thread):
|
|
|
|
def __init__(self, worker_id: int, runner: "RunWorkers") -> 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.timeout = runner.worker_timeout
|
2023-09-09 23:30:43 -03:00
|
|
|
self.log = runner.log
|
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:
|
2023-09-09 21:24:38 -03:00
|
|
|
info = [f'WorkerThread #{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:
|
2023-09-09 21:24:38 -03:00
|
|
|
# popen.kill(): the process completed, the WorkerThread thread
|
2019-10-18 10:49:08 -03:00
|
|
|
# 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,
|
2023-09-09 22:07:05 -03:00
|
|
|
tmp_dir: StrPath | None = None) -> int:
|
2019-10-01 07:29:36 -03:00
|
|
|
try:
|
2023-09-08 22:03:39 -03:00
|
|
|
popen = create_worker_process(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
|
|
|
|
|
2023-09-09 22:07:05 -03:00
|
|
|
def _runtest(self, test_name: TestName) -> MultiprocessResult:
|
2023-09-08 22:03:39 -03:00
|
|
|
self.current_test_name = test_name
|
|
|
|
|
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-08 22:03:39 -03:00
|
|
|
tests = (test_name,)
|
|
|
|
if self.runtests.rerun:
|
|
|
|
match_tests = self.runtests.get_match_tests(test_name)
|
|
|
|
else:
|
|
|
|
match_tests = None
|
2023-09-08 22:37:48 -03:00
|
|
|
kwargs = {}
|
|
|
|
if match_tests:
|
|
|
|
kwargs['match_tests'] = match_tests
|
|
|
|
worker_runtests = self.runtests.copy(tests=tests, **kwargs)
|
2023-09-03 18:37:15 -03:00
|
|
|
|
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:
|
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-09 20:41:21 -03:00
|
|
|
retcode = self._run_process(worker_runtests, 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-09 20:41:21 -03:00
|
|
|
retcode = self._run_process(worker_runtests, 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:
|
2023-09-10 20:11:22 -03:00
|
|
|
result = TestResult.from_json(worker_json)
|
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-08 22:37:48 -03:00
|
|
|
fail_fast = self.runtests.fail_fast
|
2023-09-09 21:24:38 -03:00
|
|
|
fail_env_changed = self.runtests.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:
|
2023-09-09 21:24:38 -03:00
|
|
|
# bpo-38207: RunWorkers.stop_workers() called self.stop()
|
2019-10-08 13:45:43 -03:00
|
|
|
# which killed the process. Sometimes, killing the process from the
|
|
|
|
# main thread does not interrupt popen.communicate() in
|
2023-09-09 21:24:38 -03:00
|
|
|
# WorkerThread thread. This loop with a timeout is a workaround
|
2019-10-08 13:45:43 -03:00
|
|
|
# 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
|
2023-09-09 23:30:43 -03:00
|
|
|
self.log(f"Waiting for {self} thread 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
|
|
|
|
2023-09-09 21:24:38 -03:00
|
|
|
def get_running(workers: list[WorkerThread]) -> list[str]:
|
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)
|
2023-09-09 21:24:38 -03:00
|
|
|
if not running:
|
|
|
|
return None
|
|
|
|
return f"running ({len(running)}): {', '.join(running)}"
|
2019-04-26 03:40:25 -03:00
|
|
|
|
|
|
|
|
2023-09-09 21:24:38 -03:00
|
|
|
class RunWorkers:
|
2023-09-10 22:46:26 -03:00
|
|
|
def __init__(self, num_workers: int, runtests: RunTests,
|
|
|
|
logger: Logger, results: TestResult) -> None:
|
2023-09-09 21:24:38 -03:00
|
|
|
self.num_workers = num_workers
|
2023-09-03 18:37:15 -03:00
|
|
|
self.runtests = runtests
|
2023-09-10 22:46:26 -03:00
|
|
|
self.log = logger.log
|
|
|
|
self.display_progress = logger.display_progress
|
|
|
|
self.results: TestResults = results
|
|
|
|
|
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)
|
2023-09-08 22:37:48 -03:00
|
|
|
self.timeout = runtests.timeout
|
|
|
|
if self.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-08 22:37:48 -03:00
|
|
|
self.worker_timeout = min(self.timeout * 1.5, self.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-09 21:24:38 -03:00
|
|
|
self.workers = [WorkerThread(index, self)
|
|
|
|
for index in range(1, self.num_workers + 1)]
|
2020-04-14 13:29:44 -03:00
|
|
|
msg = f"Run tests in parallel using {len(self.workers)} child processes"
|
2023-09-08 22:37:48 -03:00
|
|
|
if self.timeout:
|
2020-04-14 13:29:44 -03:00
|
|
|
msg += (" (timeout: %s, worker timeout: %s)"
|
2023-09-08 22:37:48 -03:00
|
|
|
% (format_duration(self.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-08 22:37:48 -03:00
|
|
|
pgo = self.runtests.pgo
|
|
|
|
use_faulthandler = (self.timeout is not None)
|
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:
|
2023-09-08 22:37:48 -03:00
|
|
|
return self.output.get(timeout=PROGRESS_UPDATE)
|
2015-09-29 22:05:43 -03:00
|
|
|
except queue.Empty:
|
2019-04-26 03:40:25 -03:00
|
|
|
pass
|
|
|
|
|
2023-09-09 21:24:38 -03:00
|
|
|
if not pgo:
|
|
|
|
# display progress
|
|
|
|
running = get_running(self.workers)
|
|
|
|
if running:
|
|
|
|
self.log(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-08 22:37:48 -03:00
|
|
|
pgo = self.runtests.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)
|
2023-09-09 21:24:38 -03:00
|
|
|
if not pgo:
|
|
|
|
running = get_running(self.workers)
|
|
|
|
if running:
|
|
|
|
text += f' -- {running}'
|
2023-09-09 23:30:43 -03:00
|
|
|
self.display_progress(self.test_index, text)
|
2019-04-26 03:40:25 -03:00
|
|
|
|
2021-07-22 15:25:58 -03:00
|
|
|
def _process_result(self, item: QueueOutput) -> bool:
|
|
|
|
"""Returns True if test runner must stop."""
|
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-09 23:30:43 -03:00
|
|
|
self.results.accumulate_result(result, self.runtests)
|
2023-09-03 18:37:15 -03:00
|
|
|
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
|
2023-09-09 23:30:43 -03:00
|
|
|
self.results.accumulate_result(result, self.runtests)
|
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
|
|
|
|
2023-09-09 21:24:38 -03:00
|
|
|
def run(self) -> None:
|
2023-09-08 22:37:48 -03:00
|
|
|
fail_fast = self.runtests.fail_fast
|
2023-09-09 21:24:38 -03:00
|
|
|
fail_env_changed = self.runtests.fail_env_changed
|
2023-09-03 18:37:15 -03:00
|
|
|
|
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()
|
2023-09-09 23:30:43 -03:00
|
|
|
self.results.interrupted = True
|
2019-04-26 03:40:25 -03:00
|
|
|
finally:
|
2023-09-08 22:37:48 -03:00
|
|
|
if self.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()
|