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-code
(cherry picked from commit d549c72052)
wkt/tooling_enhancements_from_mtf_spawner
parent
f7c048e535
commit
a4f8496498
|
|
@ -63,16 +63,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
|
||||
|
|
@ -105,14 +119,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(
|
||||
|
|
@ -198,11 +221,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',
|
||||
|
|
@ -211,9 +240,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
|
||||
|
|
@ -256,7 +289,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,
|
||||
|
|
@ -270,13 +302,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",
|
||||
|
|
@ -302,7 +327,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 '
|
||||
|
|
@ -328,6 +353,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:
|
||||
|
|
@ -429,6 +461,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,
|
||||
|
|
@ -553,6 +604,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!
|
||||
|
|
@ -563,3 +615,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