tractor/tests/test_aio_channel_exit.py

676 lines
25 KiB
Python
Raw Normal View History

Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
'''
Inter-loop channel teardown must handle the issued exit signal.
'''
import asyncio
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
import os
from pathlib import Path
import signal
import subprocess
import sys
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
from contextlib import asynccontextmanager as acm
from collections.abc import AsyncIterator
import pytest
import tractor
from tractor import (
to_asyncio,
trionics,
)
import trio
_num_tasks: int = 2
_rpc_repetitions: int = 2
_actor_test_timeout_s: float = 10
async def exercise_trio_side_exit(
cached: bool,
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
cancel_trio_side: bool,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
suppress: bool,
expect_aio_side_err_type: str|None,
greenback_shim: bool,
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
suppress_aio_side_tte: bool = False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
) -> None:
'''
Exit direct and cached inter-loop channels in an infected actor.
Failure mechanism:
- The trio-side parent task exits with asyncio still running.
- ``open_channel_from()`` signals the asyncio task to exit.
- Previously, that signal escaped channel teardown and became
an unhandled exception from the asyncio done callback.
Cases:
- ``cached`` wraps the channel API in ``maybe_open_context()``.
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
- ``cancel_trio_side`` cancels the caller at a Trio checkpoint.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
- ``suppress`` selects graceful or strict exit-signal reporting.
- ``expect_aio_side_err_type`` is a class name or ``None``.
Class names travel as RPC payloads; actual exception objects
remain inside this actor for identity/cause assertions.
- ``greenback_shim`` wraps the asyncio task without enabling PDB.
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
- ``suppress_aio_side_tte`` handles the issued signal and returns
normally instead of raising it or a distinct cleanup failure.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
Ordering and proof:
- ``tn.start()`` waits for a sibling subscription; ``release``
lets it retire while the original subscriber remains alive.
- The asyncio task must finish before channel-context exit ends.
- Identity assertions distinguish our signal from cleanup faults.
- Each of ``_num_tasks`` iterations opens a fresh asyncio task.
- A host-loop callback sets ``drained`` before callback-failure
inspection, independently of actor/RPC survival.
'''
if expect_aio_side_err_type not in {
None,
'RuntimeError',
'TrioTaskExited',
}:
raise ValueError(
f'Unknown aio error case: {expect_aio_side_err_type!r}'
)
# Keep the actual objects: equality/type checks cannot prove that
# teardown distinguishes an issued signal from a new same-type
# asyncio failure. Task/signal/failure entries follow iteration
# order; callback_errors records host-loop exception contexts.
callback_errors: list[dict] = []
tasks: list[asyncio.Task] = []
failures: list[Exception] = []
signals: list[BaseException] = []
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
previous_handler = loop.get_exception_handler()
loop.set_exception_handler(
lambda loop, context: callback_errors.append(context),
)
async def block_until_trio_side_exit(
chan: to_asyncio.LinkedTaskChannel,
) -> None:
'''
Block until the channel API signals trio-side parent exit.
- Record task identity, finish startup, send a channel value,
then await an event which is never set.
- Record the received ``TrioTaskExited`` object and either
re-raise it or raise the selected distinct cleanup failure.
- ``tasks``, ``signals`` and ``failures`` retain the objects
checked after teardown; ``greenback_shim`` selects whether
this coroutine initializes a Greenback portal.
'''
task: asyncio.Task|None = asyncio.current_task()
assert task is not None
tasks.append(task)
if greenback_shim:
import greenback
await greenback.ensure_portal()
chan.started_nowait('ready')
chan.send_nowait('tick')
try:
await asyncio.Event().wait()
except to_asyncio.TrioTaskExited as signal:
signals.append(signal)
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
# Returning here tests successful asyncio-side completion
# after it handles the trio-side parent's exit signal.
if suppress_aio_side_tte:
return
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
# Healthy cleanup re-raises the very signal issued by
# open_channel_from(); suppression uses object identity.
if expect_aio_side_err_type is None:
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
raise signal
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
failure: RuntimeError|to_asyncio.TrioTaskExited
if expect_aio_side_err_type == 'RuntimeError':
# Fail during cleanup after receiving the channel
# owner's exit notification.
failure = RuntimeError(
'asyncio cleanup failed after trio-side exit',
)
elif expect_aio_side_err_type == 'TrioTaskExited':
# Deliberately use the SAME TYPE as the issued signal
# but a DIFFERENT INSTANCE. A blanket TrioTaskExited
# handler would hide this distinct asyncio error.
failure = to_asyncio.TrioTaskExited(
'new asyncio TTE, not the issued exit signal',
)
# Retain both objects: assertions below require the new
# failure to propagate with the issued signal as cause.
# "Independent" means distinct from the issued signal;
# the cleanup failure still follows that notification.
failures.append(failure)
raise failure from signal
@acm
async def open_raw_aio_chan() -> AsyncIterator[
to_asyncio.LinkedTaskChannel
]:
'''
Open the blocking asyncio task through open_channel_from().
- Run ``block_until_trio_side_exit`` using the actual
``to_asyncio.open_channel_from()`` API.
- Forward captured ``suppress`` unchanged so strict reporting
and graceful suppression exercise the same ownership path.
- Yield after the asyncio task's startup handshake. Context
exit signals that task and waits for it to finish.
'''
chan: to_asyncio.LinkedTaskChannel
first: str
async with to_asyncio.open_channel_from(
block_until_trio_side_exit,
suppress_graceful_exits=suppress,
) as (chan, first):
assert first == 'ready'
yield chan
@acm
async def open_maybe_cached_aio_chan() -> AsyncIterator[
to_asyncio.LinkedTaskChannel
]:
'''
Optionally cache the channel with ``maybe_open_context()``.
With captured ``cached=True``, overlapping calling trio.Tasks
receive the same channel instance. Otherwise each call enters
``open_raw_aio_chan()`` directly.
'''
chan: to_asyncio.LinkedTaskChannel
if cached:
async with trionics.maybe_open_context(
acm_func=open_raw_aio_chan,
# Same factory/key means both calling trio.Tasks get
# the same underlying channel. Reuse it on subsequent
# iterations to check retirement and reallocation.
key='aio_chan',
) as (_, chan):
yield chan
else:
async with open_raw_aio_chan() as chan:
yield chan
async def subscribe_cached_chan(
expected: to_asyncio.LinkedTaskChannel,
release: trio.Event,
*,
task_status: trio.TaskStatus[None] = (
trio.TASK_STATUS_IGNORED
),
) -> None:
'''
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
Close a subscription while another stays open in the caller.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
- Enter ``open_maybe_cached_aio_chan`` in another trio.Task.
- Require ``expected`` identity to prove actual cache reuse.
- Report started only once the broadcast subscription exists.
- Wait for ``release`` before exiting; the caller joins this
task before checking that the asyncio task is still alive.
'''
other: to_asyncio.LinkedTaskChannel
async with open_maybe_cached_aio_chan() as other:
assert other is expected
async with other.subscribe():
task_status.started()
await release.wait()
try:
task_index: int
for task_index in range(_num_tasks):
caught: BaseException|None = None
try:
cs: trio.CancelScope
chan: to_asyncio.LinkedTaskChannel
bchan: trionics.BroadcastReceiver
with trio.CancelScope() as cs:
async with open_maybe_cached_aio_chan() as chan:
async with chan.subscribe() as bchan:
if cached:
release: trio.Event = trio.Event()
tn: trio.Nursery
async with (
trio.open_nursery() as tn
):
await tn.start(
subscribe_cached_chan,
chan,
release,
)
release.set()
# The sibling has closed its handles.
# Our subscription still retains the
# aio task; this is not final exit.
assert not tasks[task_index].done()
# Consume a real broadcast value before
# exit or last-consumer cancellation.
assert await bchan.receive() == 'tick'
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
if cancel_trio_side:
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
cs.cancel()
await trio.lowlevel.checkpoint()
except Exception as error:
caught = error
assert tasks[task_index].done()
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
num_signals: int = len(signals)
expect_num_signals: int = task_index + 1
task_error: BaseException|None = (
tasks[task_index].exception()
)
assert num_signals == expect_num_signals, (
f'Expected one issued TTE per asyncio task\n'
f'task index: {task_index}\n'
f'expected signals: {expect_num_signals}\n'
f'recorded signal count: {num_signals}\n'
f'caught trio-side error: {caught!r}\n'
f'asyncio task error: {task_error!r}\n'
f'recorded signals: {signals!r}\n'
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
)
if expect_aio_side_err_type:
# For TTE, matching the issued signal's type must NOT
# suppress a distinct asyncio-side exception.
# Keep its identity AND its original shutdown cause.
assert caught is failures[task_index]
assert caught.__cause__ is signals[task_index]
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
elif suppress_aio_side_tte:
assert caught is None, repr(caught)
assert tasks[task_index].exception() is None
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
elif not suppress:
# Strict reporting returns the exact issued signal,
# not a new wrapper or an asyncio-created substitute.
assert caught is signals[task_index]
else:
# Default policy absorbs expected owner exit, even
# when the cached resource's last consumer cancels.
assert caught is None, repr(caught)
# Cross the host loop's ready queue before accepting callback
# cleanliness, independently of actor survival.
drained: trio.Event = trio.Event()
loop.call_soon(drained.set)
await drained.wait()
if not expect_aio_side_err_type:
# Expected shutdown must have no unhandled callbacks.
# Fault cases above test propagation and causes, without
# asserting a broader callback-cleanliness policy.
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
callback_report: str = '\n'.join(
f'callback {index}:\n'
f' message: {context.get("message")!r}\n'
f' exception: {context.get("exception")!r}\n'
f' handle: {context.get("handle")!r}\n'
f' task: {context.get("task")!r}\n'
f' future: {context.get("future")!r}'
for index, context in enumerate(callback_errors)
)
assert not callback_errors, (
f'Unhandled asyncio callbacks after channel exit\n'
f'cached: {cached}\n'
f'cancel_trio_side: {cancel_trio_side}\n'
f'suppress_graceful_exits: {suppress}\n'
f'greenback_shim: {greenback_shim}\n'
f'{callback_report}\n'
)
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
finally:
loop.set_exception_handler(previous_handler)
@pytest.mark.parametrize(
'greenback_shim',
[False, True],
ids=['native', 'greenback'],
)
@pytest.mark.parametrize(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
'infect_root',
[False, True],
ids=['infected-child', 'infected-root'],
)
@pytest.mark.parametrize(
'cached,cancel_trio_side,suppress,expect_aio_side_err_type,'
'suppress_aio_side_tte',
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
[
# Expected TTE ack: direct use, last cached caller
# exit, and cancellation of that last cached caller.
pytest.param(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
False, False, True, None, False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
id='direct-close',
),
pytest.param(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
True, False, True, None, False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
id='last-cache-user',
),
pytest.param(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
True, True, True, None, False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
id='cancelled-last-cache-user',
),
# Strict reporting must preserve the issued TTE's identity.
pytest.param(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
True, False, False, None, False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
id='explicit-exit-reporting',
),
# New asyncio-side cleanup failures must escape suppression,
# even when the exception class matches the issued signal.
pytest.param(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
True, False, True, 'RuntimeError', False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
id='independent-cleanup-error',
),
pytest.param(
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
True, False, True, 'TrioTaskExited', False,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
id='independent-same-type-error',
),
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
# The asyncio task may handle the TTE and finish normally.
pytest.param(
False, False, True, None, True,
id='direct-suppress-tte-and-return',
),
pytest.param(
True, False, True, None, True,
id='cached-suppress-tte-and-return',
),
# Strict trio-side policy must not manufacture a task error
# when the asyncio-side task handled its signal successfully.
pytest.param(
False, False, False, None, True,
id='strict-direct-suppress-tte-and-return',
),
pytest.param(
True, False, False, None, True,
id='strict-cached-suppress-tte-and-return',
),
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
],
)
def test_channel_owner_exit(
reg_addr: tuple[str, int],
cached: bool,
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
cancel_trio_side: bool,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
suppress: bool,
expect_aio_side_err_type: str|None,
greenback_shim: bool,
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
infect_root: bool,
suppress_aio_side_tte: bool,
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
) -> None:
'''
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
Exercise trio-side channel exit in an infected root or child.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
Regression:
- ``open_channel_from()`` signalled the running asyncio task when
its trio-side parent task left the channel context.
- Raising that issued ``TrioTaskExited`` escaped default graceful
suppression and was also raised from the asyncio done callback.
Proof boundary:
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
- Infected-child cases invoke ``exercise_trio_side_exit`` by TCP
RPC. Infected-root cases call it directly in the guest Trio run
hosted by asyncio, with the root actor runtime open.
- Both use the actual channel API and broadcast subscriptions;
cached cases additionally wrap it in ``maybe_open_context()``.
- Sequential repeats check continued actor usability after prior
asyncio tasks finish; the child path also checks RPC usability.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
- Strict reporting and independent faults retain their identity;
broad exception-type suppression cannot satisfy the matrix.
- The deadline bounds hangs; no live broker or debugger is used.
Downstream reproduction:
Piker encountered this during quote-task restart after a farm
reset, using a cached channel. This test needs neither that
application nor caching in its direct-channel case.
'''
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
if not infect_root:
async def main() -> None:
'''
Run the selected case directly in a root or via child
RPCs.
- reg_addr isolates the runtime's TCP registry.
- Each call finishes before the next starts.
- The child path explicitly cancels the actor after its
RPCs; completing an RPC does not end the actor's
lifetime.
- A delayed done-callback failure could terminate the
child after an RPC body finishes. Another RPC checks
usability; the endpoint also inspects callback
exceptions directly.
'''
an: tractor.ActorNursery
with trio.fail_after(_actor_test_timeout_s):
async with tractor.open_nursery(
registry_addrs=[reg_addr],
) as an:
portal: tractor.Portal = await an.start_actor(
'aio_channel_owner',
enable_modules=[__name__],
infect_asyncio=True,
)
# portal.run() waits for this remote task's final
# result. These RPCs are sequential, not concurrent;
# only the endpoint's subscription tasks overlap.
for _ in range(_rpc_repetitions):
await portal.run(
exercise_trio_side_exit,
cached=cached,
cancel_trio_side=cancel_trio_side,
suppress=suppress,
expect_aio_side_err_type=(
expect_aio_side_err_type
),
greenback_shim=greenback_shim,
suppress_aio_side_tte=suppress_aio_side_tte,
)
await portal.cancel_actor()
trio.run(main)
else: # infected-root case
async def infected_root_main():
'''
Same as `main` above except invoke
`exercise_trio_side_exit` in-task with no subactor spawn.
'''
# run_as_asyncio_guest() supplies the host asyncio loop.
# No RPC/child actor is involved: call the same endpoint
# directly with an infected root runtime open.
with trio.fail_after(_actor_test_timeout_s):
async with tractor.open_root_actor(
registry_addrs=[reg_addr],
):
for _ in range(_rpc_repetitions):
await exercise_trio_side_exit(
cached=cached,
cancel_trio_side=cancel_trio_side,
suppress=suppress,
expect_aio_side_err_type=(
expect_aio_side_err_type
),
greenback_shim=greenback_shim,
suppress_aio_side_tte=(
suppress_aio_side_tte
),
)
to_asyncio.run_as_asyncio_guest(
trio_main=infected_root_main,
)
async def suppress_tte_and_block(marker: str) -> None:
'''
Leave channel teardown waiting on a task that handled its TTE.
``marker`` is a test-owned file shared with the parent process.
Write it ONLY after receiving the issued signal, so cancellation
of the actor nursery cannot race ahead of the blocked teardown.
'''
async def block_until_trio_side_exit(
chan: to_asyncio.LinkedTaskChannel,
) -> None:
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
'''
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
Handle the issued TTE but keep the asyncio task running.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
Capture marker to acknowledge that the hang precondition was
reached. Ordinary later asyncio cancellation remains enabled;
the assertion is actor termination, not mandatory hard-kill.
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
'''
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
chan.started_nowait()
try:
await asyncio.Event().wait()
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
except to_asyncio.TrioTaskExited as signal:
# Publish the marker only for this channel's issued TTE,
# after trio-side context exit has actually begun.
assert signal is chan._aio_owner_exit
assert chan._trio_exited
assert chan._aio_task is asyncio.current_task()
assert not chan._aio_task.done()
Path(marker).write_text(str(os.getpid()))
await asyncio.Event().wait()
chan: to_asyncio.LinkedTaskChannel
async with to_asyncio.open_channel_from(
block_until_trio_side_exit,
) as (chan, first):
assert first is None
assert chan._aio_task is not None
assert not chan._aio_task.done()
assert not chan._trio_exited
# Leaving this body immediately sends TTE. Context exit then
# waits for the asyncio task, which handles TTE and blocks
# again; the RPC cannot finish without external cancellation.
async def cancel_an_with_blocked_aio(
marker: str,
host: str,
port: int,
) -> None:
'''
Cancel the actor nursery after its child suppresses channel TTE.
Called in a separate parent process by the watchdog test below.
ActorNursery.cancel() must terminate the child and nursery exit
must join it. A deadline in the pytest process detects failure
independently of either actor's event loop.
'''
async def invoke_blocked_rpc() -> None:
'''
Keep one RPC pending while the parent cancels its actor tree.
The parent joins this `trio` task through `tn` after
`an.cancel()`.
'''
await portal.run(
suppress_tte_and_block,
marker=marker,
)
Test `trio`-side channel exit in an infected child Exercise the exit-signal handling fixed in 4dfcf6f6 using the real `to_asyncio.open_channel_from()` API in an infected child actor. An expected `TrioTaskExited` must not escape graceful teardown or be raised again as an unhandled asyncio done-callback exception. Deats, - run six cases with native and Greenback-wrapped asyncio tasks: direct channel closure, last cached caller exit/cancellation, strict TTE reporting, and distinct RuntimeError/TTE cleanup faults; - use `block_until_trio_side_exit()` to record task identity, complete startup, send one channel value and block until signalled; - wrap the channel API in `maybe_open_context()` for cached cases, proving overlapping `trio.Task`s receive the same channel; - synchronize the second subscription with `tn.start()` and an event, then join its task before checking asyncio is still alive; - open two asyncio tasks sequentially, checking each finishes when its channel closes and retains the expected exception identity; - raise a NEW TTE during cleanup to reject blanket type-based suppression, retaining the issued TTE as the new error's cause; - drain a host-loop callback before checking unhandled exceptions for expected shutdown, separately from actor/RPC usability; - await two sequential `Portal.run()` calls in the same child, then explicitly cancel the actor before leaving its nursery. Keep the Piker reproduction as context rather than describing the blocking asyncio task as a quote/feed implementation. Document case selection, closure contracts and the assertions at their call sites. Qualification: latest matrix 12 passed. Earlier combined asyncio and task-formatter run: 41 passed / 18 deselected. This matrix does not yet cover an infected root or an asyncio task suppressing TTE and returning or remaining blocked; those are follow-up coverage. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 15:37:16 +00:00
Avoid `raise None` after successful `asyncio` exit An asyncio-side task can handle the issued `TrioTaskExited` and return normally. With `suppress_graceful_exits=False`, the strict reporting branch in `translate_aio_errors()` nevertheless tried to raise `trio_to_raise=None`, replacing successful completion with TypeError('exceptions must derive from BaseException'). Require an actual pending exception before that strict-policy raise. Existing errors retain their reporting and cause handling; strict reporting does not manufacture an error after a successful asyncio-side return with no remaining trio-side error. Deats, - run the channel-exit matrix directly in an infected root as well as through sequential TCP RPCs in an infected child; - add direct/cached cases where the asyncio-side task suppresses the issued TTE and returns, under graceful and strict policies, with native and Greenback task execution; - test a child suppressing TTE and staying blocked: check channel startup state, then require receipt of that channel's exact issued TTE with trio-side exit set and asyncio still running; - publish the receipt marker only after those checks, before the responsive parent awaits `an.cancel()`; require nursery exit to join the retained child process handle; - bound that probe from another process, failing if its watchdog must rescue the test group; successful child exit does not prove that hard-kill escalation was needed; - explicitly `raise signal`, rename the caller-cancellation flag to `cancel_trio_side`, and format signal/callback diagnostics; - retain the maintainer's test layout and helper documentation. Qualification: expanded matrix before the guard: 8 failed / 33 passed; all eight failures are strict-policy successful returns. After the guard, selected exit/root/infected-asyncio/formatter suites: 70 passed / 18 deselected, with 24 fork deprecation warnings. Strengthened blocked-child state checks: 1 passed. Interactive SIGINT/debugger cases remain excluded. (this patch was generated in some part by `opencode` using `gpt-6.1-sol` (`openai`))
2026-10-30 17:10:53 +00:00
an: tractor.ActorNursery
async with tractor.open_nursery(
registry_addrs=[(host, port)],
debug_mode=False,
) as an:
portal: tractor.Portal = await an.start_actor(
'blocked_aio_channel',
enable_modules=[__name__],
infect_asyncio=True,
debug_mode=False,
)
# Retain the runtime's process handle across nursery cleanup;
# successful cancel-RPC delivery alone does not prove exit.
child, = an._children.values()
proc: trio.Process = child[1]
tn: trio.Nursery
async with trio.open_nursery() as tn:
tn.start_soon(invoke_blocked_rpc)
while not Path(marker).exists():
await trio.sleep(0.01)
await an.cancel()
tn.cancel_scope.cancel()
assert proc.poll() is not None, (
'ActorNursery exited without joining the blocked child'
)
def test_cancel_an_reaps_blocked_aio(
tmp_path: Path,
reg_addr: tuple[str, int],
_reap_watchdog_s: float = 30,
) -> None:
'''
Nursery cancellation must terminate a TTE-suppressing child.
- Start a dedicated parent process with its own process group.
- Wait inside that process for the child's TTE receipt marker,
then call an.cancel() and check its process handle after exit.
- Expected termination uses Tractor's own cancellation/reaping.
Watchdog rescue kills this test's group and FAILS the test.
It cannot count as evidence of successful runtime teardown.
'''
if os.name != 'posix':
pytest.skip('The watchdog uses POSIX process groups')
marker: Path = tmp_path / 'tte-suppressed'
code: str = (
'import sys, trio; '
'from tests.test_aio_channel_exit '
'import cancel_an_with_blocked_aio; '
'trio.run(cancel_an_with_blocked_aio, '
'sys.argv[1], sys.argv[2], int(sys.argv[3]))'
)
parent: subprocess.Popen = subprocess.Popen(
[
sys.executable,
'-c',
code,
str(marker),
reg_addr[0],
str(reg_addr[1]),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
start_new_session=True,
)
try:
output, _ = parent.communicate(timeout=_reap_watchdog_s)
except subprocess.TimeoutExpired:
os.killpg(parent.pid, signal.SIGKILL)
output, _ = parent.communicate()
pytest.fail(
f'Watchdog rescued blocked actor-nursery teardown\n'
f'TTE received: {marker.exists()}\n'
f'{output}'
)
assert marker.exists(), f'Child never received TTE\n{output}'
assert parent.returncode == 0, (
f'Blocked-child cancellation failed\n{output}'
)