2018-07-11 23:25:30 +00:00
|
|
|
"""
|
2025-04-03 02:40:28 +00:00
|
|
|
Top level of the testing suites!
|
|
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
"""
|
2025-04-03 02:40:28 +00:00
|
|
|
from __future__ import annotations
|
2020-08-03 18:49:46 +00:00
|
|
|
import sys
|
|
|
|
|
import subprocess
|
2020-07-26 01:20:34 +00:00
|
|
|
import os
|
2020-08-03 18:49:46 +00:00
|
|
|
import signal
|
2026-05-05 00:03:41 +00:00
|
|
|
import socket
|
2020-01-23 06:16:10 +00:00
|
|
|
import platform
|
2020-08-03 18:49:46 +00:00
|
|
|
import time
|
2026-03-25 20:50:55 +00:00
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Literal
|
2018-08-07 18:30:25 +00:00
|
|
|
|
2018-07-11 23:25:30 +00:00
|
|
|
import pytest
|
2026-03-02 19:57:40 +00:00
|
|
|
import tractor
|
2024-03-12 19:48:20 +00:00
|
|
|
from tractor._testing import (
|
|
|
|
|
examples_dir as examples_dir,
|
|
|
|
|
tractor_test as tractor_test,
|
|
|
|
|
expect_ctxc as expect_ctxc,
|
|
|
|
|
)
|
2020-07-25 16:00:04 +00:00
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
pytest_plugins: list[str] = [
|
|
|
|
|
'pytester',
|
2026-04-30 23:29:51 +00:00
|
|
|
# NOTE, now loaded in `pytest-ini` section of `pyproject.toml`
|
|
|
|
|
# 'tractor._testing.pytest',
|
2025-04-17 15:20:49 +00:00
|
|
|
]
|
|
|
|
|
|
2026-03-12 21:32:05 +00:00
|
|
|
_ci_env: bool = os.environ.get('CI', False)
|
2026-03-02 20:27:01 +00:00
|
|
|
_non_linux: bool = platform.system() != 'Linux'
|
2018-07-11 23:25:30 +00:00
|
|
|
|
2020-08-03 18:49:46 +00:00
|
|
|
# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
|
|
|
|
|
if platform.system() == 'Windows':
|
|
|
|
|
_KILL_SIGNAL = signal.CTRL_BREAK_EVENT
|
|
|
|
|
_INT_SIGNAL = signal.CTRL_C_EVENT
|
|
|
|
|
_INT_RETURN_CODE = 3221225786
|
|
|
|
|
_PROC_SPAWN_WAIT = 2
|
|
|
|
|
else:
|
|
|
|
|
_KILL_SIGNAL = signal.SIGKILL
|
|
|
|
|
_INT_SIGNAL = signal.SIGINT
|
|
|
|
|
_INT_RETURN_CODE = 1 if sys.version_info < (3, 8) else -signal.SIGINT.value
|
2025-04-03 02:40:28 +00:00
|
|
|
_PROC_SPAWN_WAIT = (
|
2026-03-12 21:32:05 +00:00
|
|
|
2 if _ci_env
|
|
|
|
|
else 1
|
2025-04-03 02:40:28 +00:00
|
|
|
)
|
2020-08-03 18:49:46 +00:00
|
|
|
|
|
|
|
|
|
2020-07-25 16:00:04 +00:00
|
|
|
no_windows = pytest.mark.skipif(
|
|
|
|
|
platform.system() == "Windows",
|
|
|
|
|
reason="Test is unsupported on windows",
|
|
|
|
|
)
|
2026-02-25 01:02:14 +00:00
|
|
|
no_macos = pytest.mark.skipif(
|
|
|
|
|
platform.system() == "Darwin",
|
|
|
|
|
reason="Test is unsupported on MacOS",
|
|
|
|
|
)
|
2020-07-25 16:00:04 +00:00
|
|
|
|
|
|
|
|
|
2026-03-25 20:50:55 +00:00
|
|
|
def get_cpu_state(
|
|
|
|
|
icpu: int = 0,
|
|
|
|
|
setting: Literal[
|
|
|
|
|
'scaling_governor',
|
|
|
|
|
'*_pstate_max_freq',
|
|
|
|
|
'scaling_max_freq',
|
|
|
|
|
# 'scaling_cur_freq',
|
|
|
|
|
] = '*_pstate_max_freq',
|
|
|
|
|
) -> tuple[
|
|
|
|
|
Path,
|
|
|
|
|
str|int,
|
|
|
|
|
]|None:
|
|
|
|
|
'''
|
|
|
|
|
Attempt to read the (first) CPU's setting according
|
|
|
|
|
to the set `setting` from under the file-sys,
|
|
|
|
|
|
|
|
|
|
/sys/devices/system/cpu/cpu0/cpufreq/{setting}
|
|
|
|
|
|
|
|
|
|
Useful to determine latency headroom for various perf affected
|
|
|
|
|
test suites.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
try:
|
|
|
|
|
# Read governor for core 0 (usually same for all)
|
|
|
|
|
setting_path: Path = list(
|
|
|
|
|
Path(f'/sys/devices/system/cpu/cpu{icpu}/cpufreq/')
|
|
|
|
|
.glob(f'{setting}')
|
|
|
|
|
)[0] # <- XXX must be single match!
|
|
|
|
|
with open(
|
|
|
|
|
setting_path,
|
|
|
|
|
'r',
|
|
|
|
|
) as f:
|
|
|
|
|
return (
|
|
|
|
|
setting_path,
|
|
|
|
|
f.read().strip(),
|
|
|
|
|
)
|
|
|
|
|
except (FileNotFoundError, IndexError):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cpu_scaling_factor() -> float:
|
|
|
|
|
'''
|
|
|
|
|
Return a latency-headroom multiplier (>= 1.0) reflecting how
|
|
|
|
|
much to inflate time-limits when CPU-freq scaling is active on
|
|
|
|
|
linux.
|
|
|
|
|
|
|
|
|
|
When no scaling info is available (non-linux, missing sysfs),
|
|
|
|
|
returns 1.0 (i.e. no headroom adjustment needed).
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
if _non_linux:
|
|
|
|
|
return 1.
|
|
|
|
|
|
|
|
|
|
mx = get_cpu_state()
|
|
|
|
|
cur = get_cpu_state(setting='scaling_max_freq')
|
|
|
|
|
if mx is None or cur is None:
|
|
|
|
|
return 1.
|
|
|
|
|
|
|
|
|
|
_mx_pth, max_freq = mx
|
|
|
|
|
_cur_pth, cur_freq = cur
|
|
|
|
|
cpu_scaled: float = int(cur_freq) / int(max_freq)
|
|
|
|
|
|
|
|
|
|
if cpu_scaled != 1.:
|
|
|
|
|
return 1. / (
|
|
|
|
|
cpu_scaled * 2 # <- bc likely "dual threaded"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return 1.
|
|
|
|
|
|
|
|
|
|
|
Lift `--ll`/`--tl` to plugin + `LogSpec` API
Two coupled changes that let downstream projects (eg. `modden`) inherit
the test-harness loglevel plumbing for free via
`tractor._testing.pytest`:
Plugin lift (`tests/conftest.py` → `_testing/pytest.py`),
- mv `pytest_addoption(--ll)`, the `loglevel` autouse
fixture, and `test_log` fixture out of the test-suite-
local conftest into the reusable plugin.
- add `--tl`/`--tractor-loglevel` as a DISTINCT flag from
`--ll`: `--ll` is the consuming-project's OWN app
loglevel (scoped to its pkg-hierarchy), `--tl` is the
`tractor.*` runtime loglevel. `--tl` falls back to
`--ll` when unset (preserves current `tractor`-suite
behavior).
- add `testing_pkg_name` session fixture (default
`'tractor'`) — downstream projects override to e.g.
`'modden'` so `--ll` scopes to their own hierarchy
instead of `tractor.*`.
- `loglevel` fixture now yields the resolved
tractor-runtime level (passed to
`open_root_actor(loglevel=<.>)` by `@tractor_test`)
AND separately applies `--ll` to the
`testing_pkg_name` hierarchy when that isn't
`tractor`. `test_log` scopes the per-test logger to
`testing_pkg_name`.
`tractor.log` "logging-spec" mini-DSL,
- `LogSpec = str|bool`. Accepted forms:
- `True` → enable `pkg_name` root at `default_level`
(fallback `'cancel'`).
- `False` → no-op.
- bare level eg. `'info'` → root-logger at that level.
- `'sub:info,x:cancel'` → per-sub-logger filter-spec;
each `<name>` is RELATIVE to `pkg_name` (must NOT
include the pkg-token).
- `parse_logspec()` → `{sublog|None: level}` mapping.
`None` key = root-logger. Mixed bare-level + filters
in one spec is rejected w/ a helpful err msg; so is
embedding the `pkg_name` token in a sub-name.
- `apply_logspec()` → `(primary_level, {name: log})`:
parses then enables a `colorlog` stderr handler per
named (sub)logger. Authoritative sub-logger filters
get `propagate=False` so they don't double-emit
through a parallel root-level handler.
- !GRANULARITY CAVEAT! sub-logger names match at
sub-pkg granularity, not leaf-module — so `devx.debug`
collapses to the same `tractor.devx` logger as a bare
`devx`, and top-level lib modules (eg.
`tractor.to_asyncio`) emit under the *root* logger
rather than a phantom `to_asyncio` child. Documented
inline on `LogSpec`.
Other,
- `tests/conftest.py` keeps a NOTE pointing to the
plugin for future-debugging clarity (don't remove
silently — the lift is the relevant signal).
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit 19a77708ba8a05411d5017ab83562f614098f99e)
2026-05-29 21:43:55 +00:00
|
|
|
# NOTE, the `--ll`/`--tl` CLI flags + the `loglevel`, `test_log`
|
|
|
|
|
# and `testing_pkg_name` fixtures have been factored into the
|
|
|
|
|
# `tractor._testing.pytest` plugin (loaded via the `-p` entry in
|
|
|
|
|
# `pyproject.toml`'s `[tool.pytest.ini_options]`) so downstream
|
|
|
|
|
# consuming projects (eg. `modden`) inherit them for free. The
|
|
|
|
|
# plugin's `testing_pkg_name` fixture defaults to `'tractor'`, so
|
|
|
|
|
# this suite keeps treating `--ll` as the runtime loglevel.
|
2026-03-02 19:57:40 +00:00
|
|
|
|
|
|
|
|
|
2020-07-26 01:20:34 +00:00
|
|
|
@pytest.fixture(scope='session')
|
2020-09-03 12:44:24 +00:00
|
|
|
def ci_env() -> bool:
|
2024-03-12 19:48:20 +00:00
|
|
|
'''
|
2025-04-04 04:05:55 +00:00
|
|
|
Detect CI environment.
|
2024-03-12 19:48:20 +00:00
|
|
|
|
|
|
|
|
'''
|
2022-07-12 17:49:36 +00:00
|
|
|
return _ci_env
|
2020-07-26 01:20:34 +00:00
|
|
|
|
|
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
def sig_prog(
|
|
|
|
|
proc: subprocess.Popen,
|
|
|
|
|
sig: int,
|
2026-04-14 17:57:01 +00:00
|
|
|
canc_timeout: float = 0.2,
|
|
|
|
|
tries: int = 3,
|
2025-04-03 02:40:28 +00:00
|
|
|
) -> int:
|
2026-04-14 17:57:01 +00:00
|
|
|
'''
|
|
|
|
|
Kill the actor-process with `sig`.
|
|
|
|
|
|
|
|
|
|
Prefer to kill with the provided signal and
|
|
|
|
|
failing a `canc_timeout`, send a `SIKILL`-like
|
|
|
|
|
to ensure termination.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
for i in range(tries):
|
|
|
|
|
proc.send_signal(sig)
|
2026-04-14 22:32:55 +00:00
|
|
|
if proc.poll() is None:
|
2026-04-14 17:57:01 +00:00
|
|
|
print(
|
|
|
|
|
f'WARNING, proc still alive after,\n'
|
|
|
|
|
f'canc_timeout={canc_timeout!r}\n'
|
|
|
|
|
f'sig={sig!r}\n'
|
|
|
|
|
f'\n'
|
|
|
|
|
f'{proc.args!r}\n'
|
|
|
|
|
)
|
|
|
|
|
time.sleep(canc_timeout)
|
|
|
|
|
else:
|
2020-08-03 18:49:46 +00:00
|
|
|
# TODO: why sometimes does SIGINT not work on teardown?
|
|
|
|
|
# seems to happen only when trace logging enabled?
|
2026-04-14 22:32:55 +00:00
|
|
|
if proc.poll() is None:
|
2026-04-14 17:57:01 +00:00
|
|
|
print(
|
|
|
|
|
f'XXX WARNING KILLING PROG WITH SIGINT XXX\n'
|
|
|
|
|
f'canc_timeout={canc_timeout!r}\n'
|
|
|
|
|
f'{proc.args!r}\n'
|
|
|
|
|
)
|
|
|
|
|
proc.send_signal(_KILL_SIGNAL)
|
|
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
ret: int = proc.wait()
|
2020-08-03 18:49:46 +00:00
|
|
|
assert ret
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 00:03:41 +00:00
|
|
|
def _wait_for_daemon_ready(
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
tpt_proto: str,
|
|
|
|
|
*,
|
|
|
|
|
deadline: float = 10.0,
|
|
|
|
|
poll_interval: float = 0.05,
|
|
|
|
|
proc: subprocess.Popen|None = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Active-poll the daemon's bind address until it
|
|
|
|
|
accepts a connection (proving it has called
|
|
|
|
|
`bind() + listen()` and is ready to handle IPC).
|
|
|
|
|
|
|
|
|
|
Replaces the historical blind `time.sleep()` in the
|
|
|
|
|
`daemon` fixture which was racy under load — see
|
|
|
|
|
`ai/conc-anal/test_register_duplicate_name_daemon_connect_race_issue.md`.
|
|
|
|
|
|
|
|
|
|
Uses stdlib `socket` directly (no trio runtime
|
|
|
|
|
bootstrap cost) — sufficient because
|
|
|
|
|
`tractor.run_daemon()` doesn't return from
|
|
|
|
|
bootstrap until the runtime is fully ready to
|
|
|
|
|
accept IPC.
|
|
|
|
|
|
|
|
|
|
Raises `TimeoutError` on `deadline` exceeded. If
|
|
|
|
|
`proc` is given, ALSO raises early if the daemon
|
|
|
|
|
process exits non-zero before the deadline (catches
|
|
|
|
|
daemon-startup-crash that the blind sleep used to
|
|
|
|
|
silently mask).
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
end: float = time.monotonic() + deadline
|
|
|
|
|
last_exc: Exception|None = None
|
|
|
|
|
while time.monotonic() < end:
|
|
|
|
|
# Daemon-died-during-startup early-exit. Without
|
|
|
|
|
# this, a crashed-on-import daemon would just
|
|
|
|
|
# eat the full deadline before raising opaque
|
|
|
|
|
# TimeoutError.
|
|
|
|
|
if proc is not None and proc.poll() is not None:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f'Daemon proc exited (rc={proc.returncode}) '
|
|
|
|
|
f'before becoming ready to accept on '
|
|
|
|
|
f'{reg_addr!r}'
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
if tpt_proto == 'tcp':
|
|
|
|
|
# `socket.create_connection` does the
|
|
|
|
|
# `socket() + connect()` dance with a
|
|
|
|
|
# builtin timeout — perfect primitive
|
|
|
|
|
# for a one-shot probe.
|
|
|
|
|
with socket.create_connection(
|
|
|
|
|
reg_addr,
|
|
|
|
|
timeout=poll_interval,
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
# UDS — `reg_addr` is a `(filedir, sockname)`
|
|
|
|
|
# tuple per `tractor.ipc._uds.UDSAddress.unwrap`.
|
|
|
|
|
sockpath: str = os.path.join(*reg_addr)
|
|
|
|
|
sock = socket.socket(socket.AF_UNIX)
|
|
|
|
|
try:
|
|
|
|
|
sock.settimeout(poll_interval)
|
|
|
|
|
sock.connect(sockpath)
|
|
|
|
|
return
|
|
|
|
|
finally:
|
|
|
|
|
sock.close()
|
|
|
|
|
except (
|
|
|
|
|
ConnectionRefusedError,
|
|
|
|
|
FileNotFoundError,
|
|
|
|
|
OSError,
|
|
|
|
|
socket.timeout,
|
|
|
|
|
) as exc:
|
|
|
|
|
last_exc = exc
|
|
|
|
|
time.sleep(poll_interval)
|
|
|
|
|
raise TimeoutError(
|
|
|
|
|
f'Daemon never accepted on {reg_addr!r} within '
|
|
|
|
|
f'{deadline}s (last connect-attempt exc: '
|
|
|
|
|
f'{last_exc!r})'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2024-03-12 19:48:20 +00:00
|
|
|
# TODO: factor into @cm and move to `._testing`?
|
2020-08-03 18:49:46 +00:00
|
|
|
@pytest.fixture
|
2023-01-26 17:43:32 +00:00
|
|
|
def daemon(
|
2025-04-03 02:40:28 +00:00
|
|
|
debug_mode: bool,
|
2023-01-26 17:43:32 +00:00
|
|
|
loglevel: str,
|
2025-06-11 22:29:24 +00:00
|
|
|
testdir: pytest.Pytester,
|
2023-10-18 19:35:35 +00:00
|
|
|
reg_addr: tuple[str, int],
|
2025-04-03 02:40:28 +00:00
|
|
|
tpt_proto: str,
|
2026-03-02 20:27:01 +00:00
|
|
|
ci_env: bool,
|
2026-03-12 21:32:05 +00:00
|
|
|
test_log: tractor.log.StackLevelAdapter,
|
2026-05-04 20:23:50 +00:00
|
|
|
# set_fork_aware_capture,
|
2025-04-03 02:40:28 +00:00
|
|
|
|
|
|
|
|
) -> subprocess.Popen:
|
2023-01-26 17:43:32 +00:00
|
|
|
'''
|
2023-10-18 19:35:35 +00:00
|
|
|
Run a daemon root actor as a separate actor-process tree and
|
|
|
|
|
"remote registrar" for discovery-protocol related tests.
|
2023-01-26 17:43:32 +00:00
|
|
|
|
|
|
|
|
'''
|
2026-05-04 20:23:50 +00:00
|
|
|
# XXX: too much logging will lock up the subproc (smh)
|
2020-08-08 19:15:43 +00:00
|
|
|
if loglevel in ('trace', 'debug'):
|
2026-05-04 20:23:50 +00:00
|
|
|
test_log.warning(
|
|
|
|
|
f'Test harness log level is too verbose: {loglevel!r}\n'
|
|
|
|
|
f'Reducing to INFO level..'
|
|
|
|
|
)
|
2023-10-18 19:35:35 +00:00
|
|
|
loglevel: str = 'info'
|
|
|
|
|
|
|
|
|
|
code: str = (
|
2025-04-03 02:40:28 +00:00
|
|
|
"import tractor; "
|
|
|
|
|
"tractor.run_daemon([], "
|
|
|
|
|
"registry_addrs={reg_addrs}, "
|
2026-03-13 00:05:43 +00:00
|
|
|
"enable_transports={enable_tpts}, "
|
2025-04-03 02:40:28 +00:00
|
|
|
"debug_mode={debug_mode}, "
|
|
|
|
|
"loglevel={ll})"
|
2023-10-18 19:35:35 +00:00
|
|
|
).format(
|
|
|
|
|
reg_addrs=str([reg_addr]),
|
2026-03-13 00:05:43 +00:00
|
|
|
enable_tpts=str([tpt_proto]),
|
2023-10-18 19:35:35 +00:00
|
|
|
ll="'{}'".format(loglevel) if loglevel else None,
|
2025-04-03 02:40:28 +00:00
|
|
|
debug_mode=debug_mode,
|
2023-10-18 19:35:35 +00:00
|
|
|
)
|
|
|
|
|
cmd: list[str] = [
|
|
|
|
|
sys.executable,
|
|
|
|
|
'-c', code,
|
2020-08-03 18:49:46 +00:00
|
|
|
]
|
2025-04-03 02:40:28 +00:00
|
|
|
# breakpoint()
|
2023-10-18 19:35:35 +00:00
|
|
|
kwargs = {}
|
2020-08-03 18:49:46 +00:00
|
|
|
if platform.system() == 'Windows':
|
|
|
|
|
# without this, tests hang on windows forever
|
|
|
|
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
proc: subprocess.Popen = testdir.popen(
|
2023-10-18 19:35:35 +00:00
|
|
|
cmd,
|
2020-08-03 18:49:46 +00:00
|
|
|
**kwargs,
|
|
|
|
|
)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
2026-05-05 00:03:41 +00:00
|
|
|
# Active-poll the daemon's bind address until it's
|
|
|
|
|
# ready to accept connections — replaces the legacy
|
|
|
|
|
# blind `time.sleep(_PROC_SPAWN_WAIT + uds_bonus)`
|
|
|
|
|
# which was racy under load (see
|
|
|
|
|
# `ai/conc-anal/test_register_duplicate_name_daemon_connect_race_issue.md`).
|
2026-03-03 18:47:04 +00:00
|
|
|
#
|
2026-05-05 00:03:41 +00:00
|
|
|
# Per-test deadline scales with platform: macOS/CI
|
|
|
|
|
# gets extra headroom; Linux dev boxes need very
|
|
|
|
|
# little.
|
|
|
|
|
deadline: float = (
|
|
|
|
|
15.0 if (_non_linux and ci_env)
|
|
|
|
|
else 10.0
|
|
|
|
|
)
|
|
|
|
|
_wait_for_daemon_ready(
|
|
|
|
|
reg_addr=reg_addr,
|
|
|
|
|
tpt_proto=tpt_proto,
|
|
|
|
|
deadline=deadline,
|
|
|
|
|
proc=proc,
|
|
|
|
|
)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
|
|
|
|
assert not proc.returncode
|
2020-08-03 18:49:46 +00:00
|
|
|
yield proc
|
|
|
|
|
sig_prog(proc, _INT_SIGNAL)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
|
|
|
|
# XXX! yeah.. just be reaaal careful with this bc sometimes it
|
|
|
|
|
# can lock up on the `_io.BufferedReader` and hang..
|
|
|
|
|
stderr: str = proc.stderr.read().decode()
|
2026-03-03 18:47:04 +00:00
|
|
|
stdout: str = proc.stdout.read().decode()
|
|
|
|
|
if (
|
|
|
|
|
stderr
|
|
|
|
|
or
|
|
|
|
|
stdout
|
|
|
|
|
):
|
2025-04-03 02:40:28 +00:00
|
|
|
print(
|
2026-03-03 18:47:04 +00:00
|
|
|
f'Daemon actor tree produced output:\n'
|
2025-04-03 02:40:28 +00:00
|
|
|
f'{proc.args}\n'
|
|
|
|
|
f'\n'
|
2026-03-03 18:47:04 +00:00
|
|
|
f'stderr: {stderr!r}\n'
|
|
|
|
|
f'stdout: {stdout!r}\n'
|
2025-04-03 02:40:28 +00:00
|
|
|
)
|
2026-03-03 18:47:04 +00:00
|
|
|
|
|
|
|
|
if (rc := proc.returncode) != -2:
|
|
|
|
|
msg: str = (
|
|
|
|
|
f'Daemon actor tree was not cancelled !?\n'
|
|
|
|
|
f'proc.args: {proc.args!r}\n'
|
|
|
|
|
f'proc.returncode: {rc!r}\n'
|
2025-04-03 02:40:28 +00:00
|
|
|
)
|
2026-03-03 18:47:04 +00:00
|
|
|
if rc < 0:
|
|
|
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
2026-03-12 21:32:05 +00:00
|
|
|
test_log.error(msg)
|
2025-04-03 02:40:28 +00:00
|
|
|
|
2025-04-04 04:05:55 +00:00
|
|
|
|
2025-04-03 02:40:28 +00:00
|
|
|
# @pytest.fixture(autouse=True)
|
|
|
|
|
# def shared_last_failed(pytestconfig):
|
|
|
|
|
# val = pytestconfig.cache.get("example/value", None)
|
|
|
|
|
# breakpoint()
|
|
|
|
|
# if val is None:
|
|
|
|
|
# pytestconfig.cache.set("example/value", val)
|
|
|
|
|
# return val
|
2025-04-04 04:05:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: a way to let test scripts (like from `examples/`)
|
|
|
|
|
# guarantee they won't `registry_addrs` collide!
|
|
|
|
|
# -[ ] maybe use some kinda standard `def main()` arg-spec that
|
|
|
|
|
# we can introspect from a fixture that is called from the test
|
|
|
|
|
# body?
|
|
|
|
|
# -[ ] test and figure out typing for below prototype! Bp
|
|
|
|
|
#
|
|
|
|
|
# @pytest.fixture
|
|
|
|
|
# def set_script_runtime_args(
|
|
|
|
|
# reg_addr: tuple,
|
|
|
|
|
# ) -> Callable[[...], None]:
|
|
|
|
|
|
|
|
|
|
# def import_n_partial_in_args_n_triorun(
|
|
|
|
|
# script: Path, # under examples?
|
|
|
|
|
# **runtime_args,
|
|
|
|
|
# ) -> Callable[[], Any]: # a `partial`-ed equiv of `trio.run()`
|
|
|
|
|
|
|
|
|
|
# # NOTE, below is taken from
|
|
|
|
|
# # `.test_advanced_faults.test_ipc_channel_break_during_stream`
|
|
|
|
|
# mod: ModuleType = import_path(
|
|
|
|
|
# examples_dir() / 'advanced_faults'
|
|
|
|
|
# / 'ipc_failure_during_stream.py',
|
|
|
|
|
# root=examples_dir(),
|
|
|
|
|
# consider_namespace_packages=False,
|
|
|
|
|
# )
|
|
|
|
|
# return partial(
|
|
|
|
|
# trio.run,
|
|
|
|
|
# partial(
|
|
|
|
|
# mod.main,
|
|
|
|
|
# **runtime_args,
|
|
|
|
|
# )
|
|
|
|
|
# )
|
|
|
|
|
# return import_n_partial_in_args_n_triorun
|