Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ async def breakpoint_forever():
async def spawn_until(depth=0):
""""A nested nursery that triggers another ``NameError``.
"""
async with tractor.open_nursery() as n:
async with tractor.open_nursery() as an:
if depth < 1:

await n.run_in_actor(breakpoint_forever)
await an.run_in_actor(breakpoint_forever)

p = await n.run_in_actor(
p = await an.run_in_actor(
name_error,
name='name_error'
)
Expand All @@ -38,7 +38,7 @@ async def spawn_until(depth=0):
# recusrive call to spawn another process branching layer of
# the tree
depth -= 1
await n.run_in_actor(
await an.run_in_actor(
spawn_until,
depth=depth,
name=f'spawn_until_{depth}',
Expand Down
61 changes: 40 additions & 21 deletions tests/devx/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,10 +709,41 @@ def test_multi_nested_subactors_error_through_nurseries(
child = spawn('multi_nested_subactors_error_up_through_nurseries')

# timed_out_early: bool = False
at_least_one: list[str] = [
"bdb.BdbQuit",

# leaf subs, which actually raise in "user code"
"src_uid=('breakpoint_forever'",
"src_uid=('name_error'",

# 2nd layer subs
"src_uid=('spawn_until_1'",
"src_uid=('spawn_until_2'",
"src_uid=('spawn_until_3'",
"relay_uid=('spawn_until_0'",

# 1st layer subs
"src_uid=('spawner0'",
"src_uid=('spawner1'",
]

for send_char in itertools.cycle(['c', 'q']):
for i, send_char in enumerate(
itertools.cycle(['c', 'q'])
):
try:
child.expect(PROMPT)

for patt in at_least_one.copy():
if in_prompt_msg(
child,
[patt],
):
print(
f'Found patt in prompt {i}\n'
f'patt: {patt!r}\n'
)
at_least_one.remove(patt)

child.sendline(send_char)
time.sleep(0.01)

Expand All @@ -721,27 +752,15 @@ def test_multi_nested_subactors_error_through_nurseries(

assert_before(
child,
[ # boxed source errors
"NameError: name 'doggypants' is not defined",
[
# boxed source errors should show in final
# post-prompt tb to console.
"tractor._exceptions.RemoteActorError:",
"('name_error'",
"bdb.BdbQuit",

# first level subtrees
# "tractor._exceptions.RemoteActorError: ('spawner0'",
"src_uid=('spawner0'",

# "tractor._exceptions.RemoteActorError: ('spawner1'",

# propagation of errors up through nested subtrees
# "tractor._exceptions.RemoteActorError: ('spawn_until_0'",
# "tractor._exceptions.RemoteActorError: ('spawn_until_1'",
# "tractor._exceptions.RemoteActorError: ('spawn_until_2'",
# ^-NOTE-^ old RAE repr, new one is below with a field
# showing the src actor's uid.
"src_uid=('spawn_until_0'",
"relay_uid=('spawn_until_1'",
"src_uid=('spawn_until_2'",
"NameError: name 'doggypants' is not defined",

# TODO? once we get more pedantic with `relay_uid` should
# prolly include all actor-IDs we expect to see in final
# tb?
]
)

Expand Down
98 changes: 98 additions & 0 deletions tests/test_actor_nursery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'''
Basic `ActorNursery` operations and closure semantics,
- basic remote error collection,
- basic multi-subactor cancellation.

'''
# import os
# import signal
# import platform
# import time
# from itertools import repeat

import pytest
import trio
import tractor
from tractor._exceptions import ActorCancelled
# from tractor._testing import (
# tractor_test,
# )
# from .conftest import no_windows


@pytest.mark.parametrize(
'num_subs',
[
1,
3,
]
)
def test_one_cancels_all(
start_method: str,
loglevel: str,
debug_mode: bool,
num_subs: int,
):
'''
Verify that ifa a single error bubbles to the an-scope the
nursery will be cancelled (just like in `trio`); this is a
one-cancels-all style strategy and are only supervision policy
at the moment.

'''
async def main():
try:
rte = RuntimeError('Uh oh something bad in parent')
async with tractor.open_nursery(
start_method=start_method,
loglevel=loglevel,
debug_mode=debug_mode,
) as an:

# spawn the same number of deamon actors which should be cancelled
dactor_portals = []
for i in range(num_subs):
name: str= f'sub_{i}'
ptl: tractor.Portal = await an.start_actor(
name=name,
enable_modules=[__name__],
)
dactor_portals.append(ptl)

# wait for booted
async with tractor.wait_for_actor(name):
print(f'{name!r} is up.')

# simulate uncaught exc
raise rte

# should error here with a ``RemoteActorError`` or ``MultiError``

except BaseExceptionGroup as _beg:
beg = _beg

# ?TODO? why can't we do `is` on beg?
assert (
beg.exceptions
==
an.maybe_error.exceptions
)

assert len(beg.exceptions) == (
num_subs
+
1 # rte from root
)

# all subactors should have been implicitly
# `Portal.cancel_actor()`ed.
excs = list(beg.exceptions)
excs.remove(rte)
for exc in excs:
assert isinstance(exc, ActorCancelled)

assert an._scope_error is rte
assert not an._children
assert an.cancelled is True

trio.run(main)
Loading
Loading