From 83c8a8ad78a0adf574d9f60d2e9ecae76423f9c9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 26 Oct 2022 11:54:13 -0400 Subject: [PATCH 01/35] Add macos run using only the `trio` spawner --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be5cb272..c4e09f7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: 'Install uv + py-${{ matrix.python-version }}' uses: astral-sh/setup-uv@v6 with: @@ -120,6 +119,38 @@ jobs: - name: Run tests run: uv run pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx + testing-macos: + name: '${{ matrix.os }} Python ${{ matrix.python }} - ${{ matrix.spawn_backend }}' + timeout-minutes: 10 + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [macos-latest] + python: ['3.13'] + spawn_backend: [ + 'trio', + ] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.python }}' + + - name: Install the project w uv + run: uv sync --all-extras --dev + + - name: List deps tree + run: uv tree + + - name: Run tests w uv + run: uv run pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx + # XXX legacy NOTE XXX # # We skip 3.10 on windows for now due to not having any collabs to From aee86f25444e808c48a1e1baa467b4f17d4f99d1 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Feb 2026 20:55:01 -0500 Subject: [PATCH 02/35] Run macos job on `uv` and newer `actions@v4` --- .github/workflows/ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4e09f7f..7a0d0d28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,13 +134,11 @@ jobs: ] steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup python - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: 'Install uv + py-${{ matrix.python-version }}' + uses: astral-sh/setup-uv@v6 with: - python-version: '${{ matrix.python }}' + python-version: ${{ matrix.python-version }} - name: Install the project w uv run: uv sync --all-extras --dev From 2631fb4ff3655c49b95a3d72644d68cf56a6b128 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Feb 2026 21:05:16 -0500 Subject: [PATCH 03/35] Only run CI on Date: Thu, 26 Feb 2026 17:20:29 -0500 Subject: [PATCH 04/35] Use py version in job `name`, consider macos in linux matrix? --- .github/workflows/ci.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 837cb72f..f85019a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,14 +75,17 @@ jobs: testing-linux: - name: '${{ matrix.os }} Python ${{ matrix.python }} - ${{ matrix.spawn_backend }}' + name: '${{ matrix.os }} Python${{ matrix.python-version }} - spawn_backend=${{ matrix.spawn_backend }}' timeout-minutes: 10 runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ + ubuntu-latest, + # macos-latest, # ?TODO, better? + ] python-version: [ '3.13', # '3.14', @@ -123,14 +126,16 @@ jobs: run: uv run pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx testing-macos: - name: '${{ matrix.os }} Python ${{ matrix.python }} - ${{ matrix.spawn_backend }}' + name: '${{ matrix.os }} Python${{ matrix.python-version }} - spawn_backend=${{ matrix.spawn_backend }}' timeout-minutes: 10 runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [macos-latest] + os: [ + macos-latest, + ] python-version: [ '3.13', # '3.14', From c5af2fa778f11d5ee5ea938c6954cce8aff7276c Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Feb 2026 20:02:14 -0500 Subject: [PATCH 05/35] Add a `@no_macos` skipif deco --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 47837fa5..c77c5407 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,6 +44,10 @@ no_windows = pytest.mark.skipif( platform.system() == "Windows", reason="Test is unsupported on windows", ) +no_macos = pytest.mark.skipif( + platform.system() == "Darwin", + reason="Test is unsupported on MacOS", +) def pytest_addoption( From 706a4b761b6ce68346602ce9ca79df0983cb1977 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Feb 2026 20:28:21 -0500 Subject: [PATCH 06/35] Add 6sec timeout around `test_simple_rpc` suite for macos --- tests/test_2way.py | 94 ++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/tests/test_2way.py b/tests/test_2way.py index db3be4d9..f5a59cfe 100644 --- a/tests/test_2way.py +++ b/tests/test_2way.py @@ -1,15 +1,25 @@ -""" -Bidirectional streaming. +''' +Audit the simplest inter-actor bidirectional (streaming) +msg patterns. -""" +''' +from __future__ import annotations +from typing import ( + Callable, + TYPE_CHECKING, +) import pytest import trio import tractor +if TYPE_CHECKING: + from tractor import ( + Portal, + ) + @tractor.context async def simple_rpc( - ctx: tractor.Context, data: int, @@ -39,15 +49,14 @@ async def simple_rpc( @tractor.context async def simple_rpc_with_forloop( - ctx: tractor.Context, data: int, ) -> None: - """Same as previous test but using ``async for`` syntax/api. - - """ + ''' + Same as previous test but using `async for` syntax/api. + ''' # signal to parent that we're up await ctx.started(data + 1) @@ -74,56 +83,59 @@ async def simple_rpc_with_forloop( 'server_func', [simple_rpc, simple_rpc_with_forloop], ) -def test_simple_rpc(server_func, use_async_for): +def test_simple_rpc( + server_func: Callabe, + use_async_for: bool, +): ''' The simplest request response pattern. ''' async def main(): - async with tractor.open_nursery() as n: + with trio.fail_after(6): + async with tractor.open_nursery() as an: + portal: Portal = await an.start_actor( + 'rpc_server', + enable_modules=[__name__], + ) - portal = await n.start_actor( - 'rpc_server', - enable_modules=[__name__], - ) + async with portal.open_context( + server_func, # taken from pytest parameterization + data=10, + ) as (ctx, sent): - async with portal.open_context( - server_func, # taken from pytest parameterization - data=10, - ) as (ctx, sent): + assert sent == 11 - assert sent == 11 + async with ctx.open_stream() as stream: - async with ctx.open_stream() as stream: + if use_async_for: - if use_async_for: - - count = 0 - # receive msgs using async for style - print('ping') - await stream.send('ping') - - async for msg in stream: - assert msg == 'pong' + count = 0 + # receive msgs using async for style print('ping') await stream.send('ping') - count += 1 - if count >= 9: - break + async for msg in stream: + assert msg == 'pong' + print('ping') + await stream.send('ping') + count += 1 - else: - # classic send/receive style - for _ in range(10): + if count >= 9: + break - print('ping') - await stream.send('ping') - assert await stream.receive() == 'pong' + else: + # classic send/receive style + for _ in range(10): - # stream should terminate here + print('ping') + await stream.send('ping') + assert await stream.receive() == 'pong' - # final context result(s) should be consumed here in __aexit__() + # stream should terminate here - await portal.cancel_actor() + # final context result(s) should be consumed here in __aexit__() + + await portal.cancel_actor() trio.run(main) From 86c95539caf0d5042a2470bed77aeed53ddea912 Mon Sep 17 00:00:00 2001 From: goodboy Date: Sun, 1 Mar 2026 18:52:48 -0500 Subject: [PATCH 07/35] Loosen shml test assert for key shortening on macos --- tests/test_shm.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_shm.py b/tests/test_shm.py index ddeb67aa..00a36f8a 100644 --- a/tests/test_shm.py +++ b/tests/test_shm.py @@ -2,6 +2,7 @@ Shared mem primitives and APIs. """ +import platform import uuid # import numpy @@ -53,7 +54,18 @@ def test_child_attaches_alot(): shm_key=shml.key, ) as (ctx, start_val), ): - assert start_val == key + assert (_key := shml.key) == start_val + + if platform.system() != 'Darwin': + # XXX, macOS has a char limit.. + # see `ipc._shm._shorten_key_for_macos` + assert ( + start_val + == + key + == + _key + ) await ctx.result() await portal.cancel_actor() From b7546fd221b11f48a56cf30132543772c8cbe1a6 Mon Sep 17 00:00:00 2001 From: goodboy Date: Sun, 1 Mar 2026 20:35:28 -0500 Subject: [PATCH 08/35] Longer timeout for `test_one_end_stream_not_opened` On non-linux that is. --- tests/test_context_stream_semantics.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_context_stream_semantics.py b/tests/test_context_stream_semantics.py index 4c347e91..f860e4d3 100644 --- a/tests/test_context_stream_semantics.py +++ b/tests/test_context_stream_semantics.py @@ -9,6 +9,7 @@ from itertools import count import math import platform from pprint import pformat +import sys from typing import ( Callable, ) @@ -941,6 +942,11 @@ def test_one_end_stream_not_opened( from tractor._runtime import Actor buf_size = buf_size_increase + Actor.msg_buffer_size + timeout: float = ( + 1 if sys.platform == 'linux' + else 3 + ) + async def main(): async with tractor.open_nursery( debug_mode=debug_mode, @@ -950,7 +956,7 @@ def test_one_end_stream_not_opened( enable_modules=[__name__], ) - with trio.fail_after(1): + with trio.fail_after(timeout): async with portal.open_context( entrypoint, ) as (ctx, sent): From 82d02ef404dc20f76adf1870c23cd5105aa1a77a Mon Sep 17 00:00:00 2001 From: goodboy Date: Sun, 1 Mar 2026 23:38:18 -0500 Subject: [PATCH 09/35] Lul, never use `'uds'` tpt for macos test-scripts It's explained in the comment and i really think it's getting more hilarious the more i learn about the arbitrary limitations of user space with this tina platform. --- examples/debugging/shield_hang_in_sub.py | 16 +++++++++++++++- examples/debugging/subactor_bp_in_ctx.py | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/examples/debugging/shield_hang_in_sub.py b/examples/debugging/shield_hang_in_sub.py index bf045fe8..280757ea 100644 --- a/examples/debugging/shield_hang_in_sub.py +++ b/examples/debugging/shield_hang_in_sub.py @@ -3,6 +3,7 @@ Verify we can dump a `stackscope` tree on a hang. ''' import os +import platform import signal import trio @@ -31,13 +32,26 @@ async def main( from_test: bool = False, ) -> None: + if platform.system() != 'Darwin': + tpt = 'uds' + else: + # XXX, precisely we can't use pytest's tmp-path generation + # for tests.. apparently because: + # + # > The OSError: AF_UNIX path too long in macOS Python occurs + # > because the path to the Unix domain socket exceeds the + # > operating system's maximum path length limit (around 104 + # + # WHICH IS just, wtf hillarious XD + tpt = 'tcp' + async with ( tractor.open_nursery( debug_mode=True, enable_stack_on_sig=True, # maybe_enable_greenback=False, loglevel='devx', - enable_transports=['uds'], + enable_transports=[tpt], ) as an, ): ptl: tractor.Portal = await an.start_actor( diff --git a/examples/debugging/subactor_bp_in_ctx.py b/examples/debugging/subactor_bp_in_ctx.py index f55d2cd4..0ca7097f 100644 --- a/examples/debugging/subactor_bp_in_ctx.py +++ b/examples/debugging/subactor_bp_in_ctx.py @@ -1,3 +1,5 @@ +import platform + import tractor import trio @@ -34,9 +36,22 @@ async def just_bp( async def main(): + if platform.system() != 'Darwin': + tpt = 'uds' + else: + # XXX, precisely we can't use pytest's tmp-path generation + # for tests.. apparently because: + # + # > The OSError: AF_UNIX path too long in macOS Python occurs + # > because the path to the Unix domain socket exceeds the + # > operating system's maximum path length limit (around 104 + # + # WHICH IS just, wtf hillarious XD + tpt = 'tcp' + async with tractor.open_nursery( debug_mode=True, - enable_transports=['uds'], + enable_transports=[tpt], loglevel='devx', ) as n: p = await n.start_actor( From 7b89204afdf32710ce21486f148c7bcd5a14bd90 Mon Sep 17 00:00:00 2001 From: goodboy Date: Sun, 1 Mar 2026 23:32:36 -0500 Subject: [PATCH 10/35] Tweak `do_ctlc()`'s `delay` default To be a null default and set to `0.1` when not passed by the caller so as to avoid having to pass `0.1` if you wanted the param-defined-default. Also, - in the `spawn()` fixtures's `unset_colors()` closure, add in a masked `os.environ['NO_COLOR'] = '1'` since i found it while trying to debug debugger tests. - always return the `child.before` content from `assert_before()` helper; again it comes in handy when debugging console matching tests. --- tests/devx/conftest.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/devx/conftest.py b/tests/devx/conftest.py index efc74d44..b7559706 100644 --- a/tests/devx/conftest.py +++ b/tests/devx/conftest.py @@ -68,7 +68,10 @@ def spawn( ''' import os + # disable colored tbs os.environ['PYTHON_COLORS'] = '0' + # disable all ANSI color output + # os.environ['NO_COLOR'] = '1' spawned: PexpectSpawner|None = None @@ -251,12 +254,13 @@ def assert_before( err_on_false=True, **kwargs ) + return str(child.before.decode()) def do_ctlc( child, count: int = 3, - delay: float = 0.1, + delay: float|None = None, patt: str|None = None, # expect repl UX to reprint the prompt after every @@ -268,6 +272,7 @@ def do_ctlc( ) -> str|None: before: str|None = None + delay = delay or 0.1 # make sure ctl-c sends don't do anything but repeat output for _ in range(count): @@ -278,7 +283,10 @@ def do_ctlc( # if you run this test manually it works just fine.. if expect_prompt: time.sleep(delay) - child.expect(PROMPT) + child.expect( + PROMPT, + # timeout=1, # TODO? if needed + ) before = str(child.before.decode()) time.sleep(delay) From 51701fc8dc838647efe8392a70735f8fae539c56 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 00:15:49 -0500 Subject: [PATCH 11/35] Ok just skip `test_shield_pause` for macos.. Something something the SIGINT handler isn't being swapped correctly? --- tests/devx/test_tooling.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/devx/test_tooling.py b/tests/devx/test_tooling.py index 697b2bc1..0eb19182 100644 --- a/tests/devx/test_tooling.py +++ b/tests/devx/test_tooling.py @@ -31,6 +31,9 @@ from .conftest import ( PROMPT, _pause_msg, ) +from ..conftest import ( + no_macos, +) import pytest from pexpect.exceptions import ( @@ -42,6 +45,7 @@ if TYPE_CHECKING: from ..conftest import PexpectSpawner +@no_macos def test_shield_pause( spawn: PexpectSpawner, ): @@ -57,6 +61,7 @@ def test_shield_pause( expect( child, 'Yo my child hanging..?', + timeout=3, ) assert_before( child, From b30faaca82edd25bf9d9cec4198dcf6cecf134c0 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 00:16:10 -0500 Subject: [PATCH 12/35] Adjust debugger test suites for macos Namely, after trying to get `test_multi_daemon_subactors` to work for the `ctlc=True` case (for way too long), give up on that (see todo/comments) and skip it; the normal case works just fine. Also tweak the `test_ctxep_pauses_n_maybe_ipc_breaks` pattern matching for non-`'UDS'` per the previous script commit; we can't use UDS alongside `pytest`'s tmp dir generation, mega lulz. --- tests/devx/test_debugger.py | 91 ++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index d3f9fa5d..f415f6c1 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -51,13 +51,14 @@ if TYPE_CHECKING: # - recurrent root errors +_non_linux: bool = platform.system() != 'Linux' + if platform.system() == 'Windows': pytest.skip( 'Debugger tests have no windows support (yet)', allow_module_level=True, ) - # TODO: was trying to this xfail style but some weird bug i see in CI # that's happening at collect time.. pretty soon gonna dump actions i'm # thinkin... @@ -480,8 +481,24 @@ def test_multi_daemon_subactors( stream. ''' - child = spawn('multi_daemon_subactors') + non_linux = _non_linux + if non_linux and ctlc: + pytest.skip( + 'Ctl-c + MacOS is too unreliable/racy for this test..\n' + ) + # !TODO, if someone with more patience then i wants to muck + # with the timings on this please feel free to see all the + # `non_linux` branching logic i added on my first attempt + # below! + # + # my conclusion was that if i were to run the script + # manually, and thus as slowly as a human would, the test + # would and should pass as described in this test fn, however + # after fighting with it for >= 1hr. i decided more then + # likely the more extensive `linux` testing should cover most + # regressions. + child = spawn('multi_daemon_subactors') child.expect(PROMPT) # there can be a race for which subactor will acquire @@ -511,8 +528,19 @@ def test_multi_daemon_subactors( else: raise ValueError('Neither log msg was found !?') + non_linux_delay: float = 0.3 if ctlc: - do_ctlc(child) + do_ctlc( + child, + delay=( + non_linux_delay + if non_linux + else None + ), + ) + + if non_linux: + time.sleep(1) # NOTE: previously since we did not have clobber prevention # in the root actor this final resume could result in the debugger @@ -543,33 +571,66 @@ def test_multi_daemon_subactors( # assert "in use by child ('bp_forever'," in before if ctlc: - do_ctlc(child) + do_ctlc( + child, + delay=( + non_linux_delay + if non_linux + else None + ), + ) + + if non_linux: + time.sleep(1) # expect another breakpoint actor entry child.sendline('c') child.expect(PROMPT) - try: - assert_before( + before: str = assert_before( child, bp_forev_parts, ) except AssertionError: - assert_before( + before: str = assert_before( child, name_error_parts, ) else: if ctlc: - do_ctlc(child) + before: str = do_ctlc( + child, + delay=( + non_linux_delay + if non_linux + else None + ), + ) + + if non_linux: + time.sleep(1) # should crash with the 2nd name error (simulates # a retry) and then the root eventually (boxed) errors # after 1 or more further bp actor entries. child.sendline('c') - child.expect(PROMPT) + try: + child.expect( + PROMPT, + timeout=3, + ) + except EOF: + before: str = child.before.decode() + print( + f'\n' + f'??? NEVER RXED `pdb` PROMPT ???\n' + f'\n' + f'{before}\n' + ) + raise + assert_before( child, name_error_parts, @@ -1133,14 +1194,20 @@ def test_ctxep_pauses_n_maybe_ipc_breaks( # closed so verify we see error reporting as well as # a failed crash-REPL request msg and can CTL-c our way # out. + + # ?TODO, match depending on `tpt_proto(s)`? + # - [ ] how can we pass it into the script tho? + tpt: str = 'UDS' + if _non_linux: + tpt: str = 'TCP' + assert_before( child, ['peer IPC channel closed abruptly?', 'another task closed this fd', 'Debug lock request was CANCELLED?', - "'MsgpackUDSStream' was already closed locally?", - "TransportClosed: 'MsgpackUDSStream' was already closed 'by peer'?", - # ?TODO^? match depending on `tpt_proto(s)`? + f"'Msgpack{tpt}Stream' was already closed locally?", + f"TransportClosed: 'Msgpack{tpt}Stream' was already closed 'by peer'?", ] # XXX races on whether these show/hit? From e8f3d64e71f750430decf6ef7ee5696462f9ffab Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 12:07:41 -0500 Subject: [PATCH 13/35] Increase prompt timeout for macos in CI --- tests/devx/test_debugger.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index f415f6c1..f57de285 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -750,7 +750,8 @@ def test_multi_subactors_root_errors( @has_nested_actors def test_multi_nested_subactors_error_through_nurseries( - spawn, + ci_env: bool, + spawn: PexpectSpawner, # TODO: address debugger issue for nested tree: # https://github.com/goodboy/tractor/issues/320 @@ -773,7 +774,16 @@ def test_multi_nested_subactors_error_through_nurseries( for send_char in itertools.cycle(['c', 'q']): try: - child.expect(PROMPT) + child.expect( + PROMPT, + timeout=( + 2 if ( + _non_linux + and + ci_env + ) else -1 + ), + ) child.sendline(send_char) time.sleep(0.01) From a1ea373f34a28473e67f46032a4abf148d77fbdb Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 13:55:14 -0500 Subject: [PATCH 14/35] Ok.. try a longer prompt timeout? --- tests/devx/test_debugger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index f57de285..43adbec1 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -777,7 +777,7 @@ def test_multi_nested_subactors_error_through_nurseries( child.expect( PROMPT, timeout=( - 2 if ( + 6 if ( _non_linux and ci_env From 9c1bcb23af11bbecbd87658724fdb4271bc6281e Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 11:28:57 -0500 Subject: [PATCH 15/35] Skip legacy-one-way suites on non-linux in CI --- tests/test_legacy_one_way_streaming.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_legacy_one_way_streaming.py b/tests/test_legacy_one_way_streaming.py index 10cf3aed..a685f924 100644 --- a/tests/test_legacy_one_way_streaming.py +++ b/tests/test_legacy_one_way_streaming.py @@ -240,6 +240,10 @@ def time_quad_ex( ci_env: bool, spawn_backend: str, ): + non_linux: bool = (_sys := platform.system()) != 'Linux' + if ci_env and non_linux: + pytest.skip("Test is too flaky on {_sys!r} in CI") + if spawn_backend == 'mp': ''' no idea but the mp *nix runs are flaking out here often... @@ -247,7 +251,7 @@ def time_quad_ex( ''' pytest.skip("Test is too flaky on mp in CI") - timeout = 7 if platform.system() in ('Windows', 'Darwin') else 4 + timeout = 7 if non_linux else 4 start = time.time() results = trio.run(cancel_after, timeout, reg_addr) diff = time.time() - start @@ -264,13 +268,12 @@ def test_a_quadruple_example( This also serves as a kind of "we'd like to be this fast test". ''' + non_linux: bool = (_sys := platform.system()) != 'Linux' + results, diff = time_quad_ex assert results this_fast = ( - 6 if platform.system() in ( - 'Windows', - 'Darwin', - ) + 6 if non_linux else 3 ) assert diff < this_fast From 94dfeb14410c971dc42d6da427c88a484de975ea Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 12:11:49 -0500 Subject: [PATCH 16/35] Add delay before root-actor open, macos in CI.. --- tests/test_multi_program.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_multi_program.py b/tests/test_multi_program.py index 17003e1c..20e13f97 100644 --- a/tests/test_multi_program.py +++ b/tests/test_multi_program.py @@ -35,6 +35,9 @@ if TYPE_CHECKING: ) +_non_linux: bool = platform.system() != 'Linux' + + def test_abort_on_sigint( daemon: subprocess.Popen, ): @@ -137,6 +140,7 @@ def test_non_registrar_spawns_child( reg_addr: UnwrappedAddress, loglevel: str, debug_mode: bool, + ci_env: bool, ): ''' Ensure a non-regristar (serving) root actor can spawn a sub and @@ -148,6 +152,12 @@ def test_non_registrar_spawns_child( ''' async def main(): + + # XXX, since apparently on macos in GH's CI it can be a race + # with the `daemon` registrar on grabbing the socket-addr.. + if ci_env and _non_linux: + await trio.sleep(.5) + async with tractor.open_nursery( registry_addrs=[reg_addr], loglevel=loglevel, From fb73935dbc241a94db55a7f73e5cdf523739d1f9 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 14:57:40 -0500 Subject: [PATCH 17/35] Add a `test_log` fixture for emitting from *within* test bodies or fixtures --- tests/conftest.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c77c5407..60225e00 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ import platform import time import pytest +import tractor from tractor._testing import ( examples_dir as examples_dir, tractor_test as tractor_test, @@ -65,7 +66,7 @@ def pytest_addoption( @pytest.fixture(scope='session', autouse=True) -def loglevel(request): +def loglevel(request) -> str: import tractor orig = tractor.log._default_loglevel level = tractor.log._default_loglevel = request.config.option.loglevel @@ -73,11 +74,46 @@ def loglevel(request): level=level, name='tractor', # <- enable root logger ) - log.info(f'Test-harness logging level: {level}\n') + log.info( + f'Test-harness set runtime loglevel: {level!r}\n' + ) yield level tractor.log._default_loglevel = orig +@pytest.fixture(scope='function') +def test_log( + request, + loglevel: str, +) -> tractor.log.StackLevelAdapter: + ''' + Deliver a per test-module-fn logger instance for reporting from + within actual test bodies/fixtures. + + For example this can be handy to report certain error cases from + exception handlers using `test_log.exception()`. + + ''' + modname: str = request.function.__module__ + log = tractor.log.get_logger( + name=modname, # <- enable root logger + # pkg_name='tests', + ) + _log = tractor.log.get_console_log( + level=loglevel, + logger=log, + name=modname, + # pkg_name='tests', + ) + _log.debug( + f'In-test-logging requested\n' + f'test_log.name: {log.name!r}\n' + f'level: {loglevel!r}\n' + + ) + yield _log + + _ci_env: bool = os.environ.get('CI', False) From 0e2949ea598806e7ddf83d236e7c69462635e2bc Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 14:58:16 -0500 Subject: [PATCH 18/35] Bump docs-exs subproc timeout, exception log any timeouts --- tests/test_docs_examples.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index b4cf85eb..8b7d4c72 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -9,8 +9,10 @@ import sys import subprocess import platform import shutil +from typing import Callable import pytest +import tractor from tractor._testing import ( examples_dir, ) @@ -101,8 +103,10 @@ def run_example_in_subproc( ids=lambda t: t[1], ) def test_example( - run_example_in_subproc, - example_script, + run_example_in_subproc: Callable, + example_script: str, + test_log: tractor.log.StackLevelAdapter, + ci_env: bool, ): ''' Load and run scripts from this repo's ``examples/`` dir as a user @@ -119,6 +123,8 @@ def test_example( if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9): pytest.skip("2-way streaming example requires py3.9 async with syntax") + timeout: float = 16 + with open(ex_file, 'r') as ex: code = ex.read() @@ -126,9 +132,12 @@ def test_example( err = None try: if not proc.poll(): - _, err = proc.communicate(timeout=15) + _, err = proc.communicate(timeout=timeout) except subprocess.TimeoutExpired as e: + test_log.exception( + f'Example failed to finish within {timeout}s ??\n' + ) proc.kill() err = e.stderr From a72bb9321e6bb2c47ae945a66ff9a24ba327e559 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 16:33:21 -0500 Subject: [PATCH 19/35] Bleh, bump timeout again for docs-exs suite when in CI --- tests/test_docs_examples.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index 8b7d4c72..d6794925 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -17,6 +17,8 @@ from tractor._testing import ( examples_dir, ) +_non_linux: bool = platform.system() != 'Linux' + @pytest.fixture def run_example_in_subproc( @@ -123,7 +125,10 @@ def test_example( if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9): pytest.skip("2-way streaming example requires py3.9 async with syntax") - timeout: float = 16 + timeout: float = ( + 30 if ci_env and _non_linux + else 16 + ) with open(ex_file, 'r') as ex: code = ex.read() From ab6c955949bd2f8fc86f6518efe5c835ee5faa81 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 17:44:21 -0500 Subject: [PATCH 20/35] Lol fine! bump it a bit more XD --- tests/test_docs_examples.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index d6794925..42a7fea5 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -126,7 +126,8 @@ def test_example( pytest.skip("2-way streaming example requires py3.9 async with syntax") timeout: float = ( - 30 if ci_env and _non_linux + 36 + if ci_env and _non_linux else 16 ) From 98a7d693415237181bcd7ade55d107d44b57f125 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 15:27:01 -0500 Subject: [PATCH 21/35] Always pre-sleep in `daemon` fixture when in non-linux CI.. --- tests/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 60225e00..09602c96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,7 @@ pytest_plugins: list[str] = [ 'tractor._testing.pytest', ] +_non_linux: bool = platform.system() != 'Linux' # Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives if platform.system() == 'Windows': @@ -150,6 +151,7 @@ def daemon( testdir: pytest.Pytester, reg_addr: tuple[str, int], tpt_proto: str, + ci_env: bool, ) -> subprocess.Popen: ''' @@ -197,6 +199,13 @@ def daemon( time.sleep(_PROC_SPAWN_WAIT) assert not proc.returncode + # TODO! we should poll for the registry socket-bind to take place + # and only once that's done yield to the requester! + # -[ ] use the `._root.open_root_actor()`::`ping_tpt_socket()` + # closure! + if _non_linux and ci_env: + time.sleep(1) + yield proc sig_prog(proc, _INT_SIGNAL) From 4639685770217fe5b4a63a7f97acd601cec7266f Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 17:13:23 -0500 Subject: [PATCH 22/35] Fill out types in `test_discovery` mod --- tests/test_discovery.py | 85 +++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 453b1aa3..3e5964ec 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1,11 +1,13 @@ """ -Actor "discovery" testing +Discovery subsys. + """ import os import signal import platform from functools import partial import itertools +from typing import Callable import psutil import pytest @@ -17,7 +19,9 @@ import trio @tractor_test -async def test_reg_then_unreg(reg_addr): +async def test_reg_then_unreg( + reg_addr: tuple, +): actor = tractor.current_actor() assert actor.is_arbiter assert len(actor._registry) == 1 # only self is registered @@ -82,11 +86,15 @@ async def say_hello_use_wait( @tractor_test -@pytest.mark.parametrize('func', [say_hello, say_hello_use_wait]) +@pytest.mark.parametrize( + 'func', + [say_hello, + say_hello_use_wait] +) async def test_trynamic_trio( - func, - start_method, - reg_addr, + func: Callable, + start_method: str, + reg_addr: tuple, ): ''' Root actor acting as the "director" and running one-shot-task-actors @@ -119,7 +127,10 @@ async def stream_forever(): await trio.sleep(0.01) -async def cancel(use_signal, delay=0): +async def cancel( + use_signal: bool, + delay: float = 0, +): # hold on there sally await trio.sleep(delay) @@ -132,13 +143,15 @@ async def cancel(use_signal, delay=0): raise KeyboardInterrupt -async def stream_from(portal): +async def stream_from(portal: tractor.Portal): async with portal.open_stream_from(stream_forever) as stream: async for value in stream: print(value) -async def unpack_reg(actor_or_portal): +async def unpack_reg( + actor_or_portal: tractor.Portal|tractor.Actor, +): ''' Get and unpack a "registry" RPC request from the "arbiter" registry system. @@ -173,7 +186,9 @@ async def spawn_and_check_registry( registry_addrs=[reg_addr], debug_mode=debug_mode, ): - async with tractor.get_registry(reg_addr) as portal: + async with tractor.get_registry( + addr=reg_addr, + ) as portal: # runtime needs to be up to call this actor = tractor.current_actor() @@ -246,10 +261,10 @@ async def spawn_and_check_registry( @pytest.mark.parametrize('with_streaming', [False, True]) def test_subactors_unregister_on_cancel( debug_mode: bool, - start_method, - use_signal, - reg_addr, - with_streaming, + start_method: str, + use_signal: bool, + reg_addr: tuple, + with_streaming: bool, ): ''' Verify that cancelling a nursery results in all subactors @@ -274,15 +289,17 @@ def test_subactors_unregister_on_cancel( def test_subactors_unregister_on_cancel_remote_daemon( daemon: subprocess.Popen, debug_mode: bool, - start_method, - use_signal, - reg_addr, - with_streaming, + start_method: str, + use_signal: bool, + reg_addr: tuple, + with_streaming: bool, ): - """Verify that cancelling a nursery results in all subactors - deregistering themselves with a **remote** (not in the local process - tree) arbiter. - """ + ''' + Verify that cancelling a nursery results in all subactors + deregistering themselves with a **remote** (not in the local + process tree) arbiter. + + ''' with pytest.raises(KeyboardInterrupt): trio.run( partial( @@ -374,14 +391,16 @@ async def close_chans_before_nursery( @pytest.mark.parametrize('use_signal', [False, True]) def test_close_channel_explicit( - start_method, - use_signal, - reg_addr, + start_method: str, + use_signal: bool, + reg_addr: tuple, ): - """Verify that closing a stream explicitly and killing the actor's + ''' + Verify that closing a stream explicitly and killing the actor's "root nursery" **before** the containing nursery tears down also results in subactor(s) deregistering from the arbiter. - """ + + ''' with pytest.raises(KeyboardInterrupt): trio.run( partial( @@ -396,14 +415,16 @@ def test_close_channel_explicit( @pytest.mark.parametrize('use_signal', [False, True]) def test_close_channel_explicit_remote_arbiter( daemon: subprocess.Popen, - start_method, - use_signal, - reg_addr, + start_method: str, + use_signal: bool, + reg_addr: tuple, ): - """Verify that closing a stream explicitly and killing the actor's + ''' + Verify that closing a stream explicitly and killing the actor's "root nursery" **before** the containing nursery tears down also results in subactor(s) deregistering from the arbiter. - """ + + ''' with pytest.raises(KeyboardInterrupt): trio.run( partial( From 776af3fce64520b6d0a2caec0480c9ea624be551 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 2 Mar 2026 18:08:58 -0500 Subject: [PATCH 23/35] Register our `ctlcs_bish` marker to avoid warnings --- tests/devx/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/devx/conftest.py b/tests/devx/conftest.py index b7559706..dc148d47 100644 --- a/tests/devx/conftest.py +++ b/tests/devx/conftest.py @@ -33,6 +33,14 @@ if TYPE_CHECKING: from pexpect import pty_spawn +def pytest_configure(config): + # register custom marks to avoid warnings see, + # https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers + config.addinivalue_line( + 'markers', + 'ctlcs_bish: test will (likely) not behave under SIGINT..' + ) + # a fn that sub-instantiates a `pexpect.spawn()` # and returns it. type PexpectSpawner = Callable[ From 5b2905b70212dd8e6db2083b9b987cc9516f79ef Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 3 Mar 2026 13:47:04 -0500 Subject: [PATCH 24/35] Xplatform tweaks for `daemon` fixture There's a very sloppy registrar-actor-bootup syncing approach used in this fixture (basically just guessing how long to sleep to wait for it to init and bind the registry socket) using a `global _PROC_SPAWN_WAIT` that needs to be made more reliable. But, for now i'm just playing along with what's there to try and make less CI runs flaky by, - sleeping *another* 1s when run from non-linux CI. - reporting stdout (if any) alongside stderr on teardown. - not strictly requiring a `proc.returncode == -2` indicating successful graceful cancellation via SIGINT; instead we now error-log and only raise the RTE on `< 0` exit code. * though i can't think of why this would happen other then an underlying crash which should propagate.. but i don't think any test suite does this intentionally rn? * though i don't think it should ever happen, having a CI run "error"-fail bc of this isn't all that illuminating, if there is some weird `.returncode == 0` termination case it's likely not a failure? For later, see the new todo list; we should sync to some kind of "ping" polling of the tpt address if possible which is already easy enough for TCP reusing an internal closure from `._root.open_root_actor()`. --- tests/conftest.py | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 09602c96..31787fe2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -189,41 +189,58 @@ def daemon( **kwargs, ) + # TODO! we should poll for the registry socket-bind to take place + # and only once that's done yield to the requester! + # -[ ] TCP: use the `._root.open_root_actor()`::`ping_tpt_socket()` + # closure! + # -[ ] UDS: can we do something similar for 'pinging" the + # file-socket? + # + global _PROC_SPAWN_WAIT # UDS sockets are **really** fast to bind()/listen()/connect() # so it's often required that we delay a bit more starting # the first actor-tree.. if tpt_proto == 'uds': - global _PROC_SPAWN_WAIT _PROC_SPAWN_WAIT = 0.6 + if _non_linux and ci_env: + _PROC_SPAWN_WAIT += 1 + + # XXX, allow time for the sub-py-proc to boot up. + # !TODO, see ping-polling ideas above! time.sleep(_PROC_SPAWN_WAIT) assert not proc.returncode - # TODO! we should poll for the registry socket-bind to take place - # and only once that's done yield to the requester! - # -[ ] use the `._root.open_root_actor()`::`ping_tpt_socket()` - # closure! - if _non_linux and ci_env: - time.sleep(1) - yield proc sig_prog(proc, _INT_SIGNAL) # XXX! yeah.. just be reaaal careful with this bc sometimes it # can lock up on the `_io.BufferedReader` and hang.. stderr: str = proc.stderr.read().decode() - if stderr: + stdout: str = proc.stdout.read().decode() + if ( + stderr + or + stdout + ): print( - f'Daemon actor tree produced STDERR:\n' + f'Daemon actor tree produced output:\n' f'{proc.args}\n' f'\n' - f'{stderr}\n' + f'stderr: {stderr!r}\n' + f'stdout: {stdout!r}\n' ) - if proc.returncode != -2: - raise RuntimeError( - 'Daemon actor tree failed !?\n' - f'{proc.args}\n' + + if (rc := proc.returncode) != -2: + msg: str = ( + f'Daemon actor tree was not cancelled !?\n' + f'proc.args: {proc.args!r}\n' + f'proc.returncode: {rc!r}\n' ) + if rc < 0: + raise RuntimeError(msg) + + log.error(msg) # @pytest.fixture(autouse=True) From 79396b4a26709734907872da535bec45c73c65f8 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 3 Mar 2026 15:46:21 -0500 Subject: [PATCH 25/35] 2x the ctl-c loop prompt-timeout for non-linux in CI --- tests/devx/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devx/conftest.py b/tests/devx/conftest.py index dc148d47..c850da7a 100644 --- a/tests/devx/conftest.py +++ b/tests/devx/conftest.py @@ -293,7 +293,7 @@ def do_ctlc( time.sleep(delay) child.expect( PROMPT, - # timeout=1, # TODO? if needed + timeout=(child.timeout * 2) if _ci_env else child.timeout, ) before = str(child.before.decode()) time.sleep(delay) From 712c009790681df4e3d51a712490ae5faf9d54f0 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 3 Mar 2026 20:55:57 -0500 Subject: [PATCH 26/35] Hike `testdir.spawn()` timeout on non-linux in CI --- tests/devx/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/devx/conftest.py b/tests/devx/conftest.py index c850da7a..c194026c 100644 --- a/tests/devx/conftest.py +++ b/tests/devx/conftest.py @@ -3,8 +3,9 @@ ''' from __future__ import annotations -import time +import platform import signal +import time from typing import ( Callable, TYPE_CHECKING, @@ -33,6 +34,9 @@ if TYPE_CHECKING: from pexpect import pty_spawn +_non_linux: bool = platform.system() != 'Linux' + + def pytest_configure(config): # register custom marks to avoid warnings see, # https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers @@ -94,7 +98,10 @@ def spawn( cmd, **mkcmd_kwargs, ), - expect_timeout=3, + expect_timeout=( + 6 if _non_linux and _ci_env + else 3 + ), # preexec_fn=unset_colors, # ^TODO? get `pytest` core to expose underlying # `pexpect.spawn()` stuff? From 016306adf5c4cf3ae028202c247842d6a49767db Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 4 Mar 2026 11:49:01 -0500 Subject: [PATCH 27/35] Allow `ctlcs_bish()` skipping Via ensuring `all(mark.args)` on wtv expressions are arg-passed to the mark decorator; use it to skip the `test_subactor_breakpoint` suite when `ctlc=True` since it seems too unreliable in CI. --- tests/devx/conftest.py | 4 +++- tests/devx/test_debugger.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/devx/conftest.py b/tests/devx/conftest.py index c194026c..fbb4ff6b 100644 --- a/tests/devx/conftest.py +++ b/tests/devx/conftest.py @@ -99,7 +99,7 @@ def spawn( **mkcmd_kwargs, ), expect_timeout=( - 6 if _non_linux and _ci_env + 10 if _non_linux and _ci_env else 3 ), # preexec_fn=unset_colors, @@ -164,6 +164,8 @@ def ctlc( mark.name == 'ctlcs_bish' and use_ctlc + and + all(mark.args) ): pytest.skip( f'Test {node} prolly uses something from the stdlib (namely `asyncio`..)\n' diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index 43adbec1..7f602441 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -37,6 +37,9 @@ from .conftest import ( in_prompt_msg, assert_before, ) +from ..conftest import ( + _ci_env, +) if TYPE_CHECKING: from ..conftest import PexpectSpawner @@ -259,6 +262,11 @@ def test_subactor_error( child.expect(EOF) +# skip on non-Linux CI +@pytest.mark.ctlcs_bish( + _non_linux, + _ci_env, +) def test_subactor_breakpoint( spawn, ctlc: bool, From bbc028e84c8692ccdaf15dd0f1bed7ebe94edb75 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 9 Mar 2026 16:17:46 -0400 Subject: [PATCH 28/35] Increase macos job timeout to 16s --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f85019a9..195a3c46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: testing-macos: name: '${{ matrix.os }} Python${{ matrix.python-version }} - spawn_backend=${{ matrix.spawn_backend }}' - timeout-minutes: 10 + timeout-minutes: 16 runs-on: ${{ matrix.os }} strategy: From b71e8575e5d5755dd91606ab9d29dff9ce9add30 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 4 Mar 2026 12:25:03 -0500 Subject: [PATCH 29/35] Skip a couple more `ctlc` flaking suites --- tests/devx/test_debugger.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index 7f602441..e6e40b51 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -197,6 +197,11 @@ def test_root_actor_bp_forever( child.expect(EOF) +# skip on non-Linux CI +@pytest.mark.ctlcs_bish( + _non_linux, + _ci_env, +) @pytest.mark.parametrize( 'do_next', (True, False), @@ -968,6 +973,11 @@ def test_different_debug_mode_per_actor( ) +# skip on non-Linux CI +@pytest.mark.ctlcs_bish( + _non_linux, + _ci_env, +) def test_post_mortem_api( spawn, ctlc: bool, From fb94aa00956bb59500008bc7d44f90ac28816f2d Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 9 Mar 2026 19:35:47 -0400 Subject: [PATCH 30/35] Tidy a typing-typo, add explicit `ids=` for paramed suites --- tests/test_2way.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/test_2way.py b/tests/test_2way.py index f5a59cfe..2d052668 100644 --- a/tests/test_2way.py +++ b/tests/test_2way.py @@ -6,23 +6,16 @@ msg patterns. from __future__ import annotations from typing import ( Callable, - TYPE_CHECKING, ) import pytest import trio import tractor -if TYPE_CHECKING: - from tractor import ( - Portal, - ) - @tractor.context async def simple_rpc( ctx: tractor.Context, data: int, - ) -> None: ''' Test a small ping-pong server. @@ -51,7 +44,6 @@ async def simple_rpc( async def simple_rpc_with_forloop( ctx: tractor.Context, data: int, - ) -> None: ''' Same as previous test but using `async for` syntax/api. @@ -77,15 +69,25 @@ async def simple_rpc_with_forloop( @pytest.mark.parametrize( 'use_async_for', - [True, False], + [ + True, + False, + ], + ids='use_async_for={}'.format, ) @pytest.mark.parametrize( 'server_func', - [simple_rpc, simple_rpc_with_forloop], + [ + simple_rpc, + simple_rpc_with_forloop, + ], + ids='server_func={}'.format, ) def test_simple_rpc( - server_func: Callabe, + server_func: Callable, use_async_for: bool, + loglevel: str, + debug_mode: bool, ): ''' The simplest request response pattern. @@ -93,8 +95,11 @@ def test_simple_rpc( ''' async def main(): with trio.fail_after(6): - async with tractor.open_nursery() as an: - portal: Portal = await an.start_actor( + async with tractor.open_nursery( + loglevel=loglevel, + debug_mode=debug_mode, + ) as an: + portal: tractor.Portal = await an.start_actor( 'rpc_server', enable_modules=[__name__], ) From d135ce94afcca42f0a0e945ebac18f6bea5ea576 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 9 Mar 2026 19:46:25 -0400 Subject: [PATCH 31/35] Restyle `test_legacy_one_way_streaming` mod - convert all doc-strings to `'''` multiline style. - rename `nursery` -> `an`, `n` -> `tn` to match project-wide conventions. - add type annotations to fn params (fixtures, test helpers). - break long lines into multiline style for fn calls, assertions, and `parametrize` decorator lists. - add `ids=` to `@pytest.mark.parametrize`. - use `'` over `"` for string literals. - add `from typing import Callable` import. - drop spurious blank lines inside generators. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_legacy_one_way_streaming.py | 139 +++++++++++++++++-------- 1 file changed, 94 insertions(+), 45 deletions(-) diff --git a/tests/test_legacy_one_way_streaming.py b/tests/test_legacy_one_way_streaming.py index a685f924..e7821661 100644 --- a/tests/test_legacy_one_way_streaming.py +++ b/tests/test_legacy_one_way_streaming.py @@ -1,9 +1,11 @@ """ -Streaming via async gen api +Streaming via the, now legacy, "async-gen API". + """ import time from functools import partial import platform +from typing import Callable import trio import tractor @@ -19,7 +21,11 @@ def test_must_define_ctx(): async def no_ctx(): pass - assert "no_ctx must be `ctx: tractor.Context" in str(err.value) + assert ( + "no_ctx must be `ctx: tractor.Context" + in + str(err.value) + ) @tractor.stream async def has_ctx(ctx): @@ -69,14 +75,14 @@ async def stream_from_single_subactor( async with tractor.open_nursery( registry_addrs=[reg_addr], start_method=start_method, - ) as nursery: + ) as an: async with tractor.find_actor('streamerd') as portals: if not portals: # no brokerd actor found - portal = await nursery.start_actor( + portal = await an.start_actor( 'streamerd', enable_modules=[__name__], ) @@ -116,11 +122,22 @@ async def stream_from_single_subactor( @pytest.mark.parametrize( - 'stream_func', [async_gen_stream, context_stream] + 'stream_func', + [ + async_gen_stream, + context_stream, + ], + ids='stream_func={}'.format ) -def test_stream_from_single_subactor(reg_addr, start_method, stream_func): - """Verify streaming from a spawned async generator. - """ +def test_stream_from_single_subactor( + reg_addr: tuple, + start_method: str, + stream_func: Callable, +): + ''' + Verify streaming from a spawned async generator. + + ''' trio.run( partial( stream_from_single_subactor, @@ -132,10 +149,9 @@ def test_stream_from_single_subactor(reg_addr, start_method, stream_func): # this is the first 2 actors, streamer_1 and streamer_2 -async def stream_data(seed): +async def stream_data(seed: int): for i in range(seed): - yield i # trigger scheduler to simulate practical usage @@ -143,15 +159,17 @@ async def stream_data(seed): # this is the third actor; the aggregator -async def aggregate(seed): - """Ensure that the two streams we receive match but only stream +async def aggregate(seed: int): + ''' + Ensure that the two streams we receive match but only stream a single set of values to the parent. - """ - async with tractor.open_nursery() as nursery: + + ''' + async with tractor.open_nursery() as an: portals = [] for i in range(1, 3): # fork point - portal = await nursery.start_actor( + portal = await an.start_actor( name=f'streamer_{i}', enable_modules=[__name__], ) @@ -164,7 +182,8 @@ async def aggregate(seed): async with send_chan: async with portal.open_stream_from( - stream_data, seed=seed, + stream_data, + seed=seed, ) as stream: async for value in stream: @@ -174,10 +193,14 @@ async def aggregate(seed): print(f"FINISHED ITERATING {portal.channel.uid}") # spawn 2 trio tasks to collect streams and push to a local queue - async with trio.open_nursery() as n: + async with trio.open_nursery() as tn: for portal in portals: - n.start_soon(push_to_chan, portal, send_chan.clone()) + tn.start_soon( + push_to_chan, + portal, + send_chan.clone(), + ) # close this local task's reference to send side await send_chan.aclose() @@ -194,20 +217,21 @@ async def aggregate(seed): print("FINISHED ITERATING in aggregator") - await nursery.cancel() + await an.cancel() print("WAITING on `ActorNursery` to finish") print("AGGREGATOR COMPLETE!") -# this is the main actor and *arbiter* -async def a_quadruple_example(): - # a nursery which spawns "actors" - async with tractor.open_nursery() as nursery: +async def a_quadruple_example() -> list[int]: + ''' + Open the root-actor which is also a "registrar". + ''' + async with tractor.open_nursery() as an: seed = int(1e3) pre_start = time.time() - portal = await nursery.start_actor( + portal = await an.start_actor( name='aggregator', enable_modules=[__name__], ) @@ -228,8 +252,14 @@ async def a_quadruple_example(): return result_stream -async def cancel_after(wait, reg_addr): - async with tractor.open_root_actor(registry_addrs=[reg_addr]): +async def cancel_after( + wait: float, + reg_addr: tuple, +) -> list[int]: + + async with tractor.open_root_actor( + registry_addrs=[reg_addr], + ): with trio.move_on_after(wait): return await a_quadruple_example() @@ -242,7 +272,7 @@ def time_quad_ex( ): non_linux: bool = (_sys := platform.system()) != 'Linux' if ci_env and non_linux: - pytest.skip("Test is too flaky on {_sys!r} in CI") + pytest.skip(f'Test is too flaky on {_sys!r} in CI') if spawn_backend == 'mp': ''' @@ -253,14 +283,18 @@ def time_quad_ex( timeout = 7 if non_linux else 4 start = time.time() - results = trio.run(cancel_after, timeout, reg_addr) - diff = time.time() - start + results: list[int] = trio.run( + cancel_after, + timeout, + reg_addr, + ) + diff: float = time.time() - start assert results return results, diff def test_a_quadruple_example( - time_quad_ex: tuple, + time_quad_ex: tuple[list[int], float], ci_env: bool, spawn_backend: str, ): @@ -284,19 +318,33 @@ def test_a_quadruple_example( list(map(lambda i: i/10, range(3, 9))) ) def test_not_fast_enough_quad( - reg_addr, time_quad_ex, cancel_delay, ci_env, spawn_backend + reg_addr: tuple, + time_quad_ex: tuple[list[int], float], + cancel_delay: float, + ci_env: bool, + spawn_backend: str, ): - """Verify we can cancel midway through the quad example and all actors - cancel gracefully. - """ + ''' + Verify we can cancel midway through the quad example and all + actors cancel gracefully. + + ''' results, diff = time_quad_ex delay = max(diff - cancel_delay, 0) - results = trio.run(cancel_after, delay, reg_addr) - system = platform.system() - if system in ('Windows', 'Darwin') and results is not None: + results = trio.run( + cancel_after, + delay, + reg_addr, + ) + system: str = platform.system() + if ( + system in ('Windows', 'Darwin') + and + results is not None + ): # In CI envoirments it seems later runs are quicker then the first # so just ignore these - print(f"Woa there {system} caught your breath eh?") + print(f'Woa there {system} caught your breath eh?') else: # should be cancelled mid-streaming assert results is None @@ -304,23 +352,24 @@ def test_not_fast_enough_quad( @tractor_test async def test_respawn_consumer_task( - reg_addr, - spawn_backend, - loglevel, + reg_addr: tuple, + spawn_backend: str, + loglevel: str, ): - """Verify that ``._portal.ReceiveStream.shield()`` + ''' + Verify that ``._portal.ReceiveStream.shield()`` sucessfully protects the underlying IPC channel from being closed when cancelling and respawning a consumer task. This also serves to verify that all values from the stream can be received despite the respawns. - """ + ''' stream = None - async with tractor.open_nursery() as n: + async with tractor.open_nursery() as an: - portal = await n.start_actor( + portal = await an.start_actor( name='streamer', enable_modules=[__name__] ) From f9bdb1b35ddaf081466a29ed3400ae21aecd919b Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 5 Mar 2026 21:52:31 -0500 Subject: [PATCH 32/35] Try one more timeout bumps for flaky docs streaming ex.. --- tests/test_docs_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index 42a7fea5..9f386c62 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -126,7 +126,7 @@ def test_example( pytest.skip("2-way streaming example requires py3.9 async with syntax") timeout: float = ( - 36 + 60 if ci_env and _non_linux else 16 ) From afd66ce3b732f75496382a5b69df782ffec5326e Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 5 Mar 2026 23:07:02 -0500 Subject: [PATCH 33/35] Final try, drop logging level in streaming example to see if macos can cope.. --- examples/full_fledged_streaming_service.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/full_fledged_streaming_service.py b/examples/full_fledged_streaming_service.py index d859f647..390a1b75 100644 --- a/examples/full_fledged_streaming_service.py +++ b/examples/full_fledged_streaming_service.py @@ -90,7 +90,7 @@ async def main() -> list[int]: # yes, a nursery which spawns `trio`-"actors" B) an: ActorNursery async with tractor.open_nursery( - loglevel='cancel', + loglevel='error', # debug_mode=True, ) as an: @@ -118,8 +118,10 @@ async def main() -> list[int]: cancelled: bool = await portal.cancel_actor() assert cancelled - print(f"STREAM TIME = {time.time() - start}") - print(f"STREAM + SPAWN TIME = {time.time() - pre_start}") + print( + f"STREAM TIME = {time.time() - start}\n" + f"STREAM + SPAWN TIME = {time.time() - pre_start}\n" + ) assert result_stream == list(range(seed)) return result_stream From 9c4cd869fb08f16f0b855df9afaa49753a2b2017 Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 5 Mar 2026 23:43:23 -0500 Subject: [PATCH 34/35] OK-FINE, skip streaming docs example on macos! It seems something is up with their VM-img or wtv bc i keep increasing the subproc timeout and nothing is changing. Since i can't try a `-xlarge` one without paying i'm just muting this test for now. --- tests/test_docs_examples.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index 9f386c62..0cf55d51 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -18,6 +18,7 @@ from tractor._testing import ( ) _non_linux: bool = platform.system() != 'Linux' +_friggin_macos: bool = platform.system() == 'Darwin' @pytest.fixture @@ -122,9 +123,26 @@ def test_example( ''' ex_file: str = os.path.join(*example_script) - if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9): + if ( + 'rpc_bidir_streaming' in ex_file + and + sys.version_info < (3, 9) + ): pytest.skip("2-way streaming example requires py3.9 async with syntax") + if ( + 'full_fledged_streaming_service' in ex_file + and + _friggin_macos + and + ci_env + ): + pytest.skip( + 'Streaming example is too flaky in CI\n' + 'AND their competitor runs this CI service..\n' + 'This test does run just fine "in person" however..' + ) + timeout: float = ( 60 if ci_env and _non_linux From 6ee0149e8dffce64c4ee5ea78e528057466a6a6a Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 6 Mar 2026 12:03:33 -0500 Subject: [PATCH 35/35] Another cancellation test timeout bump for non-linux --- tests/test_cancellation.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/test_cancellation.py b/tests/test_cancellation.py index 27fd59d7..7d14df12 100644 --- a/tests/test_cancellation.py +++ b/tests/test_cancellation.py @@ -17,8 +17,8 @@ from tractor._testing import ( from .conftest import no_windows -def is_win(): - return platform.system() == 'Windows' +_non_linux: bool = platform.system() != 'Linux' +_friggin_windows: bool = platform.system() == 'Windows' async def assert_err(delay=0): @@ -431,7 +431,7 @@ async def test_nested_multierrors(loglevel, start_method): for subexc in err.exceptions: # verify first level actor errors are wrapped as remote - if is_win(): + if _friggin_windows: # windows is often too slow and cancellation seems # to happen before an actor is spawned @@ -464,7 +464,7 @@ async def test_nested_multierrors(loglevel, start_method): # XXX not sure what's up with this.. # on windows sometimes spawning is just too slow and # we get back the (sent) cancel signal instead - if is_win(): + if _friggin_windows: if isinstance(subexc, tractor.RemoteActorError): assert subexc.boxed_type in ( BaseExceptionGroup, @@ -507,17 +507,22 @@ def test_cancel_via_SIGINT( @no_windows def test_cancel_via_SIGINT_other_task( - loglevel, - start_method, - spawn_backend, + loglevel: str, + start_method: str, + spawn_backend: str, ): - """Ensure that a control-C (SIGINT) signal cancels both the parent - and child processes in trionic fashion even a subprocess is started - from a seperate ``trio`` child task. - """ - pid = os.getpid() - timeout: float = 2 - if is_win(): # smh + ''' + Ensure that a control-C (SIGINT) signal cancels both the parent + and child processes in trionic fashion even a subprocess is + started from a seperate ``trio`` child task. + + ''' + pid: int = os.getpid() + timeout: float = ( + 4 if _non_linux + else 2 + ) + if _friggin_windows: # smh timeout += 1 async def spawn_and_sleep_forever( @@ -696,7 +701,7 @@ def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon( kbi_delay = 0.5 timeout: float = 2.9 - if is_win(): # smh + if _friggin_windows: # smh timeout += 1 async def main():