From 9c37b3f9566c63c72f9b6ada79880a1899b3f2df Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Mar 2026 14:09:16 -0400 Subject: [PATCH] Add `reg_err_types()` test suite for remote exc relay Verify registered custom error types round-trip correctly over IPC via `reg_err_types()` + `get_err_type()`. Deats, - `TestRegErrTypesPlumbing`: 5 unit tests for the type-registry plumbing (register, lookup, builtins, tractor-native types, unregistered returns `None`) - `test_registered_custom_err_relayed`: IPC end-to-end for a registered `CustomAppError` checking `.boxed_type`, `.src_type`, and `.tb_str` - `test_registered_another_err_relayed`: same for `AnotherAppError` (multi-type coverage) - `test_unregistered_custom_err_fails_lookup`: `xfail` documenting that `.boxed_type` can't resolve without `reg_err_types()` registration (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_reg_err_types.py | 313 ++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 tests/test_reg_err_types.py diff --git a/tests/test_reg_err_types.py b/tests/test_reg_err_types.py new file mode 100644 index 00000000..e1f4f249 --- /dev/null +++ b/tests/test_reg_err_types.py @@ -0,0 +1,313 @@ +''' +Verify that externally registered remote actor error +types are correctly relayed, boxed, and re-raised across +IPC actor hops via `reg_err_types()`. + +Also ensure that when custom error types are NOT registered +the framework indicates the lookup failure to the user. + +''' +import pytest +import trio +import tractor +from tractor import ( + Context, + Portal, + RemoteActorError, +) +from tractor._exceptions import ( + get_err_type, + reg_err_types, +) + + +# -- custom app-level errors for testing -- +class CustomAppError(Exception): + ''' + A hypothetical user-app error that should be + boxed+relayed by `tractor` IPC when registered. + + ''' + + +class AnotherAppError(Exception): + ''' + A second custom error for multi-type registration. + + ''' + + +class UnregisteredAppError(Exception): + ''' + A custom error that is intentionally NEVER + registered via `reg_err_types()` so we can + verify the framework's failure indication. + + ''' + + +# -- remote-task endpoints -- +@tractor.context +async def raise_custom_err( + ctx: Context, +) -> None: + ''' + Remote ep that raises a `CustomAppError` + after sync-ing with the caller. + + ''' + await ctx.started() + raise CustomAppError( + 'the app exploded remotely' + ) + + +@tractor.context +async def raise_another_err( + ctx: Context, +) -> None: + ''' + Remote ep that raises `AnotherAppError`. + + ''' + await ctx.started() + raise AnotherAppError( + 'another app-level kaboom' + ) + + +@tractor.context +async def raise_unreg_err( + ctx: Context, +) -> None: + ''' + Remote ep that raises an `UnregisteredAppError` + which has NOT been `reg_err_types()`-registered. + + ''' + await ctx.started() + raise UnregisteredAppError( + 'this error type is unknown to tractor' + ) + + +# -- unit tests for the type-registry plumbing -- + +class TestRegErrTypesPlumbing: + ''' + Low-level checks on `reg_err_types()` and + `get_err_type()` without requiring IPC. + + ''' + + def test_unregistered_type_returns_none(self): + ''' + An unregistered custom error name should yield + `None` from `get_err_type()`. + + ''' + result = get_err_type('CustomAppError') + assert result is None + + def test_register_and_lookup(self): + ''' + After `reg_err_types()`, the custom type should + be discoverable via `get_err_type()`. + + ''' + reg_err_types([CustomAppError]) + result = get_err_type('CustomAppError') + assert result is CustomAppError + + def test_register_multiple_types(self): + ''' + Registering a list of types should make each + one individually resolvable. + + ''' + reg_err_types([ + CustomAppError, + AnotherAppError, + ]) + assert ( + get_err_type('CustomAppError') + is CustomAppError + ) + assert ( + get_err_type('AnotherAppError') + is AnotherAppError + ) + + def test_builtin_types_always_resolve(self): + ''' + Builtin error types like `RuntimeError` and + `ValueError` should always be found without + any prior registration. + + ''' + assert ( + get_err_type('RuntimeError') + is RuntimeError + ) + assert ( + get_err_type('ValueError') + is ValueError + ) + + def test_tractor_native_types_resolve(self): + ''' + `tractor`-internal exc types (e.g. + `ContextCancelled`) should always resolve. + + ''' + assert ( + get_err_type('ContextCancelled') + is tractor.ContextCancelled + ) + + +# -- IPC-level integration tests -- + +def test_registered_custom_err_relayed( + debug_mode: bool, + tpt_proto: str, +): + ''' + When a custom error type is registered via + `reg_err_types()` on BOTH sides of an IPC dialog, + the parent should receive a `RemoteActorError` + whose `.boxed_type` matches the original custom + error type. + + ''' + reg_err_types([CustomAppError]) + + async def main(): + async with tractor.open_nursery( + debug_mode=debug_mode, + enable_transports=[tpt_proto], + ) as an: + ptl: Portal = await an.start_actor( + 'custom-err-raiser', + enable_modules=[__name__], + ) + async with ptl.open_context( + raise_custom_err, + ) as (ctx, sent): + assert not sent + try: + await ctx.wait_for_result() + except RemoteActorError as rae: + assert rae.boxed_type is CustomAppError + assert rae.src_type is CustomAppError + assert 'the app exploded remotely' in str( + rae.tb_str + ) + raise + + await an.cancel() + + with pytest.raises(RemoteActorError) as excinfo: + trio.run(main) + + rae = excinfo.value + assert rae.boxed_type is CustomAppError + + +def test_registered_another_err_relayed( + debug_mode: bool, + tpt_proto: str, +): + ''' + Same as above but for a different custom error + type to verify multi-type registration works + end-to-end over IPC. + + ''' + reg_err_types([AnotherAppError]) + + async def main(): + async with tractor.open_nursery( + debug_mode=debug_mode, + enable_transports=[tpt_proto], + ) as an: + ptl: Portal = await an.start_actor( + 'another-err-raiser', + enable_modules=[__name__], + ) + async with ptl.open_context( + raise_another_err, + ) as (ctx, sent): + assert not sent + try: + await ctx.wait_for_result() + except RemoteActorError as rae: + assert ( + rae.boxed_type + is AnotherAppError + ) + raise + + await an.cancel() + + with pytest.raises(RemoteActorError) as excinfo: + trio.run(main) + + rae = excinfo.value + assert rae.boxed_type is AnotherAppError + + +@pytest.mark.xfail( + reason=( + 'Unregistered custom error types are not ' + 'resolvable by `get_err_type()` and thus ' + '`.boxed_type` will be `None`, indicating ' + 'the framework cannot reconstruct the ' + 'original remote error type - the user ' + 'must call `reg_err_types()` to fix this.' + ), +) +def test_unregistered_custom_err_fails_lookup( + debug_mode: bool, + tpt_proto: str, +): + ''' + When a custom error type is NOT registered the + received `RemoteActorError.boxed_type` should NOT + match the original error type. + + This test is `xfail` to document the expected + failure mode and to alert the user that + `reg_err_types()` must be called for custom + error types to relay correctly. + + ''' + # NOTE: intentionally do NOT call + # `reg_err_types([UnregisteredAppError])` + + async def main(): + async with tractor.open_nursery( + debug_mode=debug_mode, + enable_transports=[tpt_proto], + ) as an: + ptl: Portal = await an.start_actor( + 'unreg-err-raiser', + enable_modules=[__name__], + ) + async with ptl.open_context( + raise_unreg_err, + ) as (ctx, sent): + assert not sent + await ctx.wait_for_result() + + await an.cancel() + + with pytest.raises(RemoteActorError) as excinfo: + trio.run(main) + + rae = excinfo.value + + # XXX this SHOULD fail bc the type was never + # registered and thus `get_err_type()` returns + # `None` for the boxed type lookup. + assert rae.boxed_type is UnregisteredAppError