mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[F129,F130] harden quality gate _run_one exit status + timeout cleanup
F129: _run_one returned 'proc.returncode or 0', masking a None returncode (communicate returned without a recorded exit code — process killed out-of-band) as 0 / success. Treat None as a non-zero failure (fail-closed). F130: on timeout, _run_one killed the subprocess but never awaited wait() — communicate() was cancelled so it never closed the stdout/stderr pipes, leaving a transient zombie + leaked FDs. Await wait() after kill() to reap the process and close the transports.
This commit is contained in:
@@ -4,6 +4,7 @@ i_am_done, blocking a red submit before it reaches QA. Full tests stay on CI.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
@@ -13,6 +14,7 @@ if TYPE_CHECKING:
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from roboco.services.gateway import quality_gate
|
||||
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
||||
from roboco.services.gateway.choreographer._impl import _IAmDoneContext
|
||||
from roboco.services.gateway.quality_gate import GateResult, run_quality_commands
|
||||
@@ -67,6 +69,65 @@ def test_gate_result_summary_and_excerpt() -> None:
|
||||
assert 0 < len(failed.output_excerpt) < len(failed.output)
|
||||
|
||||
|
||||
# --- _run_one None-returncode fail-closed -----------------------------------
|
||||
|
||||
_TIMEOUT_EXIT_CODE = 124
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_one_treats_none_returncode_as_failure(
|
||||
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A None returncode (process terminated without a recorded exit code, e.g.
|
||||
killed out-of-band during communicate) must NOT be masked as 0 / success.
|
||||
The gate is fail-closed — an unknown exit status is a failure, not a pass.
|
||||
"""
|
||||
fake_proc = MagicMock()
|
||||
fake_proc.returncode = None # abnormal: communicate returned, no code set
|
||||
fake_proc.communicate = AsyncMock(return_value=(b"some output", b""))
|
||||
|
||||
async def _fake_shell(*_args: object, **_kwargs: object) -> object:
|
||||
return fake_proc
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_shell", _fake_shell)
|
||||
|
||||
rc, out = await quality_gate._run_one(tmp_path, "anything")
|
||||
assert rc != 0, "a None returncode masked as 0 lets a red gate pass"
|
||||
assert "some output" in out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_one_reaps_killed_timeout_process(
|
||||
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""On timeout, _run_one must kill AND await wait() to reap the killed
|
||||
process and close the stdout/stderr pipe transports. kill alone leaves a
|
||||
transient zombie + leaked FDs (communicate() was cancelled, so it never
|
||||
closed the pipes)."""
|
||||
fake_proc = MagicMock()
|
||||
fake_proc.returncode = -9 # killed by SIGKILL
|
||||
|
||||
# communicate() never completes on its own — wait_for cancels it.
|
||||
async def _communicate() -> tuple[bytes, bytes]:
|
||||
await asyncio.sleep(30)
|
||||
return (b"", b"")
|
||||
|
||||
fake_proc.communicate = _communicate
|
||||
fake_proc.kill = MagicMock()
|
||||
fake_proc.wait = AsyncMock(return_value=-9)
|
||||
|
||||
async def _fake_shell(*_args: object, **_kwargs: object) -> object:
|
||||
return fake_proc
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_shell", _fake_shell)
|
||||
monkeypatch.setattr(quality_gate, "_GATE_TIMEOUT_SECONDS", 0.01)
|
||||
|
||||
rc, _msg = await quality_gate._run_one(tmp_path, "slow-command")
|
||||
assert rc == _TIMEOUT_EXIT_CODE
|
||||
fake_proc.kill.assert_called_once()
|
||||
fake_proc.wait.assert_awaited_once()
|
||||
|
||||
|
||||
# --- GitService command selection -------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user