From cef13a0cd6aa64ab2a9b30fe1199d60939846357 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Wed, 22 Jul 2026 18:53:45 +0300 Subject: docs-gates: hard-bound the smoke deadline (cap probe timeout + inter-round sleep to remaining budget) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex adversarial finding: DEADLINE_SECONDS was only checked BEFORE each op, so a probe or sleep starting at 479s could overshoot to ~510s — 480 was a soft target, not a hard bound. Make it hard: run() now computes an absolute deadline = start + DEADLINE_SECONDS plus a _remaining() helper. The per-probe socket timeout is capped to min(PROBE_TIMEOUT_SECONDS, max(1, remaining)) — the previously hardcoded 30 is now the PROBE_TIMEOUT_SECONDS constant; a probe with < 1s of budget is skipped and counted unresolved. The inter-round sleep is capped to min(RETRY_SLEEP_SECONDS, remaining) and is skipped entirely when the budget is exhausted (falling into the existing deadline path). No body-read-level deadline is added — pages are small, so the socket-op timeout bounds reads adequately. Deadline-path failure accounting is unchanged. Tests: the probe timeout is capped to the remaining budget (fake opener records the timeout it was opened with); the inter-round sleep is capped to the remaining budget (sleep spy); the existing suite stays green. 🤖 Generated by [robots](https://vyos.io) --- scripts/docs_gates/test_smoke.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) (limited to 'scripts/docs_gates/test_smoke.py') diff --git a/scripts/docs_gates/test_smoke.py b/scripts/docs_gates/test_smoke.py index f0a37c7a..a2654dcf 100644 --- a/scripts/docs_gates/test_smoke.py +++ b/scripts/docs_gates/test_smoke.py @@ -101,15 +101,18 @@ class _FakeResp: class _FakeOpener: - """Yields queued responses in order; an Exception item is raised (models a transport - error, or a non-2xx delivered as HTTPError). Records each opened Request for assertions.""" + """Yields queued responses in order; an Exception item is raised (models a transport error, + or a non-2xx delivered as HTTPError). Records each opened Request + the timeout it was + called with, for assertions.""" def __init__(self, responses: list[object]): self._responses = list(responses) self.calls: list[urllib.request.Request] = [] + self.timeouts: list[float | None] = [] def open(self, req: urllib.request.Request, timeout: float | None = None) -> object: self.calls.append(req) + self.timeouts.append(timeout) item = self._responses.pop(0) if isinstance(item, Exception): raise item @@ -150,10 +153,12 @@ class _PathOpener: def __init__(self, by_path: dict[str, list[object]]): self._by_path = {p: list(v) for p, v in by_path.items()} self.calls: list[str] = [] + self.timeouts: list[float | None] = [] def open(self, req: urllib.request.Request, timeout: float | None = None) -> object: path = urlsplit(req.full_url).path self.calls.append(path) + self.timeouts.append(timeout) item = self._by_path[path].pop(0) if isinstance(item, Exception): raise item @@ -331,3 +336,31 @@ def test_probe_once_detail_joins_multiple_failed_checks(monkeypatch): ok, _, _, detail = smoke._probe_once("host", probe, "sha", "id", "sec") assert ok is False assert detail == "status+apex-build" + + +# --- Adversarial round: DEADLINE_SECONDS is a HARD bound — the per-probe socket timeout and the +# inter-round sleep are both capped to the remaining budget so neither can overshoot it. --- + +def test_probe_timeout_capped_to_remaining_budget(monkeypatch): + monkeypatch.setattr(smoke, "probe_plan", lambda slug, pdf, critical: _plan(["/a"])) + monkeypatch.setattr(smoke, "DEADLINE_SECONDS", 5) # << PROBE_TIMEOUT_SECONDS (30) + opener = _PathOpener({"/a": [_FakeResp(200, {})]}) + monkeypatch.setattr(smoke, "_OPENER", opener) + smoke.run("host", "rolling", "sha", "id", "sec", None, []) + assert opener.timeouts, "the probe recorded the timeout it was opened with" + t = opener.timeouts[0] + assert 1 <= t <= smoke.DEADLINE_SECONDS # capped DOWN to the ~5s remaining budget + assert t < smoke.PROBE_TIMEOUT_SECONDS # NOT the full 30s socket timeout + + +def test_inter_round_sleep_capped_to_remaining_budget(monkeypatch): + monkeypatch.setattr(smoke, "probe_plan", lambda slug, pdf, critical: _plan(["/a"])) + monkeypatch.setattr(smoke, "DEADLINE_SECONDS", 10) # << RETRY_SLEEP_SECONDS (30) + sleeps: list[float] = [] + monkeypatch.setattr(smoke.time, "sleep", lambda s: sleeps.append(s)) + opener = _PathOpener({"/a": [_FakeResp(500, {}), _FakeResp(200, {})]}) # fail r1, pass r2 + monkeypatch.setattr(smoke, "_OPENER", opener) + assert smoke.run("host", "rolling", "sha", "id", "sec", None, []) == 0 + assert len(sleeps) == 1 + assert 0 < sleeps[0] <= smoke.DEADLINE_SECONDS # capped to the ~10s remaining budget + assert sleeps[0] < smoke.RETRY_SLEEP_SECONDS # NOT the full 30s sleep -- cgit v1.2.3