diff --git a/src/coder_eval/orchestrator.py b/src/coder_eval/orchestrator.py index 83f67da3..c087fd1c 100644 --- a/src/coder_eval/orchestrator.py +++ b/src/coder_eval/orchestrator.py @@ -76,6 +76,28 @@ _WAIT_FOR_GRACE_SECONDS = 2.0 +def _close_subprocess_transport(proc: asyncio.subprocess.Process | None) -> None: + """Release a finished subprocess's pipe transport deterministically. + + ``asyncio`` gives ``Process`` no public way to do this: the transport is held + privately and is only closed when the object is garbage-collected. On + Windows' Proactor loop ``_ProactorBasePipeTransport.__del__`` then emits + ``ResourceWarning: unclosed transport`` — one per pipe — which a strict test + run surfaces as ``PytestUnraisableExceptionWarning`` attributed to whichever + test happened to be running when the GC fired, not to the code that leaked. + That is exactly the shape of the intermittent Windows CI failures in + ``test_pre_run`` / ``test_post_run`` / ``test_preservation_mode``. + + Best-effort and idempotent: a second ``close()`` is a no-op, and any error is + swallowed because releasing a pipe must never turn into a task failure. + """ + transport = getattr(proc, "_transport", None) + if transport is None: + return + with suppress(Exception): + transport.close() + + async def _pump_stream( stream: asyncio.StreamReader | None, log_fn: Callable[..., None], @@ -2172,6 +2194,9 @@ async def _run_command_list( start = time.time() logger.info("Running %s command: %s", human.lower(), cmd.command) + # Bound outside the try so the `finally` can tell "never spawned" + # (a create_subprocess_shell failure) from "spawned, needs closing". + proc: asyncio.subprocess.Process | None = None try: proc = await asyncio.create_subprocess_shell( cmd.command, @@ -2246,6 +2271,13 @@ async def _run_command_list( if fail_on_error: raise RuntimeError(f"{human} command failed: {cmd.command!r}") from e logger.warning("%s command '%s' failed: %s", human, cmd.command, e) + finally: + # Every exit from this iteration -- normal, `continue` from the + # timeout branch, or a raised RuntimeError -- must release the + # two pipes this command opened. Without it they survive until + # GC, which on Windows reports as an unraisable ResourceWarning + # against an unrelated test. See _close_subprocess_transport. + _close_subprocess_transport(proc) async def _run_pre_run_commands(self) -> None: """Execute pre-run commands inside the sandbox before evaluation. diff --git a/uv.lock b/uv.lock index bbe0e461..211895bb 100644 --- a/uv.lock +++ b/uv.lock @@ -2253,11 +2253,11 @@ wheels = [ [[package]] name = "sqlparse" -version = "0.5.5" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, + { url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" }, ] [[package]]