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
							
								
									d099466d21
								
							
						
					
					
						commit
						5bab7648e2
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			@ -159,6 +159,10 @@ def in_prompt_msg(
 | 
			
		|||
 | 
			
		||||
    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(
 | 
			
		||||
    child,
 | 
			
		||||
    patts: list[str],
 | 
			
		||||
| 
						 | 
				
			
			@ -1123,7 +1127,112 @@ def test_pause_from_sync(
 | 
			
		|||
    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():
 | 
			
		||||
    '''
 | 
			
		||||
    Ensure that once a `tractor.pause()` enages, when the user
 | 
			
		||||
| 
						 | 
				
			
			@ -1136,4 +1245,15 @@ def test_correct_frames_below_hidden():
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
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