Honor `TRACTOR_LOGLEVEL`+`TRACTOR_SPAWN_METHOD` env-vars

Add env-var overrides inside `._root.open_root_actor()` so
devs/test-runs can swap the actor-spawn backend or crank
console verbosity *without* touching application code.

In `._root.open_root_actor()`,
- read `TRACTOR_LOGLEVEL` early, overriding any caller-passed
  `loglevel` and stashing an `env_ll_report` to emit once the
  console log is set up.
- pull the `loglevel` fallback (`or _default_loglevel`) and
  `log.get_console_log()` init *up* so the env-var report
  routes through tractor's own logger.
- read `TRACTOR_SPAWN_METHOD`, overriding any caller-passed
  `start_method` and warn-logging when the env-var clobbers
  an explicit caller value.

Wire the same vars through `tests/devx/conftest.py::spawn`,
- request the `loglevel` fixture, set both `TRACTOR_LOGLEVEL`
  and `TRACTOR_SPAWN_METHOD` in `os.environ` before each
  `pexpect.spawn()` (inherited by the example subproc).
- expand `supported_spawners` to include
  `main_thread_forkserver` and `subint_forkserver` bc
  example scripts no longer need per-script CLI plumbing.
- pop both vars in fixture teardown so a leaked value can't
  re-route a later in-process tractor test's spawn-backend
  or loglevel.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
Gud Boi 2026-04-29 17:29:38 -04:00
parent 22cdf15b73
commit 208e7c0926
2 changed files with 115 additions and 18 deletions

View File

