From ccd60b0c6eb0a6f57d3e1f8250a9cd8156d6a217 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Thu, 5 Dec 2024 20:55:12 -0500
Subject: [PATCH] Add `breakpoint()` hook restoration example + test

---
 .../debugging/restore_builtin_breakpoint.py   | 35 ++++++++++--
 tests/devx/test_debugger.py                   | 53 ++++++++++++++++++-
 2 files changed, 82 insertions(+), 6 deletions(-)

diff --git a/examples/debugging/restore_builtin_breakpoint.py b/examples/debugging/restore_builtin_breakpoint.py
index 6e141dfc..89605075 100644
--- a/examples/debugging/restore_builtin_breakpoint.py
+++ b/examples/debugging/restore_builtin_breakpoint.py
@@ -6,19 +6,46 @@ import tractor
 
 
 async def main() -> None:
-    async with tractor.open_nursery(debug_mode=True) as an:
 
-        assert os.environ['PYTHONBREAKPOINT'] == 'tractor._debug._set_trace'
+    # intially unset, no entry.
+    orig_pybp_var: int = os.environ.get('PYTHONBREAKPOINT')
+    assert orig_pybp_var in {None, "0"}
+
+    async with tractor.open_nursery(
+        debug_mode=True,
+    ) as an:
+        assert an
+        assert (
+            (pybp_var := os.environ['PYTHONBREAKPOINT'])
+            ==
+            'tractor.devx._debug._sync_pause_from_builtin'
+        )
 
         # TODO: an assert that verifies the hook has indeed been, hooked
         # XD
-        assert sys.breakpointhook is not tractor._debug._set_trace
+        assert (
+            (pybp_hook := sys.breakpointhook)
+            is not tractor.devx._debug._set_trace
+        )
 
+        print(
+            f'$PYTHONOBREAKPOINT: {pybp_var!r}\n'
+            f'`sys.breakpointhook`: {pybp_hook!r}\n'
+        )
         breakpoint()
+        pass  # first bp, tractor hook set.
 
-    # TODO: an assert that verifies the hook is unhooked..
+    # XXX AFTER EXIT (of actor-runtime) verify the hook is unset..
+    #
+    # YES, this is weird but it's how stdlib docs say to do it..
+    # https://docs.python.org/3/library/sys.html#sys.breakpointhook
+    assert os.environ.get('PYTHONBREAKPOINT') is orig_pybp_var
     assert sys.breakpointhook
+
+    # now ensure a regular builtin pause still works
     breakpoint()
+    pass  # last bp, stdlib hook restored
+
 
 if __name__ == '__main__':
     trio.run(main)
diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py
index 2a24bf98..5327fb0b 100644
--- a/tests/devx/test_debugger.py
+++ b/tests/devx/test_debugger.py
@@ -1229,6 +1229,53 @@ def test_shield_pause(
     child.expect(EOF)
 
 
+def test_breakpoint_hook_restored(
+    spawn,
+):
+    '''
+    Ensures our actor runtime sets a custom `breakpoint()` hook
+    on open then restores the stdlib's default on close.
+
+    The hook state validation is done via `assert`s inside the
+    invoked script with only `breakpoint()` (not `tractor.pause()`)
+    calls used.
+
+    '''
+    child = spawn('restore_builtin_breakpoint')
+
+    child.expect(PROMPT)
+    assert_before(
+        child,
+        [
+            _pause_msg,
+            "<Task '__main__.main'",
+            "('root'",
+            "first bp, tractor hook set",
+        ]
+    )
+    child.sendline('c')
+    child.expect(PROMPT)
+    assert_before(
+        child,
+        [
+            "last bp, stdlib hook restored",
+        ]
+    )
+
+    # since the stdlib hook was already restored there should be NO
+    # `tractor` `log.pdb()` content from console!
+    assert not in_prompt_msg(
+        child,
+        [
+            _pause_msg,
+            "<Task '__main__.main'",
+            "('root'",
+        ],
+    )
+    child.sendline('c')
+    child.expect(EOF)
+
+
 # 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
@@ -1246,6 +1293,7 @@ def test_sync_pause_from_bg_task_in_root_actor_():
     '''
     ...
 
+
 # TODO: needs ANSI code stripping tho, see `assert_before()` # above!
 def test_correct_frames_below_hidden():
     '''
@@ -1262,8 +1310,9 @@ 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.
+    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.
 
     '''
     ...