forked from goodboy/tractor
				
			Wrap `asyncio_bp.py` ex into test suite
Ensuring we can at least use `breakpoint()` from an infected actor's `asyncio.Task` spawned via a `.to_asyncio` API. Also includes a little `tests/devx/` reorging, - start splitting out non-`tractor.pause()` tests into a new `test_pause_from_non_trio.py` for all the `.pause_from_sync()` use in bg-threaded or `asyncio` applications. - factor harness commonalities to the `devx/conftest` (namely the `do_ctlc()` masher). - mv `test_pause_from_sync` to the new non`-trio` mod. NOTE, the `ctlc=True` is still failing for `test_pause_from_asyncio_task` which is a user-happiness bug but not anything fundamentally broken - just need to handle the `asyncio` case in `.devx._debug.sigint_shield()`!remotes/1757153874605917753/main
							parent
							
								
									ccd60b0c6e
								
							
						
					
					
						commit
						e1d96099fc
					
				|  | @ -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? | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -21,7 +21,9 @@ from pexpect.exceptions import ( | |||
|     EOF, | ||||
| ) | ||||
| 
 | ||||
| from tractor.devx._debug import ( | ||||
| from .conftest import ( | ||||
|     do_ctlc, | ||||
|     PROMPT, | ||||
|     _pause_msg, | ||||
|     _crash_msg, | ||||
|     _repl_fail_msg, | ||||
|  | @ -68,9 +70,6 @@ has_nested_actors = pytest.mark.has_nested_actors | |||
| # ) | ||||
| 
 | ||||
| 
 | ||||
| PROMPT = r"\(Pdb\+\)" | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     'user_in_out', | ||||
|     [ | ||||
|  | @ -121,8 +120,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') | ||||
| 
 | ||||
|  | @ -144,43 +145,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, | ||||
|  | @ -917,138 +881,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, | ||||
|             "<Task '__main__.main'", | ||||
|             "('root'", | ||||
|         ] | ||||
|     ) | ||||
|     if ctlc: | ||||
|         do_ctlc(child) | ||||
|         # ^NOTE^ subactor not spawned yet; don't need extra delay. | ||||
| 
 | ||||
|     child.sendline('c') | ||||
| 
 | ||||
|     # first `await tractor.pause()` inside `p.open_context()` body | ||||
|     child.expect(PROMPT) | ||||
| 
 | ||||
|     # XXX shouldn't see gb loaded message with PDB loglevel! | ||||
|     assert not in_prompt_msg( | ||||
|         child, | ||||
|         ['`greenback` portal opened!'], | ||||
|     ) | ||||
|     # should be same root task | ||||
|     assert_before( | ||||
|         child, | ||||
|         [ | ||||
|             _pause_msg, | ||||
|             "<Task '__main__.main'", | ||||
|             "('root'", | ||||
|         ] | ||||
|     ) | ||||
| 
 | ||||
|     if ctlc: | ||||
|         do_ctlc( | ||||
|             child, | ||||
|             # NOTE: setting this to 0 (or some other sufficient | ||||
|             # small val) can cause the test to fail since the | ||||
|             # `subactor` suffers a race where the root/parent | ||||
|             # sends an actor-cancel prior to it hitting its pause | ||||
|             # point; by def the value is 0.1 | ||||
|             delay=0.4, | ||||
|         ) | ||||
| 
 | ||||
|     # XXX, fwiw without a brief sleep here the SIGINT might actually | ||||
|     # trigger "subactor" cancellation by its parent  before the | ||||
|     # shield-handler is engaged. | ||||
|     # | ||||
|     # => 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': [ | ||||
|             "<Thread(inline_root_bg_thread", | ||||
|             "('root'", | ||||
|         ], | ||||
|         'start_soon_root_bg_thread': [ | ||||
|             "<Thread(start_soon_root_bg_thread", | ||||
|             "('root'", | ||||
|         ], | ||||
|     } | ||||
|     conts: int = 0  # for debugging below matching logic on failure | ||||
|     while attach_patts: | ||||
|         child.sendline('c') | ||||
|         conts += 1 | ||||
|         child.expect(PROMPT) | ||||
|         before = str(child.before.decode()) | ||||
|         for key in attach_patts: | ||||
|             if key in before: | ||||
|                 attach_key: str = key | ||||
|                 expected_patts: str = attach_patts.pop(key) | ||||
|                 assert_before( | ||||
|                     child, | ||||
|                     [_pause_msg] | ||||
|                     + | ||||
|                     expected_patts | ||||
|                 ) | ||||
|                 break | ||||
|         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=attach_key, | ||||
|                 # NOTE same as comment above | ||||
|                 delay=0.4, | ||||
|             ) | ||||
| 
 | ||||
