Make `@context`-cancelled tests more pedantic
In order to match a very significant and coming-soon patch set to the
IPC `Context` and `Channel` cancellation semantics with significant but
subtle changes to the primitives and runtime logic:
- a new set of `Context` state pub meth APIs for checking exact
  inter-actor-linked-task outcomes such as `.outcome`, `.maybe_error`,
  and `.cancel_acked`.
- trying to move away from `Context.cancelled_caught` usage since the
  semantics from `trio` don't really map well (in terms of cancel
  requests and how they result in cancel-scope graceful closure) and
  `.cancel_acked: bool` is a better approach for IPC req-resp msging.
  - change test usage to access `._scope.cancelled_caught` directly.
- more pedantic ctxc-raising expects around the "type of self
  cancellation" and final outcome in ctxc cases:
  - `ContextCancelled` is raised by ctx (`Context.result()`) consumer
    methods when `Portal.cancel_actor()` is called (since it's an
    out-of-band request) despite `Channel._cancel_called` being set.
  - also raised by `.open_context().__aexit__()` on close.
  - `.outcome` is always `.maybe_error` is always one of
    `._local/remote_error`.
			
			
				shielded_ctx_cancel
			
			
		
							parent
							
								
									c6ee4e5dc1
								
							
						
					
					
						commit
						d08aeaeafe
					
				|  | @ -48,11 +48,13 @@ async def do_nuthin(): | |||
|     ids=['no_args', 'unexpected_args'], | ||||
| ) | ||||
| def test_remote_error(reg_addr, args_err): | ||||
|     """Verify an error raised in a subactor that is propagated | ||||
|     ''' | ||||
|     Verify an error raised in a subactor that is propagated | ||||
|     to the parent nursery, contains the underlying boxed builtin | ||||
|     error type info and causes cancellation and reraising all the | ||||
|     way up the stack. | ||||
|     """ | ||||
| 
 | ||||
