Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .anvil.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version = 1
tool = "anvil"
tool_version = "0.6.0"
catalog_checksum = "sha256:411e8b3d247721c856c6a8a69f7b388711159325472ab687eff9c40bdd9727b0"
catalog_checksum = "sha256:06529602514854ab5676975141a7542dc31a2577f7615ddd8ea516ed74d41bd5"

[[file]]
path = ".anvil/container/Dockerfile.dockerignore"
Expand Down Expand Up @@ -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:91b67bc68bcc34a82233917112b297935fdedccf46ffd668884636a79cc4579d"

[[file]]
path = "justfiles/anvil/checks/msrv-test.just"
Expand Down Expand Up @@ -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"
Expand Down
20 changes: 17 additions & 3 deletions crates/cargo-anvil/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -400,6 +402,18 @@ 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
```

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
Expand Down Expand Up @@ -491,7 +505,7 @@ And `docs/verification.md` for the continuous-validation strategy.
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/ox-tools/tree/main/crates/cargo-anvil">source code</a>.
</sub>

[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbLvVGTNtetQUbnp9vX0Ew7_gbkZEyxfXZXyMbltL72AXa-o1hZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls
[__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
Expand Down
10 changes: 5 additions & 5 deletions crates/cargo-anvil/docs/design/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ available.

| Check | Invocation | Source |
|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
| `miri` | `cargo +<pinned-nightly> 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 +<pinned-nightly> 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 +<catalog-nightly> 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 <pkg> --release --all-features --locked --test <target> -- --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 |
Expand Down Expand Up @@ -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 +<pinned-nightly> 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 = "<reason>")]` 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 +<pinned-nightly> 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 = "<reason>")]`. | oxidizer-github |
| `miri-race-coverage` | `MIRIFLAGS="-Zmiri-many-seeds=<low>..<high>" RUSTFLAGS='--cfg miri_race_coverage' cargo +<pinned-nightly> miri test --all-features --tests`. The `<low>..<high>` 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 = "<reason>")]`. | 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 = "<reason>")]`. | 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 = "<reason>")]`. | oxidizer-github |
| `miri-race-coverage` | Uses the shared compile-once artifact runner with `MIRIFLAGS="-Zmiri-many-seeds=<low>..<high>"` and `RUSTFLAGS='--cfg miri_race_coverage'`. The `<low>..<high>` 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 = "<reason>")]`. | 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_<profile>` in `RUSTFLAGS`; the distinct cfg is what lets a test opt out of just one profile via `#[cfg_attr(miri_<profile>, 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_<profile>` 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Copilot speaking]

Explain why stricter Miri profiles retain the full Miri OS matrix

The design states that the stricter scheduled Miri profiles use the same operating-system matrix as pr-runtime-analysis, but the explanation only restates the policy: operating systems considered worth standard Miri also receive the harder profiles. It does not document why the expensive profiles must not use a smaller matrix.

Reproducible reasoning: Tree Borrows, strict provenance, and race coverage cost hours per leg, so reducing their operating-system coverage is an attractive future optimization. The current text does not give a maintainer enough information to evaluate that change. The decision depends on the catalog's single-tier-per-group constraint and on preserving detection of profile-specific undefined behavior on every operating system where Miri is supported; those considerations are not evident from the matrix counts themselves.

Consequence: A maintainer trying to reduce scheduled cost could narrow the stricter profiles to fewer operating systems and unintentionally remove coverage for undefined behavior that appears only under one of those profiles on an omitted platform.

Recommended action: Document the constraint and intended coverage outcome next to the matrix statement: using a smaller matrix would leave profile-specific behavior unchecked on supported Miri platforms, and the tier/group structure does not provide a separate subset without changing the catalog organization.

Impacted locations:

  • crates/cargo-anvil/docs/design/checks.md:255


Per-test opt-outs live in source via `#[cfg_attr(miri_<profile>, ignore = "<reason>")]`. 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.

Expand Down
13 changes: 7 additions & 6 deletions crates/cargo-anvil/docs/design/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 27 additions & 3 deletions crates/cargo-anvil/docs/design/local.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,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 = "<reason>")` 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
Expand All @@ -685,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:

Expand All @@ -700,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):

Expand Down
Loading
Loading