@ -56,6 +56,7 @@ type PexpectSpawner = Callable[
@pytest.fixture @pytest.fixture
def spawn( def spawn(
start_method: str, start_method: str,
loglevel: str,
testdir: pytest.Pytester, testdir: pytest.Pytester,
reg_addr: tuple[str, int], reg_addr: tuple[str, int],
@ -67,11 +68,12 @@ def spawn(
''' '''
supported_spawners: set[str] = { supported_spawners: set[str] = {
'trio', 'trio',
# ?TODO, other spawners that will work? # `examples/debugging/<script>.py` picks up the spawn
# - [ ] need to pass `start_method={spawner}` to underlying # backend via the `TRACTOR_SPAWN_METHOD` env-var which
# `examples/debugging/<script>.py` somehow? # is honored inside `tractor._root.open_root_actor()`,
# 'main_thread_forkserver', # so no per-script edits are required.
# 'subint_forkserver', 'main_thread_forkserver',
'subint_forkserver',
} }
if start_method not in supported_spawners: if start_method not in supported_spawners:
pytest.skip( pytest.skip(
@ -94,6 +96,32 @@ def spawn(
# disable all ANSI color output # disable all ANSI color output
# os.environ['NO_COLOR'] = '1' # os.environ['NO_COLOR'] = '1'
def set_spawn_method():
'''
Drive the actor-spawn backend inside the spawned
`examples/debugging/<script>.py` subproc via env-var
(consumed by `tractor._root.open_root_actor()`),
without requiring per-script CLI plumbing.
'''
import os
os.environ['TRACTOR_SPAWN_METHOD'] = start_method
def set_loglevel():
'''
Forward the test-suite parametrized `loglevel` into the
spawned `examples/debugging/<script>.py` subproc via
env-var (consumed by `tractor._root.open_root_actor()`),
so console verbosity can be cranked or silenced from
the test harness without per-script edits.
'''
import os
if loglevel:
os.environ['TRACTOR_LOGLEVEL'] = loglevel
else:
os.environ.pop('TRACTOR_LOGLEVEL', None)
spawned: PexpectSpawner|None = None spawned: PexpectSpawner|None = None
def _spawn( def _spawn(
@ -103,6 +131,8 @@ def spawn(
) -> pty_spawn.spawn: ) -> pty_spawn.spawn:
nonlocal spawned nonlocal spawned
unset_colors() unset_colors()
set_spawn_method()
set_loglevel()
spawned = testdir.spawn( spawned = testdir.spawn(
cmd=mk_cmd( cmd=mk_cmd(
cmd, cmd,
@ -146,6 +176,16 @@ def spawn(
if ptyproc.isalive(): if ptyproc.isalive():
ptyproc.kill(signal.SIGKILL) ptyproc.kill(signal.SIGKILL)
# Scope our env-var mutations to this single fixture
# invocation — both `TRACTOR_SPAWN_METHOD` and
# `TRACTOR_LOGLEVEL` are honored by
# `tractor._root.open_root_actor()` so leaking them past
# this test could inadvertently re-route a later
# in-process tractor test's spawn-backend / loglevel.
import os
os.environ.pop('TRACTOR_SPAWN_METHOD', None)
os.environ.pop('TRACTOR_LOGLEVEL', None)
# TODO? ensure we've cleaned up any UDS-paths? # TODO? ensure we've cleaned up any UDS-paths?
# breakpoint() # breakpoint()

View File

@ -241,6 +241,7 @@ async def open_root_actor(
f'_registry_addrs: {registry_addrs!r}\n' f'_registry_addrs: {registry_addrs!r}\n'
) )
# debug.mk_pdb().set_trace()
async with maybe_block_bp( async with maybe_block_bp(
debug_mode=debug_mode, debug_mode=debug_mode,
maybe_enable_greenback=maybe_enable_greenback, maybe_enable_greenback=maybe_enable_greenback,
@ -284,6 +285,75 @@ async def open_root_actor(
) )
enable_modules.extend(rpc_module_paths) enable_modules.extend(rpc_module_paths)
# `TRACTOR_LOGLEVEL` env-var wins over any caller-passed
# `loglevel` so devs/test-runs can crank (or silence)
# console verbosity without touching application code.
env_ll_report: str = ''
if env_ll := os.environ.get('TRACTOR_LOGLEVEL'):
loglevel = env_ll
env_ll_report: str = (
f'Detected env-var setting,\n'
f'TRACTOR_LOGLEVEL={env_ll!r}\n'
f'\n'
f'Setting console loglevel per,\n'
f'loglevel={loglevel!r}\n'
)
if (
loglevel
and
loglevel.upper() != env_ll.upper()
):
env_ll_report += (
f'\n'
f'NOTE env-var OVERRIDES caller-passed,\n'
f'loglevel={loglevel!r}\n'
)
loglevel: str = (
loglevel
or
log._default_loglevel
)
loglevel: str = loglevel.upper()
assert loglevel
_log = log.get_console_log(
level=loglevel,
name='tractor',
logger=logger,
)
assert _log
if env_ll_report:
_log.info(env_ll_report)
# `TRACTOR_SPAWN_METHOD` env-var wins over any caller-passed
# `start_method` so devs/test-runs can swap the actor spawn
# backend without touching application code (e.g. driving
# the `examples/debugging/<script>.py` suite under each
# backend from `tests/devx/conftest.py::spawn`).
if env_sm := os.environ.get('TRACTOR_SPAWN_METHOD'):
start_method: str = env_sm
env_sm_report: str = (
f'Detected env-var setting,\n'
f'TRACTOR_SPAWN_METHOD={env_sm!r}\n'
f'\n'
f'Setting spawn backend as,\n'
f'start_method={env_sm!r}\n'
)
if (
start_method
and
start_method != env_sm
):
_log.warning(
env_sm_report
+
f'NOTE env-var OVERRIDES caller-passed,\n'
f'`start_method={start_method!r}`\n'
)
else:
_log.info(env_sm_report)
if start_method is not None: if start_method is not None:
_spawn.try_set_start_method(start_method) _spawn.try_set_start_method(start_method)
@ -300,12 +370,6 @@ async def open_root_actor(
wrap_address(uw_addr) wrap_address(uw_addr)
for uw_addr in uw_reg_addrs for uw_addr in uw_reg_addrs
] ]
loglevel: str = (
loglevel
or
log._default_loglevel
)
loglevel: str = loglevel.upper()
# Debug-mode is currently only supported for backends whose # Debug-mode is currently only supported for backends whose
# subactor root runtime is trio-native (so `tractor.devx. # subactor root runtime is trio-native (so `tractor.devx.
@ -341,13 +405,6 @@ async def open_root_actor(
f'{_spawn._spawn_method!r}.' f'{_spawn._spawn_method!r}.'
) )
assert loglevel
_log = log.get_console_log(
level=loglevel,
name='tractor',
)
assert _log
# TODO: factor this into `.devx._stackscope`!! # TODO: factor this into `.devx._stackscope`!!
if ( if (
debug_mode debug_mode