Skip to content

fix(metabench): rename the duplicate basic example and gate the workspace on unique names - #751

Draft
Evgenii (Vaiz) wants to merge 2 commits into
mainfrom
u/vaiz/2026/09/13/dedupe-basic-example-name
Draft

Evgenii (Vaiz) wants to merge 2 commits into
mainfrom
u/vaiz/2026/09/13/dedupe-basic-example-name

Conversation

@Vaiz

Copy link
Copy Markdown
Contributor

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

observed and metabench both declared an example target named basic. Cargo writes every example in the workspace into one shared target/<profile>/examples/ directory, so both resolved to the same output file.

Reproduced before changing anything, on 84083b8:

cargo build -p observed -p metabench --example basic --release

warning: output filename collision at target\release\examples\basic.exe
  = note: the example target `basic` in package `observed v0.26.0` has the same
          output filename as the example target `basic` in package `metabench v0.1.1`
  = note: this may become a hard error in the future; see
          <https://github.com/rust-lang/cargo/issues/6313>

plus the same warning for basic.pdb. Cargo only warns, then builds both targets concurrently into that one path.

Why this is worth a guard and not just a rename

This class already cost a CI failure here. #676 renamed a duplicate tower_service example after the link.exe race it caused surfaced as LNK1104: cannot open file ...\examples\tower_service.exe on an unrelated pull request. On Linux and macOS the same race silently overwrites the binary instead, so only the Windows leg surfaces it — the example that runs is simply not the one that was selected, and nothing fails.

#676 renamed and moved on, so the class recurred four weeks later. This PR closes both halves.

What changed

The rename. metabench's target becomes single_benchmark, which also says what distinguishes it from the sibling parameterized example. observed's basic is the published crate's introductory example and is named in its own module docs, so renaming that side would have been the worse trade. The two call sites in metabench/tests/spawned_benchmark.rs that spawn the example by name move with it.

The guard. automation::check_unique_example_names fails on any duplicate, naming the example and every package that declares it:

example target names must be unique across the workspace, but 1 name is used by more than one package:
  - 'basic' is declared by: metabench, observed
All examples share one output directory, so these overwrite each other. Rename one side to a name
that says what distinguishes it, or give it an explicit `[[example]] name = ...` in its Cargo.toml.

scripts/run-examples.rs calls it before running anything. It is checked over every workspace package rather than the selection: a collision is a property of the workspace, and CI narrows that script with --exclude from the delta job, so checking only the selected packages would hide a collision on exactly the pull requests that did not touch either colliding crate.

The logic lives in automation rather than inline in the script because a -Zscript cargo script has no test harness. There it carries five unit tests, including one that runs the check against this workspace's real cargo metadata output — so the invariant is enforced by cargo test, not only by the examples job.

Verification

  • The guard fires on the pre-fix tree. Restoring the old name and running this_workspace_has_no_colliding_example_names fails with the message quoted above. A check that has only ever been observed passing is not a check.
  • cargo build --workspace --examples --all-features --release --locked — completes with no filename-collision warning anywhere.
  • cargo test -p automation --all-features — 10 passed.
  • cargo test -p metabench --test spawned_benchmark --all-features — 4 passed, exercising the renamed example through cargo run --example.
  • just anvil-clippy, just anvil-fmt, just anvil-spellcheck — clean.
  • just anvil-license-headers did not run: the local cargo-heather is v0.2.1 and the recipe requires v0.3.0. No file here gains or loses a header, but I am reporting that check as unrun rather than green.

Note on #750

#750 also touches scripts/run-examples.rs, but only the EXCLUDED_EXAMPLES array — a disjoint region from this change, which adds an import and one call. Whichever lands second should merge cleanly. Its new fake_vtune example does not collide with anything.

…kspace on unique names

`observed` and `metabench` both declared an example target named `basic`.
Cargo writes every example in the workspace into one shared
`target/<profile>/examples/` directory, so both resolved to the same output
file.

Reproduced before changing anything, on `84083b8`:

    cargo build -p observed -p metabench --example basic --release
    warning: output filename collision at target\release\examples\basic.exe
      = note: the example target `basic` in package `observed v0.26.0` has the
              same output filename as the example target `basic` in package
              `metabench v0.1.1`
      = note: this may become a hard error in the future; see
              <rust-lang/cargo#6313>

plus the same warning for `basic.pdb`. Cargo only warns, then builds both
targets concurrently into that one path.

This exact class already cost the repo a CI failure. PR #676 renamed a
duplicate `tower_service` example after the `link.exe` race it caused turned
into `LNK1104: cannot open file ...\examples\tower_service.exe` on an
unrelated pull request. On Linux and macOS the same race silently overwrites
the binary instead, so only the Windows leg surfaces it -- the example that
runs is simply not the one that was selected, and nothing fails.

