Skip to content

fix(cargo-coverage-gate): resolve targets without a generated fake rustc - #157

Merged
Evgenii (Vaiz) merged 2 commits into
mainfrom
u/vaiz/2026/09/03/coverage-gate-etxtbsy-flake
Sep 3, 2026
Merged

fix(cargo-coverage-gate): resolve targets without a generated fake rustc#157
Evgenii (Vaiz) merged 2 commits into
mainfrom
u/vaiz/2026/09/03/coverage-gate-etxtbsy-flake

Conversation

@Vaiz

@Vaiz Evgenii (Vaiz) commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Clawpilot here! Posted automatically by Clawpilot (an AI agent), not by a human. Please verify before acting.

Fixes the ETXTBSY flake 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

test target::tests::resolves_host_and_cfg_from_rustc ... FAILED

thread '...' panicked at crates/cargo-coverage-gate/src/target.rs:177:98:
resolve fake host: ... ExecuteRustcError { command: "/tmp/.tmpPzrhwZ/rustc -vV",
  ... Os { code: 26, kind: ExecutableFileBusy, message: "Text file busy" } }

error: recipe `anvil-msrv-test` failed with exit code 101

Failing job

Cause

The tests wrote a fake rustc shell script into a temp directory, chmodded it 0755, and executed it. Linux refuses execve with ETXTBSY while any descriptor is still open for writing on the file. std::fs::write closes 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 own exec clears 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 / .cmd pair, a chmod, printf-vs-echo branching, 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 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 — faking one means really running something, which is how the whole problem started.

The tests follow the seam:

  • Happy paths use the real 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 uses x86_64-unknown-linux-gnu, which rustc answers for regardless of host or installed targets.
  • Every failure mode is a canned value: a non-zero exit on either invocation, version output with no host: line, unparsable cfg, and a spawn error on either invocation. No file is written, so the race cannot recur.
  • The shim's own spawn-failure path is covered by pointing at a program that does not exist — no file written there either.

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 spurious ETXTBSY — 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 — clean
  • cargo clippy -p cargo-coverage-gate --all-targets --locked -- -D warnings (host) — clean
  • cargo test -p cargo-coverage-gate --locked — 141 + 24 tests pass
  • cargo fmt --check, just anvil-spellcheck — clean
  • cargo mutants --file crates/cargo-coverage-gate/src/target.rs — every mutant caught, so the rewritten tests are load-bearing rather than merely passing

Coverage 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.

Copilot AI lite review requested due to automatic review settings September 3, 2026 12:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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_executable helper that probes spawning the script and retries on ErrorKind::ExecutableFileBusy until a deadline.
  • Invoke the helper after chmod’ing the fake rustc script 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-commenter

Codecov Comments Bot (codecov-commenter) commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.6%. Comparing base (cad5174) to head (24a2542).

❌ 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             
Flag Coverage Δ
linux 100.0% <100.0%> (?)
linux-arm 100.0% <100.0%> (?)
windows 100.0% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Vaiz
Evgenii (Vaiz) enabled auto-merge (squash) September 3, 2026 12:55
Comment thread crates/cargo-coverage-gate/src/target.rs Outdated
`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>
Copilot AI review requested due to automatic review settings September 3, 2026 15:16
@Vaiz
Evgenii (Vaiz) force-pushed the u/vaiz/2026/09/03/coverage-gate-etxtbsy-flake branch from a7f7450 to 8eec7c9 Compare September 3, 2026 15:16
@Vaiz Evgenii (Vaiz) changed the title fix(cargo-coverage-gate): stop the fake rustc tests flaking on ETXTBSY fix(cargo-coverage-gate): resolve targets without a generated fake rustc Sep 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

Comment thread crates/cargo-coverage-gate/src/target.rs
@Vaiz
Evgenii (Vaiz) merged commit 3626a09 into main Sep 3, 2026
49 checks passed
@Vaiz
Evgenii (Vaiz) deleted the u/vaiz/2026/09/03/coverage-gate-etxtbsy-flake branch September 3, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants