Tighten `test_spawning` types, parametrize loglevel

Parametrize `test_loglevel_propagated_to_subactor`
across `'debug'`, `'cancel'`, `'critical'` levels
(was hardcoded to just `'critical'`) and move it
above the parent-main tests for logical grouping.

Also,
- add `start_method: str` annotations throughout
- use `portal.wait_for_result()` in
  `test_most_beautiful_word` (replaces `.result()`)
- expand mod docstring to describe test coverage
- reformat `check_parent_main_inheritance` docstr

Review: PR #438 (Copilot)
https://github.com/goodboy/tractor/pull/438

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_spawner_backend
Gud Boi 2026-04-10 12:03:59 -04:00 committed by mahmoudhas
parent 656c6c30d1
commit e8f1eca8d2
1 changed files with 70 additions and 50 deletions

View File

@ -1,5 +1,12 @@
""" """
Spawning basics Spawning basics including audit of,
- subproc boostrap, such as subactor runtime-data/config inheritance,
- basic (and mostly legacy) `ActorNursery` subactor starting and
cancel APIs.
Simple (and generally legacy) examples from the original
API design.
""" """
from functools import partial from functools import partial
@ -98,7 +105,9 @@ async def movie_theatre_question():
@tractor_test @tractor_test
async def test_movie_theatre_convo(start_method): async def test_movie_theatre_convo(
start_method: str,
):
''' '''
The main ``tractor`` routine. The main ``tractor`` routine.
@ -157,7 +166,9 @@ async def test_most_beautiful_word(
# this should pull the cached final result already captured during # this should pull the cached final result already captured during
# the nursery block exit. # the nursery block exit.
print(await portal.result()) res: Any = await portal.wait_for_result()
assert res == return_value
print(res)
async def check_loglevel(level): async def check_loglevel(level):
@ -168,53 +179,24 @@ async def check_loglevel(level):
log.critical('yoyoyo') log.critical('yoyoyo')
async def check_parent_main_inheritance( @pytest.mark.parametrize(
expect_inherited: bool, 'level', [
) -> bool: 'debug',
''' 'cancel',
Assert that the child actor's ``_parent_main_data`` matches the 'critical'
``inherit_parent_main`` flag it was spawned with. ],
ids='loglevel={}'.format,
With the trio spawn backend the parent's ``__main__`` bootstrap )
data is captured and forwarded to each child so it can replay
the parent's ``__main__`` as ``__mp_main__``, mirroring the
stdlib ``multiprocessing`` bootstrap:
https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
When ``inherit_parent_main=False`` the data dict is empty
(``{}``) so no fixup ever runs and the child keeps its own
``__main__`` untouched.
NOTE: under pytest the parent ``__main__`` is
``pytest.__main__`` whose ``_fixup_main_from_name()`` is a
no-op (the name ends with ``.__main__``), so we cannot observe
a difference in ``sys.modules['__main__'].__name__`` between
the two modes. Checking ``_parent_main_data`` directly is the
most reliable verification that the flag is threaded through
correctly; a ``RemoteActorError[AssertionError]`` propagates
on mismatch.
'''
import tractor
actor = tractor.current_actor()
has_data: bool = bool(actor._parent_main_data)
assert has_data == expect_inherited, (
f'Expected _parent_main_data to be '
f'{"non-empty" if expect_inherited else "empty"}, '
f'got: {actor._parent_main_data!r}'
)
return has_data
def test_loglevel_propagated_to_subactor( def test_loglevel_propagated_to_subactor(
start_method, capfd: pytest.CaptureFixture,
capfd, start_method: str,
reg_addr, reg_addr: tuple,
level: str,
): ):
if start_method == 'mp_forkserver': if start_method == 'mp_forkserver':
pytest.skip( pytest.skip(
"a bug with `capfd` seems to make forkserver capture not work?") "a bug with `capfd` seems to make forkserver capture not work?"
)
level = 'critical'
async def main(): async def main():
async with tractor.open_nursery( async with tractor.open_nursery(
@ -236,8 +218,46 @@ def test_loglevel_propagated_to_subactor(
assert 'yoyoyo' in captured.err assert 'yoyoyo' in captured.err
async def check_parent_main_inheritance(
expect_inherited: bool,
) -> bool:
'''
Assert that the child actor's ``_parent_main_data`` matches the
``inherit_parent_main`` flag it was spawned with.
With the trio spawn backend the parent's ``__main__`` bootstrap
data is captured and forwarded to each child so it can replay
the parent's ``__main__`` as ``__mp_main__``, mirroring the
stdlib ``multiprocessing`` bootstrap:
https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
When ``inherit_parent_main=False`` the data dict is empty
(``{}``) so no fixup ever runs and the child keeps its own
``__main__`` untouched.
NOTE: under `pytest` the parent ``__main__`` is
``pytest.__main__`` whose ``_fixup_main_from_name()`` is a no-op
(the name ends with ``.__main__``), so we cannot observe
a difference in ``sys.modules['__main__'].__name__`` between the
two modes. Checking ``_parent_main_data`` directly is the most
reliable verification that the flag is threaded through
correctly; a ``RemoteActorError[AssertionError]`` propagates on
mismatch.
'''
import tractor
actor: tractor.Actor = tractor.current_actor()
has_data: bool = bool(actor._parent_main_data)
assert has_data == expect_inherited, (
f'Expected _parent_main_data to be '
f'{"non-empty" if expect_inherited else "empty"}, '
f'got: {actor._parent_main_data!r}'
)
return has_data
def test_run_in_actor_can_skip_parent_main_inheritance( def test_run_in_actor_can_skip_parent_main_inheritance(
start_method, start_method: str, # <- only support on `trio` backend rn.
): ):
''' '''
Verify ``inherit_parent_main=False`` on ``run_in_actor()`` Verify ``inherit_parent_main=False`` on ``run_in_actor()``
@ -246,7 +266,7 @@ def test_run_in_actor_can_skip_parent_main_inheritance(
''' '''
if start_method != 'trio': if start_method != 'trio':
pytest.skip( pytest.skip(
'parent main inheritance opt-out only affects the trio spawn backend' 'parent main-inheritance opt-out only affects the trio backend'
) )
async def main(): async def main():
@ -273,7 +293,7 @@ def test_run_in_actor_can_skip_parent_main_inheritance(
def test_start_actor_can_skip_parent_main_inheritance( def test_start_actor_can_skip_parent_main_inheritance(
start_method, start_method: str, # <- only support on `trio` backend rn.
): ):
''' '''
Verify ``inherit_parent_main=False`` on ``start_actor()`` Verify ``inherit_parent_main=False`` on ``start_actor()``
@ -282,7 +302,7 @@ def test_start_actor_can_skip_parent_main_inheritance(
''' '''
if start_method != 'trio': if start_method != 'trio':
pytest.skip( pytest.skip(
'parent main inheritance opt-out only affects the trio spawn backend' 'parent main-inheritance opt-out only affects the trio backend'
) )
async def main(): async def main():