test: assert pool-saturation fail-fast by behaviour, not by a stopwatch - #2902
Conversation
test_pool_saturation_fails_fast_instead_of_queuing timed `await driver.start_rx(...)` against the saturated pool and required `elapsed < _TEST_TIMEOUT_S / 2` (25ms). That whole fail-fast path has no await points -- verified by running an asyncio ticker alongside the call, which advanced 0 times during it -- so the bound measures only whether the OS descheduled this process mid-call, not what the driver did. Observed red at elapsed=0.032s on an *idle* host, before any load was injected. Assert the behaviour instead. The fail-fast branch closes the open coroutine rather than submitting it, so the probe's handle is never started; an open that queued behind the wedged workers is started as soon as one frees. Release the gate, wait for the pool to DRAIN (all eight wedged handles closed) and then read the probe: a queued probe sits ahead of those closes in the same FIFO pool, so once the closes land it would necessarily have started. Established, all against this file's real test: - reworked test under a deterministic 30ms stall injected into the measured region: PASS; the pre-change shape under the same stall: FAIL (`elapsed=0.161s`), confirming the stall is potent and the difference is the assertion, not the driver; - product mutation disabling the saturation check (`_inflight_opens >= _CAPTURE_OPEN_MAX_WORKERS * 1000`): FAIL on the message assertion, and -- with that assertion neutralised -- FAIL on the drain assertion alone, so the new assertion discriminates on its own rather than riding on the message check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Agent Review: BLOCKED cd60821 Independent review. The test logic is sound and the mutation kills reproduce in Blocking finding (in the diff — needs a commit)
Two problems, one of them measurably false. (a) "measures only whether the OS descheduled this process mid-call" — REFUTED.
CPU is 70–85% of wall in the typical run. (b) "has no await points" — NARROWED. Note the conclusion (drop the stopwatch) is right, and I confirmed it Required fix (one commit, comment only): narrow the sentence to what is Checks to run after the fix: Claim verdictsC1 — the removed assertion measured machine speed, not driver behaviour, and C2 — the new test still fails when the guarded behaviour breaks. CONFIRMED. C3 — the drain assertion discriminates on its own. CONFIRMED. Second plugin C4 — robust, not hollow. CONFIRMED. I went through the trivially-true
One non-blocking brittleness, reported not required: the probe is located C5 — the drain-wait ordering argument. CONFIRMED. C6 — added prose. NARROWED (see the blocking finding). True and tied: C7 — the PR body's class survey. NARROWED. Correct where I checked MANDATORY SQUASH-BODY CORRECTION (commit message — does not need a commit)
REQUIRED BEFORE MERGE (PR body edits — head SHA unaffected)
Gates (re-run here, not taken from the PR body)
Not verified
Guardrails1 file, 43 changed lines (36 additions / 7 deletions). Far inside both the soft |
Independent review (PR #2902) refuted the explanation the previous commit attached to this test. The comment claimed a wall-clock bound on the fail-fast path "measures only whether the OS descheduled this process mid-call". It does not: measuring `time.thread_time()` alongside `time.perf_counter()` around the probe open gives wall=21.12ms with 10.55ms of event-loop-thread CPU -- half the window is real work, not descheduling. That work is the preamble every `start_rx()` runs before the saturation branch: device enumeration, which on macOS reaches `_get_uid_map` -> `rigplane.audio._macos_uid.get_device_uid_map`, a CoreAudio lookup with no caching (verified: no decorator on `_get_uid_map`, usb_driver.py). The same sentence also said the path "has no await points". There are three (`_rx_lock` enter/exit, `await self._open_stream`); the true and narrower property is that none of them YIELDS, which an `asyncio.sleep(0)` ticker confirms by advancing 0 times during the call. The conclusion the comment supported is unchanged and still holds -- a 25ms bound on this call pins host speed and per-call enumeration cost rather than the saturation decision, and it went red on an idle host. Only the stated mechanism was wrong. In a module whose whole subject is "a capture open must never block the loop", a comment asserting this path costs nothing measurable is exactly the false-rationale shape CLAUDE.md warns about, so it is corrected rather than softened. Also drops the specific "observed: 32ms" figure from the comment: a measured number in a committed comment has nothing that fails when it stops being true. The measurements live in the commit body and PR instead. No product code changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner ruling (2026-08-31, repo-wide): a wrong or stale comment is DELETED, not fixed, narrowed, qualified or rewritten. A replacement is written only if removing it breaks something structural AND the writer verified it, naming the file and symbol opened. Commit e9267dd did the forbidden thing. Independent review refuted the claim that a wall-clock bound on the fail-fast path "measures only whether the OS descheduled this process mid-call", and instead of deleting that sentence I wrote a narrower one in its place. The replacement was verified, but verified is not the bar -- the bar is that removing it must break something structural, and nothing here does. The assertions carry their own failure messages, and the reason the timing bound went is in this PR and in git history, which is where a measurement belongs. The reasoning behind the ruling, worth keeping: with no comment the next reader opens the code and works it out -- one extra read, always correct. With a wrong comment it reads nothing, believes the comment, and is wrong. Absence beats bad text. Three PRs were blocked today on exactly this shape, each replacing a false statement with a new unverified one. Kept, because neither is wrong and both were established rather than assumed: the paragraph describing what the assertions check, and the FIFO-ordering paragraph explaining why the drain wait is the right condition -- review confirmed the latter against `ThreadPoolExecutor._work_queue` and the two submit sites in `usb_driver.py` (`_open_stream`, `_close_late_stream`). No product code changed; no test logic changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Agent Review: PASS fe1c761 Round 2 (third head). The round-1 blocking finding is resolved by deletion, and The two judgement calls you asked me to attack1. Does the ruling reach the two kept paragraphs? No — and I would push back if
Stretching "absence beats bad text" to correct text would cost the only in-tree 2. Is "not as a stopwatch reading" now dangling? No. I tested it as a Optional, not required, and explicitly not a round: if the owner reads the Verified at this head
REQUIRED BEFORE MERGE — PR body edits (no commit, head SHA unaffected)
Squash body — no force-push neededYour PR-body directive is sufficient; do not rewrite Gates (re-run here at
|
What
test_pool_saturation_fails_fast_instead_of_queuingtimedawait driver.start_rx(...)against the saturated pool and requiredelapsed < _TEST_TIMEOUT_S / 2(25 ms). It now asserts the behaviour that bound was standing in for.Why the bound could not hold
A wall-clock bound on this call never isolated the saturation decision. That decision is one counter comparison (
_inflight_opens >= _CAPTURE_OPEN_MAX_WORKERS); everything else the call costs is the preamble everystart_rx()runs before it — device enumeration, which on macOS reaches_get_uid_map→rigplane.audio._macos_uid.get_device_uid_map, a CoreAudio lookup with no caching (no decorator on_get_uid_map).Measured with
time.thread_time()alongsidetime.perf_counter()around the probe open: wall = 21.12 ms, event-loop-thread CPU = 10.55 ms — half the window is real work on the loop thread, not descheduling. None of the path's three await points (_rx_lockenter/exit,await self._open_stream) yields: anasyncio.sleep(0)ticker advanced 0 times during the call, so the preamble cost and any descheduling land in one uninterrupted window.The pre-change assertion went red at
elapsed = 0.032son an idle host, before any load was injected.What replaces it
The fail-fast branch closes the open coroutine rather than submitting it, so the probe's handle is never started; an open that queued behind the wedged workers is started as soon as one frees. The test releases the gate, waits for the pool to drain (all eight wedged handles closed), then reads the probe. A queued probe sits ahead of those closes in the same FIFO
_work_queue, so once the closes land it would necessarily have started — that ordering is what makes the read conclusive rather than merely prompt.How each claim was established
elapsed=0.161s; the stall is potent and the difference is the assertion, not the driverFull suite (Mac mini, Python 3.11) at
cd608214:11631 passed, 126 skipped, 48 xfailed, 0 failures.ruff check,ruff format --check,mypy --strict src/rigplane/webclean at both commits. One file, well inside the 6-file/600-line soft threshold;git diff main...HEAD --shortstatgives the current figure.The other named test
TestRadioPoller::test_poller_broadcasts_meter_readingswas already converted to a wait-for-the-condition shape in #2897 (commit36c4b162), which is merged. No change was needed here.Class survey
Other tests do share both shapes — the check the request asked for came back positive.
The enumerated list that stood here has been deleted rather than corrected, per the owner's ruling of 2026-08-31. It was wrong in four places, and one of its entries went stale inside this PR: deleting nine comment lines shifted a cited line number. A list of
file:linerows in a document nobody re-runs is precisely the text that makes the next reader stop checking. Re-run the search and you get a correct list; read this one and you would have trusted a wrong one.Two things from it are worth carrying, and neither is a line number:
tests/test_poller.py, which demands three poll ticks inside a 50 ms sleep — tighter than the case that actually failed.test_normal_rx_open_timing_unaffectedandtest_normal_tx_open_timing_unaffectedassertelapsed < 0.5under the docstring "a well-behaved (non-blocking) open must not pay a meaningful penalty". The penalty named is_TEST_TIMEOUT_S, which is0.05. An open that paid the full timeout still lands ten times inside the bound, so neither test can fail for the reason it exists. Independent review confirmed this. It deserves its own ticket and is not fixed here.Review history
Reviewed independently and BLOCKED at
cd608214: the added comment claimed a wall-clock bound on the fail-fast path measured "only" OS descheduling. It does not —time.thread_time()againsttime.perf_counter()around the probe open gives wall 21.12 ms with 10.55 ms of event-loop-thread CPU. That review also found four errors in the survey above, all fixed here.e9267ddereplaced the refuted sentence with a narrower, verified one. That was the wrong move: under the owner's repo-wide ruling of 2026-08-31, a wrong or stale comment is deleted, not fixed, narrowed, qualified or rewritten — a replacement is justified only when removing the text breaks something structural. Nothing here did.fe1c7617deletes it.The measurement is not lost by that deletion; it lives on this page and in git history, which is where a measured number belongs. A number pinned in a comment has nothing that fails when it stops being true.
Two comment paragraphs are kept because neither is wrong and both were established rather than assumed: the one describing what the assertions check, and the FIFO-ordering one explaining why the drain wait is the right condition — review confirmed the latter against
ThreadPoolExecutor._work_queueand the two submit sites inusb_driver.py(_open_stream,_close_late_stream).Re-review pending at
fe1c7617.