245 lines
7.0 KiB
Python
245 lines
7.0 KiB
Python
'''
|
|
Special attention cases for using "infect `asyncio`" mode from a root
|
|
actor; i.e. not using a std `trio.run()` bootstrap.
|
|
|
|
'''
|
|
import asyncio
|
|
from functools import partial
|
|
|
|
import pytest
|
|
import trio
|
|
import tractor
|
|
from tractor import (
|
|
to_asyncio,
|
|
)
|
|
from tests.test_infected_asyncio import (
|
|
aio_echo_server,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'raise_error_mid_stream',
|
|
[
|
|
False,
|
|
Exception,
|
|
KeyboardInterrupt,
|
|
],
|
|
ids='raise_error={}'.format,
|
|
)
|
|
def test_infected_root_actor(
|
|
raise_error_mid_stream: bool|Exception,
|
|
|
|
# conftest wide
|
|
loglevel: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
Verify you can run the `tractor` runtime with `Actor.is_infected_aio() == True`
|
|
in the root actor.
|
|
|
|
'''
|
|
async def _trio_main():
|
|
with trio.fail_after(2):
|
|
first: str
|
|
chan: to_asyncio.LinkedTaskChannel
|
|
async with (
|
|
tractor.open_root_actor(
|
|
debug_mode=debug_mode,
|
|
loglevel=loglevel,
|
|
),
|
|
to_asyncio.open_channel_from(
|
|
aio_echo_server,
|
|
) as (first, chan),
|
|
):
|
|
assert first == 'start'
|
|
|
|
for i in range(1000):
|
|
await chan.send(i)
|
|
out = await chan.receive()
|
|
assert out == i
|
|
print(f'asyncio echoing {i}')
|
|
|
|
if raise_error_mid_stream and i == 500:
|
|
raise raise_error_mid_stream
|
|
|
|
if out is None:
|
|
try:
|
|
out = await chan.receive()
|
|
except trio.EndOfChannel:
|
|
break
|
|
else:
|
|
raise RuntimeError(
|
|
'aio channel never stopped?'
|
|
)
|
|
|
|
if raise_error_mid_stream:
|
|
with pytest.raises(raise_error_mid_stream):
|
|
tractor.to_asyncio.run_as_asyncio_guest(
|
|
trio_main=_trio_main,
|
|
)
|
|
else:
|
|
tractor.to_asyncio.run_as_asyncio_guest(
|
|
trio_main=_trio_main,
|
|
)
|
|
|
|
|
|
|
|
async def sync_and_err(
|
|
# just signature placeholders for compat with
|
|
# ``to_asyncio.open_channel_from()``
|
|
to_trio: trio.MemorySendChannel,
|
|
from_trio: asyncio.Queue,
|
|
ev: asyncio.Event,
|
|
|
|
):
|
|
if to_trio:
|
|
to_trio.send_nowait('start')
|
|
|
|
await ev.wait()
|
|
raise RuntimeError('asyncio-side')
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'aio_err_trigger',
|
|
[
|
|
'before_start_point',
|
|
'after_trio_task_starts',
|
|
'after_start_point',
|
|
],
|
|
ids='aio_err_triggered={}'.format
|
|
)
|
|
def test_trio_prestarted_task_bubbles(
|
|
aio_err_trigger: str,
|
|
|
|
# conftest wide
|
|
loglevel: str,
|
|
debug_mode: bool,
|
|
):
|
|
async def pre_started_err(
|
|
raise_err: bool = False,
|
|
pre_sleep: float|None = None,
|
|
aio_trigger: asyncio.Event|None = None,
|
|
task_status=trio.TASK_STATUS_IGNORED,
|
|
):
|
|
'''
|
|
Maybe pre-started error then sleep.
|
|
|
|
'''
|
|
if pre_sleep is not None:
|
|
print(f'Sleeping from trio for {pre_sleep!r}s !')
|
|
await trio.sleep(pre_sleep)
|
|
|
|
# signal aio-task to raise JUST AFTER this task
|
|
# starts but has not yet `.started()`
|
|
if aio_trigger:
|
|
print('Signalling aio-task to raise from `trio`!!')
|
|
aio_trigger.set()
|
|
|
|
if raise_err:
|
|
print('Raising from trio!')
|
|
raise TypeError('trio-side')
|
|
|
|
task_status.started()
|
|
await trio.sleep_forever()
|
|
|
|
async def _trio_main():
|
|
# with trio.fail_after(2):
|
|
with trio.fail_after(999):
|
|
first: str
|
|
chan: to_asyncio.LinkedTaskChannel
|
|
aio_ev = asyncio.Event()
|
|
|
|
async with (
|
|
tractor.open_root_actor(
|
|
debug_mode=False,
|
|
loglevel=loglevel,
|
|
),
|
|
):
|
|
# TODO, tests for this with 3.13 egs?
|
|
# from tractor.devx import open_crash_handler
|
|
# with open_crash_handler():
|
|
async with (
|
|
# where we'll start a sub-task that errors BEFORE
|
|
# calling `.started()` such that the error should
|
|
# bubble before the guest run terminates!
|
|
trio.open_nursery() as tn,
|
|
|
|
# THEN start an infect task which should error just
|
|
# after the trio-side's task does.
|
|
to_asyncio.open_channel_from(
|
|
partial(
|
|
sync_and_err,
|
|
ev=aio_ev,
|
|
)
|
|
) as (first, chan),
|
|
):
|
|
|
|
for i in range(5):
|
|
pre_sleep: float|None = None
|
|
last_iter: bool = (i == 4)
|
|
|
|
# TODO, missing cases?
|
|
# -[ ] error as well on
|
|
# 'after_start_point' case as well for
|
|
# another case?
|
|
raise_err: bool = False
|
|
|
|
if last_iter:
|
|
raise_err: bool = True
|
|
|
|
# trigger aio task to error on next loop
|
|
# tick/checkpoint
|
|
if aio_err_trigger == 'before_start_point':
|
|
aio_ev.set()
|
|
|
|
pre_sleep: float = 0
|
|
|
|
await tn.start(
|
|
pre_started_err,
|
|
raise_err,
|
|
pre_sleep,
|
|
(aio_ev if (
|
|
aio_err_trigger == 'after_trio_task_starts'
|
|
and
|
|
last_iter
|
|
) else None
|
|
),
|
|
)
|
|
|
|
if (
|
|
aio_err_trigger == 'after_start_point'
|
|
and
|
|
last_iter
|
|
):
|
|
aio_ev.set()
|
|
|
|
with pytest.raises(
|
|
expected_exception=ExceptionGroup,
|
|
) as excinfo:
|
|
tractor.to_asyncio.run_as_asyncio_guest(
|
|
trio_main=_trio_main,
|
|
)
|
|
|
|
eg = excinfo.value
|
|
rte_eg, rest_eg = eg.split(RuntimeError)
|
|
|
|
# ensure the trio-task's error bubbled despite the aio-side
|
|
# having (maybe) errored first.
|
|
if aio_err_trigger in (
|
|
'after_trio_task_starts',
|
|
'after_start_point',
|
|
):
|
|
assert len(errs := rest_eg.exceptions) == 1
|
|
typerr = errs[0]
|
|
assert (
|
|
type(typerr) is TypeError
|
|
and
|
|
'trio-side' in typerr.args
|
|
)
|
|
|
|
# when aio errors BEFORE (last) trio task is scheduled, we should
|
|
# never see anythinb but the aio-side.
|
|
else:
|
|
assert len(rtes := rte_eg.exceptions) == 1
|
|
assert 'asyncio-side' in rtes[0].args[0]
|