|     child.sendline('c') | ||||
|     child.expect(EOF) | ||||
| 
 | ||||
| 
 | ||||
| def test_post_mortem_api( | ||||
|     spawn, | ||||
|     ctlc: bool, | ||||
|  | @ -1229,53 +1061,6 @@ 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 | ||||
|  | @ -1293,7 +1078,6 @@ 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(): | ||||
|     ''' | ||||
|  | @ -1310,9 +1094,8 @@ 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. | ||||
| 
 | ||||
|     ''' | ||||
|     ... | ||||
|  |  | |||
|  | @ -0,0 +1,329 @@ | |||
| ''' | ||||
| That "foreign loop/thread" debug REPL support better ALSO WORK! | ||||
| 
 | ||||
| Same as `test_native_pause.py`. | ||||
| All these tests can be understood (somewhat) by running the | ||||
| equivalent `examples/debugging/` scripts manually. | ||||
| 
 | ||||
| ''' | ||||
| # from functools import partial | ||||
| # import itertools | ||||
| import time | ||||
| # from typing import ( | ||||
| #     Iterator, | ||||
| # ) | ||||
| 
 | ||||
| import pytest | ||||
| from pexpect.exceptions import ( | ||||
|     # TIMEOUT, | ||||
|     EOF, | ||||
| ) | ||||
| 
 | ||||
| from .conftest import ( | ||||
|     # _ci_env, | ||||
|     do_ctlc, | ||||
|     PROMPT, | ||||
|     # expect, | ||||
|     in_prompt_msg, | ||||
|     assert_before, | ||||
|     _pause_msg, | ||||
|     _crash_msg, | ||||
|     _ctlc_ignore_header, | ||||
|     # _repl_fail_msg, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| 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, | ||||
|             "<Task '__main__.main'", | ||||
|             "('root'", | ||||
|         ] | ||||
|     ) | ||||
|     if ctlc: | ||||
|         do_ctlc(child) | ||||
|         # ^NOTE^ subactor not spawned yet; don't need extra delay. | ||||
| 
 | ||||
|     child.sendline('c') | ||||
| 
 | ||||
|     # first `await tractor.pause()` inside `p.open_context()` body | ||||
|     child.expect(PROMPT) | ||||
| 
 | ||||
|     # XXX shouldn't see gb loaded message with PDB loglevel! | ||||
|     assert not in_prompt_msg( | ||||
|         child, | ||||
|         ['`greenback` portal opened!'], | ||||
|     ) | ||||
|     # should be same root task | ||||
|     assert_before( | ||||
|         child, | ||||
|         [ | ||||
|             _pause_msg, | ||||
|             "<Task '__main__.main'", | ||||
|             "('root'", | ||||
|         ] | ||||
|     ) | ||||
| 
 | ||||
|     if ctlc: | ||||
|         do_ctlc( | ||||
|             child, | ||||
|             # NOTE: setting this to 0 (or some other sufficient | ||||
|             # small val) can cause the test to fail since the | ||||
|             # `subactor` suffers a race where the root/parent | ||||
|             # sends an actor-cancel prior to it hitting its pause | ||||
|             # point; by def the value is 0.1 | ||||
|             delay=0.4, | ||||
|         ) | ||||
| 
 | ||||
|     # XXX, fwiw without a brief sleep here the SIGINT might actually | ||||
|     # trigger "subactor" cancellation by its parent  before the | ||||
|     # shield-handler is engaged. | ||||
|     # | ||||
|     # => 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': [ | ||||
|             "<Thread(inline_root_bg_thread", | ||||
|             "('root'", | ||||
|         ], | ||||
|         'start_soon_root_bg_thread': [ | ||||
|             "<Thread(start_soon_root_bg_thread", | ||||
|             "('root'", | ||||
|         ], | ||||
|     } | ||||
|     conts: int = 0  # for debugging below matching logic on failure | ||||
|     while attach_patts: | ||||
|         child.sendline('c') | ||||
|         conts += 1 | ||||
|         child.expect(PROMPT) | ||||
|         before = str(child.before.decode()) | ||||
|         for key in attach_patts: | ||||
|             if key in before: | ||||
|                 attach_key: str = key | ||||
|                 expected_patts: str = attach_patts.pop(key) | ||||
|                 assert_before( | ||||
|                     child, | ||||
|                     [_pause_msg] | ||||
|                     + | ||||
|                     expected_patts | ||||
|                 ) | ||||
|                 break | ||||
|         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=attach_key, | ||||
|                 # NOTE same as comment above | ||||
|                 delay=0.4, | ||||
|             ) | ||||
| 
 | ||||
