1414import os
1515import signal
1616import sys
17+ import threading
1718from collections .abc import Callable
1819from contextlib import AsyncExitStack , suppress
1920from pathlib import Path
21+ from types import SimpleNamespace
2022from typing import TextIO , cast
2123
2224import anyio
2325import anyio .abc
26+ import anyio .from_thread
2427import anyio .lowlevel
28+ import anyio .to_thread
2529import pytest
2630import trio
2731import trio .testing
@@ -199,6 +203,9 @@ def install_fake_process(
199203 """
200204 terminated : list [FakeProcess ] = []
201205
206+ async def fake_get_executable_command (command : str ) -> str :
207+ return command
208+
202209 async def fake_spawn (
203210 command : str ,
204211 args : list [str ],
@@ -212,6 +219,7 @@ async def fake_terminate_tree(proc: FakeProcess) -> None:
212219 terminated .append (proc )
213220 proc .exit (- 15 )
214221
222+ monkeypatch .setattr (stdio , "_get_executable_command" , fake_get_executable_command )
215223 monkeypatch .setattr (stdio , "_create_platform_compatible_process" , fake_spawn )
216224 monkeypatch .setattr (stdio , "_terminate_process_tree" , fake_terminate_tree )
217225 if grace_period is not None :
@@ -568,6 +576,45 @@ async def test_a_command_that_cannot_be_execed_raises_enoent() -> None:
568576 assert exc_info .value .errno == errno .ENOENT
569577
570578
579+ @pytest .mark .anyio
580+ async def test_cancellation_during_windows_command_resolution_returns_before_resolution_finishes (
581+ monkeypatch : pytest .MonkeyPatch ,
582+ ) -> None :
583+ """Cancelling `stdio_client` does not wait for blocked Windows command resolution."""
584+ resolution_started = anyio .Event ()
585+ resolution_release = threading .Event ()
586+ resolution_finished = threading .Event ()
587+
588+ def blocking_resolver (command : str ) -> str :
589+ anyio .from_thread .run_sync (resolution_started .set )
590+ resolution_release .wait ()
591+ resolution_finished .set ()
592+ return command
593+
594+ monkeypatch .setattr (stdio , "sys" , SimpleNamespace (platform = "win32" ))
595+ monkeypatch .setattr (stdio , "get_windows_executable_command" , blocking_resolver )
596+
597+ cancel_scope = anyio .CancelScope ()
598+ client_stopped = anyio .Event ()
599+
600+ async def run_client () -> None :
601+ with cancel_scope :
602+ async with AsyncExitStack () as stack :
603+ await stack .enter_async_context (stdio_client (FAKE_PARAMS ))
604+ client_stopped .set ()
605+
606+ with anyio .fail_after (5 ):
607+ async with anyio .create_task_group () as tg :
608+ tg .start_soon (run_client )
609+ await resolution_started .wait ()
610+ cancel_scope .cancel ()
611+ try :
612+ await client_stopped .wait ()
613+ finally :
614+ resolution_release .set ()
615+ await anyio .to_thread .run_sync (resolution_finished .wait )
616+
617+
571618@pytest .mark .anyio
572619async def test_cancellation_during_spawn_leaks_no_streams (monkeypatch : pytest .MonkeyPatch ) -> None :
573620 """Cancellation while the spawn is still in flight must not leak the internal streams.
0 commit comments