|     ''' | ||||
|     args, errtype = args_err | ||||
| 
 | ||||
|     async def main(): | ||||
|  | @ -65,7 +67,9 @@ def test_remote_error(reg_addr, args_err): | |||
|             # an exception group outside the nursery since the error | ||||
|             # here and the far end task error are one in the same? | ||||
|             portal = await nursery.run_in_actor( | ||||
|                 assert_err, name='errorer', **args | ||||
|                 assert_err, | ||||
|                 name='errorer', | ||||
|                 **args | ||||
|             ) | ||||
| 
 | ||||
|             # get result(s) from main task | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ Verify the we raise errors when streams are opened prior to | |||
| sync-opening a ``tractor.Context`` beforehand. | ||||
| 
 | ||||
| ''' | ||||
| # from contextlib import asynccontextmanager as acm | ||||
| from contextlib import asynccontextmanager as acm | ||||
| from itertools import count | ||||
| import platform | ||||
| from pprint import pformat | ||||
|  | @ -250,6 +250,17 @@ 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], | ||||
|  | @ -280,23 +291,60 @@ def test_caller_cancels( | |||
|     async def check_canceller( | ||||
|         ctx: Context, | ||||
|     ) -> None: | ||||
|         # should not raise yet return the remote | ||||
|         # context cancelled error. | ||||
|         res = await ctx.result() | ||||
|         actor: Actor = current_actor() | ||||
|         uid: tuple = actor.uid | ||||
| 
 | ||||
|         if ( | ||||
|             cancel_method == 'portal' | ||||
|             and not callee_returns_early | ||||
|         ): | ||||
|             try: | ||||
|                 res = await ctx.result() | ||||
|                 assert 0, 'Portal cancel should raise!' | ||||
| 
 | ||||
|             except ContextCancelled as ctxc: | ||||
|                 assert ctx.chan._cancel_called | ||||
|                 assert ctxc.canceller == uid | ||||
|                 assert ctxc is ctx.maybe_error | ||||
| 
 | ||||
|         # NOTE: should not ever raise even in the `ctx` | ||||
|         # case since self-cancellation should swallow the ctxc | ||||
|         # silently! | ||||
|         else: | ||||
|             res = await ctx.result() | ||||
| 
 | ||||
|         # we actually get a result | ||||
|         if callee_returns_early: | ||||
|             assert res == 'yo' | ||||
|             assert ctx.outcome is res | ||||
|             assert ctx.maybe_error is None | ||||
| 
 | ||||
|         else: | ||||
|             err = res | ||||
|             err: Exception = ctx.outcome | ||||
|             assert isinstance(err, ContextCancelled) | ||||
|             assert ( | ||||
|                 tuple(err.canceller) | ||||
|                 == | ||||
|                 current_actor().uid | ||||
|                 uid | ||||
|             ) | ||||
|             assert ( | ||||
|                 err | ||||
|                 is ctx.maybe_error | ||||
|                 is ctx._remote_error | ||||
|             ) | ||||
|             if le := ctx._local_error: | ||||
|                 assert err is le | ||||
| 
 | ||||
|             # else: | ||||
|                 # TODO: what should this be then? | ||||
|                 # not defined until block closes right? | ||||
|                 # | ||||
|                 # await tractor.pause() | ||||
|                 # assert ctx._local_error is None | ||||
| 
 | ||||
| 
 | ||||
|     async def main(): | ||||
| 
 | ||||
|         async with tractor.open_nursery( | ||||
|             debug_mode=debug_mode, | ||||
|         ) as an: | ||||
|  | @ -306,11 +354,16 @@ def test_caller_cancels( | |||
|             ) | ||||
|             timeout = 0.5 if not callee_returns_early else 2 | ||||
|             with trio.fail_after(timeout): | ||||
|                 async with portal.open_context( | ||||
|                     simple_setup_teardown, | ||||
|                     data=10, | ||||
|                     block_forever=not callee_returns_early, | ||||
|                 ) as (ctx, sent): | ||||
|                 async with ( | ||||
| 
 | ||||
|                     expect_ctxc(yay=cancel_method == 'portal'), | ||||
| 
 | ||||
|                     portal.open_context( | ||||
|                         simple_setup_teardown, | ||||
|                         data=10, | ||||
|                         block_forever=not callee_returns_early, | ||||
|                     ) as (ctx, sent), | ||||
|                 ): | ||||
| 
 | ||||
|                     if callee_returns_early: | ||||
|                         # ensure we block long enough before sending | ||||
|  | @ -332,6 +385,16 @@ 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 | ||||
|             # catch any cancellation? | ||||
|             if ( | ||||
|                 not callee_returns_early | ||||
|                 and cancel_method == 'portal' | ||||
|             ): | ||||
|                 assert ctx._scope.cancelled_caught | ||||
| 
 | ||||
|     trio.run(main) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -434,7 +497,6 @@ async def test_callee_closes_ctx_after_stream_open( | |||
| 
 | ||||
| @tractor.context | ||||
| async def expect_cancelled( | ||||
| 
 | ||||
|     ctx: Context, | ||||
| 
 | ||||
| ) -> None: | ||||
|  | @ -454,7 +516,7 @@ async def expect_cancelled( | |||
|         raise | ||||
| 
 | ||||
|     else: | ||||
|         assert 0, "Wasn't cancelled!?" | ||||
|         assert 0, "callee wasn't cancelled !?" | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  | @ -473,8 +535,8 @@ async def test_caller_closes_ctx_after_callee_opens_stream( | |||
|     async with tractor.open_nursery( | ||||
|         debug_mode=debug_mode, | ||||
|     ) as an: | ||||
|         root: Actor = current_actor() | ||||
| 
 | ||||
|         root: Actor = current_actor() | ||||
|         portal = await an.start_actor( | ||||
|             'ctx_cancelled', | ||||
|             enable_modules=[__name__], | ||||
|  | @ -487,11 +549,13 @@ async def test_caller_closes_ctx_after_callee_opens_stream( | |||
| 
 | ||||
|             await portal.run(assert_state, value=True) | ||||
| 
 | ||||
|             # call cancel explicitly | ||||
|             # call `ctx.cancel()` explicitly | ||||
|             if use_ctx_cancel_method: | ||||
| 
 | ||||
|                 await ctx.cancel() | ||||
| 
 | ||||
|                 # NOTE: means the local side `ctx._scope` will | ||||
|                 # have been cancelled by an ctxc ack and thus | ||||
|                 # `._scope.cancelled_caught` should be set. | ||||
|                 try: | ||||
|                     async with ctx.open_stream() as stream: | ||||
|                         async for msg in stream: | ||||
|  | @ -520,20 +584,35 @@ async def test_caller_closes_ctx_after_callee_opens_stream( | |||
|                 assert portal.channel.connected() | ||||
| 
 | ||||
|                 # ctx is closed here | ||||
|                 await portal.run(assert_state, value=False) | ||||
|                 await portal.run( | ||||
|                     assert_state, | ||||
|                     value=False, | ||||
|                 ) | ||||
| 
 | ||||
|             else: | ||||
|                 try: | ||||
|                     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() | ||||
| 
 | ||||
|         # local scope should have absorbed the cancellation | ||||
|         assert ctx.cancelled_caught | ||||
|         assert ctx._remote_error is ctx._local_error | ||||
|         # 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. | ||||
|         if use_ctx_cancel_method: | ||||
|             assert ctx._scope.cancelled_caught | ||||
| 
 | ||||
|         # rxed ctxc response from far end | ||||
|         assert ctx.cancel_acked | ||||
|         assert ( | ||||
|             ctx._remote_error | ||||
|             is ctx._local_error | ||||
|             is ctx.maybe_error | ||||
|             is ctx.outcome | ||||
|         ) | ||||
| 
 | ||||
|         try: | ||||
|             async with ctx.open_stream() as stream: | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue