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-codemulticast_revertable_streams
parent
8f6bc56174
commit
9c37b3f956
|
|
@ -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
|
||||||
Loading…
Reference in New Issue