diff --git a/tests/_helpers.py b/tests/_helpers.py new file mode 100644 index 00000000..9e7f3d1e --- /dev/null +++ b/tests/_helpers.py @@ -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' + ) + } diff --git a/tests/test_context_stream_semantics.py b/tests/test_context_stream_semantics.py index e7535fd4..885ba689 100644 --- a/tests/test_context_stream_semantics.py +++ b/tests/test_context_stream_semantics.py @@ -40,6 +40,11 @@ from tractor._testing import ( expect_ctxc, ) +from ._helpers import ( + CancellationMarkers, + non_registration_contexts, +) + # ``Context`` semantics are as follows, # ------------------------------------ @@ -164,31 +169,18 @@ def test_overrun_error_send_tolerates_transport_close( _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 async def startup_cancel_target( ctx: Context, started_path: str, cancelled_path: str, ) -> None: - Path(started_path).touch() - try: + with CancellationMarkers( + started_path, + cancelled_path, + ): await ctx.started() await trio.sleep_forever() - finally: - Path(cancelled_path).touch() async def return_one() -> int: @@ -317,11 +309,13 @@ async def test_cancel_during_context_startup( chan: tractor.Channel, payload: object, hide_tb: bool = False, + send_deadline: float = float('inf'), ) -> None: await original_send( chan, payload, hide_tb=hide_tb, + send_deadline=send_deadline, ) if isinstance(payload, tractor.msg.Start): if payload.func == 'startup_cancel_target': @@ -344,7 +338,7 @@ async def test_cancel_during_context_startup( 'startup_cancel_worker', enable_modules=[__name__], ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) monkeypatch.setattr( tractor.Channel, 'send', @@ -365,12 +359,12 @@ async def test_cancel_during_context_startup( original_send, ) assert cancelled_path.exists() - assert _non_registration_contexts(actor) == contexts_before + assert non_registration_contexts(actor) == contexts_before assert await portal.run_from_ns( __name__, 'return_one', ) == 1 - assert _non_registration_contexts(actor) == contexts_before + assert non_registration_contexts(actor) == contexts_before await portal.cancel_actor() @@ -396,7 +390,7 @@ async def test_start_serialization_error_cleans_context( 'serialization_error_worker', enable_modules=[__name__], ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) with pytest.raises(tractor.MsgTypeError): async with portal.open_context( simple_setup_teardown, @@ -404,7 +398,7 @@ async def test_start_serialization_error_cleans_context( ): 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( simple_setup_teardown, data=1, @@ -412,7 +406,7 @@ async def test_start_serialization_error_cleans_context( assert started == 2 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() @@ -437,7 +431,7 @@ async def test_start_module_error_cleans_context( portal: tractor.Portal = await an.start_actor( 'module_error_worker', ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) with pytest.raises(tractor.RemoteActorError) as excinfo: async with portal.open_context( simple_setup_teardown, @@ -446,7 +440,7 @@ async def test_start_module_error_cleans_context( raise AssertionError('unexposed context was started') 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() diff --git a/tests/test_to_actor.py b/tests/test_to_actor.py index 38abb78c..6970006f 100644 --- a/tests/test_to_actor.py +++ b/tests/test_to_actor.py @@ -25,6 +25,11 @@ from tractor.msg.ptr import NamespacePath from tractor.spawn import _mp as mp_spawn from tractor.to_actor import _api as to_actor_api +from ._helpers import ( + CancellationMarkers, + non_registration_contexts, +) + async def add_one( n: int, @@ -58,11 +63,11 @@ async def mark_task_cancellation( started_path: str, cancelled_path: str, ) -> None: - Path(started_path).touch() - try: + with CancellationMarkers( + started_path, + cancelled_path, + ): await trio.sleep_forever() - finally: - Path(cancelled_path).touch() async def echo_startup_control( @@ -84,19 +89,6 @@ async def collect_call( 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( monkeypatch: pytest.MonkeyPatch, ): @@ -588,7 +580,7 @@ async def test_reuse_existing_actor_via_portal( to_actor.MODULE, ], ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) for i in range(3): assert await to_actor.run( add_one, @@ -601,7 +593,7 @@ async def test_reuse_existing_actor_via_portal( 'echo_startup_control', _cancel_on_startup='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. await portal.cancel_actor() @@ -887,7 +879,7 @@ async def test_portal_task_cancelled_with_local_caller( to_actor.MODULE, ], ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) async with trio.open_nursery() as tn: tn.start_soon( @@ -905,13 +897,13 @@ async def test_portal_task_cancelled_with_local_caller( tn.cancel_scope.cancel() assert cancelled_path.exists() - assert _non_registration_contexts(actor) == contexts_before + assert non_registration_contexts(actor) == contexts_before assert await to_actor.run( add_one, 1, portal=portal, ) == 2 - assert _non_registration_contexts(actor) == contexts_before + assert non_registration_contexts(actor) == contexts_before await portal.cancel_actor() @@ -937,7 +929,7 @@ async def test_context_trampoline_preserves_module_allowlist( 'restricted_context_worker', enable_modules=[to_actor.MODULE], ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) with pytest.raises(RemoteActorError) as excinfo: await to_actor.run( add_one, @@ -946,7 +938,7 @@ async def test_context_trampoline_preserves_module_allowlist( ) 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() @@ -970,7 +962,7 @@ async def test_portal_requires_context_trampoline( 'no_context_trampoline_worker', enable_modules=[__name__], ) - contexts_before = _non_registration_contexts(actor) + contexts_before = non_registration_contexts(actor) with pytest.raises(RemoteActorError) as excinfo: await to_actor.run( add_one, @@ -981,5 +973,5 @@ async def test_portal_requires_context_trampoline( err = excinfo.value assert err.boxed_type is tractor.ModuleNotExposed 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()