Skip to content

Commit ec8e8a2

Browse files
authored
Merge pull request #426 from goodboy/remote_exc_type_registry
Fix remote exc relay + add `reg_err_types()` tests
2 parents 8f6bc56 + c3d1ec2 commit ec8e8a2

File tree

2 files changed

+417
-27
lines changed

2 files changed

+417
-27
lines changed

tests/test_reg_err_types.py

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
'''
2+
Verify that externally registered remote actor error
3+
types are correctly relayed, boxed, and re-raised across
4+
IPC actor hops via `reg_err_types()`.
5+
6+
Also ensure that when custom error types are NOT registered
7+
the framework indicates the lookup failure to the user.
8+
9+
'''
10+
import pytest
11+
import trio
12+
import tractor
13+
from tractor import (
14+
Context,
15+
Portal,
16+
RemoteActorError,
17+
)
18+
from tractor._exceptions import (
19+
get_err_type,
20+
reg_err_types,
21+
)
22+
23+
24+
# -- custom app-level errors for testing --
25+
class CustomAppError(Exception):
26+
'''
27+
A hypothetical user-app error that should be
28+
boxed+relayed by `tractor` IPC when registered.
29+
30+
'''
31+
32+
33+
class AnotherAppError(Exception):
34+
'''
35+
A second custom error for multi-type registration.
36+
37+
'''
38+
39+
40+
class UnregisteredAppError(Exception):
41+
'''
42+
A custom error that is intentionally NEVER
43+
registered via `reg_err_types()` so we can
44+
verify the framework's failure indication.
45+
46+
'''
47+
48+
49+
# -- remote-task endpoints --
50+
@tractor.context
51+
async def raise_custom_err(
52+
ctx: Context,
53+
) -> None:
54+
'''
55+
Remote ep that raises a `CustomAppError`
56+
after sync-ing with the caller.
57+
58+
'''
59+
await ctx.started()
60+
raise CustomAppError(
61+
'the app exploded remotely'
62+
)
63+
64+
65+
@tractor.context
66+
async def raise_another_err(
67+
ctx: Context,
68+
) -> None:
69+
'''
70+
Remote ep that raises `AnotherAppError`.
71+
72+
'''
73+
await ctx.started()
74+
raise AnotherAppError(
75+
'another app-level kaboom'
76+
)
77+
78+
79+
@tractor.context
80+
async def raise_unreg_err(
81+
ctx: Context,
82+
) -> None:
83+
'''
84+
Remote ep that raises an `UnregisteredAppError`
85+
which has NOT been `reg_err_types()`-registered.
86+
87+
'''
88+
await ctx.started()
89+
raise UnregisteredAppError(
90+
'this error type is unknown to tractor'
91+
)
92+
93+
94+
# -- unit tests for the type-registry plumbing --
95+
96+
class TestRegErrTypesPlumbing:
97+
'''
98+
Low-level checks on `reg_err_types()` and
99+
`get_err_type()` without requiring IPC.
100+
101+
'''
102+
103+
def test_unregistered_type_returns_none(self):
104+
'''
105+
An unregistered custom error name should yield
106+
`None` from `get_err_type()`.
107+
108+
'''
109+
result = get_err_type('CustomAppError')
110+
assert result is None
111+
112+
def test_register_and_lookup(self):
113+
'''
114+
After `reg_err_types()`, the custom type should
115+
be discoverable via `get_err_type()`.
116+
117+
'''
118+
reg_err_types([CustomAppError])
119+
result = get_err_type('CustomAppError')
120+
assert result is CustomAppError
121+
122+
def test_register_multiple_types(self):
123+
'''
124+
Registering a list of types should make each
125+
one individually resolvable.
126+
127+
'''
128+
reg_err_types([
129+
CustomAppError,
130+
AnotherAppError,
131+
])
132+
assert (
133+
get_err_type('CustomAppError')
134+
is CustomAppError
135+
)
136+
assert (
137+
get_err_type('AnotherAppError')
138+
is AnotherAppError
139+
)
140+
141+
def test_builtin_types_always_resolve(self):
142+
'''
143+
Builtin error types like `RuntimeError` and
144+
`ValueError` should always be found without
145+
any prior registration.
146+
147+
'''
148+
assert (
149+
get_err_type('RuntimeError')
150+
is RuntimeError
151+
)
152+
assert (
153+
get_err_type('ValueError')
154+
is ValueError
155+
)
156+
157+
def test_tractor_native_types_resolve(self):
158+
'''
159+
`tractor`-internal exc types (e.g.
160+
`ContextCancelled`) should always resolve.
161+
162+
'''
163+
assert (
164+
get_err_type('ContextCancelled')
165+
is tractor.ContextCancelled
166+
)
167+
168+
def test_boxed_type_str_without_ipc_msg(self):
169+
'''
170+
When a `RemoteActorError` is constructed
171+
without an IPC msg (and no resolvable type),
172+
`.boxed_type_str` should return `'<unknown>'`.
173+
174+
'''
175+
rae = RemoteActorError('test')
176+
assert rae.boxed_type_str == '<unknown>'
177+
178+
179+
# -- IPC-level integration tests --
180+
181+
def test_registered_custom_err_relayed(
182+
debug_mode: bool,
183+
tpt_proto: str,
184+
):
185+
'''
186+
When a custom error type is registered via
187+
`reg_err_types()` on BOTH sides of an IPC dialog,
188+
the parent should receive a `RemoteActorError`
189+
whose `.boxed_type` matches the original custom
190+
error type.
191+
192+
'''
193+
reg_err_types([CustomAppError])
194+
195+
async def main():
196+
async with tractor.open_nursery(
197+
debug_mode=debug_mode,
198+
enable_transports=[tpt_proto],
199+
) as an:
200+
ptl: Portal = await an.start_actor(
201+
'custom-err-raiser',
202+
enable_modules=[__name__],
203+
)
204+
async with ptl.open_context(
205+
raise_custom_err,
206+
) as (ctx, sent):
207+
assert not sent
208+
try:
209+
await ctx.wait_for_result()
210+
except RemoteActorError as rae:
211+
assert rae.boxed_type is CustomAppError
212+
assert rae.src_type is CustomAppError
213+
assert 'the app exploded remotely' in str(
214+
rae.tb_str
215+
)
216+
raise
217+
218+
with pytest.raises(RemoteActorError) as excinfo:
219+
trio.run(main)
220+
221+
rae = excinfo.value
222+
assert rae.boxed_type is CustomAppError
223+
224+
225+
def test_registered_another_err_relayed(
226+
debug_mode: bool,
227+
tpt_proto: str,
228+
):
229+
'''
230+
Same as above but for a different custom error
231+
type to verify multi-type registration works
232+
end-to-end over IPC.
233+
234+
'''
235+
reg_err_types([AnotherAppError])
236+
237+
async def main():
238+
async with tractor.open_nursery(
239+
debug_mode=debug_mode,
240+
enable_transports=[tpt_proto],
241+
) as an:
242+
ptl: Portal = await an.start_actor(
243+
'another-err-raiser',
244+
enable_modules=[__name__],
245+
)
246+
async with ptl.open_context(
247+
raise_another_err,
248+
) as (ctx, sent):
249+
assert not sent
250+
try:
251+
await ctx.wait_for_result()
252+
except RemoteActorError as rae:
253+
assert (
254+
rae.boxed_type
255+
is AnotherAppError
256+
)
257+
raise
258+
259+
await an.cancel()
260+
261+
with pytest.raises(RemoteActorError) as excinfo:
262+
trio.run(main)
263+
264+
rae = excinfo.value
265+
assert rae.boxed_type is AnotherAppError
266+
267+
268+
def test_unregistered_err_still_relayed(
269+
debug_mode: bool,
270+
tpt_proto: str,
271+
):
272+
'''
273+
Verify that even when a custom error type is NOT registered via
274+
`reg_err_types()`, the remote error is still relayed as
275+
a `RemoteActorError` with all string-level info preserved
276+
(traceback, type name, source actor uid).
277+
278+
The `.boxed_type` will be `None` (type obj can't be resolved) but
279+
`.boxed_type_str` and `.src_type_str` still report the original
280+
type name from the IPC msg.
281+
282+
This documents the expected limitation: without `reg_err_types()`
283+
the `.boxed_type` property can NOT resolve to the original Python
284+
type.
285+
286+
'''
287+
# NOTE: intentionally do NOT call
288+
# `reg_err_types([UnregisteredAppError])`
289+
290+
async def main():
291+
async with tractor.open_nursery(
292+
debug_mode=debug_mode,
293+
enable_transports=[tpt_proto],
294+
) as an:
295+
ptl: Portal = await an.start_actor(
296+
'unreg-err-raiser',
297+
enable_modules=[__name__],
298+
)
299+
async with ptl.open_context(
300+
raise_unreg_err,
301+
) as (ctx, sent):
302+
assert not sent
303+
await ctx.wait_for_result()
304+
305+
await an.cancel()
306+
307+
with pytest.raises(RemoteActorError) as excinfo:
308+
trio.run(main)
309+
310+
rae = excinfo.value
311+
312+
# the error IS relayed even without
313+
# registration; type obj is unresolvable but
314+
# all string-level info is preserved.
315+
assert rae.boxed_type is None # NOT `UnregisteredAppError`
316+
assert rae.src_type is None
317+
318+
# string names survive the IPC round-trip
319+
# via the `Error` msg fields.
320+
assert (
321+
rae.src_type_str
322+
==
323+
'UnregisteredAppError'
324+
)
325+
assert (
326+
rae.boxed_type_str
327+
==
328+
'UnregisteredAppError'
329+
)
330+
331+
# original traceback content is preserved
332+
assert 'this error type is unknown' in rae.tb_str
333+
assert 'UnregisteredAppError' in rae.tb_str

0 commit comments

Comments
 (0)