Add fork-aware capture fixtures to `_testing.pytest`
Extend the pytest plugin with helpers that detect and adapt to `--capture=sys` under fork-based spawners (`main_thread_forkserver`, `mp_forkserver`) where fd-capture causes hangs. Deats, - track `_cap_sys_passed_as_flag` + `_cap_fd_set` globals in `pytest_load_initial_conftests()`. - add `@pytest.hookimpl(tryfirst=True)` + re-parse args after appending `--capture=sys`. - `_is_forking_spawner()` predicate + fixture. - `maybe_xfail_for_spawner()` — enalbes skipping tests that need capsys but weren't passed `--capture=sys`. - `set_fork_aware_capture` fixture — returns the appropriate capture fixture per spawner backend based on `start_method: str` set via CLI. - wire `set_fork_aware_capture` into `tractor_test` wrapper's fixture injection. Also, - add `alert_on_finish` session fixture (terminal bell on completion; tho not sure it works fully..) - add `ids=` to `start_method` parametrize. - restore `default=False` on `--enable-stackscope`. - drop commented-out `--ll` option block; we will likely factor it to our plugin eventually however.. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codesubint_forkserver_backend
parent
5a9926fc32
commit
d549c72052
|
|
@ -52,16 +52,30 @@ pytest_plugins: tuple[str, ...] = (
|
|||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
_cap_sys_passed_as_flag: bool = False
|
||||
_cap_fd_set: bool = False
|
||||
|
||||
# XXX REQUIRED in order to enforce `--capture=` flag
|
||||
# pre test session.
|
||||
# https://docs.pytest.org/en/stable/reference/reference.html#bootstrapping-hooks
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: pytest.Config,
|
||||
parser: pytest.Parser,
|
||||
args: list[str],
|
||||
):
|
||||
global _cap_sys_passed_as_flag, _cap_fd_set
|
||||
|
||||
opts: Namespace = early_config.option
|
||||
if opts.capture == 'fd':
|
||||
_cap_fd_set = True
|
||||
|
||||
opts_w_args: Namespace = parser.parse_known_args(args)
|
||||
if opts_w_args.capture == 'fd':
|
||||
_cap_fd_set = True
|
||||
|
||||
if '--capture=sys' in args:
|
||||
_cap_sys_passed_as_flag = True
|
||||
|
||||
# XXX, ALWAYS apply capsys for fork based spawners:
|
||||
# * main_thread_forkserver
|
||||
|
|
@ -94,14 +108,23 @@ def pytest_load_initial_conftests(
|
|||
(spawner := opts_w_args.spawn_backend) in [
|
||||
'main_thread_forkserver',
|
||||
]
|
||||
and
|
||||
opts.capture == 'fd'
|
||||
):
|
||||
print(
|
||||
f'XXX SETTING CAPSYS due to spawning backend XXX\n'
|
||||
f'--spawn-backend={spawner!r}\n'
|
||||
)
|
||||
opts.capture = 'sys'
|
||||
# ^TODO XXX?/
|
||||
# seems like this doesn't get set by the above!?
|
||||
args.append(
|
||||
'--capture=sys',
|
||||
)
|
||||
out = parser.parse_known_and_unknown_args(
|
||||
args,
|
||||
early_config.option,
|
||||
)
|
||||
assert out[0].capture == 'sys'
|
||||
# breakpoint()
|
||||
|
||||
# TODO, set various `$TRACTOR_X*` osenv vars here!
|
||||
print(
|
||||
|
|
@ -187,11 +210,17 @@ def tractor_test(
|
|||
# injection (via `__wrapped__`) without leaking the async
|
||||
# nature.
|
||||
@wraps(wrapped)
|
||||
def wrapper(**kwargs):
|
||||
def wrapper(
|
||||
set_fork_aware_capture: pytest.CaptureFixture|None = None,
|
||||
# ^NOTE when set, the decorated fn declared as fixture-param.
|
||||
|
||||
**kwargs,
|
||||
):
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
# NOTE, ensure we inject any test-fn declared fixture
|
||||
# names.
|
||||
sig = inspect.signature(wrapped)
|
||||
for kw in [
|
||||
'reg_addr',
|
||||
'loglevel',
|
||||
|
|
@ -200,9 +229,13 @@ def tractor_test(
|
|||
'tpt_proto',
|
||||
'timeout',
|
||||
]:
|
||||
if kw in inspect.signature(wrapped).parameters:
|
||||
if kw in sig.parameters:
|
||||
assert kw in kwargs
|
||||
|
||||
if 'set_fork_aware_capture' in sig.parameters:
|
||||
assert set_fork_aware_capture
|
||||
kwargs['set_fork_aware_capture'] = set_fork_aware_capture
|
||||
|
||||
# Extract runtime settings as locals for
|
||||
# `open_root_actor()`; these must NOT leak into
|
||||
# `kwargs` when the test fn doesn't declare them
|
||||
|
|
@ -245,7 +278,6 @@ def tractor_test(
|
|||
# invoke test-fn body IN THIS task
|
||||
await wrapped(**kwargs)
|
||||
|
||||
# invoke runtime via a root task.
|
||||
return trio.run(
|
||||
partial(
|
||||
_main,
|
||||
|
|
@ -259,13 +291,6 @@ def tractor_test(
|
|||
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",
|
||||
|
|
@ -291,7 +316,7 @@ def pytest_addoption(
|
|||
"--enable-stackscope",
|
||||
action="store_true",
|
||||
dest='enable_stackscope',
|
||||
# default=False,
|
||||
default=False,
|
||||
help=(
|
||||
'Install `stackscope` SIGUSR1 handler in pytest + '
|
||||
'every spawned subactor for live trio task-tree '
|
||||
|
|
@ -317,6 +342,13 @@ def pytest_addoption(
|
|||
def pytest_configure(
|
||||
config: pytest.Config,
|
||||
):
|
||||
# opts: Namespace = config.option
|
||||
# print(
|
||||
# f'PYTEST_CONFIGURE\n'
|
||||
# f'capture={opts.capture!r}\n'
|
||||
# )
|
||||
# breakpoint()
|
||||
|
||||
backend: str = config.option.spawn_backend
|
||||
from tractor.spawn._spawn import try_set_start_method
|
||||
try:
|
||||
|
|
@ -414,6 +446,25 @@ def pytest_collection_modifyitems(
|
|||
break
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="session",
|
||||
autouse=True,
|
||||
)
|
||||
def alert_on_finish():
|
||||
'''
|
||||
Ring a terminal notification on full test session
|
||||
completion to alert any would be human.
|
||||
|
||||
'''
|
||||
# TODO, check attached to tty or skip!
|
||||
yield # run all tests
|
||||
print("\a") # trigger terminal bell
|
||||
# ?TODO, any other nice-tricks/specific tuis we could try?
|
||||
# - supposedly works in many terminals:
|
||||
# >> print("\033]5;Alert: Tests Finished\a")
|
||||
# - sway/i3-nag?
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def debug_mode(
|
||||
request: pytest.FixtureRequest,
|
||||
|
|
@ -538,6 +589,7 @@ def pytest_generate_tests(
|
|||
"start_method",
|
||||
[spawn_backend],
|
||||
scope='module',
|
||||
ids=lambda item: f'start_method={spawn_backend}',
|
||||
)
|
||||
|
||||
# TODO, parametrize any `tpt_proto: str` declaring tests!
|
||||
|
|
@ -548,3 +600,86 @@ def pytest_generate_tests(
|
|||
# proto_tpts, # TODO, double check this list usage!
|
||||
# scope='module',
|
||||
# )
|
||||
|
||||
def _is_forking_spawner(
|
||||
start_method: str,
|
||||
) -> bool:
|
||||
return start_method in [
|
||||
'main_thread_forkserver',
|
||||
'mp_forkserver',
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def is_forking_spawner(
|
||||
start_method: str,
|
||||
) -> bool:
|
||||
'''
|
||||
Is the `pytest` run using a `fork()`ing process spawning-backend?
|
||||
|
||||
'''
|
||||
return _is_forking_spawner
|
||||
|
||||
|
||||
def maybe_xfail_for_spawner(
|
||||
start_method: str,
|
||||
is_forking_spawner: bool,
|
||||
) -> None:
|
||||
'''
|
||||
Fork based spawning backends caude issues with `pytest`'s
|
||||
fd-capture mechanism and can cause various suites to hang.
|
||||
|
||||
Instead this helper allows skipping/xfailing from a test
|
||||
when a certain spawner + CLI-flag input is detected.
|
||||
|
||||
'''
|
||||
if (
|
||||
not _cap_sys_passed_as_flag
|
||||
and
|
||||
is_forking_spawner
|
||||
):
|
||||
pytest.skip(
|
||||
f'Spawner {start_method!r} requires the flag,\n'
|
||||
f'--capture=sys or similar..\n'
|
||||
)
|
||||
|
||||
|
||||
def maybe_override_capture(
|
||||
request: pytest.FixtureRequest,
|
||||
start_method: bool,
|
||||
):
|
||||
if _is_forking_spawner(start_method):
|
||||
return request.getfixturevalue('capsys')
|
||||
|
||||
return request.getfixturevalue(
|
||||
request.config.option.capture
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def set_fork_aware_capture(
|
||||
request: pytest.FixtureRequest,
|
||||
start_method: str,
|
||||
) -> pytest.CaptureFixture|str:
|
||||
'''
|
||||
Force `--capture=sys` method for tests using
|
||||
a forking-spawner backend due to fd-copying issues
|
||||
which can oddly make certain tests hang/fail.
|
||||
|
||||
'''
|
||||
if _cap_sys_passed_as_flag:
|
||||
return 'sys'
|
||||
|
||||
capsys: pytest.CaptureFixture = maybe_override_capture(
|
||||
request=request,
|
||||
start_method=start_method,
|
||||
)
|
||||
return capsys
|
||||
# XXX reset?
|
||||
# with capsys.disabled():
|
||||
# pass
|
||||
# return partial(
|
||||
# maybe_override_capture,
|
||||
# request=request,
|
||||
# start_method=start_method,
|
||||
# )
|
||||
|
|
|
|||
Loading…
Reference in New Issue