forked from goodboy/tractor
Update ctx test suites to stricter semantics
Including mostly tweaking asserts on relayed `ContextCancelled`s and the new pub ctx properties: `.outcome`, `.maybe_error`, etc. as it pertains to graceful (absorbed) remote cancellation vs. loud ctxc cases expected to be raised by any `Portal.cancel_actor()` style teardown. Start checking a variety internals like `._remote/local_error`, `._is_self_cancelled()`, `._is_final_result_set()`, `._cancel_msg` where applicable. Also factor out the new `expect_ctxc()` checker to our `conftest.py` for use in other suites.modden_spawn_from_client_req
parent
c36deb1f4d
commit
2e797ef7ee
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
``tractor`` testing!!
|
||||
"""
|
||||
from contextlib import asynccontextmanager as acm
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
@ -292,3 +293,26 @@ def daemon(
|
|||
time.sleep(_PROC_SPAWN_WAIT)
|
||||
yield proc
|
||||
sig_prog(proc, _INT_SIGNAL)
|
||||
|
||||
|
||||
@acm
|
||||
async def expect_ctxc(
|
||||
yay: bool,
|
||||
reraise: bool = False,
|
||||
) -> None:
|
||||
'''
|
||||
Small acm to catch `ContextCancelled` errors when expected
|
||||
below it in a `async with ()` block.
|
||||
|
||||
'''
|
||||
if yay:
|
||||
try:
|
||||
yield
|
||||
raise RuntimeError('Never raised ctxc?')
|
||||
except tractor.ContextCancelled:
|
||||
if reraise:
|
||||
raise
|
||||
else:
|
||||
return
|
||||
else:
|
||||
yield
|
||||
|
|
|
@ -5,7 +5,6 @@ Verify the we raise errors when streams are opened prior to
|
|||
sync-opening a ``tractor.Context`` beforehand.
|
||||
|
||||
'''
|
||||
from contextlib import asynccontextmanager as acm
|
||||
from itertools import count
|
||||
import platform
|
||||
from pprint import pformat
|
||||
|
@ -26,7 +25,10 @@ from tractor._exceptions import (
|
|||
ContextCancelled,
|
||||
)
|
||||
|
||||
from conftest import tractor_test
|
||||
from conftest import (
|
||||
tractor_test,
|
||||
expect_ctxc,
|
||||
)
|
||||
|
||||
# ``Context`` semantics are as follows,
|
||||
# ------------------------------------
|
||||
|
@ -194,12 +196,13 @@ def test_simple_context(
|
|||
)
|
||||
|
||||
try:
|
||||
async with portal.open_context(
|
||||
simple_setup_teardown,
|
||||
data=10,
|
||||
block_forever=callee_blocks_forever,
|
||||
) as (ctx, sent):
|
||||
|
||||
async with (
|
||||
portal.open_context(
|
||||
simple_setup_teardown,
|
||||
data=10,
|
||||
block_forever=callee_blocks_forever,
|
||||
) as (ctx, sent),
|
||||
):
|
||||
assert sent == 11
|
||||
|
||||
if callee_blocks_forever:
|
||||
|
@ -250,17 +253,6 @@ def test_simple_context(
|
|||
trio.run(main)
|
||||
|
||||
|
||||
@acm
|
||||
async def expect_ctxc(yay: bool) -> None:
|
||||
if yay:
|
||||
try:
|
||||
yield
|
||||
except ContextCancelled:
|
||||
return
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'callee_returns_early',
|
||||
[True, False],
|
||||
|
@ -293,6 +285,7 @@ def test_caller_cancels(
|
|||
) -> None:
|
||||
actor: Actor = current_actor()
|
||||
uid: tuple = actor.uid
|
||||
_ctxc: ContextCancelled|None = None
|
||||
|
||||
if (
|
||||
cancel_method == 'portal'
|
||||
|
@ -303,6 +296,9 @@ def test_caller_cancels(
|
|||
assert 0, 'Portal cancel should raise!'
|
||||
|
||||
except ContextCancelled as ctxc:
|
||||
# with trio.CancelScope(shield=True):
|
||||
# await tractor.pause()
|
||||
_ctxc = ctxc
|
||||
assert ctx.chan._cancel_called
|
||||
assert ctxc.canceller == uid
|
||||
assert ctxc is ctx.maybe_error
|
||||
|
@ -311,7 +307,10 @@ def test_caller_cancels(
|
|||
# case since self-cancellation should swallow the ctxc
|
||||
# silently!
|
||||
else:
|
||||
res = await ctx.result()
|
||||
try:
|
||||
res = await ctx.result()
|
||||
except ContextCancelled as ctxc:
|
||||
pytest.fail(f'should not have raised ctxc\n{ctxc}')
|
||||
|
||||
# we actually get a result
|
||||
if callee_returns_early:
|
||||
|
@ -342,6 +341,10 @@ def test_caller_cancels(
|
|||
# await tractor.pause()
|
||||
# assert ctx._local_error is None
|
||||
|
||||
# TODO: don't need this right?
|
||||
# if _ctxc:
|
||||
# raise _ctxc
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
|
@ -352,11 +355,19 @@ def test_caller_cancels(
|
|||
'simple_context',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
timeout = 0.5 if not callee_returns_early else 2
|
||||
timeout: float = (
|
||||
0.5
|
||||
if not callee_returns_early
|
||||
else 2
|
||||
)
|
||||
with trio.fail_after(timeout):
|
||||
async with (
|
||||
|
||||
expect_ctxc(yay=cancel_method == 'portal'),
|
||||
expect_ctxc(
|
||||
yay=(
|
||||
not callee_returns_early
|
||||
and cancel_method == 'portal'
|
||||
)
|
||||
),
|
||||
|
||||
portal.open_context(
|
||||
simple_setup_teardown,
|
||||
|
@ -372,10 +383,18 @@ def test_caller_cancels(
|
|||
await trio.sleep(0.5)
|
||||
|
||||
if cancel_method == 'ctx':
|
||||
print('cancelling with `Context.cancel()`')
|
||||
await ctx.cancel()
|
||||
else:
|
||||
|
||||
elif cancel_method == 'portal':
|
||||
print('cancelling with `Portal.cancel_actor()`')
|
||||
await portal.cancel_actor()
|
||||
|
||||
else:
|
||||
pytest.fail(
|
||||
f'Unknown `cancel_method={cancel_method} ?'
|
||||
)
|
||||
|
||||
if chk_ctx_result_before_exit:
|
||||
await check_canceller(ctx)
|
||||
|
||||
|
@ -385,15 +404,22 @@ def test_caller_cancels(
|
|||
if cancel_method != 'portal':
|
||||
await portal.cancel_actor()
|
||||
|
||||
# since the `.cancel_actor()` call just above
|
||||
# will cause the `.open_context().__aexit__()` raise
|
||||
# a ctxc which should in turn cause `ctx._scope` to
|
||||
# XXX NOTE XXX: non-normal yet purposeful
|
||||
# test-specific ctxc suppression is implemented!
|
||||
#
|
||||
# WHY: the `.cancel_actor()` case (cancel_method='portal')
|
||||
# will cause both:
|
||||
# * the `ctx.result()` inside `.open_context().__aexit__()`
|
||||
# * AND the `ctx.result()` inside `check_canceller()`
|
||||
# to raise ctxc.
|
||||
#
|
||||
# which should in turn cause `ctx._scope` to
|
||||
# catch any cancellation?
|
||||
if (
|
||||
not callee_returns_early
|
||||
and cancel_method == 'portal'
|
||||
and cancel_method != 'portal'
|
||||
):
|
||||
assert ctx._scope.cancelled_caught
|
||||
assert not ctx._scope.cancelled_caught
|
||||
|
||||
trio.run(main)
|
||||
|
||||
|
@ -511,6 +537,23 @@ async def expect_cancelled(
|
|||
await stream.send(msg) # echo server
|
||||
|
||||
except trio.Cancelled:
|
||||
|
||||
# on ctx.cancel() the internal RPC scope is cancelled but
|
||||
# never caught until the func exits.
|
||||
assert ctx._scope.cancel_called
|
||||
assert not ctx._scope.cancelled_caught
|
||||
|
||||
# should be the RPC cmd request for `._cancel_task()`
|
||||
assert ctx._cancel_msg
|
||||
# which, has not yet resolved to an error outcome
|
||||
# since this rpc func has not yet exited.
|
||||
assert not ctx.maybe_error
|
||||
assert not ctx._final_result_is_set()
|
||||
|
||||
# debug REPL if needed
|
||||
# with trio.CancelScope(shield=True):
|
||||
# await tractor.pause()
|
||||
|
||||
# expected case
|
||||
_state = False
|
||||
raise
|
||||
|
@ -594,16 +637,16 @@ async def test_caller_closes_ctx_after_callee_opens_stream(
|
|||
with trio.fail_after(0.2):
|
||||
await ctx.result()
|
||||
assert 0, "Callee should have blocked!?"
|
||||
|
||||
except trio.TooSlowError:
|
||||
# NO-OP -> since already called above
|
||||
await ctx.cancel()
|
||||
|
||||
# NOTE: local scope should have absorbed the cancellation since
|
||||
# in this case we call `ctx.cancel()` and the local
|
||||
# `._scope` gets `.cancel_called` on the ctxc ack.
|
||||
# `._scope` does not get `.cancel_called` and thus
|
||||
# `.cancelled_caught` neither will ever bet set.
|
||||
if use_ctx_cancel_method:
|
||||
assert ctx._scope.cancelled_caught
|
||||
assert not ctx._scope.cancelled_caught
|
||||
|
||||
# rxed ctxc response from far end
|
||||
assert ctx.cancel_acked
|
||||
|
|
|
@ -238,7 +238,12 @@ async def stream_from_peer(
|
|||
|
||||
assert peer_ctx._remote_error is ctxerr
|
||||
assert peer_ctx._remote_error.msgdata == ctxerr.msgdata
|
||||
assert peer_ctx.canceller == ctxerr.canceller
|
||||
|
||||
# the peer ctx is the canceller even though it's canceller
|
||||
# is the "canceller" XD
|
||||
assert peer_name in peer_ctx.canceller
|
||||
|
||||
assert "canceller" in ctxerr.canceller
|
||||
|
||||
# caller peer should not be the cancel requester
|
||||
assert not ctx.cancel_called
|
||||
|
@ -272,7 +277,6 @@ async def stream_from_peer(
|
|||
|
||||
# root/parent actor task should NEVER HAVE cancelled us!
|
||||
assert not ctx.canceller
|
||||
assert 'canceller' in peer_ctx.canceller
|
||||
|
||||
raise
|
||||
# TODO: IN THEORY we could have other cases depending on
|
||||
|
@ -527,27 +531,24 @@ def test_peer_canceller(
|
|||
|
||||
assert ctx.cancel_called
|
||||
|
||||
if (
|
||||
ctx is sleeper_ctx
|
||||
or ctx is caller_ctx
|
||||
):
|
||||
assert (
|
||||
re.canceller
|
||||
==
|
||||
ctx.canceller
|
||||
==
|
||||
canceller.channel.uid
|
||||
)
|
||||
if ctx is sleeper_ctx:
|
||||
assert 'canceller' in re.canceller
|
||||
assert 'sleeper' in ctx.canceller
|
||||
|
||||
else:
|
||||
if ctx is canceller_ctx:
|
||||
assert (
|
||||
re.canceller
|
||||
==
|
||||
ctx.canceller
|
||||
==
|
||||
root.uid
|
||||
)
|
||||
|
||||
else: # the other 2 ctxs
|
||||
assert (
|
||||
re.canceller
|
||||
==
|
||||
canceller.channel.uid
|
||||
)
|
||||
|
||||
# since the sleeper errors while handling a
|
||||
# peer-cancelled (by ctxc) scenario, we expect
|
||||
# that the `.open_context()` block DOES call
|
||||
|
@ -576,14 +577,16 @@ def test_peer_canceller(
|
|||
assert not sleeper_ctx._scope.cancelled_caught
|
||||
|
||||
assert isinstance(loc_err, ContextCancelled)
|
||||
assert loc_err.canceller == sleeper_ctx.canceller
|
||||
assert (
|
||||
loc_err.canceller[0]
|
||||
==
|
||||
sleeper_ctx.canceller[0]
|
||||
==
|
||||
'canceller'
|
||||
)
|
||||
|
||||
# the received remote error's `.canceller`
|
||||
# will of course be the "canceller" actor BUT
|
||||
# the canceller set on the local handle to
|
||||
# `sleeper_ctx` will be the "sleeper" uid
|
||||
# since it's the actor that relayed us the
|
||||
# error which was **caused** by the
|
||||
# "canceller".
|
||||
assert 'sleeper' in sleeper_ctx.canceller
|
||||
assert 'canceller' == loc_err.canceller[0]
|
||||
|
||||
# the sleeper's remote error is the error bubbled
|
||||
# out of the context-stack above!
|
||||
|
|
Loading…
Reference in New Issue