2024-03-12 19:48:20 +00:00
|
|
|
# tractor: structured concurrent "actors".
|
|
|
|
|
# Copyright 2018-eternity Tyler Goodlet.
|
|
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
`pytest` utils helpers and plugins for testing `tractor`'s runtime
|
|
|
|
|
and applications.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
from functools import (
|
|
|
|
|
partial,
|
2026-04-10 16:08:46 +00:00
|
|
|
wraps,
|
2024-03-12 19:48:20 +00:00
|
|
|
)
|
|
|
|
|
import inspect
|
|
|
|
|
import platform
|
2026-03-23 22:36:56 +00:00
|
|
|
from typing import (
|
|
|
|
|
Callable,
|
Mv `trio_proc`/`mp_proc` to per-backend submods
Split the monolithic `spawn._spawn` into a slim
"core" + per-backend submodules so a future
`._subint` backend (per issue #379) can drop in
without piling more onto `_spawn.py`.
`._spawn` retains the cross-backend supervisor
machinery: `SpawnMethodKey`, `_methods` registry,
`_spawn_method`/`_ctx` state, `try_set_start_method()`,
the `new_proc()` dispatcher, and the shared helpers
`exhaust_portal()`, `cancel_on_completion()`,
`hard_kill()`, `soft_kill()`, `proc_waiter()`.
Deats,
- mv `trio_proc()` → new `spawn._trio`
- mv `mp_proc()` → new `spawn._mp`, reads `_ctx` and
`_spawn_method` via `from . import _spawn` for
late binding bc both get mutated by
`try_set_start_method()`
- `_methods` wires up the new submods via late
bottom-of-module imports to side-step circular
dep (both backend mods pull shared helpers from
`._spawn`)
- prune now-unused imports from `_spawn.py` — `sys`,
`is_root_process`, `current_actor`,
`is_main_process`, `_mp_main`, `ActorFailure`,
`pretty_struct`, `_pformat`
Also,
- `_testing.pytest.pytest_generate_tests()` now
drives the valid-backend set from
`typing.get_args(SpawnMethodKey)` so adding a
new backend (e.g. `'subint'`) doesn't require
touching the harness
- refresh `spawn/__init__.py` docstring for the
new layout
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-17 05:58:05 +00:00
|
|
|
get_args,
|
2026-03-23 22:36:56 +00:00
|
|
|
)
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
import pytest
|
2024-03-12 19:48:20 +00:00
|
|
|
import tractor
|
|
|
|
|
import trio
|
|
|
|
|
|
|
|
|
|
|
2026-03-23 22:36:56 +00:00
|
|
|
def tractor_test(
|
|
|
|
|
wrapped: Callable|None = None,
|
|
|
|
|
*,
|
|
|
|
|
# @tractor_test(<deco-params>)
|
2026-04-10 16:08:46 +00:00
|
|
|
timeout: float = 30,
|
2026-03-23 22:36:56 +00:00
|
|
|
hide_tb: bool = True,
|
|
|
|
|
):
|
2024-03-12 19:48:20 +00:00
|
|
|
'''
|
2025-04-17 15:20:49 +00:00
|
|
|
Decorator for async test fns to decorator-wrap them as "native"
|
|
|
|
|
looking sync funcs runnable by `pytest` and auto invoked with
|
|
|
|
|
`trio.run()` (much like the `pytest-trio` plugin's approach).
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
Further the test fn body will be invoked AFTER booting the actor
|
|
|
|
|
runtime, i.e. from inside a `tractor.open_root_actor()` block AND
|
|
|
|
|
with various runtime and tooling parameters implicitly passed as
|
|
|
|
|
requested by by the test session's config; see immediately below.
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
Basic deco use:
|
|
|
|
|
---------------
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2026-03-23 22:36:56 +00:00
|
|
|
@tractor_test(
|
|
|
|
|
timeout=10,
|
|
|
|
|
)
|
|
|
|
|
async def test_whatever(
|
|
|
|
|
# fixture param declarations
|
|
|
|
|
loglevel: str,
|
|
|
|
|
start_method: str,
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
tpt_proto: str,
|
|
|
|
|
debug_mode: bool,
|
|
|
|
|
):
|
|
|
|
|
# already inside a root-actor runtime `trio.Task`
|
2025-04-17 15:20:49 +00:00
|
|
|
await ...
|
2024-03-12 19:48:20 +00:00
|
|
|
|
|
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
Runtime config via special fixtures:
|
|
|
|
|
------------------------------------
|
|
|
|
|
If any of the following fixture are requested by the wrapped test
|
|
|
|
|
fn (via normal func-args declaration),
|
|
|
|
|
|
Rename `Arbiter` -> `Registrar`, mv to `discovery._registry`
Move the `Arbiter` class out of `runtime._runtime` into its
logical home at `discovery._registry` as `Registrar(Actor)`.
This completes the long-standing terminology migration from
"arbiter" to "registrar/registry" throughout the codebase.
Deats,
- add new `discovery/_registry.py` mod with `Registrar`
class + backward-compat `Arbiter = Registrar` alias.
- rename `Actor.is_arbiter` attr -> `.is_registrar`;
old attr now a `@property` with `DeprecationWarning`.
- `_root.py` imports `Registrar` directly for
root-actor instantiation.
- export `Registrar` + `Arbiter` from `tractor.__init__`.
- `_runtime.py` re-imports from `discovery._registry`
for backward compat.
Also,
- update all test files to use `.is_registrar`
(`test_local`, `test_rpc`, `test_spawning`,
`test_discovery`, `test_multi_program`).
- update "arbiter" -> "registrar" in comments/docstrings
across `_discovery.py`, `_server.py`, `_transport.py`,
`_testing/pytest.py`, and examples.
- drop resolved TODOs from `_runtime.py` and `_root.py`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-23 22:56:21 +00:00
|
|
|
- `reg_addr` (a socket addr tuple where registrar is listening)
|
2025-04-17 15:20:49 +00:00
|
|
|
- `loglevel` (logging level passed to tractor internals)
|
|
|
|
|
- `start_method` (subprocess spawning backend)
|
|
|
|
|
|
|
|
|
|
(TODO support)
|
|
|
|
|
- `tpt_proto` (IPC transport protocol key)
|
|
|
|
|
|
|
|
|
|
they will be automatically injected to each test as normally
|
|
|
|
|
expected as well as passed to the initial
|
|
|
|
|
`tractor.open_root_actor()` funcargs.
|
|
|
|
|
|
2024-03-12 19:48:20 +00:00
|
|
|
'''
|
2026-03-23 22:36:56 +00:00
|
|
|
__tracebackhide__: bool = hide_tb
|
|
|
|
|
|
2026-04-10 16:08:46 +00:00
|
|
|
# handle @tractor_test (no parens) vs @tractor_test(timeout=10)
|
2026-03-23 22:36:56 +00:00
|
|
|
if wrapped is None:
|
2026-04-10 16:08:46 +00:00
|
|
|
return partial(
|
2026-03-23 22:36:56 +00:00
|
|
|
tractor_test,
|
|
|
|
|
timeout=timeout,
|
2026-04-10 16:08:46 +00:00
|
|
|
hide_tb=hide_tb,
|
2026-03-23 22:36:56 +00:00
|
|
|
)
|
|
|
|
|
|
2026-04-10 16:08:46 +00:00
|
|
|
funcname: str = wrapped.__name__
|
|
|
|
|
if not inspect.iscoroutinefunction(wrapped):
|
|
|
|
|
raise TypeError(
|
|
|
|
|
f'Test-fn {funcname!r} must be an async-function !!'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# NOTE: we intentionally use `functools.wraps` instead of
|
|
|
|
|
# `@wrapt.decorator` here bc wrapt's transparent proxy makes
|
|
|
|
|
# `inspect.iscoroutinefunction(wrapper)` return `True` (it
|
|
|
|
|
# proxies `__code__` from the wrapped async fn), which causes
|
|
|
|
|
# pytest to skip the test as an "unhandled coroutine".
|
|
|
|
|
# `functools.wraps` preserves the signature for fixture
|
|
|
|
|
# injection (via `__wrapped__`) without leaking the async
|
|
|
|
|
# nature.
|
|
|
|
|
@wraps(wrapped)
|
|
|
|
|
def wrapper(**kwargs):
|
2026-03-23 22:36:56 +00:00
|
|
|
__tracebackhide__: bool = hide_tb
|
|
|
|
|
|
2026-04-10 16:08:46 +00:00
|
|
|
# NOTE, ensure we inject any test-fn declared fixture
|
|
|
|
|
# names.
|
2026-03-23 22:36:56 +00:00
|
|
|
for kw in [
|
|
|
|
|
'reg_addr',
|
|
|
|
|
'loglevel',
|
|
|
|
|
'start_method',
|
|
|
|
|
'debug_mode',
|
|
|
|
|
'tpt_proto',
|
|
|
|
|
'timeout',
|
|
|
|
|
]:
|
|
|
|
|
if kw in inspect.signature(wrapped).parameters:
|
2026-03-29 21:30:41 +00:00
|
|
|
assert kw in kwargs
|
2026-03-23 22:36:56 +00:00
|
|
|
|
2026-04-10 20:20:01 +00:00
|
|
|
# Extract runtime settings as locals for
|
|
|
|
|
# `open_root_actor()`; these must NOT leak into
|
|
|
|
|
# `kwargs` when the test fn doesn't declare them
|
|
|
|
|
# (the original pre-wrapt code had the same guard).
|
|
|
|
|
reg_addr = kwargs.get('reg_addr')
|
|
|
|
|
loglevel = kwargs.get('loglevel')
|
|
|
|
|
debug_mode = kwargs.get('debug_mode', False)
|
2026-03-29 21:30:41 +00:00
|
|
|
start_method = kwargs.get('start_method')
|
2026-04-10 16:08:46 +00:00
|
|
|
if platform.system() == 'Windows':
|
2026-03-29 21:30:41 +00:00
|
|
|
if start_method is None:
|
2026-04-10 20:20:01 +00:00
|
|
|
start_method = 'trio'
|
2026-03-29 21:30:41 +00:00
|
|
|
elif start_method != 'trio':
|
2026-03-23 22:36:56 +00:00
|
|
|
raise ValueError(
|
|
|
|
|
'ONLY the `start_method="trio"` is supported on Windows.'
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-10 20:20:01 +00:00
|
|
|
# Open a root-actor, passing runtime-settings
|
|
|
|
|
# extracted above as closure locals, then invoke
|
|
|
|
|
# the test-fn body as the root-most task.
|
2026-03-23 22:36:56 +00:00
|
|
|
#
|
2026-04-10 20:20:01 +00:00
|
|
|
# NOTE: `kwargs` is forwarded as-is to
|
|
|
|
|
# `wrapped()` — it only contains what pytest
|
|
|
|
|
# injected based on the test fn's signature.
|
2026-04-10 16:08:46 +00:00
|
|
|
async def _main(**kwargs):
|
2026-03-23 22:36:56 +00:00
|
|
|
__tracebackhide__: bool = hide_tb
|
|
|
|
|
|
|
|
|
|
with trio.fail_after(timeout):
|
2024-03-12 19:48:20 +00:00
|
|
|
async with tractor.open_root_actor(
|
2026-04-10 16:08:46 +00:00
|
|
|
registry_addrs=(
|
|
|
|
|
[reg_addr] if reg_addr else None
|
|
|
|
|
),
|
2026-04-10 20:20:01 +00:00
|
|
|
loglevel=loglevel,
|
|
|
|
|
start_method=start_method,
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2026-04-10 16:08:46 +00:00
|
|
|
# TODO: only enable when pytest is passed
|
|
|
|
|
# --pdb
|
2026-04-10 20:20:01 +00:00
|
|
|
debug_mode=debug_mode,
|
2024-03-12 19:48:20 +00:00
|
|
|
|
|
|
|
|
):
|
2026-03-23 22:36:56 +00:00
|
|
|
# invoke test-fn body IN THIS task
|
2026-04-10 16:08:46 +00:00
|
|
|
await wrapped(**kwargs)
|
2026-03-23 22:36:56 +00:00
|
|
|
|
|
|
|
|
# invoke runtime via a root task.
|
|
|
|
|
return trio.run(
|
|
|
|
|
partial(
|
|
|
|
|
_main,
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2026-04-10 16:08:46 +00:00
|
|
|
return wrapper
|
2025-04-17 15:20:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_addoption(
|
|
|
|
|
parser: pytest.Parser,
|
|
|
|
|
):
|
|
|
|
|
# parser.addoption(
|
|
|
|
|
# "--ll",
|
|
|
|
|
# action="store",
|
|
|
|
|
# dest='loglevel',
|
|
|
|
|
# default='ERROR', help="logging level to set when testing"
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
|
|
parser.addoption(
|
|
|
|
|
"--spawn-backend",
|
|
|
|
|
action="store",
|
|
|
|
|
dest='spawn_backend',
|
|
|
|
|
default='trio',
|
|
|
|
|
help="Processing spawning backend to use for test run",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
parser.addoption(
|
|
|
|
|
"--tpdb",
|
|
|
|
|
"--debug-mode",
|
|
|
|
|
action="store_true",
|
|
|
|
|
dest='tractor_debug_mode',
|
|
|
|
|
# default=False,
|
|
|
|
|
help=(
|
|
|
|
|
'Enable a flag that can be used by tests to to set the '
|
|
|
|
|
'`debug_mode: bool` for engaging the internal '
|
|
|
|
|
'multi-proc debugger sys.'
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-29 14:32:23 +00:00
|
|
|
parser.addoption(
|
|
|
|
|
"--enable-stackscope",
|
|
|
|
|
action="store_true",
|
|
|
|
|
dest='tractor_enable_stackscope',
|
|
|
|
|
default=False,
|
|
|
|
|
help=(
|
|
|
|
|
'Install `stackscope` SIGUSR1 handler in pytest + '
|
|
|
|
|
'every spawned subactor for live trio task-tree '
|
|
|
|
|
'dumps during hang investigations. Lighter than '
|
|
|
|
|
'`--tpdb` (no pdb machinery / tty-lock contention) '
|
|
|
|
|
'— use when you only need stack visibility. To '
|
|
|
|
|
'capture: `kill -USR1 <pytest-or-subactor-pid>`.'
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
# provide which IPC transport protocols opting-in test suites
|
|
|
|
|
# should accumulatively run against.
|
|
|
|
|
parser.addoption(
|
|
|
|
|
"--tpt-proto",
|
|
|
|
|
nargs='+', # accumulate-multiple-args
|
|
|
|
|
action="store",
|
|
|
|
|
dest='tpt_protos',
|
|
|
|
|
default=['tcp'],
|
|
|
|
|
help="Transport protocol to use under the `tractor.ipc.Channel`",
|
|
|
|
|
)
|
|
|
|
|
|
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
|
|
|
# console loglevel for the test-session, scoped to the
|
|
|
|
|
# consuming-project's OWN pkg-hierarchy (see the
|
|
|
|
|
# `testing_pkg_name` fixture). For `tractor` itself this IS the
|
|
|
|
|
# runtime loglevel; downstream projects use `--ll` for their own
|
|
|
|
|
# ("internal") app-logging and `--tl` for tractor-as-runtime.
|
|
|
|
|
parser.addoption(
|
|
|
|
|
"--ll",
|
|
|
|
|
"--loglevel",
|
|
|
|
|
action="store",
|
|
|
|
|
dest='loglevel',
|
|
|
|
|
default=None,
|
|
|
|
|
help=(
|
|
|
|
|
"console loglevel to set for the test session, scoped to "
|
|
|
|
|
"the consuming-project pkg (see `testing_pkg_name`). "
|
|
|
|
|
"Falls through as the `--tl` default."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# tractor-as-runtime loglevel, DISTINCT from `--ll` so downstream
|
|
|
|
|
# projects can split their app-logs from the `tractor.*` runtime
|
|
|
|
|
# hierarchy. Accepts a `tractor.log` "logging-spec" (see
|
|
|
|
|
# `tractor.log.apply_logspec()`).
|
|
|
|
|
parser.addoption(
|
|
|
|
|
"--tl",
|
|
|
|
|
"--tractor-loglevel",
|
|
|
|
|
action="store",
|
|
|
|
|
dest='tractor_loglevel',
|
|
|
|
|
default=None,
|
|
|
|
|
help=(
|
|
|
|
|
"loglevel (or logging-spec) for `tractor`-as-runtime, "
|
|
|
|
|
"distinct from `--ll`. Accepts a bare level (eg. "
|
|
|
|
|
"'info', 'cancel') or a sub-logger filter-spec, "
|
|
|
|
|
"'<sublog>:<level>,...' (eg. "
|
|
|
|
|
"'devx:runtime,trionics:cancel'). Falls back to `--ll` "
|
|
|
|
|
"when unset. Mirrors the logging-spec grammar consumed "
|
|
|
|
|
"by `tractor.log.apply_logspec()` (see its sub-pkg "
|
|
|
|
|
"granularity caveat)."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
|
Add `skipon_spawn_backend` pytest marker
A reusable `@pytest.mark.skipon_spawn_backend( '<backend>' [, ...],
reason='...')` marker for backend-specific known-hang / -borked cases
— avoids scattering `@pytest.mark.skipif(lambda ...)` branches across
tests that misbehave under a particular `--spawn-backend`.
Deats,
- `pytest_configure()` registers the marker via
`addinivalue_line('markers', ...)`.
- New `pytest_collection_modifyitems()` hook walks
each collected item with `item.iter_markers(
name='skipon_spawn_backend')`, checks whether the
active `--spawn-backend` appears in `mark.args`, and
if so injects a concrete `pytest.mark.skip(
reason=...)`. `iter_markers()` makes the decorator
work at function, class, or module (`pytestmark =
[...]`) scope transparently.
- First matching mark wins; default reason is
`f'Borked on --spawn-backend={backend!r}'` if the
caller doesn't supply one.
Also, tighten type annotations on nearby `pytest`
integration points — `pytest_configure`, `debug_mode`,
`spawn_backend`, `tpt_protos`, `tpt_proto` — now taking
typed `pytest.Config` / `pytest.FixtureRequest` params.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit 3b26b59dad8c6b365890746ed5acc0811bec94c6)
2026-04-22 01:24:51 +00:00
|
|
|
def pytest_configure(
|
|
|
|
|
config: pytest.Config,
|
|
|
|
|
):
|
|
|
|
|
backend: str = config.option.spawn_backend
|
2026-03-23 22:36:56 +00:00
|
|
|
from tractor.spawn._spawn import try_set_start_method
|
2026-06-10 00:15:31 +00:00
|
|
|
try:
|
|
|
|
|
try_set_start_method(backend)
|
|
|
|
|
except RuntimeError as err:
|
|
|
|
|
# e.g. `--spawn-backend=subint` on Python < 3.14 — turn the
|
|
|
|
|
# runtime gate error into a clean pytest usage error so the
|
|
|
|
|
# suite exits with a helpful banner instead of a traceback.
|
|
|
|
|
raise pytest.UsageError(str(err)) from err
|
2025-04-17 15:20:49 +00:00
|
|
|
|
2026-03-13 17:36:47 +00:00
|
|
|
# register custom marks to avoid warnings see,
|
|
|
|
|
# https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers
|
|
|
|
|
config.addinivalue_line(
|
|
|
|
|
'markers',
|
|
|
|
|
'no_tpt(proto_key): test will (likely) not behave with tpt backend'
|
|
|
|
|
)
|
Add `skipon_spawn_backend` pytest marker
A reusable `@pytest.mark.skipon_spawn_backend( '<backend>' [, ...],
reason='...')` marker for backend-specific known-hang / -borked cases
— avoids scattering `@pytest.mark.skipif(lambda ...)` branches across
tests that misbehave under a particular `--spawn-backend`.
Deats,
- `pytest_configure()` registers the marker via
`addinivalue_line('markers', ...)`.
- New `pytest_collection_modifyitems()` hook walks
each collected item with `item.iter_markers(
name='skipon_spawn_backend')`, checks whether the
active `--spawn-backend` appears in `mark.args`, and
if so injects a concrete `pytest.mark.skip(
reason=...)`. `iter_markers()` makes the decorator
work at function, class, or module (`pytestmark =
[...]`) scope transparently.
- First matching mark wins; default reason is
`f'Borked on --spawn-backend={backend!r}'` if the
caller doesn't supply one.
Also, tighten type annotations on nearby `pytest`
integration points — `pytest_configure`, `debug_mode`,
`spawn_backend`, `tpt_protos`, `tpt_proto` — now taking
typed `pytest.Config` / `pytest.FixtureRequest` params.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit 3b26b59dad8c6b365890746ed5acc0811bec94c6)
2026-04-22 01:24:51 +00:00
|
|
|
config.addinivalue_line(
|
|
|
|
|
'markers',
|
|
|
|
|
'skipon_spawn_backend(*start_methods, reason=None): '
|
|
|
|
|
'skip this test under any of the given `--spawn-backend` '
|
|
|
|
|
'values; useful for backend-specific known-hang / -borked '
|
|
|
|
|
'cases (e.g. the `subint` GIL-starvation class documented '
|
|
|
|
|
'in `ai/conc-anal/subint_sigint_starvation_issue.md`).'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_collection_modifyitems(
|
|
|
|
|
config: pytest.Config,
|
|
|
|
|
items: list[pytest.Function],
|
|
|
|
|
):
|
|
|
|
|
'''
|
|
|
|
|
Expand any `@pytest.mark.skipon_spawn_backend('<backend>'[,
|
|
|
|
|
...], reason='...')` markers into concrete
|
|
|
|
|
`pytest.mark.skip(reason=...)` calls for tests whose
|
|
|
|
|
backend-arg set contains the active `--spawn-backend`.
|
|
|
|
|
|
|
|
|
|
Uses `item.iter_markers(name=...)` which walks function +
|
|
|
|
|
class + module-level marks in the correct scope order (and
|
|
|
|
|
handles both the single-`MarkDecorator` and `list[Mark]`
|
|
|
|
|
forms of a module-level `pytestmark`) — so the same marker
|
|
|
|
|
works at any level a user puts it.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
backend: str = config.option.spawn_backend
|
|
|
|
|
default_reason: str = f'Borked on --spawn-backend={backend!r}'
|
|
|
|
|
for item in items:
|
|
|
|
|
for mark in item.iter_markers(name='skipon_spawn_backend'):
|
|
|
|
|
if backend in mark.args:
|
|
|
|
|
reason: str = mark.kwargs.get(
|
|
|
|
|
'reason',
|
|
|
|
|
default_reason,
|
|
|
|
|
)
|
|
|
|
|
item.add_marker(pytest.mark.skip(reason=reason))
|
|
|
|
|
# first matching mark wins; no value in stacking
|
|
|
|
|
# multiple `skip`s on the same item.
|
|
|
|
|
break
|
2026-03-13 17:36:47 +00:00
|
|
|
|
2026-04-29 14:32:23 +00:00
|
|
|
# `--enable-stackscope`: install SIGUSR1 → trio task-tree
|
|
|
|
|
# dump in pytest itself + propagate to every subactor via
|
|
|
|
|
# an env var that fork-children inherit and the runtime
|
|
|
|
|
# gate honors. Lighter than `--tpdb` (no pdb machinery) —
|
|
|
|
|
# purely for hang-investigation stack visibility.
|
|
|
|
|
if getattr(
|
|
|
|
|
config.option, 'tractor_enable_stackscope', False
|
|
|
|
|
):
|
|
|
|
|
import os
|
|
|
|
|
# Env var inherited via fork → subactor's runtime
|
|
|
|
|
# picks it up at `Actor.async_main` startup. See the
|
|
|
|
|
# gate in `tractor.runtime._runtime` matching this
|
|
|
|
|
# var name.
|
|
|
|
|
os.environ['TRACTOR_ENABLE_STACKSCOPE'] = '1'
|
|
|
|
|
|
|
|
|
|
# Install in pytest itself so `kill -USR1 <pytest>`
|
|
|
|
|
# dumps the parent trio task-tree (which is where
|
|
|
|
|
# most Mode-A-class hangs park).
|
|
|
|
|
try:
|
|
|
|
|
from tractor.devx._stackscope import (
|
|
|
|
|
enable_stack_on_sig,
|
|
|
|
|
)
|
|
|
|
|
enable_stack_on_sig()
|
|
|
|
|
except ImportError:
|
|
|
|
|
import warnings
|
|
|
|
|
warnings.warn(
|
|
|
|
|
'`stackscope` not installed — '
|
|
|
|
|
'--enable-stackscope is a no-op. '
|
|
|
|
|
'Install via the `devx` dep group.'
|
|
|
|
|
)
|
|
|
|
|
|
2025-04-17 15:20:49 +00:00
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
Add `skipon_spawn_backend` pytest marker
A reusable `@pytest.mark.skipon_spawn_backend( '<backend>' [, ...],
reason='...')` marker for backend-specific known-hang / -borked cases
— avoids scattering `@pytest.mark.skipif(lambda ...)` branches across
tests that misbehave under a particular `--spawn-backend`.
Deats,
- `pytest_configure()` registers the marker via
`addinivalue_line('markers', ...)`.
- New `pytest_collection_modifyitems()` hook walks
each collected item with `item.iter_markers(
name='skipon_spawn_backend')`, checks whether the
active `--spawn-backend` appears in `mark.args`, and
if so injects a concrete `pytest.mark.skip(
reason=...)`. `iter_markers()` makes the decorator
work at function, class, or module (`pytestmark =
[...]`) scope transparently.
- First matching mark wins; default reason is
`f'Borked on --spawn-backend={backend!r}'` if the
caller doesn't supply one.
Also, tighten type annotations on nearby `pytest`
integration points — `pytest_configure`, `debug_mode`,
`spawn_backend`, `tpt_protos`, `tpt_proto` — now taking
typed `pytest.Config` / `pytest.FixtureRequest` params.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit 3b26b59dad8c6b365890746ed5acc0811bec94c6)
2026-04-22 01:24:51 +00:00
|
|
|
def debug_mode(
|
|
|
|
|
request: pytest.FixtureRequest,
|
|
|
|
|
) -> bool:
|
2025-04-17 15:20:49 +00:00
|
|
|
'''
|
|
|
|
|
Flag state for whether `--tpdb` (for `tractor`-py-debugger)
|
|
|
|
|
was passed to the test run.
|
|
|
|
|
|
|
|
|
|
Normally tests should pass this directly to `.open_root_actor()`
|
|
|
|
|
to allow the user to opt into suite-wide crash handling.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
debug_mode: bool = request.config.option.tractor_debug_mode
|
|
|
|
|
return debug_mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
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
|
|
|
def testing_pkg_name() -> str:
|
|
|
|
|
'''
|
|
|
|
|
Root pkg-name of the project consuming this plugin, used to
|
|
|
|
|
scope `--ll` "internal"/app-level console logging into that
|
|
|
|
|
project's OWN `tractor.log.get_logger(pkg_name=<.>)` hierarchy
|
|
|
|
|
— distinct from the `tractor.*` runtime hierarchy configured
|
|
|
|
|
via `--tl`.
|
|
|
|
|
|
|
|
|
|
Defaults to `'tractor'` (so tractor's own suite treats `--ll`
|
|
|
|
|
as the runtime level). Downstream projects override this from
|
|
|
|
|
their `conftest.py`, eg.
|
|
|
|
|
|
|
|
|
|
.. code:: python
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
|
def testing_pkg_name() -> str:
|
|
|
|
|
return 'modden'
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
return 'tractor'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(
|
|
|
|
|
scope='session',
|
|
|
|
|
autouse=True,
|
|
|
|
|
)
|
|
|
|
|
def loglevel(
|
|
|
|
|
request: pytest.FixtureRequest,
|
|
|
|
|
testing_pkg_name: str,
|
|
|
|
|
) -> str|None:
|
|
|
|
|
'''
|
|
|
|
|
Resolve + apply the test-session console loglevels and yield
|
|
|
|
|
the `tractor`-runtime level (also passed to
|
|
|
|
|
`open_root_actor(loglevel=<.>)` by `@tractor_test`).
|
|
|
|
|
|
|
|
|
|
- `--tl <logspec>`: tractor-runtime level (falls back to the
|
|
|
|
|
generic `--ll`); applied to the `tractor.*` logger hierarchy
|
|
|
|
|
and `tractor.log._default_loglevel` via
|
|
|
|
|
`tractor.log.apply_logspec()`.
|
|
|
|
|
- `--ll <level>`: the consuming-project's OWN console loglevel,
|
|
|
|
|
applied to its `testing_pkg_name` hierarchy when that isn't
|
|
|
|
|
`tractor` itself.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
import tractor
|
|
|
|
|
orig: str = tractor.log._default_loglevel
|
|
|
|
|
|
|
|
|
|
ll: str|None = request.config.option.loglevel
|
|
|
|
|
tl: str|None = request.config.option.tractor_loglevel
|
|
|
|
|
|
|
|
|
|
# tractor-runtime loglevel: explicit `--tl` wins, else fall
|
|
|
|
|
# back to the generic `--ll`, else leave the lib default.
|
|
|
|
|
logspec: str|None = tl if tl is not None else ll
|
|
|
|
|
tractor_level: str|None = None
|
|
|
|
|
if logspec is not None:
|
|
|
|
|
tractor_level, _ = tractor.log.apply_logspec(
|
|
|
|
|
logspec,
|
|
|
|
|
default_level=ll,
|
|
|
|
|
pkg_name='tractor',
|
|
|
|
|
)
|
|
|
|
|
if tractor_level is not None:
|
|
|
|
|
tractor.log._default_loglevel = tractor_level
|
|
|
|
|
|
|
|
|
|
# consuming-project ("internal") console logging at the generic
|
|
|
|
|
# `--ll` level, scoped to ITS OWN pkg-hierarchy (NOT `tractor.*`)
|
|
|
|
|
# so downstream projects can split app-logs from runtime-logs.
|
|
|
|
|
if (
|
|
|
|
|
ll is not None
|
|
|
|
|
and
|
|
|
|
|
testing_pkg_name
|
|
|
|
|
and
|
|
|
|
|
testing_pkg_name != 'tractor'
|
|
|
|
|
):
|
|
|
|
|
tractor.log.get_console_log(
|
|
|
|
|
level=ll,
|
|
|
|
|
pkg_name=testing_pkg_name,
|
|
|
|
|
name=testing_pkg_name,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
log = tractor.log.get_console_log(
|
|
|
|
|
level=tractor_level,
|
|
|
|
|
name='tractor', # <- enable root logger
|
|
|
|
|
)
|
|
|
|
|
log.info(
|
|
|
|
|
f'Test-harness set session loglevels:\n'
|
|
|
|
|
f'tractor-runtime (`--tl`/`--ll`): {tractor_level!r}\n'
|
|
|
|
|
f'{testing_pkg_name!r} (`--ll`): {ll!r}\n'
|
|
|
|
|
)
|
|
|
|
|
yield tractor_level
|
|
|
|
|
tractor.log._default_loglevel = orig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
|
|
|
def test_log(
|
|
|
|
|
request: pytest.FixtureRequest,
|
|
|
|
|
loglevel: str,
|
|
|
|
|
testing_pkg_name: str,
|
|
|
|
|
) -> tractor.log.StackLevelAdapter:
|
|
|
|
|
'''
|
|
|
|
|
Deliver a per test-module-fn logger instance for reporting from
|
|
|
|
|
within actual test bodies/fixtures.
|
|
|
|
|
|
|
|
|
|
For example this can be handy to report certain error cases from
|
|
|
|
|
exception handlers using `test_log.exception()`.
|
|
|
|
|
|
|
|
|
|
The logger is scoped to the consuming-project's
|
|
|
|
|
`testing_pkg_name` hierarchy so downstream suites' in-test logs
|
|
|
|
|
land under their own pkg, not `tractor.*`.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
modname: str = request.function.__module__
|
|
|
|
|
log = tractor.log.get_logger(
|
|
|
|
|
name=modname,
|
|
|
|
|
pkg_name=testing_pkg_name,
|
|
|
|
|
)
|
|
|
|
|
_log = tractor.log.get_console_log(
|
|
|
|
|
level=loglevel,
|
|
|
|
|
logger=log,
|
|
|
|
|
name=modname,
|
|
|
|
|
)
|
|
|
|
|
_log.debug(
|
|
|
|
|
f'In-test-logging requested\n'
|
|
|
|
|
f'test_log.name: {log.name!r}\n'
|
|
|
|
|
f'level: {loglevel!r}\n'
|
|
|
|
|
)
|
|
|
|
|
yield _log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
|
def spawn_backend(
|
|
|
|
|
request: pytest.FixtureRequest,
|
|
|
|
|
) -> str:
|
2025-04-17 15:20:49 +00:00
|
|
|
return request.config.option.spawn_backend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
Add `skipon_spawn_backend` pytest marker
A reusable `@pytest.mark.skipon_spawn_backend( '<backend>' [, ...],
reason='...')` marker for backend-specific known-hang / -borked cases
— avoids scattering `@pytest.mark.skipif(lambda ...)` branches across
tests that misbehave under a particular `--spawn-backend`.
Deats,
- `pytest_configure()` registers the marker via
`addinivalue_line('markers', ...)`.
- New `pytest_collection_modifyitems()` hook walks
each collected item with `item.iter_markers(
name='skipon_spawn_backend')`, checks whether the
active `--spawn-backend` appears in `mark.args`, and
if so injects a concrete `pytest.mark.skip(
reason=...)`. `iter_markers()` makes the decorator
work at function, class, or module (`pytestmark =
[...]`) scope transparently.
- First matching mark wins; default reason is
`f'Borked on --spawn-backend={backend!r}'` if the
caller doesn't supply one.
Also, tighten type annotations on nearby `pytest`
integration points — `pytest_configure`, `debug_mode`,
`spawn_backend`, `tpt_protos`, `tpt_proto` — now taking
typed `pytest.Config` / `pytest.FixtureRequest` params.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit 3b26b59dad8c6b365890746ed5acc0811bec94c6)
2026-04-22 01:24:51 +00:00
|
|
|
def tpt_protos(
|
|
|
|
|
request: pytest.FixtureRequest,
|
|
|
|
|
) -> list[str]:
|
2025-04-17 15:20:49 +00:00
|
|
|
|
|
|
|
|
# allow quoting on CLI
|
|
|
|
|
proto_keys: list[str] = [
|
|
|
|
|
proto_key.replace('"', '').replace("'", "")
|
|
|
|
|
for proto_key in request.config.option.tpt_protos
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# ?TODO, eventually support multiple protos per test-sesh?
|
|
|
|
|
if len(proto_keys) > 1:
|
|
|
|
|
pytest.fail(
|
|
|
|
|
'We only support one `--tpt-proto <key>` atm!\n'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# XXX ensure we support the protocol by name via lookup!
|
|
|
|
|
for proto_key in proto_keys:
|
2026-03-23 22:36:56 +00:00
|
|
|
from tractor.discovery import _addr
|
|
|
|
|
addr_type = _addr._address_types[proto_key]
|
2025-04-17 15:20:49 +00:00
|
|
|
assert addr_type.proto_key == proto_key
|
|
|
|
|
|
|
|
|
|
yield proto_keys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(
|
|
|
|
|
scope='session',
|
|
|
|
|
autouse=True,
|
|
|
|
|
)
|
|
|
|
|
def tpt_proto(
|
Add `skipon_spawn_backend` pytest marker
A reusable `@pytest.mark.skipon_spawn_backend( '<backend>' [, ...],
reason='...')` marker for backend-specific known-hang / -borked cases
— avoids scattering `@pytest.mark.skipif(lambda ...)` branches across
tests that misbehave under a particular `--spawn-backend`.
Deats,
- `pytest_configure()` registers the marker via
`addinivalue_line('markers', ...)`.
- New `pytest_collection_modifyitems()` hook walks
each collected item with `item.iter_markers(
name='skipon_spawn_backend')`, checks whether the
active `--spawn-backend` appears in `mark.args`, and
if so injects a concrete `pytest.mark.skip(
reason=...)`. `iter_markers()` makes the decorator
work at function, class, or module (`pytestmark =
[...]`) scope transparently.
- First matching mark wins; default reason is
`f'Borked on --spawn-backend={backend!r}'` if the
caller doesn't supply one.
Also, tighten type annotations on nearby `pytest`
integration points — `pytest_configure`, `debug_mode`,
`spawn_backend`, `tpt_protos`, `tpt_proto` — now taking
typed `pytest.Config` / `pytest.FixtureRequest` params.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
(cherry picked from commit 3b26b59dad8c6b365890746ed5acc0811bec94c6)
2026-04-22 01:24:51 +00:00
|
|
|
request: pytest.FixtureRequest,
|
2025-04-17 15:20:49 +00:00
|
|
|
tpt_protos: list[str],
|
|
|
|
|
) -> str:
|
|
|
|
|
proto_key: str = tpt_protos[0]
|
|
|
|
|
|
2026-03-13 17:36:47 +00:00
|
|
|
# ?TODO, but needs a fn-scoped tpt_proto fixture..
|
|
|
|
|
# @pytest.mark.no_tpt('uds')
|
|
|
|
|
# node = request.node
|
|
|
|
|
# markers = node.own_markers
|
|
|
|
|
# for mark in markers:
|
|
|
|
|
# if (
|
|
|
|
|
# mark.name == 'no_tpt'
|
|
|
|
|
# and
|
|
|
|
|
# proto_key in mark.args
|
|
|
|
|
# ):
|
|
|
|
|
# pytest.skip(
|
|
|
|
|
# f'Test {node} normally fails with '
|
|
|
|
|
# f'tpt-proto={proto_key!r}\n'
|
|
|
|
|
# )
|
|
|
|
|
|
2026-03-23 22:36:56 +00:00
|
|
|
from tractor.runtime import _state
|
2025-04-17 15:20:49 +00:00
|
|
|
if _state._def_tpt_proto != proto_key:
|
|
|
|
|
_state._def_tpt_proto = proto_key
|
2026-03-13 00:05:43 +00:00
|
|
|
_state._runtime_vars['_enable_tpts'] = [
|
|
|
|
|
proto_key,
|
|
|
|
|
]
|
2025-04-17 15:20:49 +00:00
|
|
|
|
|
|
|
|
yield proto_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
|
def reg_addr(
|
|
|
|
|
tpt_proto: str,
|
|
|
|
|
) -> tuple[str, int|str]:
|
|
|
|
|
'''
|
|
|
|
|
Deliver a test-sesh unique registry address such
|
|
|
|
|
that each run's (tests which use this fixture) will
|
|
|
|
|
have no conflicts/cross-talk when running simultaneously
|
|
|
|
|
nor will interfere with other live `tractor` apps active
|
|
|
|
|
on the same network-host (namespace).
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
from tractor._testing.addr import get_rando_addr
|
|
|
|
|
return get_rando_addr(
|
|
|
|
|
tpt_proto=tpt_proto,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_generate_tests(
|
|
|
|
|
metafunc: pytest.Metafunc,
|
|
|
|
|
):
|
|
|
|
|
spawn_backend: str = metafunc.config.option.spawn_backend
|
|
|
|
|
if not spawn_backend:
|
|
|
|
|
# XXX some weird windows bug with `pytest`?
|
|
|
|
|
spawn_backend = 'trio'
|
|
|
|
|
|
Mv `trio_proc`/`mp_proc` to per-backend submods
Split the monolithic `spawn._spawn` into a slim
"core" + per-backend submodules so a future
`._subint` backend (per issue #379) can drop in
without piling more onto `_spawn.py`.
`._spawn` retains the cross-backend supervisor
machinery: `SpawnMethodKey`, `_methods` registry,
`_spawn_method`/`_ctx` state, `try_set_start_method()`,
the `new_proc()` dispatcher, and the shared helpers
`exhaust_portal()`, `cancel_on_completion()`,
`hard_kill()`, `soft_kill()`, `proc_waiter()`.
Deats,
- mv `trio_proc()` → new `spawn._trio`
- mv `mp_proc()` → new `spawn._mp`, reads `_ctx` and
`_spawn_method` via `from . import _spawn` for
late binding bc both get mutated by
`try_set_start_method()`
- `_methods` wires up the new submods via late
bottom-of-module imports to side-step circular
dep (both backend mods pull shared helpers from
`._spawn`)
- prune now-unused imports from `_spawn.py` — `sys`,
`is_root_process`, `current_actor`,
`is_main_process`, `_mp_main`, `ActorFailure`,
`pretty_struct`, `_pformat`
Also,
- `_testing.pytest.pytest_generate_tests()` now
drives the valid-backend set from
`typing.get_args(SpawnMethodKey)` so adding a
new backend (e.g. `'subint'`) doesn't require
touching the harness
- refresh `spawn/__init__.py` docstring for the
new layout
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-17 05:58:05 +00:00
|
|
|
# drive the valid-backend set from the canonical `Literal` so
|
|
|
|
|
# adding a new spawn backend (e.g. `'subint'`) doesn't require
|
|
|
|
|
# touching the harness.
|
|
|
|
|
from tractor.spawn._spawn import SpawnMethodKey
|
|
|
|
|
assert spawn_backend in get_args(SpawnMethodKey)
|
2025-04-17 15:20:49 +00:00
|
|
|
|
|
|
|
|
# NOTE: used-to-be-used-to dyanmically parametrize tests for when
|
|
|
|
|
# you just passed --spawn-backend=`mp` on the cli, but now we expect
|
|
|
|
|
# that cli input to be manually specified, BUT, maybe we'll do
|
|
|
|
|
# something like this again in the future?
|
|
|
|
|
if 'start_method' in metafunc.fixturenames:
|
|
|
|
|
metafunc.parametrize(
|
|
|
|
|
"start_method",
|
|
|
|
|
[spawn_backend],
|
|
|
|
|
scope='module',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# TODO, parametrize any `tpt_proto: str` declaring tests!
|
|
|
|
|
# proto_tpts: list[str] = metafunc.config.option.proto_tpts
|
|
|
|
|
# if 'tpt_proto' in metafunc.fixturenames:
|
|
|
|
|
# metafunc.parametrize(
|
|
|
|
|
# 'tpt_proto',
|
|
|
|
|
# proto_tpts, # TODO, double check this list usage!
|
|
|
|
|
# scope='module',
|
|
|
|
|
# )
|