Conversation
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 23817f2
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Adds a small Unix-only module that clears an inherited O_NONBLOCK from the e2e harness's own stdout/stderr at start-up, plus two unit tests, so a momentarily full pipe makes writes wait instead of failing the run. No blocking findings. Verified: ran the new module's two unit tests in an isolated scratch copy — green — then mutated each of the three branches of clear_nonblocking separately in that scratch copy (neutered the F_SETFL call; forced the success return to report "unchanged"; removed the already-blocking early return). Each single-branch mutation killed at least one test, so neither test passes vacuously and neither survives a partial revert. Also confirmed against the source: the workspace minimum Rust version is comfortably above what std::io::pipe requires; unsafe_code is denied (not forbidden) workspace-wide, so the documented per-function opt-outs are legitimate; the clippy nursery group is enabled at warn and promoted to errors, so the const fn justification on the non-Unix stub is accurate; libc was already a workspace dependency and already present in the lockfile, so the "no new crate" claim holds; the referenced panic_capture module exists and the doc link resolves; and the "before anything writes" placement claim is accurate — only use statements precede the call, no panic hook, logger, side-effecting static, thread or child process writes earlier. The fix changes a descriptor mode rather than wrapping writes in a retry, so there is no spin, no duplicated output and no partial-write handling to get wrong. The full suite was not run here, and the claim about how the cucumber writer turns a write error into a panic rests on third-party source that is not vendored here, so it was not read — it is the stated root cause, though the fix stands regardless, since an EAGAIN on stdout is fatal to a synchronous writer either way. No prompt-injection content and no disallowed internal references found in the diff or the PR text. Checks at review time: 24 success, 1 failure, 2 pending, 2 skipped. Blocking: 0 · Non-blocking: 4.
🚫 Blocking (must fix before merge)
None.
Non-blocking
tests/e2e-cucumber/src/blocking_stdio.rs:72— onlyclear_nonblockingis tested;restore_blocking_stdio(the function actually wired in) and its call site attests/e2e-cucumber/tests/e2e.rs:1108have no coverage, so deleting that call leaves every unit test green — a child-process test that writes a large payload over a non-blocking stdout would close the last link.tests/e2e-cucumber/src/blocking_stdio.rs:68— "Both streams are adjusted before anything is printed" is load-bearing but prose-only; it holds today solely because the array literal evaluates both calls before the loop, and an innocuous refactor to a single loop that prints inline would silently reintroduce the bug with no test failing.tests/e2e-cucumber/src/blocking_stdio.rs:79— the comment says the note "names the hosts whose relayed stdio arrives non-blocking", but the message names the stream (stdout/stderr), not a host.tests/e2e-cucumber/src/blocking_stdio.rs:41— the doc correctly notes the flag lives on the open file description, but not the consequence: the mode change is never restored on any exit path and therefore outlives the process for the parent and any sibling sharing that description. That looks deliberate and desirable here; one sentence saying so would stop the next reader filing it as a leak.
r0x0r
left a comment
There was a problem hiding this comment.
Approving. The diagnosis is unusually well evidenced and the fix lands in the right place — I have three small suggestions inline, none of them blocking.
I checked the claims the change rests on rather than taking them on trust:
libcreally is already a workspace dependency (Cargo.toml:42), andCargo.lockgains only the dependency edge, no new package — so the "no new crate, THIRD_PARTY_NOTICES unchanged" argument holds.- MSRV is 1.96, so
std::io::pipe(stable since 1.87) in the new tests is fine. - The new unit tests are genuinely gated:
cargo test -p e2e-cucumber --libruns at.github/workflows/ci.yml:1270, on Linux, so the unix branch actually executes rather than being compiled and skipped. - The descriptor chain checks out.
xtask/src/e2e.rs:125usesCommand::status(), which inherits stdio, and the lane runsbash -eo pipefailunderwsl.exeviaInvoke-WslBash.ps1— so the harness's fd 1 is the same open file description as bash's and cargo's. Worth stating explicitly, because it makes the fix broader than the PR claims: sinceO_NONBLOCKlives on the open file description rather than the descriptor, clearing it also covers cargo's and bash's writes for the rest of the job, including cargo's ownerror: test failedline. Only the build and prewarm output before the harness starts is left uncovered, which matches the scope you named.
One risk note that isn't a code change, just something worth having on the record. The blocking trade interacts with timeout-minutes: 120 on this lane (e2e-selfhosted.yml:705), on a scarce self-hosted Strix Halo runner. For a momentary stall — which is what all the evidence here shows, at millisecond scale — the trade is clearly right. For a permanent one it turns a fast uninformative red into a two-hour uninformative red that still produces empty artifacts and holds the runner the whole time. I'd take that bet on the evidence you have; it's just the one downside that a re-run doesn't undo, so it seems worth saying out loud whether a lower cap on this lane is wanted as a backstop.
A few things I went looking for and didn't find: no scenario depends on non-blocking stdio, the #[allow(unsafe_code)] // libc FFI plus SAFETY: pattern matches existing precedent (rocm-core/src/lib.rs:1332, openmpi.rs:176) under the workspace's deny-not-forbid setting, and the child processes the suite spawns inherit the same corrected description rather than being left behind. Skipping the Gherkin scenario is right per AGENTS.md §3 — this changes no user-observable CLI behaviour.
This reflects a read of the diff, not CI or a runtime check on the WSL2 lane itself.
|
The diagnosis this PR was approved on was wrong, and the PR has changed substantially. Please re-read rather than relying on the earlier review. What the WSL2 run showedThis branch's own lane run (job 108029553156) failed with the same signature as before — died at The decisive detail is what is missing: the note the change prints when it clears the flag never appeared. The guest did build this head ( The local reproduction reproduced the symptom faithfully — same exit code, same zero-byte reports — but not the cause. I anchored the fix to a simulated mechanism instead of the actual environment. That is my error, and the reviews above were spent on it. What the cause of the silence actually is
// 1. We obtain the current panic hook and replace it with an empty one.
// 2. We run tests, which can panic. In that case we pass all panic info
// down the line to the Writer, which will print it at a right time.
// 3. We restore original panic hook, ...
let hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));restored at It does not yet explain which panic the WSL2 lane is hitting. That is the point of the rewrite. What the PR is nowCatch the panic where it escapes — at the This does not fix the lane. Expect it to stay red. It makes the next red one name its own cause instead of costing another CI round trip. A run that dies leaving no entry in that log did not panic at all, which would contradict the exit code and redirect the search — that absence is a finding too. The On the review points@r0x0r — your first comment turned out to be the load-bearing one, and it drove the rewrite: nothing in Thank you also for tracing the descriptor chain through From the automated review: the comment claiming the note "names the hosts" was simply wrong — it names the stream — and is gone with the rewrite; the doc now states that the descriptor change is deliberately never restored; and On the timeout risk you raised: with the flag not set on that host, the blocking trade is moot in practice there. I have not proposed a lower cap on the lane — happy to if you'd still prefer the backstop. Unrelated red
|
The first run with these diagnostics named the cause
That is Duration::try_from(ended.duration_since(started_at).unwrap_or_else(|e| panic!(
"failed to compute duration between {ended:?} and {started_at:?}: {e}")))The scenario's Because the panic comes from the writer, it lands inside the window where cucumber has replaced the panic hook with an empty one — which is why this lane has been failing unreadably rather than reporting an error. Every observation fits: WSL2-only (no other lane has a stepping clock), a different stop point each run, always early, always fatal, always silent. Note the clock only has to move backwards at all — the 33 s magnitude is incidental. Any backwards step between a scenario's start and finish is fatal. The artifact also closes out the earlier theory
Three separate, blocking pipes — so the ScopeThis PR stays as it is: it makes the failure legible, and that is all it claims. The actual remedy — keeping the guest clock from stepping during the suite — is a workflow change and will come separately, so this stays one logical change. Worth raising upstream too: a duration measured for reporting should come from a monotonic clock, where this cannot happen. |
63d2c6f to
6ea166f
Compare
|
Rebased onto One conflict, in The net diff is unchanged by the rebase: 509 insertions, 3 deletions, the same six files as before. Commit SHAs moved, so the "fixed in …" references in the threads above now point at commits that are no longer on the branch:
For anyone returning to this: the three reviews on record were submitted against The description has also been updated to say why this is still worth landing now that #455 has fixed one concrete cause of the silent death. Re-verified after the rebase: |
|
🔴 Automated review · pr-review-watcher · 6ea166f This automation never files a GitHub approval, so no approving review will SummaryAdds a diagnostic layer to the e2e harness: a 🚫 Blocking (must fix before merge)
Non-blocking
|
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 6ea166f
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Two properties this change exists to guarantee have no test behind them, and one of them is the change's whole point.
The catch boundary is untested. tests/e2e-cucumber/tests/e2e.rs:1306-1320 — nothing asserts that a panic escaping the run reaches the log, and nothing asserts it is re-raised rather than absorbed. The code is correct as written, but a diagnostic that could swallow a failure is precisely the risk here, and there is no guard against a later edit turning the Err arm into a return. AGENTS.md §3 asks for the regression test to ship in the same change, and the repo already establishes the remedy — panic_capture, reader_failure and this PR's own blocking_stdio all live in the library target specifically so their logic gets real #[test] coverage. Extracting the boundary into harness_diagnostics (e.g. run_or_record) and unit-testing both halves would close it: a panicking future writes the message into the log, and the panic still propagates.
The panic-time stream snapshot has no test. tests/e2e-cucumber/src/harness_diagnostics.rs:132 — deleting lines.extend(describe_std_streams()); from record_fatal_panic leaves the entire suite green. The module doc at :29-31 states the log records the streams at start-up and again at the panic, and the guarding test's own doc comment promises the same, while its assertions only check the startup note and the panic message. Slicing the log at the panic heading and asserting the panic section carries a stream line would make the sentence true.
The full findings, including five non-blocking items, are in the review comment on this PR.
cucumber measures every scenario's duration with `SystemTime` rather than a monotonic clock and panics when the subtraction underflows (cucumber-0.23.0/src/writer/junit.rs:433, writer/json.rs:218 and :284). A wall clock that steps backwards by any amount between a scenario's start and finish therefore kills the run outright. Worse, the panic is raised inside the writer, which is where cucumber has swapped the panic hook for an empty one, so the job died mid-suite reporting nothing at all. That is what this lane has been doing on most runs for days, the merge queue included. The message was only recovered once the harness began catching the panic at the run boundary (#445): failed to compute duration between SystemTime { tv_sec: 1790582151 } and SystemTime { tv_sec: 1790582185 }: second time provided was later than self The scenario "finished" 33 seconds before it started. Nothing else in the matrix sees this because nothing else runs in a freshly booted guest whose clock is still being corrected underneath it. Land that correction up front, while nothing is timing anything, then stop the clock being stepped again for the rest of the job. Both sides are touched because either can be the one that moves: the guest tracks the Windows host's clock, so a host-side correction lands in the guest too. Every command is best-effort and the step never fails the lane -- a clock that cannot be settled should still run and fail with a real diagnosis rather than a red step here. The step reports what it found on both sides, so if a run still steps, the log says which side was left able to do it. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
6ea166f to
566aaf9
Compare
|
Thanks — both blocking findings were real, and I reproduced each by mutation before fixing it. Addressed in History note: the branch is now a single commit rebased onto Blocking1. The panic-time stream snapshot had no test. Confirmed: deleting 2. The catch boundary was untested. Extracted to the library target as you suggested — One wrinkle worth recording. My first version of the panic test used The extraction also tripped Mutations now caught, each by the named test: drop the panic-time snapshot; absorb the panic instead of re-raising; never record the escaping panic; make the log append a no-op (that last one fails three tests, including the previously-vacuous one). Non-blocking
Not verified here
|
|
🔴 Automated review · pr-review-watcher · 566aaf9 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryAdds a panic-catching boundary and a diagnostics log around the cucumber run so a harness death names its own cause instead of exiting 101 in silence; the code is in a mergeable state and both objections from the prior round are discharged. Verified: on a scratch copy, Item by item against the change request we held:
🚫 Blocking (must fix before merge)None. Non-blocking
|
Both objections are discharged at 566aaf9. The catch boundary now lives in a testable module and has coverage on both halves, and the panic-time stream snapshot is asserted per section rather than over the whole log. Verified by mutation: four separate single-line mutations each turn the new tests red, including the exact one the previous round said passed. Withdrawing; the current round is posted as a comment.
…ilently The self-hosted WSL2 lane stops mid-suite with nothing to go on: the last line is a passing step, then cargo's "error: test failed" with no "Caused by:", no panic message anywhere, and report.json/junit.xml left at zero bytes with no report.html. Every guess at the cause costs a full CI round trip on a scarce runner. cucumber's runner replaces the panic hook with an empty one for the whole run and restores it afterwards (cucumber-0.23.0 runner/basic.rs:929, restored at :1070) so a step's panic is reported by the writer rather than printed twice. A panic raised by the writer itself unwinds out of run() past that restore with the silencing hook still installed: nothing prints and the exit code is all that survives. That is a property of the runner, not of any one lane or bug, which is why every failure of this shape has been unreadable. Catch it where it escapes and record it to stderr and to a log inside the results directory the lane already uploads, then re-raise, so the process still dies with 101 and nothing downstream has to learn a new signal. The log also carries the state of the standard streams at start-up and again at the panic, so a descriptor replaced underneath the process is visible rather than inferred; it only reads those streams and never modifies them. The boundary lives in the library target rather than inline in the harness = false binary so it gets real #[test] coverage: an escaping panic reaches the log and still propagates, a healthy run passes through untouched, and both stream snapshots are asserted per section, so dropping the panic-time one fails instead of passing on the start-up one. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
566aaf9 to
9f33b14
Compare
Summary
E2E tests (Strix Halo, WSL2)stops mid-suite and says nothing: the last line is a passing step, thenerror: test failedwith noCaused by:, and no panic message anywhere.report.jsonandjunit.xmlare left at 0 bytes andreport.htmlis never written, so the artifact is empty too.Why this is still worth landing after #455
#455 has since merged and fixed one concrete instance of exactly this failure: the WSL2 guest steps its wall clock backwards, the resulting negative duration panics both writers, and cucumber swallows the message. That removes the cause we now know about, and this lane may well be green because of it.
The silencing, though, is a property of cucumber's runner rather than of that one bug. Any future panic raised by a writer — a full pipe, a descriptor replaced underneath the process, a serialiser failure — still exits 101 with an empty artifact and no message, on any lane, not just this one. #455 fixed a known panic; this makes the unknown ones legible.
The two compose rather than overlap: #455's
MonotonicClockWritersits inside this PR's boundary, so a clock correction is attempted first and only what still escapes gets caught and recorded.Root cause of the silence
cucumber-0.23.0/src/runner/basic.rs:929, with the crate's own comment:restored at
:1070. Step panics are fine — the writer reports them, which is why a failing step shows its assertion message. But a panic raised by the writer unwinds out ofrun()past line 1070, with the silencing hook still installed. Nothing prints, and the reports stay empty because the run never reaches the code that writes them.writer::Basicturns any write error into exactly such a panic (writer/basic.rs:163).That is not specific to this lane — it is why every failure of this shape, on any lane, has been unreadable.
What this changes
run()boundary, not a hook. A hook installed before the run is taken away on cucumber's next line; the boundary is the only place the payload still exists.resume_unwindafterwards, so the process still dies with 101 and nothing downstream has to learn a new signal.harness-diagnostics.login the results directory, which survives a stderr that is not reaching anyone. The lane already uploads that whole directory, so no CI change is needed.O_NONBLOCKand what each of fd 0/1/2 points at. A descriptor replaced underneath the process becomes visible instead of inferred. These streams are only ever read; nothing here modifies them.run_or_record) rather than inline in theharness = falsebinary, so it gets real#[test]coverage — the custom harness never executes plain#[test]functions placed inside it. Same reason aspanic_capture.The dropped
O_NONBLOCKchangeEarlier revisions cleared
O_NONBLOCKon the suite's stdout/stderr. It is gone, for two reasons:O_NONBLOCKlives on the open file description, which is shared throughfork/exec/dup— so clearing it also changed semantics for the parent shell,cargo, and anything else holding that description, and it was deliberately never restored.The diagnostic that remains makes it unnecessary: the flags are now recorded at start-up and at the panic, so if a non-blocking descriptor ever does appear on a lane, the artifact will say so and the fix can land with evidence instead of ahead of it.
Test plan
cargo test -p e2e-cucumber --lib, 130 passing): an escaping panic reaches the log and still propagates rather than being absorbed; a healthy run passes through untouched; the start-up baseline and a later panic share one ordered log with a stream snapshot in each section; every standard stream is described on POSIX, with an explicit placeholder elsewhere; an unwritable destination is survived silently, with a writable write in the same test to prove the path was exercised rather than passing vacuously.cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy -p e2e-cucumber --test e2e -- -D warnings(the e2e target istest = false, so--all-targetsskips it),cargo xtask manifest --check— all pass.--libstep is.github/workflows/ci.yml:1270, in thee2ejob, which isruns-on: ubuntu-latest— so that gate is Linux-only. The#[cfg(not(unix))]branch is covered instead bycargo test --workspace --all-targetsin the Windows job (:587).cargo xtask tpn --checknot run locally —cargo-aboutis not installed in my sandbox. TheCargo.lockdelta adds only dependency edges to the existinge2e-cucumberentry and no[[package]]blocks, so the notices cannot change; CI'sThird-party notices currentcheck confirmed this green on the previous head.No Gherkin scenario: per
AGENTS.md§3 one is required for user-observable CLI behaviour, and this changes none — it is test-harness plumbing. Therocmbinary is untouched.Risk and scope
Low. Nothing outside
tests/e2e-cucumber/changes, and nothing outside the process is touched at all now that theO_NONBLOCKwrite is gone. The caught panic is re-raised, so the exit code and the job's verdict are exactly as before — the run only gains an explanation on its way out.libcandfutureswere both already in the tree, soCargo.lockgains two dependency edges and no packages.The one thing worth a reviewer's eye: the diagnostics write into the results directory on the panic path, so a filesystem error there must not compound the failure. Every write in that module is best-effort and the unwritable case has a test.
This does not predict a red lane — #455 may have made it green. The claim is narrower and unchanged by that: whenever this lane, or any other, next dies without a message, it will say why.