Add a `tractor.post_mortem()` API test + example
Since turns out we didn't have a single example using that API Bo The test granular-ly checks all use cases: - `.post_mortem()` manual calls in both subactor and root. - ensuring built-in RPC crash handling activates after each manual one from ^. - drafted some call-stack frame checking that i commented out for now since we need to first do ANSI escape code removal due to the colorization that `pdbp` does by default. |_ added a TODO with SO link on `assert_before()`. Also todo-staged a shielded-pause test to match with the already existing-but-needs-refinement example B)runtime_to_msgspec
parent
cdb1311e40
commit
2f854a3e86
|
@ -0,0 +1,56 @@
|
||||||
|
import trio
|
||||||
|
import tractor
|
||||||
|
|
||||||
|
|
||||||
|
@tractor.context
|
||||||
|
async def name_error(
|
||||||
|
ctx: tractor.Context,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Raise a `NameError`, catch it and enter `.post_mortem()`, then
|
||||||
|
expect the `._rpc._invoke()` crash handler to also engage.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
getattr(doggypants) # noqa (on purpose)
|
||||||
|
except NameError:
|
||||||
|
await tractor.post_mortem()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
'''
|
||||||
|
Test 3 `PdbREPL` entries:
|
||||||
|
- one in the child due to manual `.post_mortem()`,
|
||||||
|
- another in the child due to runtime RPC crash handling.
|
||||||
|
- final one here in parent from the RAE.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# XXX NOTE: ideally the REPL arrives at this frame in the parent
|
||||||
|
# ONE UP FROM the inner ctx block below!
|
||||||
|
async with tractor.open_nursery(
|
||||||
|
debug_mode=True,
|
||||||
|
# loglevel='cancel',
|
||||||
|
) as an:
|
||||||
|
p: tractor.Portal = await an.start_actor(
|
||||||
|
'child',
|
||||||
|
enable_modules=[__name__],
|
||||||
|
)
|
||||||
|
|
||||||
|
# XXX should raise `RemoteActorError[NameError]`
|
||||||
|
# AND be the active frame when REPL enters!
|
||||||
|
try:
|
||||||
|
async with p.open_context(name_error) as (ctx, first):
|
||||||
|
assert first
|
||||||
|
except tractor.RemoteActorError as rae:
|
||||||
|
assert rae.boxed_type is NameError
|
||||||
|
|
||||||
|
# manually handle in root's parent task
|
||||||
|
await tractor.post_mortem()
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise RuntimeError('IPC ctx should have remote errored!?')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
trio.run(main)
|
|
@ -161,6 +161,10 @@ def in_prompt_msg(
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: todo support terminal color-chars stripping so we can match
|
||||||
|
# against call stack frame output from the the 'll' command the like!
|
||||||
|
# -[ ] SO answer for stipping ANSI codes: https://stackoverflow.com/a/14693789
|
||||||
def assert_before(
|
def assert_before(
|
||||||
child,
|
child,
|
||||||
patts: list[str],
|
patts: list[str],
|
||||||
|
@ -1125,7 +1129,112 @@ def test_pause_from_sync(
|
||||||
child.expect(pexpect.EOF)
|
child.expect(pexpect.EOF)
|
||||||
|
|
||||||
|
|
||||||
# TODO!
|
def test_post_mortem_api(
|
||||||
|
spawn,
|
||||||
|
ctlc: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Verify the `tractor.post_mortem()` API works in an exception
|
||||||
|
handler block.
|
||||||
|
|
||||||
|
'''
|
||||||
|
child = spawn('pm_in_subactor')
|
||||||
|
|
||||||
|
# First entry is via manual `.post_mortem()`
|
||||||
|
child.expect(PROMPT)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_crash_msg,
|
||||||
|
"<Task 'name_error'",
|
||||||
|
"NameError",
|
||||||
|
"('child'",
|
||||||
|
"tractor.post_mortem()",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(child)
|
||||||
|
child.sendline('c')
|
||||||
|
|
||||||
|
# 2nd is RPC crash handler
|
||||||
|
child.expect(PROMPT)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_crash_msg,
|
||||||
|
"<Task 'name_error'",
|
||||||
|
"NameError",
|
||||||
|
"('child'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(child)
|
||||||
|
child.sendline('c')
|
||||||
|
|
||||||
|
# 3rd is via RAE bubbled to root's parent ctx task and
|
||||||
|
# crash-handled via another manual pm call.
|
||||||
|
child.expect(PROMPT)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_crash_msg,
|
||||||
|
"<Task '__main__.main'",
|
||||||
|
"('root'",
|
||||||
|
"NameError",
|
||||||
|
"tractor.post_mortem()",
|
||||||
|
"src_uid=('child'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(child)
|
||||||
|
child.sendline('c')
|
||||||
|
|
||||||
|
# 4th and FINAL is via RAE bubbled to root's parent ctx task and
|
||||||
|
# crash-handled via another manual pm call.
|
||||||
|
child.expect(PROMPT)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_crash_msg,
|
||||||
|
"<Task '__main__.main'",
|
||||||
|
"('root'",
|
||||||
|
"NameError",
|
||||||
|
"src_uid=('child'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if ctlc:
|
||||||
|
do_ctlc(child)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: ensure we're stopped and showing the right call stack frame
|
||||||
|
# -[ ] need a way to strip the terminal color chars in order to
|
||||||
|
# pattern match... see TODO around `assert_before()` above!
|
||||||
|
# child.sendline('w')
|
||||||
|
# child.expect(PROMPT)
|
||||||
|
# assert_before(
|
||||||
|
# child,
|
||||||
|
# [
|
||||||
|
# # error src block annot at ctx open
|
||||||
|
# '-> async with p.open_context(name_error) as (ctx, first):',
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # step up a frame to ensure the it's the root's nursery
|
||||||
|
# child.sendline('u')
|
||||||
|
# child.expect(PROMPT)
|
||||||
|
# assert_before(
|
||||||
|
# child,
|
||||||
|
# [
|
||||||
|
# # handler block annotation
|
||||||
|
# '-> async with tractor.open_nursery(',
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
|
||||||
|
child.sendline('c')
|
||||||
|
child.expect(pexpect.EOF)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: needs ANSI code stripping tho, see `assert_before()` # above!
|
||||||
def test_correct_frames_below_hidden():
|
def test_correct_frames_below_hidden():
|
||||||
'''
|
'''
|
||||||
Ensure that once a `tractor.pause()` enages, when the user
|
Ensure that once a `tractor.pause()` enages, when the user
|
||||||
|
@ -1138,4 +1247,15 @@ def test_correct_frames_below_hidden():
|
||||||
|
|
||||||
|
|
||||||
def test_cant_pause_from_paused_task():
|
def test_cant_pause_from_paused_task():
|
||||||
|
'''
|
||||||
|
Pausing from with an already paused task should raise an error.
|
||||||
|
|
||||||
|
Normally this should only happen in practise while debugging the call stack of `tractor.pause()` itself, likely
|
||||||
|
by a `.pause()` line somewhere inside our runtime.
|
||||||
|
|
||||||
|
'''
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def test_shield_pause():
|
||||||
...
|
...
|
||||||
|
|
Loading…
Reference in New Issue