diff --git a/examples/debugging/root_self_cancelled_w_error.py b/examples/debugging/root_self_cancelled_w_error.py new file mode 100644 index 00000000..b3c15288 --- /dev/null +++ b/examples/debugging/root_self_cancelled_w_error.py @@ -0,0 +1,35 @@ +import trio +import tractor + + +async def main(): + async with tractor.open_root_actor( + debug_mode=True, + loglevel='cancel', + ) as _root: + + # manually trigger self-cancellation and wait + # for it to fully trigger. + _root.cancel_soon() + await _root._cancel_complete.wait() + print('root cancelled') + + # now ensure we can still use the REPL + try: + await tractor.pause() + except trio.Cancelled as _taskc: + assert (root_cs := _root._root_tn.cancel_scope).cancel_called + # NOTE^^ above logic but inside `open_root_actor()` and + # passed to the `shield=` expression is effectively what + # we're testing here! + await tractor.pause(shield=root_cs.cancel_called) + + # XXX, if shield logic *is wrong* inside `open_root_actor()`'s + # crash-handler block this should never be interacted, + # instead `trio.Cancelled` would be bubbled up: the original + # BUG. + assert 0 + + +if __name__ == '__main__': + trio.run(main) diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index 1b279dfd..cacab803 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -1,13 +1,13 @@ """ That "native" debug mode better work! -All these tests can be understood (somewhat) by running the equivalent -`examples/debugging/` scripts manually. +All these tests can be understood (somewhat) by running the +equivalent `examples/debugging/` scripts manually. TODO: - - none of these tests have been run successfully on windows yet but - there's been manual testing that verified it works. - - wonder if any of it'll work on OS X? + - none of these tests have been run successfully on windows yet but + there's been manual testing that verified it works. + - wonder if any of it'll work on OS X? """ from __future__ import annotations @@ -1156,6 +1156,54 @@ def test_ctxep_pauses_n_maybe_ipc_breaks( ) +def test_crash_handling_within_cancelled_root_actor( + spawn: PexpectSpawner, +): + ''' + Ensure that when only a root-actor is started via `open_root_actor()` + we can crash-handle in debug-mode despite self-cancellation. + + More-or-less ensures we conditionally shield the pause in + `._root.open_root_actor()`'s `await debug._maybe_enter_pm()` + call. + + ''' + child = spawn('root_self_cancelled_w_error') + child.expect(PROMPT) + + assert_before( + child, + [ + "Actor.cancel_soon()` was called!", + "root cancelled", + _pause_msg, + "('root'", # actor name + ] + ) + + child.sendline('c') + child.expect(PROMPT) + assert_before( + child, + [ + _crash_msg, + "('root'", # actor name + "AssertionError", + "assert 0", + ] + ) + + child.sendline('c') + child.expect(EOF) + assert_before( + child, + [ + "AssertionError", + "assert 0", + ] + ) + + # TODO: better error for "non-ideal" usage from the root actor. # -[ ] if called from an async scope emit a message that suggests # using `await tractor.pause()` instead since it's less overhead