From e9579a653178d89a210974fd0c2a3cd2cba28a2a Mon Sep 17 00:00:00 2001 From: "Stanislav Andras (from Dev Box)" Date: Mon, 31 Aug 2026 19:51:43 +0200 Subject: [PATCH 1/6] perf(cargo-anvil): parallelize Miri test artifacts Compile each Miri profile once to preserve workspace feature unification, then execute independent test artifacts concurrently through cargo-miri. Add configurable concurrency, package-level exclusions, grouped artifact output, memory telemetry, and aggregated failure reporting for GitHub and Azure DevOps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98e52fe3-a435-412e-9154-73a557fef79f --- .anvil.lock | 10 +- crates/cargo-anvil/README.md | 16 +- crates/cargo-anvil/docs/design/checks.md | 10 +- crates/cargo-anvil/docs/design/local.md | 25 +- .../src/anvil/artifacts/justfile.rs | 36 ++ crates/cargo-anvil/src/lib.rs | 14 +- .../anvil/checks/miri-race-coverage.just | 2 +- .../anvil/checks/miri-strict-provenance.just | 2 +- .../anvil/checks/miri-tree-borrows.just | 2 +- .../justfiles/anvil/checks/miri.just | 246 ++++++++++++- crates/cargo-anvil/tests/recipe_contracts.rs | 336 +++++++++++++++++- .../snapshots/snapshots__ado_backend.snap | 252 ++++++++++++- .../snapshots/snapshots__github_backend.snap | 252 ++++++++++++- .../snapshots/snapshots__local_only.snap | 252 ++++++++++++- .../anvil/checks/miri-race-coverage.just | 2 +- .../anvil/checks/miri-strict-provenance.just | 2 +- justfiles/anvil/checks/miri-tree-borrows.just | 2 +- justfiles/anvil/checks/miri.just | 246 ++++++++++++- 18 files changed, 1666 insertions(+), 41 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index a127283c..1418da25 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:411e8b3d247721c856c6a8a69f7b388711159325472ab687eff9c40bdd9727b0" +catalog_checksum = "sha256:f2dbc238f698034edc01c0e78672a914cf96297f73a861a11ec94fe72ba35449" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -121,19 +121,19 @@ checksum = "sha256:f4822a8f9cb282bc426790355ba83b63e778db9327e986ba3c86ae6205cfd [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" -checksum = "sha256:f8435d359f4495a3a1b3693069b0bab371bf94c3698389d9749acf95704a3c45" +checksum = "sha256:eb6d41a49ccd72ec533aeb336103835383b81fc07c9ed38329eb09bcdc807444" [[file]] path = "justfiles/anvil/checks/miri-strict-provenance.just" -checksum = "sha256:2e3fafdb060f10635a0dc34b447db4f06d7dd1549e585440611a5018a1e4ef5c" +checksum = "sha256:cc5fa94c7eb0f6cef28a885c3e72ea4277b5b42e3cbe8e2d750fa91d9a579109" [[file]] path = "justfiles/anvil/checks/miri-tree-borrows.just" -checksum = "sha256:c82f586402f2cb05397a40eec6fe95c7590df2cc52f0dd9da6879de08517561c" +checksum = "sha256:1df669672fb1a507ae087750a3d7872a96990d5b2410c4843217152939963375" [[file]] path = "justfiles/anvil/checks/miri.just" -checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df80e2" +checksum = "sha256:69a56218860ad61a45845c97beff6d6a7651a262446a954ede5da40a31db5c8a" [[file]] path = "justfiles/anvil/checks/msrv-test.just" diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index d3eb1fb2..89567eed 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -380,8 +380,10 @@ metadata conventions — see its documentation. #### Undefined-behavior checking (`miri`) -The PR-tier `miri` check runs `cargo miri test --all-features --tests` -(libtest, not nextest — process-per-test is roughly twice as slow under miri). +The PR-tier `miri` check compiles `cargo miri test --all-features --tests` +once, then runs the resulting libtest artifacts concurrently through +`cargo-miri runner`. Set `ANVIL_MIRI_JOBS` to a positive integer to override +the default of one worker per logical processor. Opt a test out of miri when it touches the filesystem, spawns subprocesses, or otherwise can’t run under the interpreter: @@ -400,6 +402,14 @@ under tree-borrows): #[cfg_attr(miri_race_coverage, ignore = "nondeterministic across seeds")] ``` +A package whose own test targets should not run under Miri can opt out while +remaining available as a dependency: + +```toml +[package.metadata.anvil.miri] +exclude = true +``` + #### Concurrency model checking (`loom`) The `loom` check runs only the test targets that opt in, detected @@ -491,7 +501,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbLvVGTNtetQUbnp9vX0Ew7_gbkZEyxfXZXyMbltL72AXa-o1hZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQb8g6Bt5NkaJob24Zyi8qyA4obC8Lke1bbBmobpIJj7FgYp-9hZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts::container [__link10]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 7ccb2499..a662fcec 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -236,7 +236,7 @@ available. | Check | Invocation | Source | |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| -| `miri` | `cargo + miri test --all-features --tests` over the impact-affected packages. Uses libtest (one process per test binary), **not** `cargo miri nextest run`: under miri, nextest's process-per-test model pays miri's expensive std-initialization re-interpretation for *every* test and roughly doubles wall-time on a large suite (the dominant cost on the PR critical path). `--tests` runs lib/bin unit tests and integration tests (the same target set nextest ran) while excluding doctests, which miri can't run; it is used in preference to `--lib --tests` because `--lib` errors with "no library targets found" on a bin-only affected package under impact scoping. Slow tests opt out per-test with `#[cfg_attr(miri, ignore)]` -- anvil doesn't pass exotic `MIRIFLAGS`; the per-test opt-out is the canonical mechanism. libtest exits 0 when a binary's tests are all skipped, so no `--no-tests=pass` workaround is needed. The recipe reads its scope from the `target/anvil/impact/` cache via `_anvil-impact-include`; because it depends on `anvil-impact`, a clean direct or PR invocation is impact-scoped (unaffected packages are skipped). It runs the full workspace only when scoping is off — the scheduled/full tiers set `ANVIL_IMPACT=off`, and a dirty local tree widens for safety. | oxidizer, oxidizer-github | +| `miri` | Compiles `cargo + miri test --all-features --tests --no-run` once over the impact-affected packages, parses Cargo's JSON artifact stream, then runs independent libtest binaries concurrently through `cargo-miri runner`. Compiling the selected scope together preserves Cargo feature unification; artifact workers avoid serializing every binary. The default is one worker per logical processor, clamped to the artifact count; `ANVIL_MIRI_JOBS` accepts a positive integer override. Memory-load telemetry is informational and never silently changes concurrency. Output is isolated per artifact, replayed deterministically, and every artifact completes before failures are aggregated. Only artifacts with `profile.test = true` run, excluding build scripts and ordinary executables. `--tests` selects lib/bin unit tests and integration tests while excluding doctests, benches, and examples. Slow or unsupported individual tests opt out with `#[cfg_attr(miri, ignore)]`. A package can omit all its own Miri targets through `[package.metadata.anvil.miri] exclude = true`; it may still compile as a dependency of a selected consumer. The recipe remains impact-scoped for clean direct and PR invocations and full-workspace for scheduled/full tiers or a dirty local tree. | oxidizer, oxidizer-github | | `careful` | `cargo + careful test --all-features --locked` over the impact-affected packages. cargo-careful uses a debug-instrumented std in a stable cache path. Because Cargo fingerprints the sysroot path rather than its contents, the recipe records the actual `rustc -vV` and SHA-256 of the resolved `cargo-careful` executable in `target/anvil/careful-sysroot.id`; either changing triggers `cargo clean`. The executable hash is used because cargo-careful rejects version-only invocations. This remains correct when validation accepts a newer installed cargo-careful. | oxidizer-github | | `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. Anvil does not impose a global exploration bound; each model remains responsible for tractable exhaustive exploration. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | | `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Adopters that disable `bolero`'s default features must enable its `std` feature for libfuzzer support. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | @@ -288,11 +288,11 @@ commit can't surface anything new.) | Check | Invocation | Source | |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| | `miri` | Same recipe as the `pr-runtime-analysis` member, but the `scheduled-runtime-analysis` group forces `ANVIL_IMPACT=off` (emit-time `__IMPACT_MODE__`) so the run is full-workspace. PR-tier miri is impact-scoped (so a PR touching crate A never exercises crate B under miri); the scheduled re-run ensures every crate gets miri coverage on `main` at least daily, catching UB introduced by an inter-crate change whose PR happened to scope it out. | oxidizer, oxidizer-github | -| `miri-tree-borrows` | `MIRIFLAGS='-Zmiri-tree-borrows' RUSTFLAGS='--cfg miri_tree_borrows' cargo + miri test --all-features --tests`. Tree-borrows tracks per-byte aliasing provenance and can exceed the 16 GB Linux runner; tests known to OOM under tree-borrows are quarantined per-test in source via `#[cfg_attr(miri_tree_borrows, ignore = "")]` so the suppression lives next to the test rather than in a sidecar file. The recipe declares the cfg name via `--check-cfg=cfg(miri_tree_borrows)` so non-miri builds don't warn. | oxidizer-github (rewritten as cfg-based) | -| `miri-strict-provenance` | `MIRIFLAGS='-Zmiri-strict-provenance' RUSTFLAGS='--cfg miri_strict_provenance' cargo + miri test --all-features --tests`. Surfaces integer-to-pointer casts that don't satisfy strict provenance; complementary to tree-borrows. Per-test opt-outs use `#[cfg_attr(miri_strict_provenance, ignore = "")]`. | oxidizer-github | -| `miri-race-coverage` | `MIRIFLAGS="-Zmiri-many-seeds=.." RUSTFLAGS='--cfg miri_race_coverage' cargo + miri test --all-features --tests`. The `..` window rotates daily based on day-of-month (day N -> seeds `2N-1..2N+1`, exclusive upper bound -> 2 seeds/day, ~62 seeds/month). Rotating amortizes the seed space across the schedule rather than retesting the same seeds every night; race conditions surface as inter-seed nondeterminism rather than per-seed crashes, so coverage matters more than depth-per-seed. Per-test opt-outs use `#[cfg_attr(miri_race_coverage, ignore = "")]`. | oxidizer-github | +| `miri-tree-borrows` | Uses the shared compile-once artifact runner with `MIRIFLAGS='-Zmiri-tree-borrows'` and `RUSTFLAGS='--cfg miri_tree_borrows'`. Tree-borrows tracks per-byte aliasing provenance and can exceed runner memory; tests known to OOM are quarantined per-test via `#[cfg_attr(miri_tree_borrows, ignore = "")]`. | oxidizer-github (rewritten as cfg-based) | +| `miri-strict-provenance` | Uses the shared compile-once artifact runner with `MIRIFLAGS='-Zmiri-strict-provenance'` and `RUSTFLAGS='--cfg miri_strict_provenance'`. It surfaces integer-to-pointer casts that do not satisfy strict provenance; per-test opt-outs use `#[cfg_attr(miri_strict_provenance, ignore = "")]`. | oxidizer-github | +| `miri-race-coverage` | Uses the shared compile-once artifact runner with `MIRIFLAGS="-Zmiri-many-seeds=.."` and `RUSTFLAGS='--cfg miri_race_coverage'`. The `..` window rotates daily based on day-of-month (day N -> seeds `2N-1..2N+1`, exclusive upper bound -> 2 seeds/day, about 62 seeds/month). Per-test opt-outs use `#[cfg_attr(miri_race_coverage, ignore = "")]`. | oxidizer-github | -These profiles each cost hours per leg (oxidizer caps `miri-race-coverage` at 12 h), which is why they live in scheduled rather than PR. They share `miri`'s setup and run sequentially within the `scheduled-runtime-analysis` group. Each uses a profile-specific `MIRIFLAGS` (the actual miri mode, e.g. `-Zmiri-tree-borrows`) plus a profile-specific `--cfg miri_` in `RUSTFLAGS`; the distinct cfg is what lets a test opt out of just one profile via `#[cfg_attr(miri_, ignore = "…")]` without affecting the others. The OS matrix matches `pr-runtime-analysis` (4 legs on GitHub, 2 on ADO) so any OS already considered "worth running miri on" gets the harder profiles too -- the single-tier-per-group rule forbids running tree-borrows on a strict subset of the OSes where stacked-borrows runs, which would silently hide tree-borrows-only UB on the dropped legs. +These profiles each cost hours per leg, which is why they live in scheduled rather than PR. They share `miri`'s setup and run sequentially within the `scheduled-runtime-analysis` group; only independent test artifacts inside one profile run concurrently. Each uses profile-specific `MIRIFLAGS` plus a profile-specific `--cfg miri_` in `RUSTFLAGS`, letting a test opt out of one profile without affecting the others. The OS matrix matches `pr-runtime-analysis` (4 legs on GitHub, 2 on ADO) so any OS considered worth running Miri on gets the harder profiles too. Per-test opt-outs live in source via `#[cfg_attr(miri_, ignore = "")]`. Each miri-profile recipe sets the matching `--cfg` in `RUSTFLAGS`; the cfg names are also declared in the workspace lints region (`unexpected_cfgs` + `check-cfg`) so non-miri builds don't warn. This keeps the suppression next to the test (and behind code review) rather than in a sidecar file the build system has to parse out-of-band. diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index f2aecb66..ef66e636 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -629,7 +629,28 @@ moves, and the working-tree snapshot only when the tree changes; an unchanged re a full cache hit (`anvil-impact: impact set up to date`). To force a recompute, delete `target/anvil/impact/`. -### 4.4 Uncommitted changes widen to the full workspace +### 4.4 Miri artifact concurrency + +Miri compiles the selected package scope once, then executes its independent +libtest artifacts concurrently. `ANVIL_MIRI_JOBS` overrides the default of one +worker per logical processor and must be a positive integer. The worker count is +always clamped to the number of discovered artifacts. Memory-load telemetry is +reported for calibration but does not silently reduce concurrency; use the +override when a runner needs a lower memory footprint. + +Packages whose own test targets are unsuitable or unproductive under Miri can +declare: + +```toml +[package.metadata.anvil.miri] +exclude = true +``` + +The package can still be compiled as a dependency of another selected package. +Prefer per-test `cfg_attr(miri, ignore = "...")` or the profile-specific cfgs +when only individual tests need suppression. + +### 4.5 Uncommitted changes widen to the full workspace cargo-delta scopes on the **committed** diff of `HEAD` against the base ref, so an uncommitted change — a crate you are actively editing but have not committed — is invisible @@ -644,7 +665,7 @@ so CI always gets the scoped, committed-diff result. It is deliberately conserva fast — a dirty tree runs everything. Commit to scope by impact, or use `ANVIL_IMPACT=off` (which also runs the full workspace, and additionally skips cargo-delta entirely). -### 4.5 Base-ref resolution and failure modes +### 4.6 Base-ref resolution and failure modes `anvil-impact` resolves the base ref through `_anvil-base-ref` and computes the committed diff of `HEAD` against it. It deliberately **does not** run `git fetch`: mutating git state diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 7af41063..be4d082a 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -399,6 +399,42 @@ mod tests { } } + #[test] + fn miri_profiles_delegate_to_the_parallel_artifact_runner() { + let miri = CHECK_FILES + .iter() + .find_map(|(path, body)| path.ends_with("/miri.just").then_some(*body)) + .expect("miri.just is registered in CHECK_FILES"); + for needle in [ + "_anvil-miri-test *package_args:", + "miri test --all-features --tests --no-run --message-format=json-render-diagnostics", + "$message.profile.test -ne $true", + "ForEach-Object -Parallel", + "-ThrottleLimit $jobs", + "rustc $toolchain --print sysroot", + "ANVIL_MIRI_JOBS must be a positive integer", + "##[group]Miri artifact", + "::group::Miri artifact", + ] { + assert!(miri.contains(needle), "miri runner is missing '{needle}'"); + } + assert!( + miri.contains("$env:MIRI_BE_RUSTC = 'host'"), + "standalone cargo-miri runner calls need the same ambient rustc mode recorded at build time" + ); + + for check in ["miri", "miri-tree-borrows", "miri-strict-provenance", "miri-race-coverage"] { + let body = CHECK_FILES + .iter() + .find_map(|(path, body)| path.ends_with(&format!("/{check}.just")).then_some(*body)) + .unwrap_or_else(|| panic!("{check}.just is registered in CHECK_FILES")); + assert!( + body.contains("& \"{{ just_executable() }}\" _anvil-miri-test @pkg"), + "{check}.just must delegate to the shared Miri artifact runner" + ); + } + } + #[test] fn impact_recipe_is_defined_and_reuses_shared_helpers() { // The single impact building block: snapshot + compute + resolve. diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index 31fd7881..8361e258 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -381,8 +381,10 @@ //! //! ### Undefined-behavior checking (`miri`) //! -//! The PR-tier `miri` check runs `cargo miri test --all-features --tests` -//! (libtest, not nextest — process-per-test is roughly twice as slow under miri). +//! The PR-tier `miri` check compiles `cargo miri test --all-features --tests` +//! once, then runs the resulting libtest artifacts concurrently through +//! `cargo-miri runner`. Set `ANVIL_MIRI_JOBS` to a positive integer to override +//! the default of one worker per logical processor. //! Opt a test out of miri when it touches the filesystem, spawns //! subprocesses, or otherwise can't run under the interpreter: //! @@ -401,6 +403,14 @@ //! #[cfg_attr(miri_race_coverage, ignore = "nondeterministic across seeds")] //! ``` //! +//! A package whose own test targets should not run under Miri can opt out while +//! remaining available as a dependency: +//! +//! ```toml +//! [package.metadata.anvil.miri] +//! exclude = true +//! ``` +//! //! ### Concurrency model checking (`loom`) //! //! The `loom` check runs only the test targets that opt in, detected diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just index 4851799b..de4a2716 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just @@ -26,7 +26,7 @@ anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just index 60f36c27..65de202b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just @@ -15,7 +15,7 @@ anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvi $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just index c1786354..eccdcd8f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just @@ -29,7 +29,7 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just index 630e2c79..02a9f353 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just @@ -12,6 +12,249 @@ # scheduled/full tiers (which force ANVIL_IMPACT=off) or a dirty local tree # (which widens for safety). +# Compile all selected packages together, then run their Miri test artifacts in +# parallel. The single Cargo invocation preserves workspace feature unification, +# while artifact-level parallelism uses runner CPUs that `cargo miri test` +# otherwise leaves idle. +# +# Packages can opt their own test targets out through: +# +# [package.metadata.anvil.miri] +# exclude = true +# +# They can still compile as dependencies of selected consumers. Workspace runs +# translate the metadata to `--exclude`; impact-scoped runs remove matching +# `--package name@version` pairs before compilation. +# +# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# Memory telemetry is informational and never silently reduces concurrency. +[script("pwsh", "-NoProfile")] +_anvil-miri-test *package_args: + $ErrorActionPreference = 'Stop' + $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) + + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil miri: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse cargo metadata output: $($_.Exception.Message)" + exit 1 + } + + $workspaceMembers = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($member in $metadata.workspace_members) { + [void]$workspaceMembers.Add([string]$member) + } + $packageDirectories = @{} + $excludedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + $excludedPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($package in $metadata.packages) { + $packageId = [string]$package.id + if (-not $workspaceMembers.Contains($packageId)) { continue } + $packageDirectories[$packageId] = Split-Path -Parent ([string]$package.manifest_path) + if ($package.metadata.anvil.miri.exclude -eq $true) { + [void]$excludedPackages.Add([string]$package.name) + [void]$excludedPackageIds.Add($packageId) + } + } + + if ($packageArgs -contains '--workspace') { + foreach ($packageName in $excludedPackages) { + $packageArgs += @('--exclude', $packageName) + } + } else { + $filtered = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $packageArgs.Count; $i++) { + if ($packageArgs[$i] -ne '--package') { + Write-Error "anvil miri: unexpected impact argument '$($packageArgs[$i])'" + exit 1 + } + if (($i + 1) -ge $packageArgs.Count) { + Write-Error 'anvil miri: --package is missing its package spec' + exit 1 + } + $packageSpec = $packageArgs[++$i] + $packageName = ($packageSpec -split '@', 2)[0] + if ($excludedPackages.Contains($packageName)) { continue } + $filtered.Add('--package') + $filtered.Add($packageSpec) + } + $packageArgs = $filtered.ToArray() + if ($packageArgs.Count -eq 0) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } + } + + $logRoot = Join-Path ([IO.Path]::GetTempPath()) "anvil-miri-$PID-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $logRoot | Out-Null + try { + $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' + $toolchain = '+{{ rust_nightly }}' + & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + $buildExitCode = $LASTEXITCODE + + $artifacts = [System.Collections.Generic.List[object]]::new() + $seenArtifacts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($line in Get-Content -LiteralPath $artifactManifest) { + try { + $message = $line | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse Cargo JSON output: $($_.Exception.Message)" + exit 1 + } + if ($message.reason -eq 'compiler-message' -and $message.message.rendered) { + [Console]::Error.Write($message.message.rendered) + continue + } + if ($message.reason -ne 'compiler-artifact' -or -not $message.executable -or $message.profile.test -ne $true) { + continue + } + $artifactPath = [string]$message.executable + if (-not $seenArtifacts.Add($artifactPath)) { continue } + $packageId = [string]$message.package_id + if ($excludedPackageIds.Contains($packageId)) { continue } + if (-not $packageDirectories.ContainsKey($packageId)) { + Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" + exit 1 + } + $artifacts.Add([pscustomobject]@{ + Path = $artifactPath + Name = Split-Path -Leaf $artifactPath + WorkingDirectory = $packageDirectories[$packageId] + }) + } + + if ($buildExitCode -ne 0) { exit $buildExitCode } + if ($artifacts.Count -eq 0) { + Write-Host 'anvil miri: Cargo produced no runnable test artifacts; nothing to test' + exit 0 + } + + $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { + Write-Error 'anvil miri: cargo miri setup did not report a sysroot' + exit 1 + } + $env:MIRI_SYSROOT = $sysrootOutput[-1] + $toolchainSysroot = & rustc $toolchain --print sysroot + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchainSysroot)) { + Write-Error "anvil miri: could not resolve the $toolchain toolchain sysroot" + exit 1 + } + $toolchainBin = Join-Path $toolchainSysroot.Trim() 'bin' + $runner = @( + 'cargo-miri.exe', 'cargo-miri', 'cargo-miri.ps1' | + ForEach-Object { Join-Path $toolchainBin $_ } | + Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } + ) | Select-Object -First 1 + if (-not $runner) { + Write-Error "anvil miri: cargo-miri was not found under '$toolchainBin'" + exit 1 + } + # cargo-miri records MIRI_BE_RUSTC=host in each artifact. Keeping the + # same value in the standalone runner environment lets phase_runner + # suppress that recorded value before launching Miri for interpretation. + $env:MIRI_BE_RUSTC = 'host' + + $jobs = 0 + if ($env:ANVIL_MIRI_JOBS) { + if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { + Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + exit 1 + } + } else { + $jobs = [Environment]::ProcessorCount + } + $jobs = [Math]::Min($jobs, $artifacts.Count) + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $memoryInfo = [GC]::GetGCMemoryInfo() + $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) + $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) + $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) + Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" + Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" + + $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { + [pscustomobject]@{ + Index = $i + Artifact = $orderedArtifacts[$i].Path + Name = $orderedArtifacts[$i].Name + WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory + } + } + $results = @( + $work | ForEach-Object -Parallel { + $ErrorActionPreference = 'Stop' + function Get-MemoryTelemetry { + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $info = [GC]::GetGCMemoryInfo() + $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) + $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) + $headroomGiB = [Math]::Round( + [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, + 1 + ) + "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" + } + + $item = $_ + $log = Join-Path $using:logRoot "$($item.Index).log" + Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Push-Location -LiteralPath $item.WorkingDirectory + try { + & $using:runner runner $item.Artifact *> $log + $exitCode = $LASTEXITCODE + } catch { + $_ | Out-String | Set-Content -LiteralPath $log + $exitCode = 1 + } finally { + Pop-Location + } + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + [pscustomobject]@{ + Index = $item.Index + Name = $item.Name + ExitCode = $exitCode + Log = $log + } + } -ThrottleLimit $jobs + ) + + foreach ($result in $results | Sort-Object Index) { + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host "::group::Miri artifact $($result.Name)" + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host "##[group]Miri artifact $($result.Name)" + } else { + Write-Host "`n=== Miri artifact $($result.Name) ===" + } + Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host '::endgroup::' + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host '##[endgroup]' + } + } + + $failed = @($results | Where-Object ExitCode -ne 0) + if ($failed.Count -ne 0) { + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + exit 1 + } + } finally { + Remove-Item -LiteralPath $logRoot -Recurse -Force -ErrorAction SilentlyContinue + } + # Run Miri tests for affected workspace packages. [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact @@ -43,7 +286,8 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 7b1f8220..fbcab268 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -23,6 +23,8 @@ const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.ju const FMT: &str = include_str!("../templates/justfiles/anvil/checks/fmt.just"); const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); const LOOM: &str = include_str!("../templates/justfiles/anvil/checks/loom.just"); +const MIRI: &str = include_str!("../templates/justfiles/anvil/checks/miri.just"); +const MIRI_TREE_BORROWS: &str = include_str!("../templates/justfiles/anvil/checks/miri-tree-borrows.just"); const MSRV_TEST: &str = include_str!("../templates/justfiles/anvil/checks/msrv-test.just"); const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); @@ -101,6 +103,14 @@ if ($args -contains 'metadata') { } else { [System.IO.Path]::Combine($root, 'Cargo.toml') } + $packageMetadata = [pscustomobject]@{ + 'coverage-gate' = [pscustomobject]@{ 'min-lines-percent' = 0 } + } + if ($env:FAKE_MIRI_EXCLUDE) { + $packageMetadata | Add-Member -NotePropertyName anvil -NotePropertyValue ( + [pscustomobject]@{ miri = [pscustomobject]@{ exclude = $true } } + ) + } $packages = @( [pscustomobject]@{ name = $packageName @@ -114,9 +124,7 @@ if ($args -contains 'metadata') { } else { $null } - metadata = [pscustomobject]@{ - 'coverage-gate' = [pscustomobject]@{ 'min-lines-percent' = 0 } - } + metadata = $packageMetadata } ) if ($env:FAKE_SECOND_PACKAGE_NAME) { @@ -125,6 +133,12 @@ if ($args -contains 'metadata') { } else { $env:FAKE_PACKAGE_DIR_LEAF } + $secondMetadata = [pscustomobject]@{} + if ($env:FAKE_SECOND_MIRI_EXCLUDE) { + $secondMetadata | Add-Member -NotePropertyName anvil -NotePropertyValue ( + [pscustomobject]@{ miri = [pscustomobject]@{ exclude = $true } } + ) + } $packages += [pscustomobject]@{ name = $env:FAKE_SECOND_PACKAGE_NAME version = '0.1.0' @@ -132,7 +146,7 @@ if ($args -contains 'metadata') { manifest_path = [System.IO.Path]::Combine($root, 'nested', $secondDirLeaf, 'Cargo.toml') targets = @([pscustomobject]@{ name = $env:FAKE_SECOND_PACKAGE_NAME; kind = @('lib') }) publish = $null - metadata = [pscustomobject]@{} + metadata = $secondMetadata } } if ($env:FAKE_THIRD_PACKAGE_NAME) { @@ -173,6 +187,47 @@ if ($args -contains 'metadata') { $metadata | ConvertTo-Json -Depth 8 -Compress exit 0 } +if ($args -contains 'miri') { + if ($args -contains 'setup') { + Write-Output ([System.IO.Path]::Combine($env:FAKE_WORKSPACE_ROOT, 'fake-miri-sysroot')) + exit [int]$env:FAKE_MIRI_SETUP_EXIT + } + if ($args -contains '--no-run') { + if ($env:FAKE_MIRI_INVALID_JSON) { + Write-Output '{invalid cargo json' + exit 0 + } + $artifactRoot = [System.IO.Path]::Combine($env:FAKE_WORKSPACE_ROOT, 'fake artifacts') + [System.IO.Directory]::CreateDirectory($artifactRoot) | Out-Null + $definitions = @() + if ($env:FAKE_MIRI_ARTIFACTS) { + $definitions = @($env:FAKE_MIRI_ARTIFACTS | ConvertFrom-Json) + } + foreach ($definition in $definitions) { + $path = [System.IO.Path]::Combine($artifactRoot, [string]$definition.name) + Set-Content -LiteralPath $path -Value '{}' + [pscustomobject]@{ + reason = 'compiler-artifact' + package_id = [string]$definition.package_id + executable = $path + profile = [pscustomobject]@{ test = [bool]$definition.test } + } | ConvertTo-Json -Depth 4 -Compress + if ($definition.duplicate) { + [pscustomobject]@{ + reason = 'compiler-artifact' + package_id = [string]$definition.package_id + executable = $path + profile = [pscustomobject]@{ test = [bool]$definition.test } + } | ConvertTo-Json -Depth 4 -Compress + } + } + [pscustomobject]@{ + reason = 'build-finished' + success = ([int]$env:FAKE_MIRI_BUILD_EXIT -eq 0) + } | ConvertTo-Json -Compress + exit [int]$env:FAKE_MIRI_BUILD_EXIT + } +} if ($args -contains 'semver-checks') { if ($env:FAKE_SEMVER_OUTPUT) { Write-Output $env:FAKE_SEMVER_OUTPUT } exit [int]$env:FAKE_SEMVER_EXIT @@ -247,6 +302,38 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { fs::create_dir_all(&bin).unwrap(); write(&bin.join("cargo.ps1"), FAKE_CARGO_PS1); write(&bin.join("git.ps1"), "exit 0\n"); + write( + &bin.join("rustc.ps1"), + r#" +if ($args -contains 'sysroot') { + Write-Output ([System.IO.Path]::Combine($env:FAKE_WORKSPACE_ROOT, 'fake-toolchain')) + exit 0 +} +exit 1 +"#, + ); + let fake_toolchain_bin = tmp.path().join("fake-toolchain/bin"); + fs::create_dir_all(&fake_toolchain_bin).unwrap(); + write( + &fake_toolchain_bin.join("cargo-miri.ps1"), + r#" +$artifact = [System.IO.Path]::GetFileName([string]$args[-1]) +$prefix = "$($env:FAKE_MIRI_RUN_LOG).$artifact" +(Get-Location).Path | Set-Content -LiteralPath "$prefix.cwd" +$env:MIRI_SYSROOT | Set-Content -LiteralPath "$prefix.sysroot" +$env:MIRI_BE_RUSTC | Set-Content -LiteralPath "$prefix.miri-be-rustc" +$env:MIRIFLAGS | Set-Content -LiteralPath "$prefix.miriflags" +$env:RUSTFLAGS | Set-Content -LiteralPath "$prefix.rustflags" +(Get-Date).ToUniversalTime().Ticks | Set-Content -LiteralPath "$prefix.start" +if ($env:FAKE_MIRI_SLEEP_MS) { + Start-Sleep -Milliseconds ([int]$env:FAKE_MIRI_SLEEP_MS) +} +Write-Output "miri output: $artifact" +(Get-Date).ToUniversalTime().Ticks | Set-Content -LiteralPath "$prefix.end" +if ($artifact -like '*fail*') { exit 9 } +exit 0 +"#, + ); tmp } @@ -294,6 +381,247 @@ fn assert_failed(output: &Output, context: &str) { ); } +#[test] +fn miri_runner_filters_artifacts_and_runs_in_parallel() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("miri.just", MIRI)], + &[ + "anvil-component-nightly-miri-validate-prereqs", + "anvil-component-nightly-rust-src-validate-prereqs", + "anvil-component-nightly-miri-install", + "anvil-component-nightly-rust-src-install", + "anvil-impact", + ], + ); + let nested = tmp.path().join("nested/other-package"); + fs::create_dir_all(&nested).unwrap(); + write( + &nested.join("Cargo.toml"), + "[package]\nname = \"other-package\"\nversion = \"0.1.0\"\n", + ); + let cargo_log = tmp.path().join("cargo.log"); + let run_log = tmp.path().join("miri-run"); + let artifacts = r#"[ + {"name":"zeta-test","package_id":"fixture 0.1.0","test":true}, + {"name":"alpha-test","package_id":"fixture 0.1.0","test":true,"duplicate":true}, + {"name":"ordinary-bin","package_id":"fixture 0.1.0","test":false}, + {"name":"excluded-test","package_id":"other-package 0.1.0","test":true} + ]"#; + let output = run_just( + tmp.path(), + &["_anvil-miri-test", "--workspace"], + &[ + ("ANVIL_MIRI_JOBS", OsStr::new("2")), + ("FAKE_CARGO_LOG", cargo_log.as_os_str()), + ("FAKE_MIRI_ARTIFACTS", OsStr::new(artifacts)), + ("FAKE_MIRI_RUN_LOG", run_log.as_os_str()), + ("FAKE_MIRI_SLEEP_MS", OsStr::new("300")), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("other-package")), + ("FAKE_SECOND_PACKAGE_DIR_LEAF", OsStr::new("other-package")), + ("FAKE_SECOND_MIRI_EXCLUDE", OsStr::new("1")), + ], + ); + + assert!( + output.status.success(), + "parallel Miri runner should succeed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let cargo_calls = fs::read_to_string(cargo_log).unwrap(); + assert!( + cargo_calls.contains( + "miri test --all-features --tests --no-run --message-format=json-render-diagnostics --workspace --exclude other-package" + ), + "workspace Miri compilation must exclude metadata-opted-out packages:\n{cargo_calls}" + ); + assert!(run_log.with_extension("alpha-test.start").is_file()); + assert!(run_log.with_extension("zeta-test.start").is_file()); + assert!(!run_log.with_extension("ordinary-bin.start").exists()); + assert!(!run_log.with_extension("excluded-test.start").exists()); + + assert_eq!( + fs::read_to_string(run_log.with_extension("alpha-test.cwd")).unwrap().trim(), + tmp.path().to_str().unwrap() + ); + assert_eq!( + fs::read_to_string(run_log.with_extension("alpha-test.sysroot")).unwrap().trim(), + tmp.path().join("fake-miri-sysroot").to_str().unwrap() + ); + assert_eq!( + fs::read_to_string(run_log.with_extension("alpha-test.miri-be-rustc")) + .unwrap() + .trim(), + "host" + ); + let alpha_started = fs::read_to_string(run_log.with_extension("alpha-test.start")) + .unwrap() + .trim() + .parse::() + .unwrap(); + let zeta_started = fs::read_to_string(run_log.with_extension("zeta-test.start")) + .unwrap() + .trim() + .parse::() + .unwrap(); + let alpha_ended = fs::read_to_string(run_log.with_extension("alpha-test.end")) + .unwrap() + .trim() + .parse::() + .unwrap(); + let zeta_ended = fs::read_to_string(run_log.with_extension("zeta-test.end")) + .unwrap() + .trim() + .parse::() + .unwrap(); + assert!( + alpha_started < zeta_ended && zeta_started < alpha_ended, + "two workers should overlap: alpha {alpha_started}..{alpha_ended}, zeta {zeta_started}..{zeta_ended}" + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let alpha_group = stdout.find("=== Miri artifact alpha-test ===").unwrap(); + let zeta_group = stdout.find("=== Miri artifact zeta-test ===").unwrap(); + assert!(alpha_group < zeta_group, "artifact logs must replay deterministically"); +} + +#[test] +fn miri_runner_preserves_profile_flags_and_impact_filtering() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("miri.just", MIRI), ("miri-tree-borrows.just", MIRI_TREE_BORROWS)], + &[ + "anvil-component-nightly-miri-validate-prereqs", + "anvil-component-nightly-rust-src-validate-prereqs", + "anvil-component-nightly-miri-install", + "anvil-component-nightly-rust-src-install", + "anvil-impact", + ], + ); + let justfile_path = tmp.path().join("Justfile"); + let mut justfile = fs::read_to_string(&justfile_path).unwrap(); + justfile.push_str( + r#" +[script("pwsh", "-NoProfile")] +_anvil-impact-include tier: + Write-Output $env:FAKE_INCLUDE +"#, + ); + write(&justfile_path, &justfile); + let run_log = tmp.path().join("miri-profile"); + let artifacts = r#"[{"name":"profile-test","package_id":"fixture 0.1.0","test":true}]"#; + let output = run_just( + tmp.path(), + &["anvil-miri-tree-borrows"], + &[ + ("FAKE_INCLUDE", OsStr::new("--package fixture@0.1.0")), + ("FAKE_MIRI_ARTIFACTS", OsStr::new(artifacts)), + ("FAKE_MIRI_RUN_LOG", run_log.as_os_str()), + ], + ); + + assert!( + output.status.success(), + "Tree Borrows profile should delegate to the shared runner:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + fs::read_to_string(run_log.with_extension("profile-test.miriflags")) + .unwrap() + .contains("-Zmiri-tree-borrows") + ); + assert!( + fs::read_to_string(run_log.with_extension("profile-test.rustflags")) + .unwrap() + .contains("--cfg miri_tree_borrows") + ); +} + +#[test] +fn miri_runner_handles_no_work_and_aggregates_failures() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("miri.just", MIRI)], + &[ + "anvil-component-nightly-miri-validate-prereqs", + "anvil-component-nightly-rust-src-validate-prereqs", + "anvil-component-nightly-miri-install", + "anvil-component-nightly-rust-src-install", + "anvil-impact", + ], + ); + let no_artifacts = run_just(tmp.path(), &["_anvil-miri-test", "--workspace"], &[]); + assert!( + no_artifacts.status.success(), + "a selected package set with no test artifacts should succeed:\n{}", + String::from_utf8_lossy(&no_artifacts.stderr) + ); + assert!(String::from_utf8_lossy(&no_artifacts.stdout).contains("no runnable test artifacts")); + + let all_excluded = run_just( + tmp.path(), + &["_anvil-miri-test", "--package", "fixture@0.1.0"], + &[("FAKE_MIRI_EXCLUDE", OsStr::new("1"))], + ); + assert!(all_excluded.status.success()); + assert!(String::from_utf8_lossy(&all_excluded.stdout).contains("all selected packages are excluded")); + + let run_log = tmp.path().join("miri-failure"); + let artifacts = r#"[ + {"name":"fail-test","package_id":"fixture 0.1.0","test":true}, + {"name":"pass-test","package_id":"fixture 0.1.0","test":true} + ]"#; + let failed = run_just( + tmp.path(), + &["_anvil-miri-test", "--workspace"], + &[ + ("FAKE_MIRI_ARTIFACTS", OsStr::new(artifacts)), + ("FAKE_MIRI_RUN_LOG", run_log.as_os_str()), + ("ANVIL_MIRI_JOBS", OsStr::new("2")), + ("TF_BUILD", OsStr::new("True")), + ], + ); + assert_failed(&failed, "one failed Miri artifact"); + let stdout = String::from_utf8_lossy(&failed.stdout); + let stderr = String::from_utf8_lossy(&failed.stderr); + assert!(stdout.contains("miri output: fail-test")); + assert!(stdout.contains("miri output: pass-test")); + assert!(stdout.contains("##[group]Miri artifact fail-test")); + assert!(stdout.contains("##[endgroup]")); + assert!(stderr.contains("failed artifacts: fail-test")); + + let invalid_jobs = run_just( + tmp.path(), + &["_anvil-miri-test", "--workspace"], + &[ + ( + "FAKE_MIRI_ARTIFACTS", + OsStr::new(r#"[{"name":"jobs-test","package_id":"fixture 0.1.0","test":true}]"#), + ), + ("FAKE_MIRI_RUN_LOG", run_log.as_os_str()), + ("ANVIL_MIRI_JOBS", OsStr::new("0")), + ], + ); + assert_failed(&invalid_jobs, "invalid ANVIL_MIRI_JOBS"); + assert!(String::from_utf8_lossy(&invalid_jobs.stderr).contains("must be a positive integer")); + + let invalid_json = run_just( + tmp.path(), + &["_anvil-miri-test", "--workspace"], + &[("FAKE_MIRI_INVALID_JSON", OsStr::new("1"))], + ); + assert_failed(&invalid_json, "malformed Cargo JSON"); + assert!(String::from_utf8_lossy(&invalid_json.stderr).contains("could not parse Cargo JSON output")); +} + #[test] fn stable_command_leaves_environment_toolchain_selection_native() { if !tools_available() { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index e8063b70..9458b886 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2616,7 +2616,7 @@ anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. @@ -2645,7 +2645,7 @@ anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvi $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. @@ -2688,7 +2688,7 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -2719,6 +2719,249 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # scheduled/full tiers (which force ANVIL_IMPACT=off) or a dirty local tree # (which widens for safety). +# Compile all selected packages together, then run their Miri test artifacts in +# parallel. The single Cargo invocation preserves workspace feature unification, +# while artifact-level parallelism uses runner CPUs that `cargo miri test` +# otherwise leaves idle. +# +# Packages can opt their own test targets out through: +# +# [package.metadata.anvil.miri] +# exclude = true +# +# They can still compile as dependencies of selected consumers. Workspace runs +# translate the metadata to `--exclude`; impact-scoped runs remove matching +# `--package name@version` pairs before compilation. +# +# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# Memory telemetry is informational and never silently reduces concurrency. +[script("pwsh", "-NoProfile")] +_anvil-miri-test *package_args: + $ErrorActionPreference = 'Stop' + $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) + + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil miri: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse cargo metadata output: $($_.Exception.Message)" + exit 1 + } + + $workspaceMembers = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($member in $metadata.workspace_members) { + [void]$workspaceMembers.Add([string]$member) + } + $packageDirectories = @{} + $excludedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + $excludedPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($package in $metadata.packages) { + $packageId = [string]$package.id + if (-not $workspaceMembers.Contains($packageId)) { continue } + $packageDirectories[$packageId] = Split-Path -Parent ([string]$package.manifest_path) + if ($package.metadata.anvil.miri.exclude -eq $true) { + [void]$excludedPackages.Add([string]$package.name) + [void]$excludedPackageIds.Add($packageId) + } + } + + if ($packageArgs -contains '--workspace') { + foreach ($packageName in $excludedPackages) { + $packageArgs += @('--exclude', $packageName) + } + } else { + $filtered = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $packageArgs.Count; $i++) { + if ($packageArgs[$i] -ne '--package') { + Write-Error "anvil miri: unexpected impact argument '$($packageArgs[$i])'" + exit 1 + } + if (($i + 1) -ge $packageArgs.Count) { + Write-Error 'anvil miri: --package is missing its package spec' + exit 1 + } + $packageSpec = $packageArgs[++$i] + $packageName = ($packageSpec -split '@', 2)[0] + if ($excludedPackages.Contains($packageName)) { continue } + $filtered.Add('--package') + $filtered.Add($packageSpec) + } + $packageArgs = $filtered.ToArray() + if ($packageArgs.Count -eq 0) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } + } + + $logRoot = Join-Path ([IO.Path]::GetTempPath()) "anvil-miri-$PID-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $logRoot | Out-Null + try { + $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' + $toolchain = '+{{ rust_nightly }}' + & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + $buildExitCode = $LASTEXITCODE + + $artifacts = [System.Collections.Generic.List[object]]::new() + $seenArtifacts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($line in Get-Content -LiteralPath $artifactManifest) { + try { + $message = $line | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse Cargo JSON output: $($_.Exception.Message)" + exit 1 + } + if ($message.reason -eq 'compiler-message' -and $message.message.rendered) { + [Console]::Error.Write($message.message.rendered) + continue + } + if ($message.reason -ne 'compiler-artifact' -or -not $message.executable -or $message.profile.test -ne $true) { + continue + } + $artifactPath = [string]$message.executable + if (-not $seenArtifacts.Add($artifactPath)) { continue } + $packageId = [string]$message.package_id + if ($excludedPackageIds.Contains($packageId)) { continue } + if (-not $packageDirectories.ContainsKey($packageId)) { + Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" + exit 1 + } + $artifacts.Add([pscustomobject]@{ + Path = $artifactPath + Name = Split-Path -Leaf $artifactPath + WorkingDirectory = $packageDirectories[$packageId] + }) + } + + if ($buildExitCode -ne 0) { exit $buildExitCode } + if ($artifacts.Count -eq 0) { + Write-Host 'anvil miri: Cargo produced no runnable test artifacts; nothing to test' + exit 0 + } + + $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { + Write-Error 'anvil miri: cargo miri setup did not report a sysroot' + exit 1 + } + $env:MIRI_SYSROOT = $sysrootOutput[-1] + $toolchainSysroot = & rustc $toolchain --print sysroot + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchainSysroot)) { + Write-Error "anvil miri: could not resolve the $toolchain toolchain sysroot" + exit 1 + } + $toolchainBin = Join-Path $toolchainSysroot.Trim() 'bin' + $runner = @( + 'cargo-miri.exe', 'cargo-miri', 'cargo-miri.ps1' | + ForEach-Object { Join-Path $toolchainBin $_ } | + Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } + ) | Select-Object -First 1 + if (-not $runner) { + Write-Error "anvil miri: cargo-miri was not found under '$toolchainBin'" + exit 1 + } + # cargo-miri records MIRI_BE_RUSTC=host in each artifact. Keeping the + # same value in the standalone runner environment lets phase_runner + # suppress that recorded value before launching Miri for interpretation. + $env:MIRI_BE_RUSTC = 'host' + + $jobs = 0 + if ($env:ANVIL_MIRI_JOBS) { + if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { + Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + exit 1 + } + } else { + $jobs = [Environment]::ProcessorCount + } + $jobs = [Math]::Min($jobs, $artifacts.Count) + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $memoryInfo = [GC]::GetGCMemoryInfo() + $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) + $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) + $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) + Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" + Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" + + $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { + [pscustomobject]@{ + Index = $i + Artifact = $orderedArtifacts[$i].Path + Name = $orderedArtifacts[$i].Name + WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory + } + } + $results = @( + $work | ForEach-Object -Parallel { + $ErrorActionPreference = 'Stop' + function Get-MemoryTelemetry { + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $info = [GC]::GetGCMemoryInfo() + $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) + $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) + $headroomGiB = [Math]::Round( + [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, + 1 + ) + "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" + } + + $item = $_ + $log = Join-Path $using:logRoot "$($item.Index).log" + Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Push-Location -LiteralPath $item.WorkingDirectory + try { + & $using:runner runner $item.Artifact *> $log + $exitCode = $LASTEXITCODE + } catch { + $_ | Out-String | Set-Content -LiteralPath $log + $exitCode = 1 + } finally { + Pop-Location + } + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + [pscustomobject]@{ + Index = $item.Index + Name = $item.Name + ExitCode = $exitCode + Log = $log + } + } -ThrottleLimit $jobs + ) + + foreach ($result in $results | Sort-Object Index) { + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host "::group::Miri artifact $($result.Name)" + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host "##[group]Miri artifact $($result.Name)" + } else { + Write-Host "`n=== Miri artifact $($result.Name) ===" + } + Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host '::endgroup::' + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host '##[endgroup]' + } + } + + $failed = @($results | Where-Object ExitCode -ne 0) + if ($failed.Count -ne 0) { + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + exit 1 + } + } finally { + Remove-Item -LiteralPath $logRoot -Recurse -Force -ErrorAction SilentlyContinue + } + # Run Miri tests for affected workspace packages. [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact @@ -2750,7 +2993,8 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 45d27caf..eca2f156 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2602,7 +2602,7 @@ anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. @@ -2631,7 +2631,7 @@ anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvi $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. @@ -2674,7 +2674,7 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -2705,6 +2705,249 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # scheduled/full tiers (which force ANVIL_IMPACT=off) or a dirty local tree # (which widens for safety). +# Compile all selected packages together, then run their Miri test artifacts in +# parallel. The single Cargo invocation preserves workspace feature unification, +# while artifact-level parallelism uses runner CPUs that `cargo miri test` +# otherwise leaves idle. +# +# Packages can opt their own test targets out through: +# +# [package.metadata.anvil.miri] +# exclude = true +# +# They can still compile as dependencies of selected consumers. Workspace runs +# translate the metadata to `--exclude`; impact-scoped runs remove matching +# `--package name@version` pairs before compilation. +# +# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# Memory telemetry is informational and never silently reduces concurrency. +[script("pwsh", "-NoProfile")] +_anvil-miri-test *package_args: + $ErrorActionPreference = 'Stop' + $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) + + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil miri: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse cargo metadata output: $($_.Exception.Message)" + exit 1 + } + + $workspaceMembers = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($member in $metadata.workspace_members) { + [void]$workspaceMembers.Add([string]$member) + } + $packageDirectories = @{} + $excludedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + $excludedPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($package in $metadata.packages) { + $packageId = [string]$package.id + if (-not $workspaceMembers.Contains($packageId)) { continue } + $packageDirectories[$packageId] = Split-Path -Parent ([string]$package.manifest_path) + if ($package.metadata.anvil.miri.exclude -eq $true) { + [void]$excludedPackages.Add([string]$package.name) + [void]$excludedPackageIds.Add($packageId) + } + } + + if ($packageArgs -contains '--workspace') { + foreach ($packageName in $excludedPackages) { + $packageArgs += @('--exclude', $packageName) + } + } else { + $filtered = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $packageArgs.Count; $i++) { + if ($packageArgs[$i] -ne '--package') { + Write-Error "anvil miri: unexpected impact argument '$($packageArgs[$i])'" + exit 1 + } + if (($i + 1) -ge $packageArgs.Count) { + Write-Error 'anvil miri: --package is missing its package spec' + exit 1 + } + $packageSpec = $packageArgs[++$i] + $packageName = ($packageSpec -split '@', 2)[0] + if ($excludedPackages.Contains($packageName)) { continue } + $filtered.Add('--package') + $filtered.Add($packageSpec) + } + $packageArgs = $filtered.ToArray() + if ($packageArgs.Count -eq 0) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } + } + + $logRoot = Join-Path ([IO.Path]::GetTempPath()) "anvil-miri-$PID-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $logRoot | Out-Null + try { + $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' + $toolchain = '+{{ rust_nightly }}' + & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + $buildExitCode = $LASTEXITCODE + + $artifacts = [System.Collections.Generic.List[object]]::new() + $seenArtifacts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($line in Get-Content -LiteralPath $artifactManifest) { + try { + $message = $line | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse Cargo JSON output: $($_.Exception.Message)" + exit 1 + } + if ($message.reason -eq 'compiler-message' -and $message.message.rendered) { + [Console]::Error.Write($message.message.rendered) + continue + } + if ($message.reason -ne 'compiler-artifact' -or -not $message.executable -or $message.profile.test -ne $true) { + continue + } + $artifactPath = [string]$message.executable + if (-not $seenArtifacts.Add($artifactPath)) { continue } + $packageId = [string]$message.package_id + if ($excludedPackageIds.Contains($packageId)) { continue } + if (-not $packageDirectories.ContainsKey($packageId)) { + Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" + exit 1 + } + $artifacts.Add([pscustomobject]@{ + Path = $artifactPath + Name = Split-Path -Leaf $artifactPath + WorkingDirectory = $packageDirectories[$packageId] + }) + } + + if ($buildExitCode -ne 0) { exit $buildExitCode } + if ($artifacts.Count -eq 0) { + Write-Host 'anvil miri: Cargo produced no runnable test artifacts; nothing to test' + exit 0 + } + + $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { + Write-Error 'anvil miri: cargo miri setup did not report a sysroot' + exit 1 + } + $env:MIRI_SYSROOT = $sysrootOutput[-1] + $toolchainSysroot = & rustc $toolchain --print sysroot + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchainSysroot)) { + Write-Error "anvil miri: could not resolve the $toolchain toolchain sysroot" + exit 1 + } + $toolchainBin = Join-Path $toolchainSysroot.Trim() 'bin' + $runner = @( + 'cargo-miri.exe', 'cargo-miri', 'cargo-miri.ps1' | + ForEach-Object { Join-Path $toolchainBin $_ } | + Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } + ) | Select-Object -First 1 + if (-not $runner) { + Write-Error "anvil miri: cargo-miri was not found under '$toolchainBin'" + exit 1 + } + # cargo-miri records MIRI_BE_RUSTC=host in each artifact. Keeping the + # same value in the standalone runner environment lets phase_runner + # suppress that recorded value before launching Miri for interpretation. + $env:MIRI_BE_RUSTC = 'host' + + $jobs = 0 + if ($env:ANVIL_MIRI_JOBS) { + if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { + Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + exit 1 + } + } else { + $jobs = [Environment]::ProcessorCount + } + $jobs = [Math]::Min($jobs, $artifacts.Count) + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $memoryInfo = [GC]::GetGCMemoryInfo() + $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) + $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) + $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) + Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" + Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" + + $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { + [pscustomobject]@{ + Index = $i + Artifact = $orderedArtifacts[$i].Path + Name = $orderedArtifacts[$i].Name + WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory + } + } + $results = @( + $work | ForEach-Object -Parallel { + $ErrorActionPreference = 'Stop' + function Get-MemoryTelemetry { + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $info = [GC]::GetGCMemoryInfo() + $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) + $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) + $headroomGiB = [Math]::Round( + [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, + 1 + ) + "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" + } + + $item = $_ + $log = Join-Path $using:logRoot "$($item.Index).log" + Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Push-Location -LiteralPath $item.WorkingDirectory + try { + & $using:runner runner $item.Artifact *> $log + $exitCode = $LASTEXITCODE + } catch { + $_ | Out-String | Set-Content -LiteralPath $log + $exitCode = 1 + } finally { + Pop-Location + } + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + [pscustomobject]@{ + Index = $item.Index + Name = $item.Name + ExitCode = $exitCode + Log = $log + } + } -ThrottleLimit $jobs + ) + + foreach ($result in $results | Sort-Object Index) { + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host "::group::Miri artifact $($result.Name)" + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host "##[group]Miri artifact $($result.Name)" + } else { + Write-Host "`n=== Miri artifact $($result.Name) ===" + } + Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host '::endgroup::' + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host '##[endgroup]' + } + } + + $failed = @($results | Where-Object ExitCode -ne 0) + if ($failed.Count -ne 0) { + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + exit 1 + } + } finally { + Remove-Item -LiteralPath $logRoot -Recurse -Force -ErrorAction SilentlyContinue + } + # Run Miri tests for affected workspace packages. [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact @@ -2736,7 +2979,8 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index dd2197ec..55c1b2c8 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1486,7 +1486,7 @@ anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. @@ -1515,7 +1515,7 @@ anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvi $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. @@ -1558,7 +1558,7 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -1589,6 +1589,249 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # scheduled/full tiers (which force ANVIL_IMPACT=off) or a dirty local tree # (which widens for safety). +# Compile all selected packages together, then run their Miri test artifacts in +# parallel. The single Cargo invocation preserves workspace feature unification, +# while artifact-level parallelism uses runner CPUs that `cargo miri test` +# otherwise leaves idle. +# +# Packages can opt their own test targets out through: +# +# [package.metadata.anvil.miri] +# exclude = true +# +# They can still compile as dependencies of selected consumers. Workspace runs +# translate the metadata to `--exclude`; impact-scoped runs remove matching +# `--package name@version` pairs before compilation. +# +# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# Memory telemetry is informational and never silently reduces concurrency. +[script("pwsh", "-NoProfile")] +_anvil-miri-test *package_args: + $ErrorActionPreference = 'Stop' + $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) + + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil miri: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse cargo metadata output: $($_.Exception.Message)" + exit 1 + } + + $workspaceMembers = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($member in $metadata.workspace_members) { + [void]$workspaceMembers.Add([string]$member) + } + $packageDirectories = @{} + $excludedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + $excludedPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($package in $metadata.packages) { + $packageId = [string]$package.id + if (-not $workspaceMembers.Contains($packageId)) { continue } + $packageDirectories[$packageId] = Split-Path -Parent ([string]$package.manifest_path) + if ($package.metadata.anvil.miri.exclude -eq $true) { + [void]$excludedPackages.Add([string]$package.name) + [void]$excludedPackageIds.Add($packageId) + } + } + + if ($packageArgs -contains '--workspace') { + foreach ($packageName in $excludedPackages) { + $packageArgs += @('--exclude', $packageName) + } + } else { + $filtered = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $packageArgs.Count; $i++) { + if ($packageArgs[$i] -ne '--package') { + Write-Error "anvil miri: unexpected impact argument '$($packageArgs[$i])'" + exit 1 + } + if (($i + 1) -ge $packageArgs.Count) { + Write-Error 'anvil miri: --package is missing its package spec' + exit 1 + } + $packageSpec = $packageArgs[++$i] + $packageName = ($packageSpec -split '@', 2)[0] + if ($excludedPackages.Contains($packageName)) { continue } + $filtered.Add('--package') + $filtered.Add($packageSpec) + } + $packageArgs = $filtered.ToArray() + if ($packageArgs.Count -eq 0) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } + } + + $logRoot = Join-Path ([IO.Path]::GetTempPath()) "anvil-miri-$PID-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $logRoot | Out-Null + try { + $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' + $toolchain = '+{{ rust_nightly }}' + & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + $buildExitCode = $LASTEXITCODE + + $artifacts = [System.Collections.Generic.List[object]]::new() + $seenArtifacts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($line in Get-Content -LiteralPath $artifactManifest) { + try { + $message = $line | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse Cargo JSON output: $($_.Exception.Message)" + exit 1 + } + if ($message.reason -eq 'compiler-message' -and $message.message.rendered) { + [Console]::Error.Write($message.message.rendered) + continue + } + if ($message.reason -ne 'compiler-artifact' -or -not $message.executable -or $message.profile.test -ne $true) { + continue + } + $artifactPath = [string]$message.executable + if (-not $seenArtifacts.Add($artifactPath)) { continue } + $packageId = [string]$message.package_id + if ($excludedPackageIds.Contains($packageId)) { continue } + if (-not $packageDirectories.ContainsKey($packageId)) { + Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" + exit 1 + } + $artifacts.Add([pscustomobject]@{ + Path = $artifactPath + Name = Split-Path -Leaf $artifactPath + WorkingDirectory = $packageDirectories[$packageId] + }) + } + + if ($buildExitCode -ne 0) { exit $buildExitCode } + if ($artifacts.Count -eq 0) { + Write-Host 'anvil miri: Cargo produced no runnable test artifacts; nothing to test' + exit 0 + } + + $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { + Write-Error 'anvil miri: cargo miri setup did not report a sysroot' + exit 1 + } + $env:MIRI_SYSROOT = $sysrootOutput[-1] + $toolchainSysroot = & rustc $toolchain --print sysroot + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchainSysroot)) { + Write-Error "anvil miri: could not resolve the $toolchain toolchain sysroot" + exit 1 + } + $toolchainBin = Join-Path $toolchainSysroot.Trim() 'bin' + $runner = @( + 'cargo-miri.exe', 'cargo-miri', 'cargo-miri.ps1' | + ForEach-Object { Join-Path $toolchainBin $_ } | + Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } + ) | Select-Object -First 1 + if (-not $runner) { + Write-Error "anvil miri: cargo-miri was not found under '$toolchainBin'" + exit 1 + } + # cargo-miri records MIRI_BE_RUSTC=host in each artifact. Keeping the + # same value in the standalone runner environment lets phase_runner + # suppress that recorded value before launching Miri for interpretation. + $env:MIRI_BE_RUSTC = 'host' + + $jobs = 0 + if ($env:ANVIL_MIRI_JOBS) { + if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { + Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + exit 1 + } + } else { + $jobs = [Environment]::ProcessorCount + } + $jobs = [Math]::Min($jobs, $artifacts.Count) + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $memoryInfo = [GC]::GetGCMemoryInfo() + $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) + $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) + $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) + Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" + Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" + + $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { + [pscustomobject]@{ + Index = $i + Artifact = $orderedArtifacts[$i].Path + Name = $orderedArtifacts[$i].Name + WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory + } + } + $results = @( + $work | ForEach-Object -Parallel { + $ErrorActionPreference = 'Stop' + function Get-MemoryTelemetry { + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $info = [GC]::GetGCMemoryInfo() + $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) + $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) + $headroomGiB = [Math]::Round( + [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, + 1 + ) + "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" + } + + $item = $_ + $log = Join-Path $using:logRoot "$($item.Index).log" + Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Push-Location -LiteralPath $item.WorkingDirectory + try { + & $using:runner runner $item.Artifact *> $log + $exitCode = $LASTEXITCODE + } catch { + $_ | Out-String | Set-Content -LiteralPath $log + $exitCode = 1 + } finally { + Pop-Location + } + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + [pscustomobject]@{ + Index = $item.Index + Name = $item.Name + ExitCode = $exitCode + Log = $log + } + } -ThrottleLimit $jobs + ) + + foreach ($result in $results | Sort-Object Index) { + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host "::group::Miri artifact $($result.Name)" + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host "##[group]Miri artifact $($result.Name)" + } else { + Write-Host "`n=== Miri artifact $($result.Name) ===" + } + Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host '::endgroup::' + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host '##[endgroup]' + } + } + + $failed = @($results | Where-Object ExitCode -ne 0) + if ($failed.Count -ne 0) { + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + exit 1 + } + } finally { + Remove-Item -LiteralPath $logRoot -Recurse -Force -ErrorAction SilentlyContinue + } + # Run Miri tests for affected workspace packages. [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact @@ -1620,7 +1863,8 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- diff --git a/justfiles/anvil/checks/miri-race-coverage.just b/justfiles/anvil/checks/miri-race-coverage.just index 4851799b..de4a2716 100644 --- a/justfiles/anvil/checks/miri-race-coverage.just +++ b/justfiles/anvil/checks/miri-race-coverage.just @@ -26,7 +26,7 @@ anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. diff --git a/justfiles/anvil/checks/miri-strict-provenance.just b/justfiles/anvil/checks/miri-strict-provenance.just index 60f36c27..65de202b 100644 --- a/justfiles/anvil/checks/miri-strict-provenance.just +++ b/justfiles/anvil/checks/miri-strict-provenance.just @@ -15,7 +15,7 @@ anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvi $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. diff --git a/justfiles/anvil/checks/miri-tree-borrows.just b/justfiles/anvil/checks/miri-tree-borrows.just index c1786354..eccdcd8f 100644 --- a/justfiles/anvil/checks/miri-tree-borrows.just +++ b/justfiles/anvil/checks/miri-tree-borrows.just @@ -29,7 +29,7 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs diff --git a/justfiles/anvil/checks/miri.just b/justfiles/anvil/checks/miri.just index 630e2c79..02a9f353 100644 --- a/justfiles/anvil/checks/miri.just +++ b/justfiles/anvil/checks/miri.just @@ -12,6 +12,249 @@ # scheduled/full tiers (which force ANVIL_IMPACT=off) or a dirty local tree # (which widens for safety). +# Compile all selected packages together, then run their Miri test artifacts in +# parallel. The single Cargo invocation preserves workspace feature unification, +# while artifact-level parallelism uses runner CPUs that `cargo miri test` +# otherwise leaves idle. +# +# Packages can opt their own test targets out through: +# +# [package.metadata.anvil.miri] +# exclude = true +# +# They can still compile as dependencies of selected consumers. Workspace runs +# translate the metadata to `--exclude`; impact-scoped runs remove matching +# `--package name@version` pairs before compilation. +# +# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# Memory telemetry is informational and never silently reduces concurrency. +[script("pwsh", "-NoProfile")] +_anvil-miri-test *package_args: + $ErrorActionPreference = 'Stop' + $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) + + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil miri: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse cargo metadata output: $($_.Exception.Message)" + exit 1 + } + + $workspaceMembers = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($member in $metadata.workspace_members) { + [void]$workspaceMembers.Add([string]$member) + } + $packageDirectories = @{} + $excludedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + $excludedPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($package in $metadata.packages) { + $packageId = [string]$package.id + if (-not $workspaceMembers.Contains($packageId)) { continue } + $packageDirectories[$packageId] = Split-Path -Parent ([string]$package.manifest_path) + if ($package.metadata.anvil.miri.exclude -eq $true) { + [void]$excludedPackages.Add([string]$package.name) + [void]$excludedPackageIds.Add($packageId) + } + } + + if ($packageArgs -contains '--workspace') { + foreach ($packageName in $excludedPackages) { + $packageArgs += @('--exclude', $packageName) + } + } else { + $filtered = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $packageArgs.Count; $i++) { + if ($packageArgs[$i] -ne '--package') { + Write-Error "anvil miri: unexpected impact argument '$($packageArgs[$i])'" + exit 1 + } + if (($i + 1) -ge $packageArgs.Count) { + Write-Error 'anvil miri: --package is missing its package spec' + exit 1 + } + $packageSpec = $packageArgs[++$i] + $packageName = ($packageSpec -split '@', 2)[0] + if ($excludedPackages.Contains($packageName)) { continue } + $filtered.Add('--package') + $filtered.Add($packageSpec) + } + $packageArgs = $filtered.ToArray() + if ($packageArgs.Count -eq 0) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } + } + + $logRoot = Join-Path ([IO.Path]::GetTempPath()) "anvil-miri-$PID-$([guid]::NewGuid())" + New-Item -ItemType Directory -Path $logRoot | Out-Null + try { + $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' + $toolchain = '+{{ rust_nightly }}' + & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + $buildExitCode = $LASTEXITCODE + + $artifacts = [System.Collections.Generic.List[object]]::new() + $seenArtifacts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($line in Get-Content -LiteralPath $artifactManifest) { + try { + $message = $line | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Error "anvil miri: could not parse Cargo JSON output: $($_.Exception.Message)" + exit 1 + } + if ($message.reason -eq 'compiler-message' -and $message.message.rendered) { + [Console]::Error.Write($message.message.rendered) + continue + } + if ($message.reason -ne 'compiler-artifact' -or -not $message.executable -or $message.profile.test -ne $true) { + continue + } + $artifactPath = [string]$message.executable + if (-not $seenArtifacts.Add($artifactPath)) { continue } + $packageId = [string]$message.package_id + if ($excludedPackageIds.Contains($packageId)) { continue } + if (-not $packageDirectories.ContainsKey($packageId)) { + Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" + exit 1 + } + $artifacts.Add([pscustomobject]@{ + Path = $artifactPath + Name = Split-Path -Leaf $artifactPath + WorkingDirectory = $packageDirectories[$packageId] + }) + } + + if ($buildExitCode -ne 0) { exit $buildExitCode } + if ($artifacts.Count -eq 0) { + Write-Host 'anvil miri: Cargo produced no runnable test artifacts; nothing to test' + exit 0 + } + + $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { + Write-Error 'anvil miri: cargo miri setup did not report a sysroot' + exit 1 + } + $env:MIRI_SYSROOT = $sysrootOutput[-1] + $toolchainSysroot = & rustc $toolchain --print sysroot + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchainSysroot)) { + Write-Error "anvil miri: could not resolve the $toolchain toolchain sysroot" + exit 1 + } + $toolchainBin = Join-Path $toolchainSysroot.Trim() 'bin' + $runner = @( + 'cargo-miri.exe', 'cargo-miri', 'cargo-miri.ps1' | + ForEach-Object { Join-Path $toolchainBin $_ } | + Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } + ) | Select-Object -First 1 + if (-not $runner) { + Write-Error "anvil miri: cargo-miri was not found under '$toolchainBin'" + exit 1 + } + # cargo-miri records MIRI_BE_RUSTC=host in each artifact. Keeping the + # same value in the standalone runner environment lets phase_runner + # suppress that recorded value before launching Miri for interpretation. + $env:MIRI_BE_RUSTC = 'host' + + $jobs = 0 + if ($env:ANVIL_MIRI_JOBS) { + if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { + Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + exit 1 + } + } else { + $jobs = [Environment]::ProcessorCount + } + $jobs = [Math]::Min($jobs, $artifacts.Count) + + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $memoryInfo = [GC]::GetGCMemoryInfo() + $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) + $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) + $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) + Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" + Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" + + $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { + [pscustomobject]@{ + Index = $i + Artifact = $orderedArtifacts[$i].Path + Name = $orderedArtifacts[$i].Name + WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory + } + } + $results = @( + $work | ForEach-Object -Parallel { + $ErrorActionPreference = 'Stop' + function Get-MemoryTelemetry { + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + $info = [GC]::GetGCMemoryInfo() + $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) + $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) + $headroomGiB = [Math]::Round( + [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, + 1 + ) + "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" + } + + $item = $_ + $log = Join-Path $using:logRoot "$($item.Index).log" + Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Push-Location -LiteralPath $item.WorkingDirectory + try { + & $using:runner runner $item.Artifact *> $log + $exitCode = $LASTEXITCODE + } catch { + $_ | Out-String | Set-Content -LiteralPath $log + $exitCode = 1 + } finally { + Pop-Location + } + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + [pscustomobject]@{ + Index = $item.Index + Name = $item.Name + ExitCode = $exitCode + Log = $log + } + } -ThrottleLimit $jobs + ) + + foreach ($result in $results | Sort-Object Index) { + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host "::group::Miri artifact $($result.Name)" + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host "##[group]Miri artifact $($result.Name)" + } else { + Write-Host "`n=== Miri artifact $($result.Name) ===" + } + Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host '::endgroup::' + } elseif ($env:TF_BUILD -eq 'True') { + Write-Host '##[endgroup]' + } + } + + $failed = @($results | Where-Object ExitCode -ne 0) + if ($failed.Count -ne 0) { + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + exit 1 + } + } finally { + Remove-Item -LiteralPath $logRoot -Recurse -Force -ErrorAction SilentlyContinue + } + # Run Miri tests for affected workspace packages. [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact @@ -43,7 +286,8 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & "{{ just_executable() }}" _anvil-miri-test @pkg if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- From 431138f567a00365d549bfb3f308d2bc35a60e6a Mon Sep 17 00:00:00 2001 From: "Stanislav Andras (from Dev Box)" Date: Tue, 1 Sep 2026 11:31:43 +0200 Subject: [PATCH 2/6] perf(cargo-anvil): remove Miri memory telemetry Remove tuning telemetry inherited from oxidizer #706, including the process-wide forced collections around every artifact. Keep the generic concurrency control through ANVIL_MIRI_JOBS and document the private recipe clearly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98e52fe3-a435-412e-9154-73a557fef79f --- .anvil.lock | 4 +-- crates/cargo-anvil/docs/design/checks.md | 2 +- crates/cargo-anvil/docs/design/local.md | 5 ++-- .../justfiles/anvil/checks/miri.just | 27 +++---------------- .../snapshots/snapshots__ado_backend.snap | 27 +++---------------- .../snapshots/snapshots__github_backend.snap | 27 +++---------------- .../snapshots/snapshots__local_only.snap | 27 +++---------------- justfiles/anvil/checks/miri.just | 27 +++---------------- 8 files changed, 25 insertions(+), 121 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 1418da25..98a92ae8 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:f2dbc238f698034edc01c0e78672a914cf96297f73a861a11ec94fe72ba35449" +catalog_checksum = "sha256:8021251ba18b9b53645cdf1472e074e16c0607530bd3cca21333d17930522580" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -133,7 +133,7 @@ checksum = "sha256:1df669672fb1a507ae087750a3d7872a96990d5b2410c4843217152939963 [[file]] path = "justfiles/anvil/checks/miri.just" -checksum = "sha256:69a56218860ad61a45845c97beff6d6a7651a262446a954ede5da40a31db5c8a" +checksum = "sha256:6091539a5790f1772db1c8ea7aaab8edfadf349ebaadd48b27af30773138f5c5" [[file]] path = "justfiles/anvil/checks/msrv-test.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index a662fcec..cf4bb167 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -236,7 +236,7 @@ available. | Check | Invocation | Source | |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| -| `miri` | Compiles `cargo + miri test --all-features --tests --no-run` once over the impact-affected packages, parses Cargo's JSON artifact stream, then runs independent libtest binaries concurrently through `cargo-miri runner`. Compiling the selected scope together preserves Cargo feature unification; artifact workers avoid serializing every binary. The default is one worker per logical processor, clamped to the artifact count; `ANVIL_MIRI_JOBS` accepts a positive integer override. Memory-load telemetry is informational and never silently changes concurrency. Output is isolated per artifact, replayed deterministically, and every artifact completes before failures are aggregated. Only artifacts with `profile.test = true` run, excluding build scripts and ordinary executables. `--tests` selects lib/bin unit tests and integration tests while excluding doctests, benches, and examples. Slow or unsupported individual tests opt out with `#[cfg_attr(miri, ignore)]`. A package can omit all its own Miri targets through `[package.metadata.anvil.miri] exclude = true`; it may still compile as a dependency of a selected consumer. The recipe remains impact-scoped for clean direct and PR invocations and full-workspace for scheduled/full tiers or a dirty local tree. | oxidizer, oxidizer-github | +| `miri` | Compiles `cargo + miri test --all-features --tests --no-run` once over the impact-affected packages, parses Cargo's JSON artifact stream, then runs independent libtest binaries concurrently through `cargo-miri runner`. Compiling the selected scope together preserves Cargo feature unification; artifact workers avoid serializing every binary. The default is one worker per logical processor, clamped to the artifact count; `ANVIL_MIRI_JOBS` accepts a positive integer override. Output is isolated per artifact, replayed deterministically, and every artifact completes before failures are aggregated. Only artifacts with `profile.test = true` run, excluding build scripts and ordinary executables. `--tests` selects lib/bin unit tests and integration tests while excluding doctests, benches, and examples. Slow or unsupported individual tests opt out with `#[cfg_attr(miri, ignore)]`. A package can omit all its own Miri targets through `[package.metadata.anvil.miri] exclude = true`; it may still compile as a dependency of a selected consumer. The recipe remains impact-scoped for clean direct and PR invocations and full-workspace for scheduled/full tiers or a dirty local tree. | oxidizer, oxidizer-github | | `careful` | `cargo + careful test --all-features --locked` over the impact-affected packages. cargo-careful uses a debug-instrumented std in a stable cache path. Because Cargo fingerprints the sysroot path rather than its contents, the recipe records the actual `rustc -vV` and SHA-256 of the resolved `cargo-careful` executable in `target/anvil/careful-sysroot.id`; either changing triggers `cargo clean`. The executable hash is used because cargo-careful rejects version-only invocations. This remains correct when validation accepts a newer installed cargo-careful. | oxidizer-github | | `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. Anvil does not impose a global exploration bound; each model remains responsible for tractable exhaustive exploration. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | | `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Adopters that disable `bolero`'s default features must enable its `std` feature for libfuzzer support. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index ef66e636..cca71aec 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -634,9 +634,8 @@ a full cache hit (`anvil-impact: impact set up to date`). To force a recompute, Miri compiles the selected package scope once, then executes its independent libtest artifacts concurrently. `ANVIL_MIRI_JOBS` overrides the default of one worker per logical processor and must be a positive integer. The worker count is -always clamped to the number of discovered artifacts. Memory-load telemetry is -reported for calibration but does not silently reduce concurrency; use the -override when a runner needs a lower memory footprint. +always clamped to the number of discovered artifacts. Use the override when a +runner needs a lower memory footprint. Packages whose own test targets are unsuitable or unproductive under Miri can declare: diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just index 02a9f353..e00066b1 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just @@ -27,7 +27,8 @@ # `--package name@version` pairs before compilation. # # ANVIL_MIRI_JOBS overrides the default of one process per logical processor. -# Memory telemetry is informational and never silently reduces concurrency. + +# Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' @@ -173,13 +174,6 @@ _anvil-miri-test *package_args: } $jobs = [Math]::Min($jobs, $artifacts.Count) - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $memoryInfo = [GC]::GetGCMemoryInfo() - $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) - $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) - $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) - Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" $orderedArtifacts = @($artifacts | Sort-Object Name, Path) @@ -194,22 +188,9 @@ _anvil-miri-test *package_args: $results = @( $work | ForEach-Object -Parallel { $ErrorActionPreference = 'Stop' - function Get-MemoryTelemetry { - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $info = [GC]::GetGCMemoryInfo() - $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) - $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) - $headroomGiB = [Math]::Round( - [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, - 1 - ) - "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" - } - $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Write-Host "anvil miri: starting $($item.Name)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -220,7 +201,7 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index Name = $item.Name diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 9458b886..847d493b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2734,7 +2734,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # `--package name@version` pairs before compilation. # # ANVIL_MIRI_JOBS overrides the default of one process per logical processor. -# Memory telemetry is informational and never silently reduces concurrency. + +# Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' @@ -2880,13 +2881,6 @@ _anvil-miri-test *package_args: } $jobs = [Math]::Min($jobs, $artifacts.Count) - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $memoryInfo = [GC]::GetGCMemoryInfo() - $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) - $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) - $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) - Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" $orderedArtifacts = @($artifacts | Sort-Object Name, Path) @@ -2901,22 +2895,9 @@ _anvil-miri-test *package_args: $results = @( $work | ForEach-Object -Parallel { $ErrorActionPreference = 'Stop' - function Get-MemoryTelemetry { - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $info = [GC]::GetGCMemoryInfo() - $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) - $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) - $headroomGiB = [Math]::Round( - [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, - 1 - ) - "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" - } - $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Write-Host "anvil miri: starting $($item.Name)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -2927,7 +2908,7 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index Name = $item.Name diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index eca2f156..e5c9409b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2720,7 +2720,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # `--package name@version` pairs before compilation. # # ANVIL_MIRI_JOBS overrides the default of one process per logical processor. -# Memory telemetry is informational and never silently reduces concurrency. + +# Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' @@ -2866,13 +2867,6 @@ _anvil-miri-test *package_args: } $jobs = [Math]::Min($jobs, $artifacts.Count) - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $memoryInfo = [GC]::GetGCMemoryInfo() - $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) - $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) - $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) - Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" $orderedArtifacts = @($artifacts | Sort-Object Name, Path) @@ -2887,22 +2881,9 @@ _anvil-miri-test *package_args: $results = @( $work | ForEach-Object -Parallel { $ErrorActionPreference = 'Stop' - function Get-MemoryTelemetry { - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $info = [GC]::GetGCMemoryInfo() - $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) - $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) - $headroomGiB = [Math]::Round( - [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, - 1 - ) - "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" - } - $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Write-Host "anvil miri: starting $($item.Name)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -2913,7 +2894,7 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index Name = $item.Name diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 55c1b2c8..39e040b1 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1604,7 +1604,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # `--package name@version` pairs before compilation. # # ANVIL_MIRI_JOBS overrides the default of one process per logical processor. -# Memory telemetry is informational and never silently reduces concurrency. + +# Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' @@ -1750,13 +1751,6 @@ _anvil-miri-test *package_args: } $jobs = [Math]::Min($jobs, $artifacts.Count) - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $memoryInfo = [GC]::GetGCMemoryInfo() - $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) - $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) - $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) - Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" $orderedArtifacts = @($artifacts | Sort-Object Name, Path) @@ -1771,22 +1765,9 @@ _anvil-miri-test *package_args: $results = @( $work | ForEach-Object -Parallel { $ErrorActionPreference = 'Stop' - function Get-MemoryTelemetry { - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $info = [GC]::GetGCMemoryInfo() - $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) - $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) - $headroomGiB = [Math]::Round( - [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, - 1 - ) - "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" - } - $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Write-Host "anvil miri: starting $($item.Name)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -1797,7 +1778,7 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index Name = $item.Name diff --git a/justfiles/anvil/checks/miri.just b/justfiles/anvil/checks/miri.just index 02a9f353..e00066b1 100644 --- a/justfiles/anvil/checks/miri.just +++ b/justfiles/anvil/checks/miri.just @@ -27,7 +27,8 @@ # `--package name@version` pairs before compilation. # # ANVIL_MIRI_JOBS overrides the default of one process per logical processor. -# Memory telemetry is informational and never silently reduces concurrency. + +# Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' @@ -173,13 +174,6 @@ _anvil-miri-test *package_args: } $jobs = [Math]::Min($jobs, $artifacts.Count) - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $memoryInfo = [GC]::GetGCMemoryInfo() - $gcAvailableGiB = [Math]::Round($memoryInfo.TotalAvailableMemoryBytes / 1GB, 1) - $memoryLoadGiB = [Math]::Round($memoryInfo.MemoryLoadBytes / 1GB, 1) - $highMemoryLoadGiB = [Math]::Round($memoryInfo.HighMemoryLoadThresholdBytes / 1GB, 1) - Write-Host "anvil miri: $([Environment]::ProcessorCount) logical processor(s); GC reports $gcAvailableGiB GiB available, $memoryLoadGiB GiB load, $highMemoryLoadGiB GiB high-load threshold" Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" $orderedArtifacts = @($artifacts | Sort-Object Name, Path) @@ -194,22 +188,9 @@ _anvil-miri-test *package_args: $results = @( $work | ForEach-Object -Parallel { $ErrorActionPreference = 'Stop' - function Get-MemoryTelemetry { - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - $info = [GC]::GetGCMemoryInfo() - $capacityGiB = [Math]::Round($info.TotalAvailableMemoryBytes / 1GB, 1) - $loadGiB = [Math]::Round($info.MemoryLoadBytes / 1GB, 1) - $headroomGiB = [Math]::Round( - [Math]::Max([long]0, $info.TotalAvailableMemoryBytes - $info.MemoryLoadBytes) / 1GB, - 1 - ) - "GC load $loadGiB/$capacityGiB GiB, estimated headroom $headroomGiB GiB" - } - $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name) ($(Get-MemoryTelemetry))" + Write-Host "anvil miri: starting $($item.Name)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -220,7 +201,7 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode; $(Get-MemoryTelemetry))" + Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index Name = $item.Name From 4797f909004ffd9f7ea518b1b0767552e8312a19 Mon Sep 17 00:00:00 2001 From: "Stanislav Andras (from Dev Box)" Date: Tue, 1 Sep 2026 18:29:23 +0200 Subject: [PATCH 3/6] test(cargo-anvil): isolate Miri recipe fixtures Use the minimal raw-string delimiter accepted by Rust 1.97 and clear inherited GitHub and ADO markers so each recipe-contract test selects its output backend explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98e52fe3-a435-412e-9154-73a557fef79f --- crates/cargo-anvil/tests/recipe_contracts.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index fbcab268..dd8ea361 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -304,13 +304,13 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { write(&bin.join("git.ps1"), "exit 0\n"); write( &bin.join("rustc.ps1"), - r#" + r" if ($args -contains 'sysroot') { Write-Output ([System.IO.Path]::Combine($env:FAKE_WORKSPACE_ROOT, 'fake-toolchain')) exit 0 } exit 1 -"#, +", ); let fake_toolchain_bin = tmp.path().join("fake-toolchain/bin"); fs::create_dir_all(&fake_toolchain_bin).unwrap(); @@ -352,15 +352,12 @@ fn run_just(root: &Path, arguments: &[&str], environment: &[(&str, &OsStr)]) -> .current_dir(root); command.env("PATH", path_with_fake_bin(root)); command.env("FAKE_WORKSPACE_ROOT", root); - // A fixture is a scratch workspace, so it must not inherit the impact - // scoping of whatever invoked the test suite. A CI group job exports - // `ANVIL_IMPACT=consume` and downloads a cache into the real repository; - // inherited into a temp directory that has no cache, `anvil-impact` fails - // hard and takes the recipe under test with it. `ANVIL_INCLUDE_*` is the - // same hazard one level down: a leg whose scope resolved to `--skip` would - // silently short-circuit the recipe before it did anything. A test that - // cares about either value passes it explicitly below. + // A fixture is a scratch workspace, so it must not inherit impact scoping + // or output-backend markers from the process running the test suite. Tests + // that exercise those contracts pass the relevant values explicitly. command.env_remove("ANVIL_IMPACT"); + command.env_remove("GITHUB_ACTIONS"); + command.env_remove("TF_BUILD"); for key in std::env::vars_os().map(|(key, _)| key) { if key.to_string_lossy().starts_with("ANVIL_INCLUDE_") { command.env_remove(key); From d855c419fbc0ecf61e3a4bc9e4fc7453dda2e17a Mon Sep 17 00:00:00 2001 From: "Stanislav Andras (from Dev Box)" Date: Tue, 1 Sep 2026 21:15:30 +0200 Subject: [PATCH 4/6] fix(cargo-anvil): select Miri Cargo toolchains explicitly Use the resolved stable toolchain for metadata and expose the pinned nightly directly on Miri invocations, matching cargo-anvil's deterministic toolchain contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98e52fe3-a435-412e-9154-73a557fef79f --- .anvil.lock | 4 ++-- .../cargo-anvil/templates/justfiles/anvil/checks/miri.just | 6 +++--- .../cargo-anvil/tests/snapshots/snapshots__ado_backend.snap | 6 +++--- .../tests/snapshots/snapshots__github_backend.snap | 6 +++--- .../cargo-anvil/tests/snapshots/snapshots__local_only.snap | 6 +++--- justfiles/anvil/checks/miri.just | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 98a92ae8..6383403a 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:8021251ba18b9b53645cdf1472e074e16c0607530bd3cca21333d17930522580" +catalog_checksum = "sha256:b4e9c4ff46378ef386092227f5b22baae9da0c1c16e92194420d8a059a4c1c90" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -133,7 +133,7 @@ checksum = "sha256:1df669672fb1a507ae087750a3d7872a96990d5b2410c4843217152939963 [[file]] path = "justfiles/anvil/checks/miri.just" -checksum = "sha256:6091539a5790f1772db1c8ea7aaab8edfadf349ebaadd48b27af30773138f5c5" +checksum = "sha256:92f8500c3304a2d7d117ea3823c68f3a676daa43f0e918cb9d06b20125dc3105" [[file]] path = "justfiles/anvil/checks/msrv-test.just" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just index e00066b1..1864e6c0 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just @@ -34,7 +34,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil miri: cargo metadata failed' exit $LASTEXITCODE @@ -96,7 +96,7 @@ _anvil-miri-test *package_args: try { $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' $toolchain = '+{{ rust_nightly }}' - & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + & cargo '+{{ rust_nightly }}' miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest $buildExitCode = $LASTEXITCODE $artifacts = [System.Collections.Generic.List[object]]::new() @@ -136,7 +136,7 @@ _anvil-miri-test *package_args: exit 0 } - $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + $sysrootOutput = @(& cargo '+{{ rust_nightly }}' miri setup --print-sysroot) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { Write-Error 'anvil miri: cargo miri setup did not report a sysroot' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 847d493b..8a962dfc 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2741,7 +2741,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil miri: cargo metadata failed' exit $LASTEXITCODE @@ -2803,7 +2803,7 @@ _anvil-miri-test *package_args: try { $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' $toolchain = '+{{ rust_nightly }}' - & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + & cargo '+{{ rust_nightly }}' miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest $buildExitCode = $LASTEXITCODE $artifacts = [System.Collections.Generic.List[object]]::new() @@ -2843,7 +2843,7 @@ _anvil-miri-test *package_args: exit 0 } - $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + $sysrootOutput = @(& cargo '+{{ rust_nightly }}' miri setup --print-sysroot) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { Write-Error 'anvil miri: cargo miri setup did not report a sysroot' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index e5c9409b..d433b2ac 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2727,7 +2727,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil miri: cargo metadata failed' exit $LASTEXITCODE @@ -2789,7 +2789,7 @@ _anvil-miri-test *package_args: try { $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' $toolchain = '+{{ rust_nightly }}' - & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + & cargo '+{{ rust_nightly }}' miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest $buildExitCode = $LASTEXITCODE $artifacts = [System.Collections.Generic.List[object]]::new() @@ -2829,7 +2829,7 @@ _anvil-miri-test *package_args: exit 0 } - $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + $sysrootOutput = @(& cargo '+{{ rust_nightly }}' miri setup --print-sysroot) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { Write-Error 'anvil miri: cargo miri setup did not report a sysroot' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 39e040b1..8348e5cb 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1611,7 +1611,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil miri: cargo metadata failed' exit $LASTEXITCODE @@ -1673,7 +1673,7 @@ _anvil-miri-test *package_args: try { $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' $toolchain = '+{{ rust_nightly }}' - & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + & cargo '+{{ rust_nightly }}' miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest $buildExitCode = $LASTEXITCODE $artifacts = [System.Collections.Generic.List[object]]::new() @@ -1713,7 +1713,7 @@ _anvil-miri-test *package_args: exit 0 } - $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + $sysrootOutput = @(& cargo '+{{ rust_nightly }}' miri setup --print-sysroot) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { Write-Error 'anvil miri: cargo miri setup did not report a sysroot' diff --git a/justfiles/anvil/checks/miri.just b/justfiles/anvil/checks/miri.just index e00066b1..1864e6c0 100644 --- a/justfiles/anvil/checks/miri.just +++ b/justfiles/anvil/checks/miri.just @@ -34,7 +34,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $packageArgs = @('{{ replace(package_args, "'", "''") }}' -split '\s+' | Where-Object { $_ }) - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil miri: cargo metadata failed' exit $LASTEXITCODE @@ -96,7 +96,7 @@ _anvil-miri-test *package_args: try { $artifactManifest = Join-Path $logRoot 'artifacts.jsonl' $toolchain = '+{{ rust_nightly }}' - & cargo $toolchain miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest + & cargo '+{{ rust_nightly }}' miri test --all-features --tests --no-run --message-format=json-render-diagnostics @packageArgs 1> $artifactManifest $buildExitCode = $LASTEXITCODE $artifacts = [System.Collections.Generic.List[object]]::new() @@ -136,7 +136,7 @@ _anvil-miri-test *package_args: exit 0 } - $sysrootOutput = @(& cargo $toolchain miri setup --print-sysroot) + $sysrootOutput = @(& cargo '+{{ rust_nightly }}' miri setup --print-sysroot) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($sysrootOutput.Count -eq 0 -or -not $sysrootOutput[-1]) { Write-Error 'anvil miri: cargo miri setup did not report a sysroot' From e8c91b56fa79b87ce82f0968cfc9c15698c2d11f Mon Sep 17 00:00:00 2001 From: "Stanislav Andras (from Dev Box)" Date: Tue, 1 Sep 2026 22:09:13 +0200 Subject: [PATCH 5/6] fix(cargo-anvil): address Miri review feedback Forward the Miri worker cap into containers, handle fully excluded workspaces, and identify artifacts by package and target. Strengthen impact/exclusion contracts and document the corresponding behavior and rationale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98e52fe3-a435-412e-9154-73a557fef79f --- .anvil.lock | 6 +- crates/cargo-anvil/README.md | 6 +- crates/cargo-anvil/docs/design/checks.md | 4 +- crates/cargo-anvil/docs/design/containers.md | 13 +- crates/cargo-anvil/docs/design/local.md | 54 +++---- .../src/anvil/artifacts/container.rs | 5 +- .../src/anvil/artifacts/justfile.rs | 3 +- crates/cargo-anvil/src/lib.rs | 4 + .../justfiles/anvil/checks/miri.just | 47 ++++-- .../templates/justfiles/anvil/container.just | 9 +- crates/cargo-anvil/tests/recipe_contracts.rs | 134 ++++++++++++++++-- .../snapshots/snapshots__ado_backend.snap | 56 +++++--- .../snapshots/snapshots__github_backend.snap | 56 +++++--- .../snapshots/snapshots__local_only.snap | 56 +++++--- justfiles/anvil/checks/miri.just | 47 ++++-- justfiles/anvil/container.just | 9 +- 16 files changed, 370 insertions(+), 139 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 6383403a..baaf8b9b 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:b4e9c4ff46378ef386092227f5b22baae9da0c1c16e92194420d8a059a4c1c90" +catalog_checksum = "sha256:06529602514854ab5676975141a7542dc31a2577f7615ddd8ea516ed74d41bd5" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -133,7 +133,7 @@ checksum = "sha256:1df669672fb1a507ae087750a3d7872a96990d5b2410c4843217152939963 [[file]] path = "justfiles/anvil/checks/miri.just" -checksum = "sha256:92f8500c3304a2d7d117ea3823c68f3a676daa43f0e918cb9d06b20125dc3105" +checksum = "sha256:91b67bc68bcc34a82233917112b297935fdedccf46ffd668884636a79cc4579d" [[file]] path = "justfiles/anvil/checks/msrv-test.just" @@ -169,7 +169,7 @@ checksum = "sha256:6efd7378a2cd0f5d86519bd32fd86f2055a60191187dd77a8842b374b8eb7 [[file]] path = "justfiles/anvil/container.just" -checksum = "sha256:3363992c6c006c649eae3732b2a7b04c2a4925eff369179b128d43380e2800d4" +checksum = "sha256:a772d84b20c796fb37d283781a956916097783d1a8e9a132e4e933fc8fc23865" [[file]] path = "justfiles/anvil/groups/pr-fast.just" diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 89567eed..ca171ecf 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -410,6 +410,10 @@ remaining available as a dependency: exclude = true ``` +Reserve this package-wide switch for constraints that apply to every test +target. Prefer per-test ignores with reasons for narrower or temporary +suppressions. + #### Concurrency model checking (`loom`) The `loom` check runs only the test targets that opt in, detected @@ -501,7 +505,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQb8g6Bt5NkaJob24Zyi8qyA4obC8Lke1bbBmobpIJj7FgYp-9hZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbKr_fs3dc0BAbULxaTH4TvIUbQX7XkOZRWOQbkl8wl0Pm6CNhZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts::container [__link10]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index cf4bb167..ee84141a 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -236,7 +236,7 @@ available. | Check | Invocation | Source | |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| -| `miri` | Compiles `cargo + miri test --all-features --tests --no-run` once over the impact-affected packages, parses Cargo's JSON artifact stream, then runs independent libtest binaries concurrently through `cargo-miri runner`. Compiling the selected scope together preserves Cargo feature unification; artifact workers avoid serializing every binary. The default is one worker per logical processor, clamped to the artifact count; `ANVIL_MIRI_JOBS` accepts a positive integer override. Output is isolated per artifact, replayed deterministically, and every artifact completes before failures are aggregated. Only artifacts with `profile.test = true` run, excluding build scripts and ordinary executables. `--tests` selects lib/bin unit tests and integration tests while excluding doctests, benches, and examples. Slow or unsupported individual tests opt out with `#[cfg_attr(miri, ignore)]`. A package can omit all its own Miri targets through `[package.metadata.anvil.miri] exclude = true`; it may still compile as a dependency of a selected consumer. The recipe remains impact-scoped for clean direct and PR invocations and full-workspace for scheduled/full tiers or a dirty local tree. | oxidizer, oxidizer-github | +| `miri` | Compiles `cargo + miri test --all-features --tests --no-run` once over the impact-affected packages, parses Cargo's JSON artifact stream, then runs each compiled Miri test executable concurrently through `cargo-miri runner`. Compiling the selected scope together preserves Cargo feature unification: shared dependencies receive the union of features requested by all selected packages, matching a normal workspace-scoped Cargo invocation. The default is one artifact worker per logical processor, clamped to the executable count; `ANVIL_MIRI_JOBS` accepts a positive integer override. Output is isolated per executable and labelled with its package and test target, then replayed deterministically; every executable completes before failures are aggregated. Only compiler artifacts with `profile.test = true` run, excluding build scripts and ordinary executables. `--tests` selects lib/bin unit tests and integration tests while excluding doctests, benches, and examples. Slow or unsupported individual tests opt out with `#[cfg_attr(miri, ignore)]`. A package can omit all its own Miri targets through `[package.metadata.anvil.miri] exclude = true`; it may still compile as a dependency of a selected consumer. Clean direct and PR invocations are impact-scoped; scheduled/full tiers and dirty local trees run the full workspace. | oxidizer, oxidizer-github | | `careful` | `cargo + careful test --all-features --locked` over the impact-affected packages. cargo-careful uses a debug-instrumented std in a stable cache path. Because Cargo fingerprints the sysroot path rather than its contents, the recipe records the actual `rustc -vV` and SHA-256 of the resolved `cargo-careful` executable in `target/anvil/careful-sysroot.id`; either changing triggers `cargo clean`. The executable hash is used because cargo-careful rejects version-only invocations. This remains correct when validation accepts a newer installed cargo-careful. | oxidizer-github | | `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. Anvil does not impose a global exploration bound; each model remains responsible for tractable exhaustive exploration. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | | `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Adopters that disable `bolero`'s default features must enable its `std` feature for libfuzzer support. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | @@ -290,7 +290,7 @@ commit can't surface anything new.) | `miri` | Same recipe as the `pr-runtime-analysis` member, but the `scheduled-runtime-analysis` group forces `ANVIL_IMPACT=off` (emit-time `__IMPACT_MODE__`) so the run is full-workspace. PR-tier miri is impact-scoped (so a PR touching crate A never exercises crate B under miri); the scheduled re-run ensures every crate gets miri coverage on `main` at least daily, catching UB introduced by an inter-crate change whose PR happened to scope it out. | oxidizer, oxidizer-github | | `miri-tree-borrows` | Uses the shared compile-once artifact runner with `MIRIFLAGS='-Zmiri-tree-borrows'` and `RUSTFLAGS='--cfg miri_tree_borrows'`. Tree-borrows tracks per-byte aliasing provenance and can exceed runner memory; tests known to OOM are quarantined per-test via `#[cfg_attr(miri_tree_borrows, ignore = "")]`. | oxidizer-github (rewritten as cfg-based) | | `miri-strict-provenance` | Uses the shared compile-once artifact runner with `MIRIFLAGS='-Zmiri-strict-provenance'` and `RUSTFLAGS='--cfg miri_strict_provenance'`. It surfaces integer-to-pointer casts that do not satisfy strict provenance; per-test opt-outs use `#[cfg_attr(miri_strict_provenance, ignore = "")]`. | oxidizer-github | -| `miri-race-coverage` | Uses the shared compile-once artifact runner with `MIRIFLAGS="-Zmiri-many-seeds=.."` and `RUSTFLAGS='--cfg miri_race_coverage'`. The `..` window rotates daily based on day-of-month (day N -> seeds `2N-1..2N+1`, exclusive upper bound -> 2 seeds/day, about 62 seeds/month). Per-test opt-outs use `#[cfg_attr(miri_race_coverage, ignore = "")]`. | oxidizer-github | +| `miri-race-coverage` | Uses the shared compile-once artifact runner with `MIRIFLAGS="-Zmiri-many-seeds=.."` and `RUSTFLAGS='--cfg miri_race_coverage'`. The `..` window rotates daily based on day-of-month (day N -> seeds `2N-1..2N+1`, exclusive upper bound -> 2 seeds/day, about 62 seeds/month). Rotation keeps each scheduled run bounded while exploring different nondeterministic schedules over successive days instead of repeating one fixed seed set. Per-test opt-outs use `#[cfg_attr(miri_race_coverage, ignore = "")]`. | oxidizer-github | These profiles each cost hours per leg, which is why they live in scheduled rather than PR. They share `miri`'s setup and run sequentially within the `scheduled-runtime-analysis` group; only independent test artifacts inside one profile run concurrently. Each uses profile-specific `MIRIFLAGS` plus a profile-specific `--cfg miri_` in `RUSTFLAGS`, letting a test opt out of one profile without affecting the others. The OS matrix matches `pr-runtime-analysis` (4 legs on GitHub, 2 on ADO) so any OS considered worth running Miri on gets the harder profiles too. diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index b4753c5b..be374d65 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -373,12 +373,13 @@ alone. The driver therefore follows each nested target a plan names, until nothi tier reads as needing nothing and `anvil-aprz` runs unauthenticated inside an image that has no `gh` of its own. It also forwards the recipe contract's own inputs when they are set — `PR_TITLE`, `BASE_REF`, `GITHUB_BASE_REF`, -`SYSTEM_PULLREQUEST_TARGETBRANCH` and `ANVIL_IMPACT` — because a check that reads one natively must read -the same value in a container. `anvil-pr-title` is the sharp case: with `PR_TITLE` unset it exits 0 with a skip notice, -so dropping it at the boundary would let a title a native run rejects pass in a container while the tier still reported -green. `ANVIL_IMPACT` is the other: a CI group job exports `consume`, and a container that did not inherit it would -recompute scoping from a diff instead of trusting the artifact the group downloaded. They are forwarded by name and -only when set, so an unset variable stays unset rather than arriving empty. +`SYSTEM_PULLREQUEST_TARGETBRANCH`, `ANVIL_IMPACT` and `ANVIL_MIRI_JOBS` — because a check that reads one +natively must read the same value in a container. `anvil-pr-title` is the sharp case: with `PR_TITLE` unset it exits 0 +with a skip notice, so dropping it at the boundary would let a title a native run rejects pass in a container while the +tier still reported green. `ANVIL_IMPACT` controls whether a CI group trusts its downloaded impact artifact. +`ANVIL_MIRI_JOBS` caps the number of memory-heavy Miri artifact workers; dropping it could turn a deliberately bounded +container run into one worker per logical processor. They are forwarded by name and only when set, so an unset variable +stays unset rather than arriving empty. A resolved token is set on the driver process, passed by name, and unset after the run, so it never reaches a host command line. Inside the container it is readable by everything the run executes, including build scripts and proc diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index cca71aec..74dfb2fc 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -629,27 +629,7 @@ moves, and the working-tree snapshot only when the tree changes; an unchanged re a full cache hit (`anvil-impact: impact set up to date`). To force a recompute, delete `target/anvil/impact/`. -### 4.4 Miri artifact concurrency - -Miri compiles the selected package scope once, then executes its independent -libtest artifacts concurrently. `ANVIL_MIRI_JOBS` overrides the default of one -worker per logical processor and must be a positive integer. The worker count is -always clamped to the number of discovered artifacts. Use the override when a -runner needs a lower memory footprint. - -Packages whose own test targets are unsuitable or unproductive under Miri can -declare: - -```toml -[package.metadata.anvil.miri] -exclude = true -``` - -The package can still be compiled as a dependency of another selected package. -Prefer per-test `cfg_attr(miri, ignore = "...")` or the profile-specific cfgs -when only individual tests need suppression. - -### 4.5 Uncommitted changes widen to the full workspace +### 4.4 Uncommitted changes widen to the full workspace cargo-delta scopes on the **committed** diff of `HEAD` against the base ref, so an uncommitted change — a crate you are actively editing but have not committed — is invisible @@ -664,7 +644,7 @@ so CI always gets the scoped, committed-diff result. It is deliberately conserva fast — a dirty tree runs everything. Commit to scope by impact, or use `ANVIL_IMPACT=off` (which also runs the full workspace, and additionally skips cargo-delta entirely). -### 4.6 Base-ref resolution and failure modes +### 4.5 Base-ref resolution and failure modes `anvil-impact` resolves the base ref through `_anvil-base-ref` and computes the committed diff of `HEAD` against it. It deliberately **does not** run `git fetch`: mutating git state @@ -691,7 +671,31 @@ baseline — so an environment without the base ref must either provide it, run `ANVIL_IMPACT=off`, or (in CI) download the cache and set `ANVIL_IMPACT=consume`. -## 5. Daily driver +## 5. Miri execution customization + +Miri compiles the selected package scope once, then executes each compiled Miri +test executable concurrently. `ANVIL_MIRI_JOBS` overrides the default of one +artifact worker per logical processor and must be a positive integer. The worker +count is always clamped to the number of discovered executables. Use the override +when a runner needs a lower memory footprint; containerized runs forward the +setting unchanged. + +Packages whose own test targets are inherently unsuitable or unproductive under +Miri can declare: + +```toml +[package.metadata.anvil.miri] +exclude = true +``` + +This Boolean disables every Miri test target owned by the package, in both +impact-scoped and full-workspace runs, although the package can still compile as +a dependency of another selected package. Because the metadata has no place to +record a reason, reserve it for package-wide constraints. Prefer per-test +`cfg_attr(miri, ignore = "")` or the profile-specific cfgs when a narrower +or temporary suppression can keep its rationale beside the affected test. + +## 6. Daily driver ```text $ just anvil @@ -705,7 +709,7 @@ anvil OK (`anvil-pr`, `anvil-scheduled`, `anvil-full`) are first-class -- locally reproducible with exactly the same arguments cloud workflows uses, because cloud workflows invokes the same `just` recipes. -## 6. No-tooling fallback +## 7. No-tooling fallback A user with only `cargo` (no `just`, no `cargo-anvil`) can still run the basics: @@ -720,7 +724,7 @@ The same commands appear as the body of the corresponding `just` recipes under covers core hygiene only — coverage, miri, mutants, etc. still require their respective tools. -## 7. Customization at the recipe level +## 8. Customization at the recipe level Per the four customization tiers in [README.md §7](./README.md#7-customization): diff --git a/crates/cargo-anvil/src/anvil/artifacts/container.rs b/crates/cargo-anvil/src/anvil/artifacts/container.rs index 8e01f276..a7a320df 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/container.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/container.rs @@ -831,6 +831,7 @@ mod tests { "GITHUB_BASE_REF", "SYSTEM_PULLREQUEST_TARGETBRANCH", "ANVIL_IMPACT", + "ANVIL_MIRI_JOBS", ] { assert!(RECIPE.contains(name), "{name} must be forwarded"); } @@ -839,8 +840,8 @@ mod tests { // container that did not inherit it would recompute from a diff instead // of trusting the artifact the group downloaded. assert!( - RECIPE.contains("'ANVIL_IMPACT')) {"), - "ANVIL_IMPACT must be in the forwarded set, not merely mentioned" + RECIPE.contains("'ANVIL_IMPACT', 'ANVIL_MIRI_JOBS')) {"), + "ANVIL_IMPACT and ANVIL_MIRI_JOBS must be in the forwarded set, not merely mentioned" ); // Nothing reads these; forwarding them only implied a contract that // does not exist. See justfile.rs, which asserts they stay removed. diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index be4d082a..4fed54a5 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -412,7 +412,8 @@ mod tests { "ForEach-Object -Parallel", "-ThrottleLimit $jobs", "rustc $toolchain --print sysroot", - "ANVIL_MIRI_JOBS must be a positive integer", + "anvil miri: ANVIL_MIRI_JOBS must be a positive integer", + "Sort-Object PackageId, TargetKind, TargetName, Path", "##[group]Miri artifact", "::group::Miri artifact", ] { diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index 8361e258..3f287052 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -411,6 +411,10 @@ //! exclude = true //! ``` //! +//! Reserve this package-wide switch for constraints that apply to every test +//! target. Prefer per-test ignores with reasons for narrower or temporary +//! suppressions. +//! //! ### Concurrency model checking (`loom`) //! //! The `loom` check runs only the test targets that opt in, detected diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just index 1864e6c0..10f5ca6b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just @@ -14,8 +14,8 @@ # Compile all selected packages together, then run their Miri test artifacts in # parallel. The single Cargo invocation preserves workspace feature unification, -# while artifact-level parallelism uses runner CPUs that `cargo miri test` -# otherwise leaves idle. +# while artifact-level parallelism uses host logical processors that +# `cargo miri test` otherwise leaves idle. # # Packages can opt their own test targets out through: # @@ -26,7 +26,7 @@ # translate the metadata to `--exclude`; impact-scoped runs remove matching # `--package name@version` pairs before compilation. # -# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# ANVIL_MIRI_JOBS overrides the default of one artifact worker per logical processor. # Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] @@ -64,6 +64,10 @@ _anvil-miri-test *package_args: } if ($packageArgs -contains '--workspace') { + if ($workspaceMembers.Count -gt 0 -and $excludedPackageIds.Count -eq $workspaceMembers.Count) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } foreach ($packageName in $excludedPackages) { $packageArgs += @('--exclude', $packageName) } @@ -123,9 +127,20 @@ _anvil-miri-test *package_args: Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" exit 1 } + $targetName = [string]$message.target.name + $targetKinds = @($message.target.kind | ForEach-Object { [string]$_ }) + if ([string]::IsNullOrWhiteSpace($targetName) -or $targetKinds.Count -eq 0) { + Write-Error "anvil miri: target metadata not found for artifact '$artifactPath'" + exit 1 + } + $targetKind = $targetKinds -join '/' + $artifactFileName = Split-Path -Leaf $artifactPath $artifacts.Add([pscustomobject]@{ Path = $artifactPath - Name = Split-Path -Leaf $artifactPath + PackageId = $packageId + TargetName = $targetName + TargetKind = $targetKind + Label = "$packageId :: $targetKind $targetName ($artifactFileName)" WorkingDirectory = $packageDirectories[$packageId] }) } @@ -166,7 +181,7 @@ _anvil-miri-test *package_args: $jobs = 0 if ($env:ANVIL_MIRI_JOBS) { if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { - Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + Write-Error "anvil miri: ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" exit 1 } } else { @@ -176,12 +191,14 @@ _anvil-miri-test *package_args: Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" - $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + # Cargo normally runs each test executable from its owning package root. + # The direct cargo-miri invocation below restores that working directory. + $orderedArtifacts = @($artifacts | Sort-Object PackageId, TargetKind, TargetName, Path) $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { [pscustomobject]@{ Index = $i Artifact = $orderedArtifacts[$i].Path - Name = $orderedArtifacts[$i].Name + Label = $orderedArtifacts[$i].Label WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory } } @@ -190,7 +207,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name)" + Write-Host "anvil miri: starting $($item.Label)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -201,23 +218,25 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" + Write-Host "anvil miri: completed $($item.Label) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index - Name = $item.Name + Label = $item.Label ExitCode = $exitCode Log = $log } } -ThrottleLimit $jobs ) + # Parallel workers buffer into separate logs. Replaying those logs by + # stable artifact index keeps diagnostics isolated and deterministic. foreach ($result in $results | Sort-Object Index) { if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Host "::group::Miri artifact $($result.Name)" + Write-Host "::group::Miri artifact $($result.Label)" } elseif ($env:TF_BUILD -eq 'True') { - Write-Host "##[group]Miri artifact $($result.Name)" + Write-Host "##[group]Miri artifact $($result.Label)" } else { - Write-Host "`n=== Miri artifact $($result.Name) ===" + Write-Host "`n=== Miri artifact $($result.Label) ===" } Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } if ($env:GITHUB_ACTIONS -eq 'true') { @@ -229,7 +248,7 @@ _anvil-miri-test *package_args: $failed = @($results | Where-Object ExitCode -ne 0) if ($failed.Count -ne 0) { - [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Label -join ', ')") exit 1 } } finally { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/container.just b/crates/cargo-anvil/templates/justfiles/anvil/container.just index 6e3a91d8..82a98d51 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/container.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/container.just @@ -972,9 +972,10 @@ anvil-container *command: # The recipe contract's own inputs. These are read by generated checks -- # `anvil-pr-title` reads PR_TITLE, `_anvil-base-ref` reads BASE_REF and its - # CI equivalents, and `anvil-impact` reads ANVIL_IMPACT to decide whether to - # compute scoping, consume a downloaded cache, or skip -- so dropping them at - # the boundary makes the same command mean different things inside and out. + # CI equivalents, `anvil-impact` reads ANVIL_IMPACT to decide whether to + # compute scoping, consume a downloaded cache, or skip, and the Miri runner + # reads ANVIL_MIRI_JOBS to cap artifact workers -- so dropping them at the + # boundary makes the same command mean different things inside and out. # anvil-pr-title is the sharp case: with PR_TITLE unset it exits 0 with a # skip notice, so a title a native run rejects passes in a container and the # tier still reports green. ANVIL_IMPACT is the other: a CI group job exports @@ -987,7 +988,7 @@ anvil-container *command: foreach ($name in @( 'PR_TITLE', 'BASE_REF', 'GITHUB_BASE_REF', 'SYSTEM_PULLREQUEST_TARGETBRANCH', - 'ANVIL_IMPACT')) { + 'ANVIL_IMPACT', 'ANVIL_MIRI_JOBS')) { if ((Test-Path -LiteralPath "Env:$name") -and -not [string]::IsNullOrEmpty((Get-Item -LiteralPath "Env:$name").Value)) { $forwardedEnv += $name $runArgs += @('-e', $name) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index dd8ea361..e60dbbd3 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -37,6 +37,10 @@ const CONTAINER: &str = include_str!("../templates/justfiles/anvil/container.jus // Any nonzero value works; naming it prevents tests from implying an external exit-code contract. const ARBITRARY_FAILURE_EXIT: &str = "23"; +// The Miri fixture mirrors the production protocol rather than executing Rust: +// fake Cargo supplies workspace metadata and compiler-artifact JSON, fake rustc +// identifies the pinned toolchain sysroot, and cargo-miri under that sysroot +// records the environment and working directory restored by the direct runner. #[test] fn regeneration_check_runs_on_every_pull_request() { assert!(REGENERATE_WORKFLOW.contains("pull_request: {}")); @@ -204,13 +208,31 @@ if ($args -contains 'miri') { $definitions = @($env:FAKE_MIRI_ARTIFACTS | ConvertFrom-Json) } foreach ($definition in $definitions) { - $path = [System.IO.Path]::Combine($artifactRoot, [string]$definition.name) + $artifactDirectory = if ($definition.artifact_dir) { + [System.IO.Path]::Combine($artifactRoot, [string]$definition.artifact_dir) + } else { + $artifactRoot + } + [System.IO.Directory]::CreateDirectory($artifactDirectory) | Out-Null + $path = [System.IO.Path]::Combine($artifactDirectory, [string]$definition.name) Set-Content -LiteralPath $path -Value '{}' [pscustomobject]@{ reason = 'compiler-artifact' package_id = [string]$definition.package_id executable = $path profile = [pscustomobject]@{ test = [bool]$definition.test } + target = [pscustomobject]@{ + name = if ($definition.target_name) { + [string]$definition.target_name + } else { + [string]$definition.name + } + kind = @(if ($definition.target_kind) { + [string]$definition.target_kind + } else { + 'test' + }) + } } | ConvertTo-Json -Depth 4 -Compress if ($definition.duplicate) { [pscustomobject]@{ @@ -218,6 +240,18 @@ if ($args -contains 'miri') { package_id = [string]$definition.package_id executable = $path profile = [pscustomobject]@{ test = [bool]$definition.test } + target = [pscustomobject]@{ + name = if ($definition.target_name) { + [string]$definition.target_name + } else { + [string]$definition.name + } + kind = @(if ($definition.target_kind) { + [string]$definition.target_kind + } else { + 'test' + }) + } } | ConvertTo-Json -Depth 4 -Compress } } @@ -402,8 +436,8 @@ fn miri_runner_filters_artifacts_and_runs_in_parallel() { let cargo_log = tmp.path().join("cargo.log"); let run_log = tmp.path().join("miri-run"); let artifacts = r#"[ - {"name":"zeta-test","package_id":"fixture 0.1.0","test":true}, - {"name":"alpha-test","package_id":"fixture 0.1.0","test":true,"duplicate":true}, + {"name":"zeta-test","package_id":"fixture 0.1.0","target_name":"zeta","target_kind":"test","test":true}, + {"name":"alpha-test","package_id":"fixture 0.1.0","target_name":"alpha","target_kind":"test","test":true,"duplicate":true}, {"name":"ordinary-bin","package_id":"fixture 0.1.0","test":false}, {"name":"excluded-test","package_id":"other-package 0.1.0","test":true} ]"#; @@ -480,11 +514,60 @@ fn miri_runner_filters_artifacts_and_runs_in_parallel() { ); let stdout = String::from_utf8_lossy(&output.stdout); - let alpha_group = stdout.find("=== Miri artifact alpha-test ===").unwrap(); - let zeta_group = stdout.find("=== Miri artifact zeta-test ===").unwrap(); + let alpha_group = stdout.find("test alpha (alpha-test)").unwrap(); + let zeta_group = stdout.find("test zeta (zeta-test)").unwrap(); assert!(alpha_group < zeta_group, "artifact logs must replay deterministically"); } +#[test] +fn miri_runner_labels_same_named_artifacts_with_package_identity() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("miri.just", MIRI)], + &[ + "anvil-component-nightly-miri-validate-prereqs", + "anvil-component-nightly-rust-src-validate-prereqs", + "anvil-component-nightly-miri-install", + "anvil-component-nightly-rust-src-install", + "anvil-impact", + ], + ); + let other_package = tmp.path().join("nested/other-package"); + fs::create_dir_all(&other_package).unwrap(); + write( + &other_package.join("Cargo.toml"), + "[package]\nname = \"other-package\"\nversion = \"0.1.0\"\n", + ); + let run_log = tmp.path().join("same-name"); + let artifacts = r#"[ + {"name":"shared-test","artifact_dir":"fixture","package_id":"fixture 0.1.0","target_name":"shared","target_kind":"test","test":true}, + {"name":"shared-test","artifact_dir":"other","package_id":"other-package 0.1.0","target_name":"shared","target_kind":"test","test":true} + ]"#; + let output = run_just( + tmp.path(), + &["_anvil-miri-test", "--workspace"], + &[ + ("FAKE_MIRI_ARTIFACTS", OsStr::new(artifacts)), + ("FAKE_MIRI_RUN_LOG", run_log.as_os_str()), + ("ANVIL_MIRI_JOBS", OsStr::new("1")), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("other-package")), + ("FAKE_SECOND_PACKAGE_DIR_LEAF", OsStr::new("other-package")), + ], + ); + + assert!( + output.status.success(), + "same-named Miri artifacts should both run:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Miri artifact fixture 0.1.0 :: test shared (shared-test)")); + assert!(stdout.contains("Miri artifact other-package 0.1.0 :: test shared (shared-test)")); +} + #[test] fn miri_runner_preserves_profile_flags_and_impact_filtering() { if !tools_available() { @@ -511,14 +594,18 @@ _anvil-impact-include tier: ); write(&justfile_path, &justfile); let run_log = tmp.path().join("miri-profile"); + let cargo_log = tmp.path().join("miri-profile-cargo.log"); let artifacts = r#"[{"name":"profile-test","package_id":"fixture 0.1.0","test":true}]"#; let output = run_just( tmp.path(), &["anvil-miri-tree-borrows"], &[ - ("FAKE_INCLUDE", OsStr::new("--package fixture@0.1.0")), + ("FAKE_INCLUDE", OsStr::new("--package fixture@0.1.0 --package excluded@0.1.0")), + ("FAKE_CARGO_LOG", cargo_log.as_os_str()), ("FAKE_MIRI_ARTIFACTS", OsStr::new(artifacts)), ("FAKE_MIRI_RUN_LOG", run_log.as_os_str()), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("excluded")), + ("FAKE_SECOND_MIRI_EXCLUDE", OsStr::new("1")), ], ); @@ -538,6 +625,19 @@ _anvil-impact-include tier: .unwrap() .contains("--cfg miri_tree_borrows") ); + let cargo_calls = fs::read_to_string(cargo_log).unwrap(); + let build_call = cargo_calls + .lines() + .find(|call| call.contains("miri test")) + .expect("the profile must compile its selected Miri targets"); + assert!( + build_call.contains("--package fixture@0.1.0"), + "impact-selected included packages must be forwarded:\n{build_call}" + ); + assert!( + !build_call.contains("excluded@0.1.0"), + "impact-selected opted-out packages must be filtered:\n{build_call}" + ); } #[test] @@ -571,6 +671,22 @@ fn miri_runner_handles_no_work_and_aggregates_failures() { assert!(all_excluded.status.success()); assert!(String::from_utf8_lossy(&all_excluded.stdout).contains("all selected packages are excluded")); + let workspace_cargo_log = tmp.path().join("all-excluded-workspace-cargo.log"); + let all_workspace_excluded = run_just( + tmp.path(), + &["_anvil-miri-test", "--workspace"], + &[ + ("FAKE_CARGO_LOG", workspace_cargo_log.as_os_str()), + ("FAKE_MIRI_EXCLUDE", OsStr::new("1")), + ], + ); + assert!(all_workspace_excluded.status.success()); + assert!(String::from_utf8_lossy(&all_workspace_excluded.stdout).contains("all selected packages are excluded")); + assert!( + !fs::read_to_string(workspace_cargo_log).unwrap().contains("miri test"), + "an all-excluded workspace must stop before asking Cargo to select no packages" + ); + let run_log = tmp.path().join("miri-failure"); let artifacts = r#"[ {"name":"fail-test","package_id":"fixture 0.1.0","test":true}, @@ -591,9 +707,9 @@ fn miri_runner_handles_no_work_and_aggregates_failures() { let stderr = String::from_utf8_lossy(&failed.stderr); assert!(stdout.contains("miri output: fail-test")); assert!(stdout.contains("miri output: pass-test")); - assert!(stdout.contains("##[group]Miri artifact fail-test")); + assert!(stdout.contains("##[group]Miri artifact fixture 0.1.0 :: test fail-test (fail-test)")); assert!(stdout.contains("##[endgroup]")); - assert!(stderr.contains("failed artifacts: fail-test")); + assert!(stderr.contains("failed artifacts: fixture 0.1.0 :: test fail-test (fail-test)")); let invalid_jobs = run_just( tmp.path(), @@ -608,7 +724,7 @@ fn miri_runner_handles_no_work_and_aggregates_failures() { ], ); assert_failed(&invalid_jobs, "invalid ANVIL_MIRI_JOBS"); - assert!(String::from_utf8_lossy(&invalid_jobs.stderr).contains("must be a positive integer")); + assert!(String::from_utf8_lossy(&invalid_jobs.stderr).contains("anvil miri: ANVIL_MIRI_JOBS must be a positive integer")); let invalid_json = run_just( tmp.path(), diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 8a962dfc..95e80fad 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2721,8 +2721,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # Compile all selected packages together, then run their Miri test artifacts in # parallel. The single Cargo invocation preserves workspace feature unification, -# while artifact-level parallelism uses runner CPUs that `cargo miri test` -# otherwise leaves idle. +# while artifact-level parallelism uses host logical processors that +# `cargo miri test` otherwise leaves idle. # # Packages can opt their own test targets out through: # @@ -2733,7 +2733,7 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # translate the metadata to `--exclude`; impact-scoped runs remove matching # `--package name@version` pairs before compilation. # -# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# ANVIL_MIRI_JOBS overrides the default of one artifact worker per logical processor. # Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] @@ -2771,6 +2771,10 @@ _anvil-miri-test *package_args: } if ($packageArgs -contains '--workspace') { + if ($workspaceMembers.Count -gt 0 -and $excludedPackageIds.Count -eq $workspaceMembers.Count) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } foreach ($packageName in $excludedPackages) { $packageArgs += @('--exclude', $packageName) } @@ -2830,9 +2834,20 @@ _anvil-miri-test *package_args: Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" exit 1 } + $targetName = [string]$message.target.name + $targetKinds = @($message.target.kind | ForEach-Object { [string]$_ }) + if ([string]::IsNullOrWhiteSpace($targetName) -or $targetKinds.Count -eq 0) { + Write-Error "anvil miri: target metadata not found for artifact '$artifactPath'" + exit 1 + } + $targetKind = $targetKinds -join '/' + $artifactFileName = Split-Path -Leaf $artifactPath $artifacts.Add([pscustomobject]@{ Path = $artifactPath - Name = Split-Path -Leaf $artifactPath + PackageId = $packageId + TargetName = $targetName + TargetKind = $targetKind + Label = "$packageId :: $targetKind $targetName ($artifactFileName)" WorkingDirectory = $packageDirectories[$packageId] }) } @@ -2873,7 +2888,7 @@ _anvil-miri-test *package_args: $jobs = 0 if ($env:ANVIL_MIRI_JOBS) { if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { - Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + Write-Error "anvil miri: ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" exit 1 } } else { @@ -2883,12 +2898,14 @@ _anvil-miri-test *package_args: Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" - $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + # Cargo normally runs each test executable from its owning package root. + # The direct cargo-miri invocation below restores that working directory. + $orderedArtifacts = @($artifacts | Sort-Object PackageId, TargetKind, TargetName, Path) $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { [pscustomobject]@{ Index = $i Artifact = $orderedArtifacts[$i].Path - Name = $orderedArtifacts[$i].Name + Label = $orderedArtifacts[$i].Label WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory } } @@ -2897,7 +2914,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name)" + Write-Host "anvil miri: starting $($item.Label)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -2908,23 +2925,25 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" + Write-Host "anvil miri: completed $($item.Label) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index - Name = $item.Name + Label = $item.Label ExitCode = $exitCode Log = $log } } -ThrottleLimit $jobs ) + # Parallel workers buffer into separate logs. Replaying those logs by + # stable artifact index keeps diagnostics isolated and deterministic. foreach ($result in $results | Sort-Object Index) { if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Host "::group::Miri artifact $($result.Name)" + Write-Host "::group::Miri artifact $($result.Label)" } elseif ($env:TF_BUILD -eq 'True') { - Write-Host "##[group]Miri artifact $($result.Name)" + Write-Host "##[group]Miri artifact $($result.Label)" } else { - Write-Host "`n=== Miri artifact $($result.Name) ===" + Write-Host "`n=== Miri artifact $($result.Label) ===" } Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } if ($env:GITHUB_ACTIONS -eq 'true') { @@ -2936,7 +2955,7 @@ _anvil-miri-test *package_args: $failed = @($results | Where-Object ExitCode -ne 0) if ($failed.Count -ne 0) { - [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Label -join ', ')") exit 1 } } finally { @@ -4689,9 +4708,10 @@ anvil-container *command: # The recipe contract's own inputs. These are read by generated checks -- # `anvil-pr-title` reads PR_TITLE, `_anvil-base-ref` reads BASE_REF and its - # CI equivalents, and `anvil-impact` reads ANVIL_IMPACT to decide whether to - # compute scoping, consume a downloaded cache, or skip -- so dropping them at - # the boundary makes the same command mean different things inside and out. + # CI equivalents, `anvil-impact` reads ANVIL_IMPACT to decide whether to + # compute scoping, consume a downloaded cache, or skip, and the Miri runner + # reads ANVIL_MIRI_JOBS to cap artifact workers -- so dropping them at the + # boundary makes the same command mean different things inside and out. # anvil-pr-title is the sharp case: with PR_TITLE unset it exits 0 with a # skip notice, so a title a native run rejects passes in a container and the # tier still reports green. ANVIL_IMPACT is the other: a CI group job exports @@ -4704,7 +4724,7 @@ anvil-container *command: foreach ($name in @( 'PR_TITLE', 'BASE_REF', 'GITHUB_BASE_REF', 'SYSTEM_PULLREQUEST_TARGETBRANCH', - 'ANVIL_IMPACT')) { + 'ANVIL_IMPACT', 'ANVIL_MIRI_JOBS')) { if ((Test-Path -LiteralPath "Env:$name") -and -not [string]::IsNullOrEmpty((Get-Item -LiteralPath "Env:$name").Value)) { $forwardedEnv += $name $runArgs += @('-e', $name) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index d433b2ac..c64e4db5 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2707,8 +2707,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # Compile all selected packages together, then run their Miri test artifacts in # parallel. The single Cargo invocation preserves workspace feature unification, -# while artifact-level parallelism uses runner CPUs that `cargo miri test` -# otherwise leaves idle. +# while artifact-level parallelism uses host logical processors that +# `cargo miri test` otherwise leaves idle. # # Packages can opt their own test targets out through: # @@ -2719,7 +2719,7 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # translate the metadata to `--exclude`; impact-scoped runs remove matching # `--package name@version` pairs before compilation. # -# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# ANVIL_MIRI_JOBS overrides the default of one artifact worker per logical processor. # Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] @@ -2757,6 +2757,10 @@ _anvil-miri-test *package_args: } if ($packageArgs -contains '--workspace') { + if ($workspaceMembers.Count -gt 0 -and $excludedPackageIds.Count -eq $workspaceMembers.Count) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } foreach ($packageName in $excludedPackages) { $packageArgs += @('--exclude', $packageName) } @@ -2816,9 +2820,20 @@ _anvil-miri-test *package_args: Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" exit 1 } + $targetName = [string]$message.target.name + $targetKinds = @($message.target.kind | ForEach-Object { [string]$_ }) + if ([string]::IsNullOrWhiteSpace($targetName) -or $targetKinds.Count -eq 0) { + Write-Error "anvil miri: target metadata not found for artifact '$artifactPath'" + exit 1 + } + $targetKind = $targetKinds -join '/' + $artifactFileName = Split-Path -Leaf $artifactPath $artifacts.Add([pscustomobject]@{ Path = $artifactPath - Name = Split-Path -Leaf $artifactPath + PackageId = $packageId + TargetName = $targetName + TargetKind = $targetKind + Label = "$packageId :: $targetKind $targetName ($artifactFileName)" WorkingDirectory = $packageDirectories[$packageId] }) } @@ -2859,7 +2874,7 @@ _anvil-miri-test *package_args: $jobs = 0 if ($env:ANVIL_MIRI_JOBS) { if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { - Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + Write-Error "anvil miri: ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" exit 1 } } else { @@ -2869,12 +2884,14 @@ _anvil-miri-test *package_args: Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" - $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + # Cargo normally runs each test executable from its owning package root. + # The direct cargo-miri invocation below restores that working directory. + $orderedArtifacts = @($artifacts | Sort-Object PackageId, TargetKind, TargetName, Path) $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { [pscustomobject]@{ Index = $i Artifact = $orderedArtifacts[$i].Path - Name = $orderedArtifacts[$i].Name + Label = $orderedArtifacts[$i].Label WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory } } @@ -2883,7 +2900,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name)" + Write-Host "anvil miri: starting $($item.Label)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -2894,23 +2911,25 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" + Write-Host "anvil miri: completed $($item.Label) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index - Name = $item.Name + Label = $item.Label ExitCode = $exitCode Log = $log } } -ThrottleLimit $jobs ) + # Parallel workers buffer into separate logs. Replaying those logs by + # stable artifact index keeps diagnostics isolated and deterministic. foreach ($result in $results | Sort-Object Index) { if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Host "::group::Miri artifact $($result.Name)" + Write-Host "::group::Miri artifact $($result.Label)" } elseif ($env:TF_BUILD -eq 'True') { - Write-Host "##[group]Miri artifact $($result.Name)" + Write-Host "##[group]Miri artifact $($result.Label)" } else { - Write-Host "`n=== Miri artifact $($result.Name) ===" + Write-Host "`n=== Miri artifact $($result.Label) ===" } Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } if ($env:GITHUB_ACTIONS -eq 'true') { @@ -2922,7 +2941,7 @@ _anvil-miri-test *package_args: $failed = @($results | Where-Object ExitCode -ne 0) if ($failed.Count -ne 0) { - [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Label -join ', ')") exit 1 } } finally { @@ -4675,9 +4694,10 @@ anvil-container *command: # The recipe contract's own inputs. These are read by generated checks -- # `anvil-pr-title` reads PR_TITLE, `_anvil-base-ref` reads BASE_REF and its - # CI equivalents, and `anvil-impact` reads ANVIL_IMPACT to decide whether to - # compute scoping, consume a downloaded cache, or skip -- so dropping them at - # the boundary makes the same command mean different things inside and out. + # CI equivalents, `anvil-impact` reads ANVIL_IMPACT to decide whether to + # compute scoping, consume a downloaded cache, or skip, and the Miri runner + # reads ANVIL_MIRI_JOBS to cap artifact workers -- so dropping them at the + # boundary makes the same command mean different things inside and out. # anvil-pr-title is the sharp case: with PR_TITLE unset it exits 0 with a # skip notice, so a title a native run rejects passes in a container and the # tier still reports green. ANVIL_IMPACT is the other: a CI group job exports @@ -4690,7 +4710,7 @@ anvil-container *command: foreach ($name in @( 'PR_TITLE', 'BASE_REF', 'GITHUB_BASE_REF', 'SYSTEM_PULLREQUEST_TARGETBRANCH', - 'ANVIL_IMPACT')) { + 'ANVIL_IMPACT', 'ANVIL_MIRI_JOBS')) { if ((Test-Path -LiteralPath "Env:$name") -and -not [string]::IsNullOrEmpty((Get-Item -LiteralPath "Env:$name").Value)) { $forwardedEnv += $name $runArgs += @('-e', $name) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 8348e5cb..3683060b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1591,8 +1591,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # Compile all selected packages together, then run their Miri test artifacts in # parallel. The single Cargo invocation preserves workspace feature unification, -# while artifact-level parallelism uses runner CPUs that `cargo miri test` -# otherwise leaves idle. +# while artifact-level parallelism uses host logical processors that +# `cargo miri test` otherwise leaves idle. # # Packages can opt their own test targets out through: # @@ -1603,7 +1603,7 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- # translate the metadata to `--exclude`; impact-scoped runs remove matching # `--package name@version` pairs before compilation. # -# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# ANVIL_MIRI_JOBS overrides the default of one artifact worker per logical processor. # Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] @@ -1641,6 +1641,10 @@ _anvil-miri-test *package_args: } if ($packageArgs -contains '--workspace') { + if ($workspaceMembers.Count -gt 0 -and $excludedPackageIds.Count -eq $workspaceMembers.Count) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } foreach ($packageName in $excludedPackages) { $packageArgs += @('--exclude', $packageName) } @@ -1700,9 +1704,20 @@ _anvil-miri-test *package_args: Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" exit 1 } + $targetName = [string]$message.target.name + $targetKinds = @($message.target.kind | ForEach-Object { [string]$_ }) + if ([string]::IsNullOrWhiteSpace($targetName) -or $targetKinds.Count -eq 0) { + Write-Error "anvil miri: target metadata not found for artifact '$artifactPath'" + exit 1 + } + $targetKind = $targetKinds -join '/' + $artifactFileName = Split-Path -Leaf $artifactPath $artifacts.Add([pscustomobject]@{ Path = $artifactPath - Name = Split-Path -Leaf $artifactPath + PackageId = $packageId + TargetName = $targetName + TargetKind = $targetKind + Label = "$packageId :: $targetKind $targetName ($artifactFileName)" WorkingDirectory = $packageDirectories[$packageId] }) } @@ -1743,7 +1758,7 @@ _anvil-miri-test *package_args: $jobs = 0 if ($env:ANVIL_MIRI_JOBS) { if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { - Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + Write-Error "anvil miri: ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" exit 1 } } else { @@ -1753,12 +1768,14 @@ _anvil-miri-test *package_args: Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" - $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + # Cargo normally runs each test executable from its owning package root. + # The direct cargo-miri invocation below restores that working directory. + $orderedArtifacts = @($artifacts | Sort-Object PackageId, TargetKind, TargetName, Path) $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { [pscustomobject]@{ Index = $i Artifact = $orderedArtifacts[$i].Path - Name = $orderedArtifacts[$i].Name + Label = $orderedArtifacts[$i].Label WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory } } @@ -1767,7 +1784,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name)" + Write-Host "anvil miri: starting $($item.Label)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -1778,23 +1795,25 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" + Write-Host "anvil miri: completed $($item.Label) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index - Name = $item.Name + Label = $item.Label ExitCode = $exitCode Log = $log } } -ThrottleLimit $jobs ) + # Parallel workers buffer into separate logs. Replaying those logs by + # stable artifact index keeps diagnostics isolated and deterministic. foreach ($result in $results | Sort-Object Index) { if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Host "::group::Miri artifact $($result.Name)" + Write-Host "::group::Miri artifact $($result.Label)" } elseif ($env:TF_BUILD -eq 'True') { - Write-Host "##[group]Miri artifact $($result.Name)" + Write-Host "##[group]Miri artifact $($result.Label)" } else { - Write-Host "`n=== Miri artifact $($result.Name) ===" + Write-Host "`n=== Miri artifact $($result.Label) ===" } Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } if ($env:GITHUB_ACTIONS -eq 'true') { @@ -1806,7 +1825,7 @@ _anvil-miri-test *package_args: $failed = @($results | Where-Object ExitCode -ne 0) if ($failed.Count -ne 0) { - [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Label -join ', ')") exit 1 } } finally { @@ -3559,9 +3578,10 @@ anvil-container *command: # The recipe contract's own inputs. These are read by generated checks -- # `anvil-pr-title` reads PR_TITLE, `_anvil-base-ref` reads BASE_REF and its - # CI equivalents, and `anvil-impact` reads ANVIL_IMPACT to decide whether to - # compute scoping, consume a downloaded cache, or skip -- so dropping them at - # the boundary makes the same command mean different things inside and out. + # CI equivalents, `anvil-impact` reads ANVIL_IMPACT to decide whether to + # compute scoping, consume a downloaded cache, or skip, and the Miri runner + # reads ANVIL_MIRI_JOBS to cap artifact workers -- so dropping them at the + # boundary makes the same command mean different things inside and out. # anvil-pr-title is the sharp case: with PR_TITLE unset it exits 0 with a # skip notice, so a title a native run rejects passes in a container and the # tier still reports green. ANVIL_IMPACT is the other: a CI group job exports @@ -3574,7 +3594,7 @@ anvil-container *command: foreach ($name in @( 'PR_TITLE', 'BASE_REF', 'GITHUB_BASE_REF', 'SYSTEM_PULLREQUEST_TARGETBRANCH', - 'ANVIL_IMPACT')) { + 'ANVIL_IMPACT', 'ANVIL_MIRI_JOBS')) { if ((Test-Path -LiteralPath "Env:$name") -and -not [string]::IsNullOrEmpty((Get-Item -LiteralPath "Env:$name").Value)) { $forwardedEnv += $name $runArgs += @('-e', $name) diff --git a/justfiles/anvil/checks/miri.just b/justfiles/anvil/checks/miri.just index 1864e6c0..10f5ca6b 100644 --- a/justfiles/anvil/checks/miri.just +++ b/justfiles/anvil/checks/miri.just @@ -14,8 +14,8 @@ # Compile all selected packages together, then run their Miri test artifacts in # parallel. The single Cargo invocation preserves workspace feature unification, -# while artifact-level parallelism uses runner CPUs that `cargo miri test` -# otherwise leaves idle. +# while artifact-level parallelism uses host logical processors that +# `cargo miri test` otherwise leaves idle. # # Packages can opt their own test targets out through: # @@ -26,7 +26,7 @@ # translate the metadata to `--exclude`; impact-scoped runs remove matching # `--package name@version` pairs before compilation. # -# ANVIL_MIRI_JOBS overrides the default of one process per logical processor. +# ANVIL_MIRI_JOBS overrides the default of one artifact worker per logical processor. # Compile selected packages once and run their Miri test artifacts concurrently. [script("pwsh", "-NoProfile")] @@ -64,6 +64,10 @@ _anvil-miri-test *package_args: } if ($packageArgs -contains '--workspace') { + if ($workspaceMembers.Count -gt 0 -and $excludedPackageIds.Count -eq $workspaceMembers.Count) { + Write-Host 'anvil miri: all selected packages are excluded; nothing to test' + exit 0 + } foreach ($packageName in $excludedPackages) { $packageArgs += @('--exclude', $packageName) } @@ -123,9 +127,20 @@ _anvil-miri-test *package_args: Write-Error "anvil miri: package metadata not found for artifact package '$packageId'" exit 1 } + $targetName = [string]$message.target.name + $targetKinds = @($message.target.kind | ForEach-Object { [string]$_ }) + if ([string]::IsNullOrWhiteSpace($targetName) -or $targetKinds.Count -eq 0) { + Write-Error "anvil miri: target metadata not found for artifact '$artifactPath'" + exit 1 + } + $targetKind = $targetKinds -join '/' + $artifactFileName = Split-Path -Leaf $artifactPath $artifacts.Add([pscustomobject]@{ Path = $artifactPath - Name = Split-Path -Leaf $artifactPath + PackageId = $packageId + TargetName = $targetName + TargetKind = $targetKind + Label = "$packageId :: $targetKind $targetName ($artifactFileName)" WorkingDirectory = $packageDirectories[$packageId] }) } @@ -166,7 +181,7 @@ _anvil-miri-test *package_args: $jobs = 0 if ($env:ANVIL_MIRI_JOBS) { if (-not [int]::TryParse($env:ANVIL_MIRI_JOBS, [ref]$jobs) -or $jobs -lt 1) { - Write-Error "ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" + Write-Error "anvil miri: ANVIL_MIRI_JOBS must be a positive integer, got '$($env:ANVIL_MIRI_JOBS)'" exit 1 } } else { @@ -176,12 +191,14 @@ _anvil-miri-test *package_args: Write-Host "anvil miri: running $($artifacts.Count) test artifacts with $jobs concurrent process(es)" - $orderedArtifacts = @($artifacts | Sort-Object Name, Path) + # Cargo normally runs each test executable from its owning package root. + # The direct cargo-miri invocation below restores that working directory. + $orderedArtifacts = @($artifacts | Sort-Object PackageId, TargetKind, TargetName, Path) $work = for ($i = 0; $i -lt $orderedArtifacts.Count; $i++) { [pscustomobject]@{ Index = $i Artifact = $orderedArtifacts[$i].Path - Name = $orderedArtifacts[$i].Name + Label = $orderedArtifacts[$i].Label WorkingDirectory = $orderedArtifacts[$i].WorkingDirectory } } @@ -190,7 +207,7 @@ _anvil-miri-test *package_args: $ErrorActionPreference = 'Stop' $item = $_ $log = Join-Path $using:logRoot "$($item.Index).log" - Write-Host "anvil miri: starting $($item.Name)" + Write-Host "anvil miri: starting $($item.Label)" Push-Location -LiteralPath $item.WorkingDirectory try { & $using:runner runner $item.Artifact *> $log @@ -201,23 +218,25 @@ _anvil-miri-test *package_args: } finally { Pop-Location } - Write-Host "anvil miri: completed $($item.Name) (exit $exitCode)" + Write-Host "anvil miri: completed $($item.Label) (exit $exitCode)" [pscustomobject]@{ Index = $item.Index - Name = $item.Name + Label = $item.Label ExitCode = $exitCode Log = $log } } -ThrottleLimit $jobs ) + # Parallel workers buffer into separate logs. Replaying those logs by + # stable artifact index keeps diagnostics isolated and deterministic. foreach ($result in $results | Sort-Object Index) { if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Host "::group::Miri artifact $($result.Name)" + Write-Host "::group::Miri artifact $($result.Label)" } elseif ($env:TF_BUILD -eq 'True') { - Write-Host "##[group]Miri artifact $($result.Name)" + Write-Host "##[group]Miri artifact $($result.Label)" } else { - Write-Host "`n=== Miri artifact $($result.Name) ===" + Write-Host "`n=== Miri artifact $($result.Label) ===" } Get-Content -LiteralPath $result.Log | ForEach-Object { Write-Host $_ } if ($env:GITHUB_ACTIONS -eq 'true') { @@ -229,7 +248,7 @@ _anvil-miri-test *package_args: $failed = @($results | Where-Object ExitCode -ne 0) if ($failed.Count -ne 0) { - [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Name -join ', ')") + [Console]::Error.WriteLine("anvil miri: failed artifacts: $($failed.Label -join ', ')") exit 1 } } finally { diff --git a/justfiles/anvil/container.just b/justfiles/anvil/container.just index 6e3a91d8..82a98d51 100644 --- a/justfiles/anvil/container.just +++ b/justfiles/anvil/container.just @@ -972,9 +972,10 @@ anvil-container *command: # The recipe contract's own inputs. These are read by generated checks -- # `anvil-pr-title` reads PR_TITLE, `_anvil-base-ref` reads BASE_REF and its - # CI equivalents, and `anvil-impact` reads ANVIL_IMPACT to decide whether to - # compute scoping, consume a downloaded cache, or skip -- so dropping them at - # the boundary makes the same command mean different things inside and out. + # CI equivalents, `anvil-impact` reads ANVIL_IMPACT to decide whether to + # compute scoping, consume a downloaded cache, or skip, and the Miri runner + # reads ANVIL_MIRI_JOBS to cap artifact workers -- so dropping them at the + # boundary makes the same command mean different things inside and out. # anvil-pr-title is the sharp case: with PR_TITLE unset it exits 0 with a # skip notice, so a title a native run rejects passes in a container and the # tier still reports green. ANVIL_IMPACT is the other: a CI group job exports @@ -987,7 +988,7 @@ anvil-container *command: foreach ($name in @( 'PR_TITLE', 'BASE_REF', 'GITHUB_BASE_REF', 'SYSTEM_PULLREQUEST_TARGETBRANCH', - 'ANVIL_IMPACT')) { + 'ANVIL_IMPACT', 'ANVIL_MIRI_JOBS')) { if ((Test-Path -LiteralPath "Env:$name") -and -not [string]::IsNullOrEmpty((Get-Item -LiteralPath "Env:$name").Value)) { $forwardedEnv += $name $runArgs += @('-e', $name) From d45940703e7a9167cee5312ae3ae855dca2c4acf Mon Sep 17 00:00:00 2001 From: "Stanislav Andras (from Dev Box)" Date: Tue, 1 Sep 2026 22:46:51 +0200 Subject: [PATCH 6/6] test(cargo-anvil): make Miri contracts portable Match deterministic replay group headers instead of parallel progress messages, and canonicalize fixture working directories before comparing Windows path spellings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98e52fe3-a435-412e-9154-73a557fef79f --- crates/cargo-anvil/tests/recipe_contracts.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index e60dbbd3..487834ff 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -474,10 +474,9 @@ fn miri_runner_filters_artifacts_and_runs_in_parallel() { assert!(!run_log.with_extension("ordinary-bin.start").exists()); assert!(!run_log.with_extension("excluded-test.start").exists()); - assert_eq!( - fs::read_to_string(run_log.with_extension("alpha-test.cwd")).unwrap().trim(), - tmp.path().to_str().unwrap() - ); + let recorded_cwd = fs::read_to_string(run_log.with_extension("alpha-test.cwd")).unwrap(); + let expected_cwd = fs::canonicalize(tmp.path()).unwrap(); + assert_eq!(fs::canonicalize(recorded_cwd.trim()).unwrap(), expected_cwd); assert_eq!( fs::read_to_string(run_log.with_extension("alpha-test.sysroot")).unwrap().trim(), tmp.path().join("fake-miri-sysroot").to_str().unwrap() @@ -514,8 +513,8 @@ fn miri_runner_filters_artifacts_and_runs_in_parallel() { ); let stdout = String::from_utf8_lossy(&output.stdout); - let alpha_group = stdout.find("test alpha (alpha-test)").unwrap(); - let zeta_group = stdout.find("test zeta (zeta-test)").unwrap(); + let alpha_group = stdout.find("=== Miri artifact fixture 0.1.0 :: test alpha").unwrap(); + let zeta_group = stdout.find("=== Miri artifact fixture 0.1.0 :: test zeta").unwrap(); assert!(alpha_group < zeta_group, "artifact logs must replay deterministically"); }