Mark `subint`-hanging tests with `skipon_spawn_backend`
Adopt the `@pytest.mark.skipon_spawn_backend('subint',
reason=...)` marker (a617b521) across the suites
reproducing the `subint` GIL-contention / starvation
hang classes doc'd in `ai/conc-anal/subint_*_issue.md`.
Deats,
- Module-level `pytestmark` on full-file-hanging suites:
- `tests/test_cancellation.py`
- `tests/test_inter_peer_cancellation.py`
- `tests/test_pubsub.py`
- `tests/test_shm.py`
- Per-test decorator where only one test in the file
hangs:
- `tests/discovery/test_registrar.py
::test_stale_entry_is_deleted` — replaces the
inline `if start_method == 'subint': pytest.skip`
branch with a declarative skip.
- `tests/test_subint_cancellation.py
::test_subint_non_checkpointing_child`.
- A few per-test decorators are left commented-in-
place as breadcrumbs for later finer-grained unskips.
Also, some nearby tidying in the affected files:
- Annotate loose fixture / test params
(`pytest.FixtureRequest`, `str`, `tuple`, `bool`) in
`tests/conftest.py`, `tests/devx/conftest.py`, and
`tests/test_cancellation.py`.
- Normalize `"""..."""` → `'''...'''` docstrings per
repo convention on a few touched tests.
- Add `timeout=6` / `timeout=10` to
`@tractor_test(...)` on `test_cancel_infinite_streamer`
and `test_some_cancels_all`.
- Drop redundant `spawn_backend` param from
`test_cancel_via_SIGINT`; use `start_method` in the
`'mp' in ...` check instead.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
parent
3b26b59dad
commit
4b2a0886c3
|
|
@ -139,7 +139,9 @@ def pytest_addoption(
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session', autouse=True)
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
def loglevel(request) -> str:
|
def loglevel(
|
||||||
|
request: pytest.FixtureRequest,
|
||||||
|
) -> str:
|
||||||
import tractor
|
import tractor
|
||||||
orig = tractor.log._default_loglevel
|
orig = tractor.log._default_loglevel
|
||||||
level = tractor.log._default_loglevel = request.config.option.loglevel
|
level = tractor.log._default_loglevel = request.config.option.loglevel
|
||||||
|
|
@ -156,7 +158,7 @@ def loglevel(request) -> str:
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def test_log(
|
def test_log(
|
||||||
request,
|
request: pytest.FixtureRequest,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
) -> tractor.log.StackLevelAdapter:
|
) -> tractor.log.StackLevelAdapter:
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -146,13 +146,12 @@ def spawn(
|
||||||
ids='ctl-c={}'.format,
|
ids='ctl-c={}'.format,
|
||||||
)
|
)
|
||||||
def ctlc(
|
def ctlc(
|
||||||
request,
|
request: pytest.FixtureRequest,
|
||||||
ci_env: bool,
|
ci_env: bool,
|
||||||
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
use_ctlc = request.param
|
use_ctlc: bool = request.param
|
||||||
|
|
||||||
node = request.node
|
node = request.node
|
||||||
markers = node.own_markers
|
markers = node.own_markers
|
||||||
for mark in markers:
|
for mark in markers:
|
||||||
|
|
|
||||||
|
|
@ -520,8 +520,6 @@ async def kill_transport(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @pytest.mark.parametrize('use_signal', [False, True])
|
|
||||||
#
|
|
||||||
# Wall-clock bound via `pytest-timeout` (`method='thread'`).
|
# Wall-clock bound via `pytest-timeout` (`method='thread'`).
|
||||||
# Under `--spawn-backend=subint` this test can wedge in an
|
# Under `--spawn-backend=subint` this test can wedge in an
|
||||||
# un-Ctrl-C-able state (abandoned-subint + shared-GIL
|
# un-Ctrl-C-able state (abandoned-subint + shared-GIL
|
||||||
|
|
@ -537,6 +535,16 @@ async def kill_transport(
|
||||||
3, # NOTE should be a 2.1s happy path.
|
3, # NOTE should be a 2.1s happy path.
|
||||||
method='thread',
|
method='thread',
|
||||||
)
|
)
|
||||||
|
@pytest.mark.skipon_spawn_backend(
|
||||||
|
'subint',
|
||||||
|
reason=(
|
||||||
|
'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
'See oustanding issue(s)\n'
|
||||||
|
# TODO, put issue link!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# @pytest.mark.parametrize('use_signal', [False, True])
|
||||||
|
#
|
||||||
def test_stale_entry_is_deleted(
|
def test_stale_entry_is_deleted(
|
||||||
debug_mode: bool,
|
debug_mode: bool,
|
||||||
daemon: subprocess.Popen,
|
daemon: subprocess.Popen,
|
||||||
|
|
@ -549,12 +557,6 @@ def test_stale_entry_is_deleted(
|
||||||
stale entry and not delivering a bad portal.
|
stale entry and not delivering a bad portal.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if start_method == 'subint':
|
|
||||||
pytest.skip(
|
|
||||||
'XXX SUBINT HANGING TEST XXX\n'
|
|
||||||
'See oustanding issue(s)\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
name: str = 'transport_fails_actor'
|
name: str = 'transport_fails_actor'
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,16 @@ _non_linux: bool = platform.system() != 'Linux'
|
||||||
_friggin_windows: bool = platform.system() == 'Windows'
|
_friggin_windows: bool = platform.system() == 'Windows'
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipon_spawn_backend(
|
||||||
|
'subint',
|
||||||
|
reason=(
|
||||||
|
'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
'See oustanding issue(s)\n'
|
||||||
|
# TODO, put issue link!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def assert_err(delay=0):
|
async def assert_err(delay=0):
|
||||||
await trio.sleep(delay)
|
await trio.sleep(delay)
|
||||||
assert 0
|
assert 0
|
||||||
|
|
@ -110,8 +120,17 @@ def test_remote_error(reg_addr, args_err):
|
||||||
assert exc.boxed_type == errtype
|
assert exc.boxed_type == errtype
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.mark.skipon_spawn_backend(
|
||||||
|
# 'subint',
|
||||||
|
# reason=(
|
||||||
|
# 'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
# 'See oustanding issue(s)\n'
|
||||||
|
# # TODO, put issue link!
|
||||||
|
# )
|
||||||
|
# )
|
||||||
def test_multierror(
|
def test_multierror(
|
||||||
reg_addr: tuple[str, int],
|
reg_addr: tuple[str, int],
|
||||||
|
start_method: str,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Verify we raise a ``BaseExceptionGroup`` out of a nursery where
|
Verify we raise a ``BaseExceptionGroup`` out of a nursery where
|
||||||
|
|
@ -141,15 +160,28 @@ def test_multierror(
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('delay', (0, 0.5))
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'num_subactors', range(25, 26),
|
'delay',
|
||||||
|
(0, 0.5),
|
||||||
|
ids='delays={}'.format,
|
||||||
)
|
)
|
||||||
def test_multierror_fast_nursery(reg_addr, start_method, num_subactors, delay):
|
@pytest.mark.parametrize(
|
||||||
"""Verify we raise a ``BaseExceptionGroup`` out of a nursery where
|
'num_subactors',
|
||||||
|
range(25, 26),
|
||||||
|
ids= 'num_subs={}'.format,
|
||||||
|
)
|
||||||
|
def test_multierror_fast_nursery(
|
||||||
|
reg_addr: tuple,
|
||||||
|
start_method: str,
|
||||||
|
num_subactors: int,
|
||||||
|
delay: float,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Verify we raise a ``BaseExceptionGroup`` out of a nursery where
|
||||||
more then one actor errors and also with a delay before failure
|
more then one actor errors and also with a delay before failure
|
||||||
to test failure during an ongoing spawning.
|
to test failure during an ongoing spawning.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
async def main():
|
async def main():
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
|
|
@ -189,8 +221,15 @@ async def do_nothing():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('mechanism', ['nursery_cancel', KeyboardInterrupt])
|
@pytest.mark.parametrize(
|
||||||
def test_cancel_single_subactor(reg_addr, mechanism):
|
'mechanism', [
|
||||||
|
'nursery_cancel',
|
||||||
|
KeyboardInterrupt,
|
||||||
|
])
|
||||||
|
def test_cancel_single_subactor(
|
||||||
|
reg_addr: tuple,
|
||||||
|
mechanism: str|KeyboardInterrupt,
|
||||||
|
):
|
||||||
'''
|
'''
|
||||||
Ensure a ``ActorNursery.start_actor()`` spawned subactor
|
Ensure a ``ActorNursery.start_actor()`` spawned subactor
|
||||||
cancels when the nursery is cancelled.
|
cancels when the nursery is cancelled.
|
||||||
|
|
@ -232,9 +271,12 @@ async def stream_forever():
|
||||||
await trio.sleep(0.01)
|
await trio.sleep(0.01)
|
||||||
|
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test(
|
||||||
async def test_cancel_infinite_streamer(start_method):
|
timeout=6,
|
||||||
|
)
|
||||||
|
async def test_cancel_infinite_streamer(
|
||||||
|
start_method: str
|
||||||
|
):
|
||||||
# stream for at most 1 seconds
|
# stream for at most 1 seconds
|
||||||
with (
|
with (
|
||||||
trio.fail_after(4),
|
trio.fail_after(4),
|
||||||
|
|
@ -257,6 +299,14 @@ async def test_cancel_infinite_streamer(start_method):
|
||||||
assert n.cancelled
|
assert n.cancelled
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.mark.skipon_spawn_backend(
|
||||||
|
# 'subint',
|
||||||
|
# reason=(
|
||||||
|
# 'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
# 'See oustanding issue(s)\n'
|
||||||
|
# # TODO, put issue link!
|
||||||
|
# )
|
||||||
|
# )
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'num_actors_and_errs',
|
'num_actors_and_errs',
|
||||||
[
|
[
|
||||||
|
|
@ -286,7 +336,9 @@ async def test_cancel_infinite_streamer(start_method):
|
||||||
'no_daemon_actors_fail_all_run_in_actors_sleep_then_fail',
|
'no_daemon_actors_fail_all_run_in_actors_sleep_then_fail',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@tractor_test
|
@tractor_test(
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
async def test_some_cancels_all(
|
async def test_some_cancels_all(
|
||||||
num_actors_and_errs: tuple,
|
num_actors_and_errs: tuple,
|
||||||
start_method: str,
|
start_method: str,
|
||||||
|
|
@ -370,7 +422,10 @@ async def test_some_cancels_all(
|
||||||
pytest.fail("Should have gotten a remote assertion error?")
|
pytest.fail("Should have gotten a remote assertion error?")
|
||||||
|
|
||||||
|
|
||||||
async def spawn_and_error(breadth, depth) -> None:
|
async def spawn_and_error(
|
||||||
|
breadth: int,
|
||||||
|
depth: int,
|
||||||
|
) -> None:
|
||||||
name = tractor.current_actor().name
|
name = tractor.current_actor().name
|
||||||
async with tractor.open_nursery() as nursery:
|
async with tractor.open_nursery() as nursery:
|
||||||
for i in range(breadth):
|
for i in range(breadth):
|
||||||
|
|
@ -396,7 +451,10 @@ async def spawn_and_error(breadth, depth) -> None:
|
||||||
|
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test
|
||||||
async def test_nested_multierrors(loglevel, start_method):
|
async def test_nested_multierrors(
|
||||||
|
loglevel: str,
|
||||||
|
start_method: str,
|
||||||
|
):
|
||||||
'''
|
'''
|
||||||
Test that failed actor sets are wrapped in `BaseExceptionGroup`s. This
|
Test that failed actor sets are wrapped in `BaseExceptionGroup`s. This
|
||||||
test goes only 2 nurseries deep but we should eventually have tests
|
test goes only 2 nurseries deep but we should eventually have tests
|
||||||
|
|
@ -483,20 +541,21 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
|
|
||||||
@no_windows
|
@no_windows
|
||||||
def test_cancel_via_SIGINT(
|
def test_cancel_via_SIGINT(
|
||||||
loglevel,
|
loglevel: str,
|
||||||
start_method,
|
start_method: str,
|
||||||
spawn_backend,
|
|
||||||
):
|
):
|
||||||
"""Ensure that a control-C (SIGINT) signal cancels both the parent and
|
'''
|
||||||
|
Ensure that a control-C (SIGINT) signal cancels both the parent and
|
||||||
child processes in trionic fashion
|
child processes in trionic fashion
|
||||||
"""
|
|
||||||
|
'''
|
||||||
pid: int = os.getpid()
|
pid: int = os.getpid()
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
with trio.fail_after(2):
|
with trio.fail_after(2):
|
||||||
async with tractor.open_nursery() as tn:
|
async with tractor.open_nursery() as tn:
|
||||||
await tn.start_actor('sucka')
|
await tn.start_actor('sucka')
|
||||||
if 'mp' in spawn_backend:
|
if 'mp' in start_method:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
os.kill(pid, signal.SIGINT)
|
os.kill(pid, signal.SIGINT)
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
@ -580,6 +639,14 @@ async def spawn_sub_with_sync_blocking_task():
|
||||||
print('exiting first subactor layer..\n')
|
print('exiting first subactor layer..\n')
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.mark.skipon_spawn_backend(
|
||||||
|
# 'subint',
|
||||||
|
# reason=(
|
||||||
|
# 'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
# 'See oustanding issue(s)\n'
|
||||||
|
# # TODO, put issue link!
|
||||||
|
# )
|
||||||
|
# )
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'man_cancel_outer',
|
'man_cancel_outer',
|
||||||
[
|
[
|
||||||
|
|
@ -694,7 +761,7 @@ def test_cancel_while_childs_child_in_sync_sleep(
|
||||||
|
|
||||||
|
|
||||||
def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
|
def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
|
||||||
start_method,
|
start_method: str,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
This is a very subtle test which demonstrates how cancellation
|
This is a very subtle test which demonstrates how cancellation
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,15 @@ from tractor._testing import (
|
||||||
|
|
||||||
from .conftest import cpu_scaling_factor
|
from .conftest import cpu_scaling_factor
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipon_spawn_backend(
|
||||||
|
'subint',
|
||||||
|
reason=(
|
||||||
|
'XXX SUBINT GIL-CONTENTION HANGING TEST XXX\n'
|
||||||
|
'See oustanding issue(s)\n'
|
||||||
|
# TODO, put issue link!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# XXX TODO cases:
|
# XXX TODO cases:
|
||||||
# - [x] WE cancelled the peer and thus should not see any raised
|
# - [x] WE cancelled the peer and thus should not see any raised
|
||||||
# `ContextCancelled` as it should be reaped silently?
|
# `ContextCancelled` as it should be reaped silently?
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,14 @@ import tractor
|
||||||
from tractor.experimental import msgpub
|
from tractor.experimental import msgpub
|
||||||
from tractor._testing import tractor_test
|
from tractor._testing import tractor_test
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipon_spawn_backend(
|
||||||
|
'subint',
|
||||||
|
reason=(
|
||||||
|
'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
'See oustanding issue(s)\n'
|
||||||
|
# TODO, put issue link!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_type_checks():
|
def test_type_checks():
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,14 @@ from tractor.ipc._shm import (
|
||||||
attach_shm_list,
|
attach_shm_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipon_spawn_backend(
|
||||||
|
'subint',
|
||||||
|
reason=(
|
||||||
|
'XXX SUBINT GIL-CONTENTION HANGING TEST XXX\n'
|
||||||
|
'See oustanding issue(s)\n'
|
||||||
|
# TODO, put issue link!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def child_attach_shml_alot(
|
async def child_attach_shml_alot(
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,14 @@ def test_subint_happy_teardown(
|
||||||
trio.run(partial(_happy_path, reg_addr, deadline))
|
trio.run(partial(_happy_path, reg_addr, deadline))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipon_spawn_backend(
|
||||||
|
'subint',
|
||||||
|
reason=(
|
||||||
|
'XXX SUBINT HANGING TEST XXX\n'
|
||||||
|
'See oustanding issue(s)\n'
|
||||||
|
# TODO, put issue link!
|
||||||
|
)
|
||||||
|
)
|
||||||
# Wall-clock bound via `pytest-timeout` (`method='thread'`)
|
# Wall-clock bound via `pytest-timeout` (`method='thread'`)
|
||||||
# as defense-in-depth over the inner `trio.fail_after(15)`.
|
# as defense-in-depth over the inner `trio.fail_after(15)`.
|
||||||
# Under the orphaned-channel hang class described in
|
# Under the orphaned-channel hang class described in
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue