Share actor-context test helpers
The context and one-shot suites duplicated cancellation file markers and filtering of registrar-owned runtime contexts. Move those mechanics into `tests._helpers` while retaining each endpoint's distinct startup handshake. Also update the startup-cancel `Channel.send()` mock to accept and forward the new `send_deadline` arg. Caught-during: review remediation Found-via: `/run-tests` test_cancel_during_context_startup[trio] Review: PR #481 (goodboy) https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328 (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/to_actor_subpkg
parent
617ca1de43
commit
0a580df63d
|
|
@ -0,0 +1,50 @@
|
||||||
|
'''
|
||||||
|
Shared helpers for actor-runtime test suites.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from pathlib import Path
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
import tractor
|
||||||
|
|
||||||
|
|
||||||
|
class CancellationMarkers:
|
||||||
|
'''
|
||||||
|
Mark a test endpoint's lifetime without cleanup checkpoints.
|
||||||
|
|
||||||
|
'''
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
started_path: str,
|
||||||
|
cancelled_path: str,
|
||||||
|
) -> None:
|
||||||
|
self.started_path = started_path
|
||||||
|
self.cancelled_path = cancelled_path
|
||||||
|
|
||||||
|
def __enter__(self) -> None:
|
||||||
|
Path(self.started_path).touch()
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type[BaseException]|None,
|
||||||
|
exc_value: BaseException|None,
|
||||||
|
traceback: TracebackType|None,
|
||||||
|
) -> None:
|
||||||
|
Path(self.cancelled_path).touch()
|
||||||
|
|
||||||
|
|
||||||
|
def non_registration_contexts(
|
||||||
|
actor: tractor.Actor,
|
||||||
|
) -> dict[tuple, str]:
|
||||||
|
'''
|
||||||
|
Snapshot application contexts without registrar-service traffic.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return {
|
||||||
|
key: str(ctx._nsf)
|
||||||
|
for key, ctx in actor._contexts.items()
|
||||||
|
if str(ctx._nsf) != (
|
||||||
|
'tractor.discovery._registry:'
|
||||||
|
'Registrar.register_actor'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,11 @@ from tractor._testing import (
|
||||||
expect_ctxc,
|
expect_ctxc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ._helpers import (
|
||||||
|
CancellationMarkers,
|
||||||
|
non_registration_contexts,
|
||||||
|
)
|
||||||
|
|
||||||
# ``Context`` semantics are as follows,
|
# ``Context`` semantics are as follows,
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
|
|
||||||
|
|
@ -164,31 +169,18 @@ def test_overrun_error_send_tolerates_transport_close(
|
||||||
_state: bool = False
|
_state: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _non_registration_contexts(
|
|
||||||
actor: Actor,
|
|
||||||
) -> dict[tuple, str]:
|
|
||||||
return {
|
|
||||||
key: str(ctx._nsf)
|
|
||||||
for key, ctx in actor._contexts.items()
|
|
||||||
if str(ctx._nsf) != (
|
|
||||||
'tractor.discovery._registry:'
|
|
||||||
'Registrar.register_actor'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def startup_cancel_target(
|
async def startup_cancel_target(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
started_path: str,
|
started_path: str,
|
||||||
cancelled_path: str,
|
cancelled_path: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
Path(started_path).touch()
|
with CancellationMarkers(
|
||||||
try:
|
started_path,
|
||||||
|
cancelled_path,
|
||||||
|
):
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
finally:
|
|
||||||
Path(cancelled_path).touch()
|
|
||||||
|
|
||||||
|
|
||||||
async def return_one() -> int:
|
async def return_one() -> int:
|
||||||
|
|
@ -317,11 +309,13 @@ async def test_cancel_during_context_startup(
|
||||||
chan: tractor.Channel,
|
chan: tractor.Channel,
|
||||||
payload: object,
|
payload: object,
|
||||||
hide_tb: bool = False,
|
hide_tb: bool = False,
|
||||||
|
send_deadline: float = float('inf'),
|
||||||
) -> None:
|
) -> None:
|
||||||
await original_send(
|
await original_send(
|
||||||
chan,
|
chan,
|
||||||
payload,
|
payload,
|
||||||
hide_tb=hide_tb,
|
hide_tb=hide_tb,
|
||||||
|
send_deadline=send_deadline,
|
||||||
)
|
)
|
||||||
if isinstance(payload, tractor.msg.Start):
|
if isinstance(payload, tractor.msg.Start):
|
||||||
if payload.func == 'startup_cancel_target':
|
if payload.func == 'startup_cancel_target':
|
||||||
|
|
@ -344,7 +338,7 @@ async def test_cancel_during_context_startup(
|
||||||
'startup_cancel_worker',
|
'startup_cancel_worker',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
tractor.Channel,
|
tractor.Channel,
|
||||||
'send',
|
'send',
|
||||||
|
|
@ -365,12 +359,12 @@ async def test_cancel_during_context_startup(
|
||||||
original_send,
|
original_send,
|
||||||
)
|
)
|
||||||
assert cancelled_path.exists()
|
assert cancelled_path.exists()
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
assert await portal.run_from_ns(
|
assert await portal.run_from_ns(
|
||||||
__name__,
|
__name__,
|
||||||
'return_one',
|
'return_one',
|
||||||
) == 1
|
) == 1
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -396,7 +390,7 @@ async def test_start_serialization_error_cleans_context(
|
||||||
'serialization_error_worker',
|
'serialization_error_worker',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
with pytest.raises(tractor.MsgTypeError):
|
with pytest.raises(tractor.MsgTypeError):
|
||||||
async with portal.open_context(
|
async with portal.open_context(
|
||||||
simple_setup_teardown,
|
simple_setup_teardown,
|
||||||
|
|
@ -404,7 +398,7 @@ async def test_start_serialization_error_cleans_context(
|
||||||
):
|
):
|
||||||
raise AssertionError('invalid `Start` was accepted')
|
raise AssertionError('invalid `Start` was accepted')
|
||||||
|
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
async with portal.open_context(
|
async with portal.open_context(
|
||||||
simple_setup_teardown,
|
simple_setup_teardown,
|
||||||
data=1,
|
data=1,
|
||||||
|
|
@ -412,7 +406,7 @@ async def test_start_serialization_error_cleans_context(
|
||||||
assert started == 2
|
assert started == 2
|
||||||
assert await ctx.wait_for_result() == 'yo'
|
assert await ctx.wait_for_result() == 'yo'
|
||||||
|
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -437,7 +431,7 @@ async def test_start_module_error_cleans_context(
|
||||||
portal: tractor.Portal = await an.start_actor(
|
portal: tractor.Portal = await an.start_actor(
|
||||||
'module_error_worker',
|
'module_error_worker',
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
with pytest.raises(tractor.RemoteActorError) as excinfo:
|
with pytest.raises(tractor.RemoteActorError) as excinfo:
|
||||||
async with portal.open_context(
|
async with portal.open_context(
|
||||||
simple_setup_teardown,
|
simple_setup_teardown,
|
||||||
|
|
@ -446,7 +440,7 @@ async def test_start_module_error_cleans_context(
|
||||||
raise AssertionError('unexposed context was started')
|
raise AssertionError('unexposed context was started')
|
||||||
|
|
||||||
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ from tractor.msg.ptr import NamespacePath
|
||||||
from tractor.spawn import _mp as mp_spawn
|
from tractor.spawn import _mp as mp_spawn
|
||||||
from tractor.to_actor import _api as to_actor_api
|
from tractor.to_actor import _api as to_actor_api
|
||||||
|
|
||||||
|
from ._helpers import (
|
||||||
|
CancellationMarkers,
|
||||||
|
non_registration_contexts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def add_one(
|
async def add_one(
|
||||||
n: int,
|
n: int,
|
||||||
|
|
@ -58,11 +63,11 @@ async def mark_task_cancellation(
|
||||||
started_path: str,
|
started_path: str,
|
||||||
cancelled_path: str,
|
cancelled_path: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
Path(started_path).touch()
|
with CancellationMarkers(
|
||||||
try:
|
started_path,
|
||||||
|
cancelled_path,
|
||||||
|
):
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
finally:
|
|
||||||
Path(cancelled_path).touch()
|
|
||||||
|
|
||||||
|
|
||||||
async def echo_startup_control(
|
async def echo_startup_control(
|
||||||
|
|
@ -84,19 +89,6 @@ async def collect_call(
|
||||||
return args, kwargs
|
return args, kwargs
|
||||||
|
|
||||||
|
|
||||||
def _non_registration_contexts(
|
|
||||||
actor: tractor.Actor,
|
|
||||||
) -> dict[tuple, str]:
|
|
||||||
return {
|
|
||||||
key: str(ctx._nsf)
|
|
||||||
for key, ctx in actor._contexts.items()
|
|
||||||
if str(ctx._nsf) != (
|
|
||||||
'tractor.discovery._registry:'
|
|
||||||
'Registrar.register_actor'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_namespace_path_retains_target_ref(
|
def test_namespace_path_retains_target_ref(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
):
|
):
|
||||||
|
|
@ -588,7 +580,7 @@ async def test_reuse_existing_actor_via_portal(
|
||||||
to_actor.MODULE,
|
to_actor.MODULE,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
assert await to_actor.run(
|
assert await to_actor.run(
|
||||||
add_one,
|
add_one,
|
||||||
|
|
@ -601,7 +593,7 @@ async def test_reuse_existing_actor_via_portal(
|
||||||
'echo_startup_control',
|
'echo_startup_control',
|
||||||
_cancel_on_startup='target_value',
|
_cancel_on_startup='target_value',
|
||||||
) == 'target_value'
|
) == 'target_value'
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
|
|
||||||
# still alive: caller owns the actor's lifetime.
|
# still alive: caller owns the actor's lifetime.
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
@ -887,7 +879,7 @@ async def test_portal_task_cancelled_with_local_caller(
|
||||||
to_actor.MODULE,
|
to_actor.MODULE,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
|
|
||||||
async with trio.open_nursery() as tn:
|
async with trio.open_nursery() as tn:
|
||||||
tn.start_soon(
|
tn.start_soon(
|
||||||
|
|
@ -905,13 +897,13 @@ async def test_portal_task_cancelled_with_local_caller(
|
||||||
tn.cancel_scope.cancel()
|
tn.cancel_scope.cancel()
|
||||||
|
|
||||||
assert cancelled_path.exists()
|
assert cancelled_path.exists()
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
assert await to_actor.run(
|
assert await to_actor.run(
|
||||||
add_one,
|
add_one,
|
||||||
1,
|
1,
|
||||||
portal=portal,
|
portal=portal,
|
||||||
) == 2
|
) == 2
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
|
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
@ -937,7 +929,7 @@ async def test_context_trampoline_preserves_module_allowlist(
|
||||||
'restricted_context_worker',
|
'restricted_context_worker',
|
||||||
enable_modules=[to_actor.MODULE],
|
enable_modules=[to_actor.MODULE],
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
with pytest.raises(RemoteActorError) as excinfo:
|
with pytest.raises(RemoteActorError) as excinfo:
|
||||||
await to_actor.run(
|
await to_actor.run(
|
||||||
add_one,
|
add_one,
|
||||||
|
|
@ -946,7 +938,7 @@ async def test_context_trampoline_preserves_module_allowlist(
|
||||||
)
|
)
|
||||||
|
|
||||||
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -970,7 +962,7 @@ async def test_portal_requires_context_trampoline(
|
||||||
'no_context_trampoline_worker',
|
'no_context_trampoline_worker',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
contexts_before = _non_registration_contexts(actor)
|
contexts_before = non_registration_contexts(actor)
|
||||||
with pytest.raises(RemoteActorError) as excinfo:
|
with pytest.raises(RemoteActorError) as excinfo:
|
||||||
await to_actor.run(
|
await to_actor.run(
|
||||||
add_one,
|
add_one,
|
||||||
|
|
@ -981,5 +973,5 @@ async def test_portal_requires_context_trampoline(
|
||||||
err = excinfo.value
|
err = excinfo.value
|
||||||
assert err.boxed_type is tractor.ModuleNotExposed
|
assert err.boxed_type is tractor.ModuleNotExposed
|
||||||
assert to_actor.MODULE in str(err)
|
assert to_actor.MODULE in str(err)
|
||||||
assert _non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue