diff --git a/tests/test_infected_asyncio.py b/tests/test_infected_asyncio.py index 2beac71..ad7758d 100644 --- a/tests/test_infected_asyncio.py +++ b/tests/test_infected_asyncio.py @@ -2,10 +2,15 @@ The most hipster way to force SC onto the stdlib's "async". ''' +from typing import Optional import asyncio +import builtins +import importlib import pytest +import trio import tractor +from tractor import RemoteActorError async def sleep_and_err(): @@ -13,24 +18,78 @@ async def sleep_and_err(): assert 0 -async def asyncio_actor(): - assert tractor.current_actor().is_infected_aio() +async def sleep_forever(): + await asyncio.sleep(float('inf')) - await tractor.to_asyncio.run_task(sleep_and_err) + +async def asyncio_actor( + + target: str, + expect_err: Optional[Exception] = None + +) -> None: + + assert tractor.current_actor().is_infected_aio() + target = globals()[target] + + if '.' in expect_err: + modpath, _, name = expect_err.rpartition('.') + mod = importlib.import_module(modpath) + error = getattr(mod, name) + error = builtins.__dict__.get(expect_err) + + try: + # spawn an ``asyncio`` task to run a func and return result + await tractor.to_asyncio.run_task(target) + except Exception as err: + if expect_err: + assert isinstance(err, error) + + raise def test_aio_simple_error(arb_addr): + ''' + Verify a simple remote asyncio error propagates back through trio + to the parent actor. + + ''' + async def main(): + async with tractor.open_nursery( + arbiter_addr=arb_addr + ) as n: + await n.run_in_actor( + asyncio_actor, + target='sleep_and_err', + expect_err='AssertionError', + infect_asyncio=True, + ) + + with pytest.raises(RemoteActorError) as excinfo: + trio.run(main) + + err = excinfo.value + assert isinstance(err, RemoteActorError) + assert err.type == AssertionError + + +def test_tractor_cancels_aio(arb_addr): + ''' + Verify we can cancel a spawned asyncio task gracefully. + + ''' async def main(): async with tractor.open_nursery() as n: - await n.run_in_actor(asyncio_actor, infected_asyncio=True) + portal = await n.run_in_actor( + asyncio_actor, + target='sleep_forever', + expect_err='asyncio.CancelledError', + infect_asyncio=True, + ) + await portal.cancel_actor() - with pytest.raises(tractor.RemoteActorError) as excinfo: - tractor.run(main, arbiter_addr=arb_addr) - - -def test_aio_cancel_from_trio(arb_addr): - ... + trio.run(main) def test_aio_cancelled_from_aio_causes_trio_cancelled(arb_addr): @@ -49,10 +108,6 @@ def test_basic_interloop_channel_stream(arb_addr): ... -def test_basic_interloop_channel_stream(arb_addr): - ... - - def test_trio_cancels_and_channel_exits(arb_addr): ... diff --git a/tractor/_actor.py b/tractor/_actor.py index 47806be..8e5d548 100644 --- a/tractor/_actor.py +++ b/tractor/_actor.py @@ -475,6 +475,7 @@ class Actor: self._mods[modpath] = mod if modpath == '__main__': self._mods['__mp_main__'] = mod + except ModuleNotFoundError: # it is expected the corresponding `ModuleNotExposed` error # will be raised later