From 9ff448faa31dced65cdc5f429cb13dc1308c3ed5 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Jul 2025 17:55:18 -0400 Subject: [PATCH] Add `open_crash_handler()` / `repl_fixture` suite Nicely nailing 2 birds by leveraging the new `repl_fixture` support to actually avoid use of a `pexpect`-style test B) Functionality audit summary, - ensures `open_crash_handler() as bxerr:` adheres to, - `raise_on_exit` semantics including only raising from a list of exc-types, - `repl_fixture` setup/teardown invocation and that `yield False` blocks REPL interaction, - delivering a `BoxedMaybeException` with the correct state set post crash. - all the above outside the actor-runtime existing. Also luckily enough, this seems to have found a bug for which a fix is coming right up! --- tests/devx/test_tooling.py | 119 +++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/devx/test_tooling.py b/tests/devx/test_tooling.py index c1bb8692..c8cf4c8d 100644 --- a/tests/devx/test_tooling.py +++ b/tests/devx/test_tooling.py @@ -14,6 +14,9 @@ TODO: ''' from __future__ import annotations +from contextlib import ( + contextmanager as cm, +) import os import signal import time @@ -28,6 +31,8 @@ from .conftest import ( PROMPT, _pause_msg, ) + +import pytest from pexpect.exceptions import ( # TIMEOUT, EOF, @@ -183,3 +188,117 @@ def test_breakpoint_hook_restored( ) child.sendline('c') child.expect(EOF) + + + +_to_raise = Exception('Triggering a crash') + + +@pytest.mark.parametrize( + 'to_raise', + [ + None, + _to_raise, + RuntimeError('Never crash handle this!'), + ], +) +@pytest.mark.parametrize( + 'raise_on_exit', + [ + True, + [type(_to_raise)], + False, + ] +) +def test_crash_handler_cms( + debug_mode: bool, + to_raise: Exception, + raise_on_exit: bool|list[Exception], +): + ''' + Verify the `.devx.open_crash_handler()` API(s) by also + (conveniently enough) tesing its `repl_fixture: ContextManager` + param support which for this suite allows use to avoid use of + a `pexpect`-style-test since we use the fixture to avoid actually + entering `PdbpREPL.iteract()` :smirk: + + ''' + import tractor + # import trio + + # state flags + repl_acquired: bool = False + repl_released: bool = False + + @cm + def block_repl_ux( + repl: tractor.devx.debug.PdbREPL, + maybe_bxerr: ( + tractor.devx._debug.BoxedMaybeException + |None + ) = None, + enter_repl: bool = True, + + ) -> bool: + ''' + Set pre/post-REPL state vars and bypass actual conole + interaction. + + ''' + nonlocal repl_acquired, repl_released + + # task: trio.Task = trio.lowlevel.current_task() + # print(f'pre-REPL active_task={task.name}') + + print('pre-REPL') + repl_acquired = True + yield False # never actually .interact() + print('post-REPL') + repl_released = True + + try: + # TODO, with runtime's `debug_mode` setting + # -[ ] need to open runtime tho obvi.. + # + # with tractor.devx.maybe_open_crash_handler( + # pdb=True, + + with tractor.devx.open_crash_handler( + raise_on_exit=raise_on_exit, + repl_fixture=block_repl_ux + ) as bxerr: + if to_raise is not None: + raise to_raise + + except Exception as _exc: + exc = _exc + if ( + raise_on_exit is True + or + type(to_raise) in raise_on_exit + ): + assert ( + exc + is + to_raise + is + bxerr.value + ) + + else: + raise + else: + assert ( + to_raise is None + or + not raise_on_exit + or + type(to_raise) not in raise_on_exit + ) + assert bxerr.value is to_raise + + assert bxerr.raise_on_exit == raise_on_exit + + if to_raise is not None: + assert repl_acquired + assert repl_released