`metabench`'s target is renamed to `single_benchmark`, which also says what
distinguishes it from the sibling `parameterized` example. `observed`'s
`basic` is the published crate's introductory example and is referenced from
its own module docs, so renaming that side would have been the worse trade.
The two call sites in `metabench/tests/spawned_benchmark.rs` that spawn the
example by name move with it.

A rename alone repairs this pair and prevents nothing, which is why the
previous occurrence recurred. `automation::check_unique_example_names` now
fails on any duplicate, naming the example and every package that declares
it. `scripts/run-examples.rs` calls it before running anything, and it is
checked over *every* workspace package rather than the selection: a collision
is a property of the workspace, and CI narrows that script with `--exclude`
from the `delta` job, so checking only the selected packages would hide a
collision on exactly the pull requests that did not touch either colliding
crate.

The logic lives in `automation` rather than inline in the script because a
`-Zscript` cargo script has no test harness. There it carries five unit
tests, including one that runs the check against this workspace's real
`cargo metadata` output, so the invariant is enforced by `cargo test` and not
only by the examples job.

Verified:
- The guard fires on the pre-fix tree. Restoring the old name and running
  `this_workspace_has_no_colliding_example_names` fails with
  "1 name is used by more than one package: - 'basic' is declared by:
  metabench, observed".
- `cargo build --workspace --examples --all-features --release --locked`
  completes with no filename-collision warning anywhere.
- `cargo test -p automation --all-features`: 10 passed.
- `cargo test -p metabench --test spawned_benchmark --all-features`:
  4 passed, exercising the renamed example through `cargo run --example`.
- `just anvil-clippy`, `just anvil-fmt` and `just anvil-spellcheck` clean.
  `just anvil-license-headers` did NOT run: the local `cargo-heather` is
  v0.2.1 and the recipe requires v0.3.0. No file gains or loses a header
  here, but the check is unrun rather than green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 14, 2026 03:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Case-only collisions remain undetected, and generated Anvil examples validation does not invoke the guard.

Pull request overview

Renames the metabench example and adds workspace-wide validation against duplicate Cargo example names.

Changes:

  • Renames basic to single_benchmark.
  • Updates spawned benchmark references.
  • Adds duplicate-name detection and invokes it from the example runner.
File summaries
File Summary
scripts/run-examples.rs Runs workspace example-name validation.
crates/metabench/tests/spawned_benchmark.rs Uses the renamed example target.
crates/metabench/examples/single_benchmark.rs Defines the renamed benchmark example.
crates/automation/src/lib.rs Re-exports validation functionality.
crates/automation/src/cargo_metadata.rs Implements validation and unit tests.
Review details

Suppressed comments (2)