|     child.sendline('c') | ||||
|     child.expect(EOF) | ||||
| 
 | ||||
| 
 | ||||
| def expect_any_of( | ||||
|     attach_patts: dict[str, list[str]], | ||||
|     child,   # what type? | ||||
|     ctlc: bool = False, | ||||
|     prompt: str = _ctlc_ignore_header, | ||||
|     ctlc_delay: float = .4, | ||||
| 
 | ||||
| ) -> 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, | ||||
|             "<Task 'trio_ctx'", | ||||
|             "('aio_daemon'", | ||||
|         ], | ||||
| 
 | ||||
|         # `breakpoint()` from `asyncio.Task`. | ||||
|         'asyncio-side': [ | ||||
|             _pause_msg, | ||||
|             "<Task pending name='Task-2' coro=<greenback_shim()", | ||||
|             "('aio_daemon'", | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
|     while attach_patts: | ||||
|         expect_any_of( | ||||
|             attach_patts=attach_patts, | ||||
|             child=child, | ||||
|             ctlc=ctlc, | ||||
|         ) | ||||
|         child.sendline('c') | ||||
| 
 | ||||
|     # NOW in race order, | ||||
|     # - the asyncio-task will error | ||||
|     # - the root-actor parent task will pause | ||||
|     # | ||||
|     attach_patts: dict[str, list[str]] = { | ||||
| 
 | ||||
|         # error raised in `asyncio.Task` | ||||
|         "raise ValueError('asyncio side error!')": [ | ||||
|             _crash_msg, | ||||
|             'return await chan.receive()',  # `.to_asyncio` impl internals in tb | ||||
|             "<Task 'trio_ctx'", | ||||
|             "@ ('aio_daemon'", | ||||
|             "ValueError: asyncio side error!", | ||||
|         ], | ||||
| 
 | ||||
|         # parent-side propagation via actor-nursery/portal | ||||
|         # "tractor._exceptions.RemoteActorError: remote task raised a 'ValueError'": [ | ||||
|         "remote task raised a 'ValueError'": [ | ||||
|             _crash_msg, | ||||
|             "src_uid=('aio_daemon'", | ||||
|             "('aio_daemon'", | ||||
|         ], | ||||
| 
 | ||||
|         # a final pause in root-actor | ||||
|         "<Task '__main__.main'": [ | ||||
|             _pause_msg, | ||||
|             "<Task '__main__.main'", | ||||
|             "('root'", | ||||
|         ], | ||||
|     } | ||||
|     while attach_patts: | ||||
|         expect_any_of( | ||||
|             attach_patts=attach_patts, | ||||
|             child=child, | ||||
|             ctlc=ctlc, | ||||
|         ) | ||||
|         child.sendline('c') | ||||
| 
 | ||||
|     assert not attach_patts | ||||
| 
 | ||||
|     # final boxed error propagates to root | ||||
|     assert_before( | ||||
|         child, | ||||
|         [ | ||||
|             _crash_msg, | ||||
|             "<Task '__main__.main'", | ||||
|             "('root'", | ||||
|             "remote task raised a 'ValueError'", | ||||
|             "ValueError: asyncio side error!", | ||||
|         ] | ||||
|     ) | ||||
| 
 | ||||
|     if ctlc: | ||||
|         do_ctlc( | ||||
|             child, | ||||
|             # NOTE: setting this to 0 (or some other sufficient | ||||
|             # small val) can cause the test to fail since the | ||||
|             # `subactor` suffers a race where the root/parent | ||||
|             # sends an actor-cancel prior to it hitting its pause | ||||
|             # point; by def the value is 0.1 | ||||
|             delay=0.4, | ||||
|         ) | ||||
| 
 | ||||
|     child.sendline('c') | ||||
|     child.expect(EOF) | ||||
		Loading…
	
		Reference in New Issue