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
|
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
|
|
|
|
|
else:
|
|
|
|
|
_KILL_SIGNAL = signal.SIGKILL
|
|
|
|
|
_INT_SIGNAL = signal.SIGINT
|
|
|
|
|
_INT_RETURN_CODE = 1 if sys.version_info < (3, 8) else -signal.SIGINT.value
|
|
|
|
|
|
|
|
|
|
|
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
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-06 17:23:42 +00:00
|
|
|
# NOTE, the `daemon` fixture (+ its `_wait_for_daemon_ready`
|
|
|
|
|
# helper + the post-yield teardown drain logic) has been
|
|
|
|
|
# moved to `tests/discovery/conftest.py` since 100% of its
|
|
|
|
|
# consumers are discovery-protocol tests now living under
|
|
|
|
|
# that subdir. See:
|
|
|
|
|
# - `tests/discovery/test_multi_program.py`
|
|
|
|
|
# - `tests/discovery/test_registrar.py`
|
|
|
|
|
# - `tests/discovery/test_tpt_bind_addrs.py`
|
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
|