diff --git a/examples/debugging/asyncio_bp.py b/examples/debugging/asyncio_bp.py index 50a0eea..b55b28f 100644 --- a/examples/debugging/asyncio_bp.py +++ b/examples/debugging/asyncio_bp.py @@ -1,3 +1,8 @@ +''' +Examples of using the builtin `breakpoint()` from an `asyncio.Task` +running in a subactor spawned with `infect_asyncio=True`. + +''' import asyncio import trio @@ -26,15 +31,16 @@ async def bp_then_error( # NOTE: what happens here inside the hook needs some refinement.. # => seems like it's still `._debug._set_trace()` but # we set `Lock.local_task_in_debug = 'sync'`, we probably want - # some further, at least, meta-data about the task/actoq in debug - # in terms of making it clear it's asyncio mucking about. + # some further, at least, meta-data about the task/actor in debug + # in terms of making it clear it's `asyncio` mucking about. breakpoint() + # short checkpoint / delay - await asyncio.sleep(0.5) + await asyncio.sleep(0.5) # asyncio-side if raise_after_bp: - raise ValueError('blah') + raise ValueError('asyncio side error!') # TODO: test case with this so that it gets cancelled? else: @@ -46,7 +52,7 @@ async def bp_then_error( @tractor.context async def trio_ctx( ctx: tractor.Context, - bp_before_started: bool = True, + bp_before_started: bool = False, ): # this will block until the ``asyncio`` task sends a "first" @@ -55,7 +61,7 @@ async def trio_ctx( to_asyncio.open_channel_from( bp_then_error, - raise_after_bp=not bp_before_started, + # raise_after_bp=not bp_before_started, ) as (first, chan), trio.open_nursery() as tn, @@ -63,9 +69,9 @@ async def trio_ctx( assert first == 'start' if bp_before_started: - await tractor.breakpoint() + await tractor.pause() - await ctx.started(first) + await ctx.started(first) # trio-side tn.start_soon( to_asyncio.run_task, @@ -77,6 +83,10 @@ async def trio_ctx( async def main( bps_all_over: bool = True, + # TODO, WHICH OF THESE HAZ BUGZ? + cancel_from_root: bool = False, + err_from_root: bool = False, + ) -> None: async with tractor.open_nursery( @@ -99,12 +109,18 @@ async def main( assert first == 'start' - if bps_all_over: - await tractor.breakpoint() + # pause in parent to ensure no cross-actor + # locking problems exist! + await tractor.pause() + + if cancel_from_root: + await ctx.cancel() + + if err_from_root: + assert 0 + else: + await trio.sleep_forever() - # await trio.sleep_forever() - await ctx.cancel() - assert 0 # TODO: case where we cancel from trio-side while asyncio task # has debugger lock? diff --git a/tests/devx/conftest.py b/tests/devx/conftest.py index 28a14cb..e1ad2ea 100644 --- a/tests/devx/conftest.py +++ b/tests/devx/conftest.py @@ -2,6 +2,7 @@ `tractor.devx.*` tooling sub-pkg test space. ''' +import time from typing import ( Callable, ) @@ -11,9 +12,19 @@ from pexpect.exceptions import ( TIMEOUT, ) from pexpect.spawnbase import SpawnBase + from tractor._testing import ( mk_cmd, ) +from tractor.devx._debug import ( + _pause_msg as _pause_msg, + _crash_msg as _crash_msg, + _repl_fail_msg as _repl_fail_msg, + _ctlc_ignore_header as _ctlc_ignore_header, +) +from conftest import ( + _ci_env, +) @pytest.fixture @@ -107,6 +118,9 @@ def expect( raise +PROMPT = r"\(Pdb\+\)" + + def in_prompt_msg( child: SpawnBase, parts: list[str], @@ -166,3 +180,40 @@ def assert_before( err_on_false=True, **kwargs ) + + +def do_ctlc( + child, + count: int = 3, + delay: float = 0.1, + patt: str|None = None, + + # expect repl UX to reprint the prompt after every + # ctrl-c send. + # XXX: no idea but, in CI this never seems to work even on 3.10 so + # needs some further investigation potentially... + expect_prompt: bool = not _ci_env, + +) -> str|None: + + before: str|None = None + + # make sure ctl-c sends don't do anything but repeat output + for _ in range(count): + time.sleep(delay) + child.sendcontrol('c') + + # TODO: figure out why this makes CI fail.. + # if you run this test manually it works just fine.. + if expect_prompt: + time.sleep(delay) + child.expect(PROMPT) + before = str(child.before.decode()) + time.sleep(delay) + + if patt: + # should see the last line on console + assert patt in before + + # return the console content up to the final prompt + return before diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index 3eeda65..8b723c6 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -21,14 +21,13 @@ from pexpect.exceptions import ( EOF, ) -from tractor.devx._debug import ( +from .conftest import ( + do_ctlc, + PROMPT, _pause_msg, _crash_msg, _repl_fail_msg, ) -from conftest import ( - _ci_env, -) from .conftest import ( expect, in_prompt_msg, @@ -70,9 +69,6 @@ has_nested_actors = pytest.mark.has_nested_actors # ) -PROMPT = r"\(Pdb\+\)" - - @pytest.mark.parametrize( 'user_in_out', [ @@ -123,8 +119,10 @@ def test_root_actor_error( ids=lambda item: f'{item[0]} -> {item[1]}', ) def test_root_actor_bp(spawn, user_in_out): - """Demonstrate breakpoint from in root actor. - """ + ''' + Demonstrate breakpoint from in root actor. + + ''' user_input, expect_err_str = user_in_out child = spawn('root_actor_breakpoint') @@ -146,43 +144,6 @@ def test_root_actor_bp(spawn, user_in_out): assert expect_err_str in str(child.before) -def do_ctlc( - child, - count: int = 3, - delay: float = 0.1, - patt: str|None = None, - - # expect repl UX to reprint the prompt after every - # ctrl-c send. - # XXX: no idea but, in CI this never seems to work even on 3.10 so - # needs some further investigation potentially... - expect_prompt: bool = not _ci_env, - -) -> str|None: - - before: str|None = None - - # make sure ctl-c sends don't do anything but repeat output - for _ in range(count): - time.sleep(delay) - child.sendcontrol('c') - - # TODO: figure out why this makes CI fail.. - # if you run this test manually it works just fine.. - if expect_prompt: - time.sleep(delay) - child.expect(PROMPT) - before = str(child.before.decode()) - time.sleep(delay) - - if patt: - # should see the last line on console - assert patt in before - - # return the console content up to the final prompt - return before - - def test_root_actor_bp_forever( spawn, ctlc: bool, @@ -919,138 +880,6 @@ def test_different_debug_mode_per_actor( ) -def test_pause_from_sync( - spawn, - ctlc: bool -): - ''' - Verify we can use the `pdbp` REPL from sync functions AND from - any thread spawned with `trio.to_thread.run_sync()`. - - `examples/debugging/sync_bp.py` - - ''' - child = spawn('sync_bp') - - # first `sync_pause()` after nurseries open - child.expect(PROMPT) - assert_before( - child, - [ - # pre-prompt line - _pause_msg, - " similar to the `delay` input to `do_ctlc()` below, setting - # this too low can cause the test to fail since the `subactor` - # suffers a race where the root/parent sends an actor-cancel - # prior to the context task hitting its pause point (and thus - # engaging the `sigint_shield()` handler in time); this value - # seems be good enuf? - time.sleep(0.6) - - # one of the bg thread or subactor should have - # `Lock.acquire()`-ed - # (NOT both, which will result in REPL clobbering!) - attach_patts: dict[str, list[str]] = { - 'subactor': [ - "'start_n_sync_pause'", - "('subactor'", - ], - 'inline_root_bg_thread': [ - " similar to the `delay` input to `do_ctlc()` below, setting + # this too low can cause the test to fail since the `subactor` + # suffers a race where the root/parent sends an actor-cancel + # prior to the context task hitting its pause point (and thus + # engaging the `sigint_shield()` handler in time); this value + # seems be good enuf? + time.sleep(0.6) + + # one of the bg thread or subactor should have + # `Lock.acquire()`-ed + # (NOT both, which will result in REPL clobbering!) + attach_patts: dict[str, list[str]] = { + 'subactor': [ + "'start_n_sync_pause'", + "('subactor'", + ], + 'inline_root_bg_thread': [ + " list[str]: + ''' + Receive any of a `list[str]` of patterns provided in + `attach_patts`. + + Used to test racing prompts from multiple actors and/or + tasks using a common root process' `pdbp` REPL. + + ''' + assert attach_patts + + child.expect(PROMPT) + before = str(child.before.decode()) + + for attach_key in attach_patts: + if attach_key in before: + expected_patts: str = attach_patts.pop(attach_key) + assert_before( + child, + expected_patts + ) + break # from for + else: + pytest.fail( + f'No keys found?\n\n' + f'{attach_patts.keys()}\n\n' + f'{before}\n' + ) + + # ensure no other task/threads engaged a REPL + # at the same time as the one that was detected above. + for key, other_patts in attach_patts.copy().items(): + assert not in_prompt_msg( + child, + other_patts, + ) + + if ctlc: + do_ctlc( + child, + patt=prompt, + # NOTE same as comment above + delay=ctlc_delay, + ) + + return expected_patts + # yield child + + +def test_pause_from_asyncio_task( + spawn, + ctlc: bool + # ^TODO, fix for `asyncio`!! +): + ''' + Verify we can use the `pdbp` REPL from an `asyncio.Task` spawned using + APIs in `.to_asyncio`. + + `examples/debugging/asycio_bp.py` + + ''' + child = spawn('asyncio_bp') + + # RACE on whether trio/asyncio task bps first + attach_patts: dict[str, list[str]] = { + + # first pause in guest-mode (aka "infecting") + # `trio.Task`. + 'trio-side': [ + _pause_msg, + "