Exercise parent-main inheritance through spawn test support

Move the subprocess probe into dedicated spawn test support files so the inheritance tests cover the real __main__ replay path without monkeypatching or inline script strings.
subint_spawner_backend
mahmoud 2026-04-06 23:14:17 +00:00 committed by mahmoudhas
parent 00637764d9
commit b883b27646
4 changed files with 127 additions and 81 deletions

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,63 @@
import json
from pathlib import Path
import sys
import trio
import tractor
from spawn_test_support.parent_main_inheritance_support import get_main_mod_name
async def main(api: str, output_path: str) -> None:
async with tractor.open_nursery(start_method='trio') as an:
if api == 'run_in_actor':
replaying = await an.run_in_actor(
get_main_mod_name,
name='replaying-parent-main',
)
isolated = await an.run_in_actor(
get_main_mod_name,
name='isolated-parent-main',
inherit_parent_main=False,
)
replaying_name = await replaying.result()
isolated_name = await isolated.result()
elif api == 'start_actor':
replaying = await an.start_actor(
'replaying-parent-main',
enable_modules=[
'spawn_test_support.parent_main_inheritance_support',
],
)
isolated = await an.start_actor(
'isolated-parent-main',
enable_modules=[
'spawn_test_support.parent_main_inheritance_support',
],
inherit_parent_main=False,
)
try:
replaying_name = await replaying.run_from_ns(
'spawn_test_support.parent_main_inheritance_support',
'get_main_mod_name',
)
isolated_name = await isolated.run_from_ns(
'spawn_test_support.parent_main_inheritance_support',
'get_main_mod_name',
)
finally:
await replaying.cancel_actor()
await isolated.cancel_actor()
else:
raise ValueError(f'Unknown api: {api}')
Path(output_path).write_text(
json.dumps({
'replaying': replaying_name,
'isolated': isolated_name,
})
)
if __name__ == '__main__':
trio.run(main, sys.argv[1], sys.argv[2])

View File

@ -0,0 +1,5 @@
import sys
async def get_main_mod_name() -> str:
return sys.modules['__main__'].__name__

View File

@ -3,6 +3,11 @@ Spawning basics
"""
from functools import partial
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import (
Any,
)
@ -17,6 +22,11 @@ data_to_pass_down = {
'doggy': 10,
'kitty': 4,
}
_tests_dir = Path(__file__).resolve().parent
_repo_root = _tests_dir.parent
_parent_main_case_script = (
_tests_dir / 'spawn_test_support' / 'parent_main_inheritance_case.py'
)
async def spawn(
@ -173,6 +183,38 @@ async def get_main_mod_name() -> str:
return sys.modules['__main__'].__name__
def _run_parent_main_inheritance_script(
tmp_path: Path,
*,
api: str,
) -> dict[str, str]:
output_file = tmp_path / 'out.json'
env = os.environ.copy()
old_pythonpath = env.get('PYTHONPATH')
env['PYTHONPATH'] = (
f'{_tests_dir}{os.pathsep}{_repo_root}{os.pathsep}{old_pythonpath}'
if old_pythonpath
else f'{_tests_dir}{os.pathsep}{_repo_root}'
)
proc = subprocess.run(
[
sys.executable,
str(_parent_main_case_script),
api,
str(output_file),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
)
assert proc.returncode == 0, (
f'stdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}'
)
return json.loads(output_file.read_text())
def test_loglevel_propagated_to_subactor(
start_method,
capfd,
@ -206,98 +248,33 @@ def test_loglevel_propagated_to_subactor(
def test_run_in_actor_can_skip_parent_main_inheritance(
start_method,
reg_addr,
monkeypatch,
tmp_path,
):
if start_method != 'trio':
pytest.skip(
'parent main inheritance opt-out only affects the trio spawn backend'
)
from tractor.spawn import _mp_fixup_main
monkeypatch.setattr(
_mp_fixup_main,
'_mp_figure_out_main',
lambda inherit_parent_main=True: (
{'init_main_from_name': __name__}
if inherit_parent_main
else {}
),
)
async def main():
async with tractor.open_nursery(
name='registrar',
start_method=start_method,
registry_addrs=[reg_addr],
) as an:
replaying = await an.run_in_actor(
get_main_mod_name,
name='replaying-parent-main',
)
isolated = await an.run_in_actor(
get_main_mod_name,
name='isolated-parent-main',
inherit_parent_main=False,
)
# Stdlib spawn re-runs an importable parent ``__main__`` as
# ``__mp_main__``; opting out should leave the child bootstrap
# module alone instead.
# https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
assert await replaying.result() == '__mp_main__'
assert await isolated.result() == '__main__'
trio.run(main)
assert _run_parent_main_inheritance_script(
tmp_path,
api='run_in_actor',
) == {
'replaying': '__mp_main__',
'isolated': '__main__',
}
def test_start_actor_can_skip_parent_main_inheritance(
start_method,
reg_addr,
monkeypatch,
tmp_path,
):
if start_method != 'trio':
pytest.skip(
'parent main inheritance opt-out only affects the trio spawn backend'
)
from tractor.spawn import _mp_fixup_main
monkeypatch.setattr(
_mp_fixup_main,
'_mp_figure_out_main',
lambda inherit_parent_main=True: (
{'init_main_from_name': __name__}
if inherit_parent_main
else {}
),
)
async def main():
async with tractor.open_nursery(
name='registrar',
start_method=start_method,
registry_addrs=[reg_addr],
) as an:
replaying = await an.start_actor(
'replaying-parent-main',
enable_modules=[__name__],
)
isolated = await an.start_actor(
'isolated-parent-main',
enable_modules=[__name__],
inherit_parent_main=False,
)
try:
assert await replaying.run_from_ns(
__name__,
'get_main_mod_name',
) == '__mp_main__'
assert await isolated.run_from_ns(
__name__,
'get_main_mod_name',
) == '__main__'
finally:
await replaying.cancel_actor()
await isolated.cancel_actor()
trio.run(main)
assert _run_parent_main_inheritance_script(
tmp_path,
api='start_actor',
) == {
'replaying': '__mp_main__',
'isolated': '__main__',
}