crates/automation/src/cargo_metadata.rs:78

  • This map compares target names byte-for-byte, but the invariant being guarded is an output-file collision across the workspace. If two examples are named only by case (for example basic and Basic), this check passes while Windows and the usual case-insensitive macOS volumes resolve both to the same target/<profile>/examples path, so the race this guard documents remains possible. Compare normalized output names (or otherwise apply the host filesystem's case rules) and add a case-only regression test.
    let mut owners: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
    for package in packages {
        for target in &package.targets {
            if target.kind.iter().any(|kind| kind == "example") {
                owners.entry(target.name.as_str()).or_default().push(package.name.as_str());

scripts/run-examples.rs:78

  • This call only gates the legacy scripts/run-examples.rs path. The generated Anvil PR test includes anvil-examples, whose recipe builds and runs examples directly (justfiles/anvil/checks/examples.just:11-35, 83-96) and never invokes this script. A future duplicate can therefore still pass the current Anvil gate (Cargo only warns about the collision), so the workspace-wide invariant is not enforced by the repository's generated validation path. Wire the check into a shared/unscoped Anvil validation step or otherwise make the Anvil examples check invoke it.
    automation::check_unique_example_names(&packages)?;
  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (84083b8) to head (050003c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #751   +/-   ##
=======================================
  Coverage   100.0%   100.0%           
=======================================
  Files         634      634           
  Lines       84746    84746           
=======================================
  Hits        84746    84746           
Flag Coverage Δ
linux 100.0% <ø> (ø)
linux-arm 100.0% <ø> (ø)
scheduled ?
windows 100.0% <ø> (ø)

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

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

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


//! One shared workload and input registered with Criterion and Gungraun.
//!
//! Named for the single benchmark it registers, to contrast with the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this comment has 0 value

@Vaiz
Evgenii (Vaiz) marked this pull request as draft September 14, 2026 06:10
…sion

Review finding on this PR: the guard compared target names byte-for-byte while
the invariant it claims to enforce is an output-file collision. Correct, and
the case-only variant turns out to be the worse of the two.

Both halves were measured rather than argued.

The filesystem folds case. `Cargo.toml` and `CARGO.TOML` stat to one file on
this Windows volume, so `examples/basic.exe` and `examples/Basic.exe` are one
path. Default macOS volumes behave the same way.

Cargo does not warn. Renaming metabench's example to `Basic` against
`observed`'s `basic` and building both:

    cargo build -p observed -p metabench --examples --all-features --release
    Finished `release` profile [optimized + debuginfo] target(s) in 16.61s

No output-filename-collision warning anywhere -- compare the exact-duplicate
case, which does warn twice. Cargo's own detection compares `PathBuf`s, which
are case-sensitive in Rust, so it is blind to precisely the collision the
filesystem still performs. The exact duplicate at least announces itself; the
case-only one is silent on both sides.

With the fix in place the same tree is rejected:

    - 'basic' is declared by: metabench ('Basic'), observed ('basic')

Names are now folded with `to_ascii_lowercase` before comparison. ASCII folding
is exact rather than an approximation here, because Cargo target names are
ASCII -- no Unicode case-folding subtleties apply.

Each package's own spelling is printed only when the spellings actually differ.
Printing it unconditionally would make the common exact-duplicate message
noisier for no gain, and omitting it in the case-only report would leave the
author unable to tell which of the two files to rename.

Three tests added: a case-only collision across two packages, a case-only
collision inside one package (Cargo permits `basic.rs` and `Basic.rs` as
distinct targets, the output paths are not distinct), and one pinning that the
exact-duplicate message keeps its terse `alpha, beta` form.

`cargo test -p automation --all-features` 13 passed; `just anvil-clippy`,
`just anvil-fmt` and `just anvil-spellcheck` clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Vaiz

Copy link
Copy Markdown
Contributor Author

Both review findings addressed in 108a0e0d. One was correct and is fixed; the other I am pushing back on, with the measurement.

1. Case-only collisions — correct, and worse than the exact duplicate

Fixed. I measured both halves rather than reasoning about them.

The filesystem folds case. Cargo.toml and CARGO.TOML stat to the same file on this Windows volume, so examples/basic.exe and examples/Basic.exe are one path.

Cargo does not warn about it. I renamed metabench's example to Basic against observed's basic and built both:

cargo build -p observed -p metabench --examples --all-features --release
    Finished `release` profile [optimized + debuginfo] target(s) in 16.61s

No collision warning anywhere — compare the exact-duplicate case, which warns twice (.exe and .pdb). Cargo's detection compares PathBufs, which are case-sensitive in Rust, so it is blind to precisely the collision the filesystem still performs. The case-only variant is the more dangerous of the two: the exact duplicate at least announces itself, this one is silent on both sides. That makes this the more valuable half of the guard, not an edge case.

With the fix, that same tree is rejected:

  - 'basic' is declared by: metabench ('Basic'), observed ('basic')

Names are folded with to_ascii_lowercase. ASCII folding is exact here rather than an approximation, because Cargo target names are ASCII — no Unicode folding subtleties apply. Each package's own spelling prints only when the spellings differ, so the common message keeps its terse alpha, beta form while a case-only report still tells you which file to rename. Three tests added, including the case-only regression you asked for and one inside a single package (Cargo permits basic.rs and Basic.rs as distinct targets; the output paths are not distinct).

2. anvil-examples does not invoke the guard — observation right, conclusion wrong

Your observation is accurate: justfiles/anvil/checks/examples.just builds and runs examples directly and never calls scripts/run-examples.rs. But the conclusion — that "the workspace-wide invariant is not enforced by the repository's generated validation path" — does not hold, for a reason the diff makes easy to miss.

The check is not only wired into that script. It is also a unit test in automation, this_workspace_has_no_colliding_example_names, which runs the check against the real cargo metadata output. That executes under cargo test, which anvil-pr-test reaches via anvil-llvm-cov. So the invariant is enforced by the generated path — through the test group rather than the examples group. That was deliberate: a -Zscript cargo script has no test harness, so logic living only there cannot be tested or shown to fire.

I am also not taking the suggested remedy. examples.just carries GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY — editing it would be overwritten on the next regeneration and would stop automatic updates for this repo. The durable version of your point belongs in cargo-anvil itself, and is already tracked as AB#7871260, which proposes a workspace-wide duplicate-target check covering examples, binaries and benchmarks. This PR delivers the example half inside oxidizer; the generated-recipe half is that work item's.

One caveat worth stating, since it is the real residue of your point: anvil-llvm-cov is impact-scoped, so a PR that adds a colliding example without touching automation could have the test skipped by delta selection. That is a narrower gap than "not enforced", but it is not nothing, and it is the same scoping argument the check itself already makes internally (it reads every package, never the selection). I have left it rather than papering over it, because the fix is the cargo-anvil one above.

CI

050003cd finished 56 success / 2 failing. Both failures were Runtime Analysis (windows-arm) and the Required Anvil checks roll-up that reports it — the job ran 68 minutes and its anvil-run-group step has no conclusion, which is a cancellation rather than an assertion failure, and I could not retrieve the log to confirm (gh returns HTTP 404 for this repo from both configured accounts). It is not reproduced locally and PR #750 passed the same job. I am flagging it as unexplained rather than claiming it is flaky.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants