fix(cargo-coverage-gate): resolve targets without a generated fake rustc - #157
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
The change is confined to the test helper under #[cfg(test)]/#[cfg(unix)] and directly addresses the documented ExecutableFileBusy spawn flake without altering production behavior.
Pull request overview
This PR addresses a CI-only flake in cargo-coverage-gate’s test helper by ensuring the freshly-written fake rustc script is actually executable before tests attempt to spawn it, avoiding intermittent ETXTBSY (“Text file busy”) failures on Unix.
Changes:
- Add a Unix-only
wait_until_executablehelper that probes spawning the script and retries onErrorKind::ExecutableFileBusyuntil a deadline. - Invoke the helper after chmod’ing the fake
rustcscript to ensure subsequent test spawns won’t race the filesystem state.
File summaries
| File | Description |
|---|---|
| crates/cargo-coverage-gate/src/target.rs | Adds a Unix-only retry probe in the test helper to prevent ETXTBSY flakiness when spawning the fake rustc. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ Your project status has failed because the head coverage (97.6%) is below the target coverage (100.0%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #157 +/- ##
=======================================
- Coverage 97.6% 97.6% -0.1%
=======================================
Files 298 298
Lines 67348 67347 -1
=======================================
- Hits 65763 65762 -1
Misses 1585 1585
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`target::tests::resolves_host_and_cfg_from_rustc` intermittently failed the
Linux MSRV Tests check with `Os { code: 26, kind: ExecutableFileBusy }`.
The tests wrote a fake `rustc` shell script into a temp directory and then
executed it. Linux refuses `execve` while any descriptor is still open for
writing on the file, and this test binary is multi-threaded: a fork landing
inside the write's open/close window leaves the child holding a duplicate
write descriptor until its own `exec` clears it. Waiting the race out would
have worked, but the script itself was the problem -- it also carried a
`#!/bin/sh` and `.cmd` pair, a chmod, and shell quoting of interpolated
paths, none of which the code under test needs.
Target resolution now splits along the only seam that requires a process.
`resolve_with_rustc` is a thin shim that spawns `rustc` and reduces the
result to `RustcRun`; `resolve_with_runner` takes that invocation as a
closure and holds all the parsing and error mapping. `RustcRun` exists
because `std::process::Output` carries an `ExitStatus` that no portable API
can construct, so faking one means really running something.
The tests follow the seam. The happy paths run the real `rustc` that is by
definition present, asserting the host's own cfg rather than a hard-coded
triple. Every failure mode -- a non-zero exit, version output with no
`host:` line, unparsable cfg, and a spawn error on either invocation --
becomes a canned value. No file is written, so the race cannot recur.
The assertions also get stricter. They previously matched the rendered
string "failed to resolve", which every one of these errors produces --
including a spurious ETXTBSY, so the race could pass for the wrong reason.
They now assert the specific typed cause.
Test-only change; the resolution logic is unchanged. Verified with
cargo-mutants over target.rs: every mutant is caught.
Related work item: AB#7834991
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
a7f7450 to
8eec7c9
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped to removing flaky test behavior via a clear seam, and the remaining feedback is a small test assertion-strengthening nit.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
🤖 Clawpilot here! Posted automatically by Clawpilot (an AI agent), not by a human. Please verify before acting.
Fixes the
ETXTBSYflake that failed the MSRV Tests (linux) check on #156, a PR that only bumps crate versions and touches nothing in this crate.Tracked as AB#7834991.
The failure
Failing job
Cause
The tests wrote a fake
rustcshell script into a temp directory, chmodded it0755, and executed it. Linux refusesexecvewithETXTBSYwhile any descriptor is still open for writing on the file.std::fs::writecloses its own descriptor before returning, but this test binary is multi-threaded and other tests spawn child processes: a fork landing inside the write's open/close window leaves the child holding a duplicate write descriptor until its ownexecclears it. The window is short, which is why it only bit occasionally and under load.Nothing in the resolution logic was at fault, and this PR does not change it.
Approach
An earlier revision of this PR waited the race out, retrying while the spawn reported
ExecutableFileBusy. That worked, but it treated the symptom: the generated script was itself the liability, carrying a#!/bin/sh/.cmdpair, a chmod,printf-vs-echobranching, and shell quoting of interpolated paths — none of which the code under test needs.So the script is gone instead. Target resolution now splits along the only seam that genuinely requires a process:
resolve_with_rustc— a thin shim that spawnsrustcand reduces the result toRustcRun.resolve_with_runner— takes that invocation as a closure and holds all the parsing and error mapping.RustcRunexists becausestd::process::Outputcarries anExitStatusthat no portable API can construct — faking one means really running something, which is how the whole problem started.The tests follow the seam:
rustc, which is by definition present wherever these tests run. Host resolution asserts the host's own cfg rather than a hard-coded triple, so it is meaningful on every platform; explicit-target resolution usesx86_64-unknown-linux-gnu, which rustc answers for regardless of host or installed targets.host:line, unparsable cfg, and a spawn error on either invocation. No file is written, so the race cannot recur.The assertions get stricter, too
The negative tests previously asserted only that the rendered error contained
"failed to resolve". Every one of these errors renders that way — including a spuriousETXTBSY— so the race was silently swallowed there and those assertions could pass for the wrong reason. They now assert the specific typed cause (RustcCommandFailedError,MissingRustcHostTargetError,InvalidRustcCfgError,ExecuteRustcError) and, where relevant, that the offending value reaches the error.Two cases split out of the old single test are new coverage: a spawn failure on the second (cfg) invocation, and blank lines in cfg output being skipped rather than parsed.
Validation
The changed code was
#[cfg(unix)]-heavy before and is now platform-neutral, but the crate still builds differently per platform, so it was checked against both:cargo clippy -p cargo-coverage-gate --all-targets --target x86_64-unknown-linux-gnu --locked -- -D warnings— cleancargo clippy -p cargo-coverage-gate --all-targets --locked -- -D warnings(host) — cleancargo test -p cargo-coverage-gate --locked— 141 + 24 tests passcargo fmt --check,just anvil-spellcheck— cleancargo mutants --file crates/cargo-coverage-gate/src/target.rs— every mutant caught, so the rewritten tests are load-bearing rather than merely passingCoverage is preserved: the crate has no
[package.metadata.coverage-gate]override and so must hold the repo-wide 100% line default, which is why deleting the tests outright was not an option.The race itself is nondeterministic and was not reproduced locally; the diagnosis comes from the CI log plus the source.