forked from goodboy/tractor
				
			Add a test for the real issue: error overriding
The underlying issue is actually that a nested `Context` which was cancelled was overriding the local error that triggered that secondary's context's cancellation in the first place XD. This test catches that case. Relates to https://github.com/pikers/piker/issues/244expected_ctx_cancelled
							parent
							
								
									5d424e3703
								
							
						
					
					
						commit
						9650b010de
					
				| 
						 | 
				
			
			@ -5,6 +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 itertools import count
 | 
			
		||||
import platform
 | 
			
		||||
from typing import Optional
 | 
			
		||||
| 
						 | 
				
			
			@ -58,33 +59,6 @@ from conftest import tractor_test
 | 
			
		|||
_state: bool = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@tractor.context
 | 
			
		||||
async def error_before_started(
 | 
			
		||||
    ctx: tractor.Context,
 | 
			
		||||
) -> None:
 | 
			
		||||
    # send an unserializable type
 | 
			
		||||
    await ctx.started(object())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_error_before_started():
 | 
			
		||||
    async def main():
 | 
			
		||||
        async with tractor.open_nursery() as n:
 | 
			
		||||
            portal = await n.start_actor(
 | 
			
		||||
                'errorer',
 | 
			
		||||
                enable_modules=[__name__],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            async with portal.open_context(
 | 
			
		||||
                error_before_started
 | 
			
		||||
            ) as (ctx, sent):
 | 
			
		||||
                await trio.sleep(1)
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(tractor.RemoteActorError) as excinfo:
 | 
			
		||||
        trio.run(main)
 | 
			
		||||
 | 
			
		||||
    assert excinfo.value.type == TypeError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@tractor.context
 | 
			
		||||
async def too_many_starteds(
 | 
			
		||||
    ctx: tractor.Context,
 | 
			
		||||
| 
						 | 
				
			
			@ -494,7 +468,10 @@ async def cancel_self(
 | 
			
		|||
        async with ctx.open_stream():
 | 
			
		||||
            pass
 | 
			
		||||
    except tractor.ContextCancelled:
 | 
			
		||||
        # suppress for now so we can do checkpoint tests below
 | 
			
		||||
        pass
 | 
			
		||||
    else:
 | 
			
		||||
        raise RuntimeError('Context didnt cancel itself?!')
 | 
			
		||||
 | 
			
		||||
    # check a real ``trio.Cancelled`` is raised on a checkpoint
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			@ -534,6 +511,9 @@ async def test_callee_cancels_before_started():
 | 
			
		|||
        except tractor.ContextCancelled as ce:
 | 
			
		||||
            ce.type == trio.Cancelled
 | 
			
		||||
 | 
			
		||||
            # the traceback should be informative
 | 
			
		||||
            assert 'cancelled itself' in ce.msgdata['tb_str']
 | 
			
		||||
 | 
			
		||||
        # teardown the actor
 | 
			
		||||
        await portal.cancel_actor()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -739,3 +719,92 @@ def test_stream_backpressure():
 | 
			
		|||
            await portal.cancel_actor()
 | 
			
		||||
 | 
			
		||||
    trio.run(main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@tractor.context
 | 
			
		||||
async def sleep_forever(
 | 
			
		||||
    ctx: tractor.Context,
 | 
			
		||||
) -> None:
 | 
			
		||||
    await ctx.started()
 | 
			
		||||
    async with ctx.open_stream():
 | 
			
		||||
        await trio.sleep_forever()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@acm
 | 
			
		||||
async def attach_to_sleep_forever():
 | 
			
		||||
    '''
 | 
			
		||||
    Cancel a context **before** any underlying error is raised in order
 | 
			
		||||
    to trigger a local reception of a ``ContextCancelled`` which **should not**
 | 
			
		||||
    be re-raised in the local surrounding ``Context`` *iff* the cancel was
 | 
			
		||||
    requested by **this** side of the context.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    async with tractor.wait_for_actor('sleeper') as p2:
 | 
			
		||||
        async with (
 | 
			
		||||
            p2.open_context(sleep_forever) as (peer_ctx, first),
 | 
			
		||||
            peer_ctx.open_stream(),
 | 
			
		||||
        ):
 | 
			
		||||
            try:
 | 
			
		||||
                yield
 | 
			
		||||
            finally:
 | 
			
		||||
                # XXX: previously this would trigger local
 | 
			
		||||
                # ``ContextCancelled`` to be received and raised in the
 | 
			
		||||
                # local context overriding any local error due to
 | 
			
		||||
                # logic inside ``_invoke()`` which checked for
 | 
			
		||||
                # an error set on ``Context._error`` and raised it in
 | 
			
		||||
                # under a cancellation scenario.
 | 
			
		||||
 | 
			
		||||
                # The problem is you can have a remote cancellation
 | 
			
		||||
                # that is part of a local error and we shouldn't raise
 | 
			
		||||
                # ``ContextCancelled`` **iff** we weren't the side of
 | 
			
		||||
                # the context to initiate it, i.e.
 | 
			
		||||
                # ``Context._cancel_called`` should **NOT** have been
 | 
			
		||||
                # set. The special logic to handle this case is now
 | 
			
		||||
                # inside ``Context._may_raise_from_remote_msg()`` XD
 | 
			
		||||
                await peer_ctx.cancel()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@tractor.context
 | 
			
		||||
async def error_before_started(
 | 
			
		||||
    ctx: tractor.Context,
 | 
			
		||||
) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
    This simulates exactly an original bug discovered in:
 | 
			
		||||
    https://github.com/pikers/piker/issues/244
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    async with attach_to_sleep_forever():
 | 
			
		||||
        # send an unserializable type which should raise a type error
 | 
			
		||||
        # here and **NOT BE SWALLOWED** by the surrounding acm!!?!
 | 
			
		||||
        await ctx.started(object())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_do_not_swallow_error_before_started_by_remote_contextcancelled():
 | 
			
		||||
    '''
 | 
			
		||||
    Verify that an error raised in a remote context which itself opens another
 | 
			
		||||
    remote context, which it cancels, does not ovverride the original error that
 | 
			
		||||
    caused the cancellation of the secondardy context.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    async def main():
 | 
			
		||||
        async with tractor.open_nursery() as n:
 | 
			
		||||
            portal = await n.start_actor(
 | 
			
		||||
                'errorer',
 | 
			
		||||
                enable_modules=[__name__],
 | 
			
		||||
            )
 | 
			
		||||
            await n.start_actor(
 | 
			
		||||
                'sleeper',
 | 
			
		||||
                enable_modules=[__name__],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            async with (
 | 
			
		||||
                portal.open_context(
 | 
			
		||||
                    error_before_started
 | 
			
		||||
                ) as (ctx, sent),
 | 
			
		||||
            ):
 | 
			
		||||
                await trio.sleep_forever()
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(tractor.RemoteActorError) as excinfo:
 | 
			
		||||
        trio.run(main)
 | 
			
		||||
 | 
			
		||||
    assert excinfo.value.type == TypeError
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue