Make ctx tests support `debug_mode: bool` fixture

Such that with `--tpdb` passed (sub)actors will engage the `pdbp` REPL
automatically and so that we can use the new `stackscope` support when
complex cases hang Bo

Also,
- simplified some type-annots (ns paths),
- doc-ed an inter-peer test func with some ascii msg flows,
- added a bottom #TODO for replicating the scenario i hit in `modden`
  where a separate client actor-tree was hanging on cancelling a `bigd`
  sub-workspace..
modden_spawn_from_client_req
Tyler Goodlet 2024-02-20 15:14:58 -05:00
parent 1d7cf7d1dd
commit 6c9bc627d8
2 changed files with 150 additions and 67 deletions

View File

@ -8,7 +8,9 @@ sync-opening a ``tractor.Context`` beforehand.
# from contextlib import asynccontextmanager as acm
from itertools import count
import platform
from typing import Optional
from typing import (
Callable,
)
import pytest
import trio
@ -69,7 +71,7 @@ _state: bool = False
@tractor.context
async def too_many_starteds(
ctx: tractor.Context,
ctx: Context,
) -> None:
'''
Call ``Context.started()`` more then once (an error).
@ -84,7 +86,7 @@ async def too_many_starteds(
@tractor.context
async def not_started_but_stream_opened(
ctx: tractor.Context,
ctx: Context,
) -> None:
'''
Enter ``Context.open_stream()`` without calling ``.started()``.
@ -105,11 +107,15 @@ async def not_started_but_stream_opened(
],
ids='misuse_type={}'.format,
)
def test_started_misuse(target):
def test_started_misuse(
target: Callable,
debug_mode: bool,
):
async def main():
async with tractor.open_nursery() as n:
portal = await n.start_actor(
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
target.__name__,
enable_modules=[__name__],
)
@ -124,7 +130,7 @@ def test_started_misuse(target):
@tractor.context
async def simple_setup_teardown(
ctx: tractor.Context,
ctx: Context,
data: int,
block_forever: bool = False,
@ -170,6 +176,7 @@ def test_simple_context(
error_parent,
callee_blocks_forever,
pointlessly_open_stream,
debug_mode: bool,
):
timeout = 1.5 if not platform.system() == 'Windows' else 4
@ -177,9 +184,10 @@ def test_simple_context(
async def main():
with trio.fail_after(timeout):
async with tractor.open_nursery() as nursery:
portal = await nursery.start_actor(
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
'simple_context',
enable_modules=[__name__],
)
@ -260,6 +268,7 @@ def test_caller_cancels(
cancel_method: str,
chk_ctx_result_before_exit: bool,
callee_returns_early: bool,
debug_mode: bool,
):
'''
Verify that when the opening side of a context (aka the caller)
@ -268,7 +277,7 @@ def test_caller_cancels(
'''
async def check_canceller(
ctx: tractor.Context,
ctx: Context,
) -> None:
# should not raise yet return the remote
# context cancelled error.
@ -287,8 +296,10 @@ def test_caller_cancels(
)
async def main():
async with tractor.open_nursery() as nursery:
portal = await nursery.start_actor(
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
'simple_context',
enable_modules=[__name__],
)
@ -338,7 +349,7 @@ def test_caller_cancels(
@tractor.context
async def close_ctx_immediately(
ctx: tractor.Context,
ctx: Context,
) -> None:
@ -350,17 +361,33 @@ async def close_ctx_immediately(
@tractor_test
async def test_callee_closes_ctx_after_stream_open():
'callee context closes without using stream'
async def test_callee_closes_ctx_after_stream_open(
debug_mode: bool,
):
'''
callee context closes without using stream.
async with tractor.open_nursery() as n:
This should result in a msg sequence
|_<root>_
|_<fast_stream_closer>
portal = await n.start_actor(
<= {'started': <Any>, 'cid': <str>}
<= {'stop': True, 'cid': <str>}
<= {'result': Any, ..}
(ignored by child)
=> {'stop': True, 'cid': <str>}
'''
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
'fast_stream_closer',
enable_modules=[__name__],
)
with trio.fail_after(2):
with trio.fail_after(0.5):
async with portal.open_context(
close_ctx_immediately,
@ -368,10 +395,9 @@ async def test_callee_closes_ctx_after_stream_open():
# cancel_on_exit=True,
) as (ctx, sent):
assert sent is None
with trio.fail_after(0.5):
with trio.fail_after(0.4):
async with ctx.open_stream() as stream:
# should fall through since ``StopAsyncIteration``
@ -379,12 +405,15 @@ async def test_callee_closes_ctx_after_stream_open():
# a ``trio.EndOfChannel`` by
# ``trio.abc.ReceiveChannel.__anext__()``
async for _ in stream:
# trigger failure if we DO NOT
# get an EOC!
assert 0
else:
# verify stream is now closed
try:
await stream.receive()
with trio.fail_after(0.3):
await stream.receive()
except trio.EndOfChannel:
pass
@ -405,7 +434,7 @@ async def test_callee_closes_ctx_after_stream_open():
@tractor.context
async def expect_cancelled(
ctx: tractor.Context,
ctx: Context,
) -> None:
global _state
@ -434,11 +463,15 @@ async def expect_cancelled(
@tractor_test
async def test_caller_closes_ctx_after_callee_opens_stream(
use_ctx_cancel_method: bool,
debug_mode: bool,
):
'caller context closes without using stream'
async with tractor.open_nursery() as an:
'''
caller context closes without using/opening stream
'''
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
root: Actor = current_actor()
portal = await an.start_actor(
@ -522,11 +555,13 @@ async def test_caller_closes_ctx_after_callee_opens_stream(
@tractor_test
async def test_multitask_caller_cancels_from_nonroot_task():
async with tractor.open_nursery() as n:
portal = await n.start_actor(
async def test_multitask_caller_cancels_from_nonroot_task(
debug_mode: bool,
):
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
'ctx_cancelled',
enable_modules=[__name__],
)
@ -573,7 +608,7 @@ async def test_multitask_caller_cancels_from_nonroot_task():
@tractor.context
async def cancel_self(
ctx: tractor.Context,
ctx: Context,
) -> None:
global _state
@ -610,16 +645,20 @@ async def cancel_self(
raise RuntimeError('Context didnt cancel itself?!')
@tractor_test
async def test_callee_cancels_before_started():
async def test_callee_cancels_before_started(
debug_mode: bool,
):
'''
Callee calls `Context.cancel()` while streaming and caller
sees stream terminated in `ContextCancelled`.
'''
async with tractor.open_nursery() as n:
portal = await n.start_actor(
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
'cancels_self',
enable_modules=[__name__],
)
@ -645,7 +684,7 @@ async def test_callee_cancels_before_started():
@tractor.context
async def never_open_stream(
ctx: tractor.Context,
ctx: Context,
) -> None:
'''
@ -659,8 +698,8 @@ async def never_open_stream(
@tractor.context
async def keep_sending_from_callee(
ctx: tractor.Context,
msg_buffer_size: Optional[int] = None,
ctx: Context,
msg_buffer_size: int|None = None,
) -> None:
'''
@ -685,7 +724,10 @@ async def keep_sending_from_callee(
],
ids='overrun_condition={}'.format,
)
def test_one_end_stream_not_opened(overrun_by):
def test_one_end_stream_not_opened(
overrun_by: tuple[str, int, Callable],
debug_mode: bool,
):
'''
This should exemplify the bug from:
https://github.com/goodboy/tractor/issues/265
@ -696,8 +738,10 @@ def test_one_end_stream_not_opened(overrun_by):
buf_size = buf_size_increase + Actor.msg_buffer_size
async def main():
async with tractor.open_nursery() as n:
portal = await n.start_actor(
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
entrypoint.__name__,
enable_modules=[__name__],
)
@ -754,7 +798,7 @@ def test_one_end_stream_not_opened(overrun_by):
@tractor.context
async def echo_back_sequence(
ctx: tractor.Context,
ctx: Context,
seq: list[int],
wait_for_cancel: bool,
allow_overruns_side: str,
@ -837,6 +881,7 @@ def test_maybe_allow_overruns_stream(
slow_side: str,
allow_overruns_side: str,
loglevel: str,
debug_mode: bool,
):
'''
Demonstrate small overruns of each task back and forth
@ -855,13 +900,14 @@ def test_maybe_allow_overruns_stream(
'''
async def main():
async with tractor.open_nursery() as n:
portal = await n.start_actor(
async with tractor.open_nursery(
debug_mode=debug_mode,
) as an:
portal = await an.start_actor(
'callee_sends_forever',
enable_modules=[__name__],
loglevel=loglevel,
# debug_mode=True,
debug_mode=debug_mode,
)
seq = list(range(10))
async with portal.open_context(

View File

@ -123,7 +123,9 @@ async def error_before_started(
await peer_ctx.cancel()
def test_do_not_swallow_error_before_started_by_remote_contextcancelled():
def test_do_not_swallow_error_before_started_by_remote_contextcancelled(
debug_mode: bool,
):
'''
Verify that an error raised in a remote context which itself
opens YET ANOTHER remote context, which it then cancels, does not
@ -132,7 +134,9 @@ def test_do_not_swallow_error_before_started_by_remote_contextcancelled():
'''
async def main():
async with tractor.open_nursery() as n:
async with tractor.open_nursery(
debug_mode=debug_mode,
) as n:
portal = await n.start_actor(
'errorer',
enable_modules=[__name__],
@ -225,13 +229,16 @@ async def stream_from_peer(
# NOTE: cancellation of the (sleeper) peer should always
# cause a `ContextCancelled` raise in this streaming
# actor.
except ContextCancelled as ctxerr:
err = ctxerr
except ContextCancelled as ctxc:
ctxerr = ctxc
assert peer_ctx._remote_error is ctxerr
assert peer_ctx._remote_error.msgdata == ctxerr.msgdata
assert peer_ctx.canceller == ctxerr.canceller
# caller peer should not be the cancel requester
assert not ctx.cancel_called
# XXX can never be true since `._invoke` only
# sets this AFTER the nursery block this task
# was started in, exits.
@ -269,9 +276,7 @@ async def stream_from_peer(
# assert ctx.canceller[0] == 'root'
# assert peer_ctx.canceller[0] == 'sleeper'
raise RuntimeError(
'peer never triggered local `ContextCancelled`?'
)
raise RuntimeError('Never triggered local `ContextCancelled` ?!?')
@pytest.mark.parametrize(
@ -280,6 +285,7 @@ async def stream_from_peer(
)
def test_peer_canceller(
error_during_ctxerr_handling: bool,
debug_mode: bool,
):
'''
Verify that a cancellation triggered by an in-actor-tree peer
@ -336,7 +342,7 @@ def test_peer_canceller(
async def main():
async with tractor.open_nursery(
# NOTE: to halt the peer tasks on ctxc, uncomment this.
# debug_mode=True
debug_mode=debug_mode,
) as an:
canceller: Portal = await an.start_actor(
'canceller',
@ -377,7 +383,8 @@ def test_peer_canceller(
try:
print('PRE CONTEXT RESULT')
await sleeper_ctx.result()
res = await sleeper_ctx.result()
assert res
# should never get here
pytest.fail(
@ -387,7 +394,10 @@ def test_peer_canceller(
# should always raise since this root task does
# not request the sleeper cancellation ;)
except ContextCancelled as ctxerr:
print(f'CAUGHT REMOTE CONTEXT CANCEL {ctxerr}')
print(
'CAUGHT REMOTE CONTEXT CANCEL FOM\n'
f'{ctxerr}'
)
# canceller and caller peers should not
# have been remotely cancelled.
@ -410,16 +420,31 @@ def test_peer_canceller(
# XXX SHOULD NEVER EVER GET HERE XXX
except BaseException as berr:
err = berr
pytest.fail('did not rx ctx-cancelled error?')
raise
# XXX if needed to debug failure
# _err = berr
# await tractor.pause()
# await trio.sleep_forever()
pytest.fail(
'did not rx ctxc ?!?\n\n'
f'{berr}\n'
)
else:
pytest.fail('did not rx ctx-cancelled error?')
pytest.fail(
'did not rx ctxc ?!?\n\n'
f'{ctxs}\n'
)
except (
ContextCancelled,
RuntimeError,
)as ctxerr:
_err = ctxerr
)as loc_err:
_loc_err = loc_err
# NOTE: the main state to check on `Context` is:
# - `.cancelled_caught` (maps to nursery cs)
@ -436,7 +461,7 @@ def test_peer_canceller(
# `ContextCancelled` inside `.open_context()`
# block
if error_during_ctxerr_handling:
assert isinstance(ctxerr, RuntimeError)
assert isinstance(loc_err, RuntimeError)
# NOTE: this root actor task should have
# called `Context.cancel()` on the
@ -472,9 +497,10 @@ def test_peer_canceller(
# CASE: standard teardown inside in `.open_context()` block
else:
assert ctxerr.canceller == sleeper_ctx.canceller
assert isinstance(loc_err, ContextCancelled)
assert loc_err.canceller == sleeper_ctx.canceller
assert (
ctxerr.canceller[0]
loc_err.canceller[0]
==
sleeper_ctx.canceller[0]
==
@ -484,7 +510,7 @@ def test_peer_canceller(
# the sleeper's remote error is the error bubbled
# out of the context-stack above!
re = sleeper_ctx._remote_error
assert re is ctxerr
assert re is loc_err
for ctx in ctxs:
re: BaseException | None = ctx._remote_error
@ -554,3 +580,14 @@ def test_peer_canceller(
assert excinfo.value.type == ContextCancelled
assert excinfo.value.canceller[0] == 'canceller'
def test_client_tree_spawns_and_cancels_service_subactor():
...
# TODO: test for the modden `mod wks open piker` bug!
# -> start actor-tree (server) that offers sub-actor spawns via
# context API
# -> start another full actor-tree (client) which requests to the first to
# spawn over its `@context` ep / api.
# -> client actor cancels the context and should exit gracefully
# and the server's spawned child should cancel and terminate!