From b883b27646aa7d5a9d040144b9136c439395d406 Mon Sep 17 00:00:00 2001 From: mahmoud Date: Mon, 6 Apr 2026 23:14:17 +0000 Subject: [PATCH] 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. --- tests/spawn_test_support/__init__.py | 1 + .../parent_main_inheritance_case.py | 63 ++++++++ .../parent_main_inheritance_support.py | 5 + tests/test_spawning.py | 139 ++++++++---------- 4 files changed, 127 insertions(+), 81 deletions(-) create mode 100644 tests/spawn_test_support/__init__.py create mode 100644 tests/spawn_test_support/parent_main_inheritance_case.py create mode 100644 tests/spawn_test_support/parent_main_inheritance_support.py diff --git a/tests/spawn_test_support/__init__.py b/tests/spawn_test_support/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/spawn_test_support/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/spawn_test_support/parent_main_inheritance_case.py b/tests/spawn_test_support/parent_main_inheritance_case.py new file mode 100644 index 00000000..fd9ccc1e --- /dev/null +++ b/tests/spawn_test_support/parent_main_inheritance_case.py @@ -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]) diff --git a/tests/spawn_test_support/parent_main_inheritance_support.py b/tests/spawn_test_support/parent_main_inheritance_support.py new file mode 100644 index 00000000..bffe7766 --- /dev/null +++ b/tests/spawn_test_support/parent_main_inheritance_support.py @@ -0,0 +1,5 @@ +import sys + + +async def get_main_mod_name() -> str: + return sys.modules['__main__'].__name__ diff --git a/tests/test_spawning.py b/tests/test_spawning.py index ffa4b72c..77732050 100644 --- a/tests/test_spawning.py +++ b/tests/test_spawning.py @@ -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__', + }