tests/build_variants.rs
//! **The guard over `.nornir/build-variants.toml`** — nornir's declared SUITE ×
//! ATOM build space.
//!
//! Until 2026-08-01 nornir had no such test. The only thing validating the file at
//! all was `tests/inotify_watch_wiring.rs::a_build_variant_names_the_feature`,
//! which asks one question about one feature. Everything else about the
//! declaration — that its atoms name real bins, that no two of them write the same
//! file, that exactly one is the normal case per role, that `--suite` selects —
//! was unchecked, and the file was wrong in two independent ways for months:
//!
//! ```text
//! check_bins() → 3 variants emit the SAME artifact — bin 'nornir' (package
//! nornir, profile release, target <host>) — so the last build silently
//! OVERWRITES the others and nothing on disk says which one you have:
//! nornir, nornir-cli, nornir-watch.
//! ```
//!
//! …and all three of those declared `role = "server"` while `nornir` is the CLI,
//! with the real daemon (`nornir-server`) and the real GUI (`nornir-ui`) built by
//! no variant at all.
//!
//! # What each test here would catch (read before adding one)
//!
//! * [`the_declaration_is_valid`] — RED on any shape error `validate_all` knows:
//! a `thin` atom that embeds, a fat atom embedding a non-downstream sibling, a
//! `kind` missing its delegate table.
//! * [`exactly_one_default_per_role`] — RED when a `(suite, role)` group has zero
//! or two `default = true`, i.e. when a caller has to guess the normal case.
//! * [`no_two_atoms_write_the_same_file`] — RED on the collision above.
//! * [`every_declared_bin_is_a_real_cargo_target`] — RED on a PHANTOM atom: a
//! declared `bin` with no `[[bin]]` behind it. The oracle is
//! `BuildVariantSet::cargo_bin_targets`, which walks the workspace's manifests
//! (explicit `[[bin]] name` AND the implicit bin cargo synthesises for a
//! `[package]` with `src/main.rs`) — never a hardcoded list, which would be the
//! third declaration of the same thing.
//! * [`the_suite_axis_is_declared_and_selects`] — RED when the `ORIGINAL` tag is
//! dropped from any atom (so `cargo xtask build --suite ORIGINAL` stops being
//! the same command it is in korp/dwarves/modgunn), or when an unknown suite
//! stops being an error.
//! * [`the_four_target_atoms_are_all_present`] — RED when the server / CLI / UI /
//! solo quartet loses a member or one of them stops pointing at its own bin.
//! * [`the_atom_marker_reaches_the_binary`] — RED when `nornir` and `nornir-cli`
//! stop printing different `--version` text, i.e. when the atom selector goes
//! back to producing indistinguishable builds. This one asserts APPLIED OUTPUT:
//! it runs the two BUILT binaries.
//!
//! Run: `cargo test --test build_variants` (the last one needs
//! `--features atom-cli`, and says so rather than skipping silently).
use std::path::{Path, PathBuf};
#[path = "support/ci_commands.rs"]
mod ci_commands;
use ci_commands::{ci_commands_listing, ci_runs};
use nornir::build::variant::BuildVariantSet;
use nornir_build_thing::plugin::Role;
use nornir_build_thing::suite_cli;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
/// The declared set, loaded through the PRODUCTION loader (`discover`), so a file
/// this test can read but the product cannot is a failure here too.
fn declared() -> BuildVariantSet {
BuildVariantSet::discover(&repo_root())
.expect("read .nornir/build-variants.toml")
.expect("nornir declares a build-variant set")
}
/// nornir's delivery name. One delivery today; the constant exists so the tests
/// below read as "the declared delivery" and not as a magic string sprinkled
/// around, and so a second delivery is a one-line change here.
const SUITE: &str = "ORIGINAL";
#[test]
fn the_declaration_is_valid() {
declared()
.validate_all()
.expect("every declared atom must be valid for the set");
}
#[test]
fn exactly_one_default_per_role() {
let problems = declared().check_defaults();
assert!(
problems.is_empty(),
"check_defaults() is not empty:\n{}",
problems.join("\n")
);
}
/// ★ **THE COLLISION GUARD.** Measured RED against the declaration as it stood on
/// 2026-08-01 (the text is in the module header); green only because `nornir`,
/// `nornir-cli` and `nornir-watch` now emit three distinct `[[bin]]` names off one
/// source, holger-style.
#[test]
fn no_two_atoms_write_the_same_file() {
let problems = declared().check_bins();
assert!(
problems.is_empty(),
"two or more atoms of one delivery emit the same artifact — the last build \
silently overwrites the others:\n{}",
problems.join("\n")
);
}
/// ★ **THE PHANTOM GUARD.** Every declared `bin` must be something cargo can
/// actually build.
#[test]
fn every_declared_bin_is_a_real_cargo_target() {
let set = declared();
let real = BuildVariantSet::cargo_bin_targets(&repo_root())
.expect("walk the workspace manifests for [[bin]] targets");
// NON-VACUOUS ANCHOR (a): the oracle must have found the targets we know exist.
// `check_bins_declared` on an EMPTY list reports rather than passing, but a list
// that is merely INCOMPLETE would red the guard on bins that are real — so pin
// the ones this repo definitely ships.
// ⚠ These are BIN names, not atom names, and the two lists are no longer the
// same — which is the point of pinning them separately. (`nornir-watch`, a
// real `[[bin]]` no atom declared, was removed with its `inotify` feature in
// Mímir v2 — the syn delta re-scan it drove is gone.)
for known in [
"nornir",
"nornir-cli",
"nornir-server",
"nornir-ui",
"nornir-solo",
] {
assert!(
real.iter().any(|b| b == known),
"cargo_bin_targets() did not find the `{known}` [[bin]] — the oracle is \
not reading this repo's manifests, so its verdict is vacuous. Found: {real:?}"
);
}
// NON-VACUOUS ANCHOR (b): the check itself can still say no. Feed it an oracle
// that omits one real target and require it to name that atom.
let short: Vec<&str> = real
.iter()
.map(String::as_str)
.filter(|b| *b != "nornir-ui")
.collect();
let seeded = set.check_bins_declared(&short);
assert!(
seeded.iter().any(|p| p.contains("nornir-ui")),
"check_bins_declared did not flag `nornir-ui` when it was removed from the \
oracle — it cannot report red, so its green means nothing. Got: {seeded:?}"
);
let problems = set.check_bins_declared(&real);
assert!(
problems.is_empty(),
"an atom declares a bin no [[bin]] target exists for:\n{}",
problems.join("\n")
);
}
/// ★ **`--suite` MUST SELECT.** `--suite ORIGINAL` resolves to exactly the declared
/// atoms in declaration order; `--suite NOPE` FAILS and names the real delivery.
///
/// `check_suites()` is VACUOUS below two deliveries — with one delivery there is
/// nothing to be a subset of, so an empty result is correct and must NOT be read as
/// "checked and clean". `declares_suites()` is the thing that tells the two apart,
/// so both are asserted.
#[test]
fn the_suite_axis_is_declared_and_selects() {
let set = declared();
let problems = set.check_suites();
assert!(
problems.is_empty(),
"check_suites() is not empty:\n{}",
problems.join("\n")
);
assert!(
set.declares_suites(),
"no atom carries `suites = [\"{SUITE}\"]`, so the empty check_suites() above is \
NOT APPLICABLE rather than clean — and `cargo xtask build --suite {SUITE}` (the \
command shape korp/dwarves/modgunn/holger all answer to) errors out in nornir"
);
assert_eq!(
set.suites(),
vec![SUITE],
"nornir ships exactly one delivery, named {SUITE}"
);
let declared_names: Vec<&str> = set.variants.iter().map(|v| v.name.as_str()).collect();
let args = suite_cli::parse(&["--suite", SUITE]).expect("parse --suite");
let (suite, chosen) = suite_cli::resolve(&set, &args).expect("resolve the declared delivery");
assert_eq!(suite, SUITE);
assert_eq!(
chosen.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(),
declared_names,
"--suite {SUITE} did not resolve to the declared atoms"
);
// …and an unknown one is LOUD. A selector that shrugs is the silent-green shape.
let bad = suite_cli::parse(&["--suite", "NOPE"]).expect("parse");
let err = suite_cli::resolve(&set, &bad).expect_err("--suite NOPE must fail");
let msg = format!("{err:#}");
assert!(msg.contains("no such delivery"), "{msg}");
assert!(
msg.contains(SUITE),
"the error must name the real delivery: {msg}"
);
}
/// The four-atom target as landed in nornir: a server, a CLI client, a UI client
/// and a solo appliance, each pointing at its OWN bin. RED if any of them is
/// dropped, re-roled, or re-aimed at another atom's binary — which is exactly the
/// state this file found the declaration in (three CLI copies calling themselves
/// servers, no daemon atom, no GUI atom).
#[test]
fn the_four_target_atoms_are_all_present() {
let set = declared();
let find = |name: &str| {
set.get(name).unwrap_or_else(|| {
panic!(
"atom '{name}' is not declared. Declared: {:?}",
set.variants.iter().map(|v| &v.name).collect::<Vec<_>>()
)
})
};
for (atom, role, bin) in [
("nornir-server", Role::Server, "nornir-server"),
("nornir", Role::Client, "nornir"),
("nornir-ui", Role::Client, "nornir-ui"),
("nornir-solo", Role::Solo, "nornir-solo"),
] {
let v = find(atom);
assert_eq!(v.role, Some(role), "atom '{atom}' has the wrong role");
assert_eq!(
v.bin.as_deref(),
Some(bin),
"atom '{atom}' points at the wrong bin"
);
}
// `solo` is FAT by definition — a solo build that reaches a sibling over the
// wire is not solo, it is a mixed client.
let solo = find("nornir-solo");
assert!(
solo.features.iter().any(|f| f == "solo"),
"the solo atom must name the `solo` feature or the [[bin]] cannot build"
);
assert!(
solo.features.iter().any(|f| f == "server"),
"the solo atom must compile the server IN — that is the whole role"
);
// The two-front-doors invariant: the CLI's own `nornir build --variant <x>`
// must resolve every declared atom through the SAME suite_cli resolver the
// xtask verb uses. Assert it can find each one by name.
for v in &set.variants {
let args = suite_cli::parse(&["--suite", SUITE, "--atom", &v.name]).expect("parse");
let (_, one) = suite_cli::resolve(&set, &args)
.unwrap_or_else(|e| panic!("resolve --atom {}: {e:#}", v.name));
assert_eq!(one.len(), 1);
assert_eq!(one[0].name, v.name);
}
}
/// The feature every atom is DISCRIMINATED by must actually differ. Two atoms whose
/// materialized cargo lines are byte-identical are one atom with two names — the
/// "fat nornir and thin nornir are the same bytes" defect this file's header
/// records, one level up.
#[test]
fn no_two_atoms_materialize_the_same_cargo_line() {
let set = declared();
let mut seen: std::collections::BTreeMap<String, &str> = std::collections::BTreeMap::new();
for v in &set.variants {
let line = v.command_line();
if let Some(other) = seen.insert(line.clone(), v.name.as_str()) {
panic!(
"atoms '{other}' and '{}' render the IDENTICAL cargo line: {line}",
v.name
);
}
}
assert_eq!(seen.len(), set.variants.len());
}
// ─────────────────────────────────────────────────────────────────────────────
// APPLIED OUTPUT — the atom marker has to reach the artifact
// ─────────────────────────────────────────────────────────────────────────────
/// Run a built binary's `--version` and return stdout.
fn version_of(exe: &Path) -> String {
let out = std::process::Command::new(exe)
.arg("--version")
.output()
.unwrap_or_else(|e| panic!("run {} --version: {e}", exe.display()));
assert!(
out.status.success(),
"{} --version exited {}:\n{}",
exe.display(),
out.status,
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).to_string()
}
/// ★ **THE NON-VACUOUS PROOF THAT THE ATOM SELECTOR SELECTS.**
///
/// `nornir` and `nornir-cli` are the same source file. Distinct `[[bin]]` names
/// stop them overwriting each other on disk, but that alone does not prove the
/// FEATURE difference reached the compiler — two identically-compiled files under
/// different names would pass `check_bins` perfectly. So run BOTH built binaries
/// and require their `--version` text to differ, in the specific way the
/// declaration says it should: `nornir` is fat with holger embedded, `nornir-cli`
/// is mixed with holger remote.
///
/// This test compares APPLIED OUTPUT of two real processes. It is not a
/// round-trip: nothing here reads the declaration back to itself.
#[cfg(feature = "atom-cli")]
#[test]
fn the_atom_marker_reaches_the_binary() {
let fat = version_of(Path::new(env!("CARGO_BIN_EXE_nornir")));
let thin = version_of(Path::new(env!("CARGO_BIN_EXE_nornir-cli")));
assert_ne!(
fat.trim(),
thin.trim(),
"`nornir --version` and `nornir-cli --version` print the SAME text — the two \
atoms are indistinguishable at runtime, which is the defect distinct bin names \
only half-fixed"
);
assert!(
fat.contains("atom nornir ·"),
"the fat CLI must name its atom: {fat}"
);
assert!(
thin.contains("atom nornir-cli ·"),
"the thin CLI must name its atom: {thin}"
);
// ⚠ AND THIS RUN IS THE HOSTILE CASE, ON PURPOSE — which is the whole reason the
// two lines above are a real assertion and not a formality. Cargo unifies
// features across every bin of one package in one invocation, so
// `--features atom-cli` compiles `atom-cli` into BOTH of these binaries, `nornir`
// included, which does not require it. Until 2026-08-01 `atom_line` derived the
// atom name from `cfg!(feature = "atom-cli")` (then spelled `thin`), so under this
// printed `atom nornir-cli` and the marker marked nothing:
//
// left: "nornir v0.5.4 (339d6b2) — atom nornir-cli · linkage fat · holger=embedded …"
// right: "nornir v0.5.4 (339d6b2) — atom nornir-cli · linkage fat · holger=embedded …"
//
// `atom_line` now takes `env!("CARGO_BIN_NAME")`, which is per-BIN and cannot be
// unified. If anyone puts the name back behind a `cfg!`, this goes red here.
//
// The `nornir-ui` leg is the same defect caught from the other side: its
// DECLARED feature set names none of the discriminators, so the feature-derived
// version fell through to the `else` arm and the GUI atom shipped calling itself
// `atom nornir`. That was measured on the real artifact, not in a test build.
let ui = version_of(Path::new(env!("CARGO_BIN_EXE_nornir-ui")));
assert!(
ui.contains("atom nornir-ui ·"),
"the GUI must name its own atom: {ui}"
);
// The REST of the line describes THIS compilation, and all three binaries ARE
// this compilation, so they must agree. Demanding `holger=remote` from
// `nornir-cli` here would demand that a process report a feature state its own
// build did not have — the per-atom holger split is a property of the DECLARED
// feature sets and is proven by building the atoms (see
// `.nornir/suite-atoms-design.md`), not by this unified run.
let want = if cfg!(feature = "embed-holger") {
"holger=embedded"
} else {
"holger=remote"
};
for (name, text) in [("nornir", &fat), ("nornir-cli", &thin), ("nornir-ui", &ui)] {
assert!(
text.contains(want),
"{name} must report the holger linkage THIS build actually has ({want}): {text}"
);
}
}
/// **The anti-vacuum companion.** [`the_atom_marker_reaches_the_binary`] is
/// `#[cfg(feature = "atom-cli")]`, so a plain `cargo test` compiles it out entirely and
/// the file would look green while its only applied assertion never ran. This one
/// is always compiled and requires CI to build the feature — the same shape
/// `tests/inotify_watch_wiring.rs::something_in_ci_compiles_the_feature` uses, and
/// for the same reason.
#[test]
fn ci_compiles_the_atom_marker_feature() {
assert!(
ci_runs("--features atom-cli"),
"no CI COMMAND builds `--features atom-cli`, so `the_atom_marker_reaches_the_binary` \
is compiled out of every automatic run and this file's only APPLIED assertion \
never executes. Restore the line in .woodpecker.yml's build-test step — and note \
that a `#` in front of it counts as gone, which is why this reads the command \
list and not the file text (measured blind 2026-08-23). What CI does run:\n{}",
ci_commands_listing()
);
}
// ─────────────────────────────────────────────────────────────────────────────
// ★ THE SET IS THE MATRIX — added 2026-08-03
// ─────────────────────────────────────────────────────────────────────────────
/// The five rows of `workspace_nordisk/.nornir/build-matrix-2026-08-02.md` for
/// nornir, as `(atom, role, bin)`. The owner, 2026-08-03: *"ta bort allt inte
/// stämmer med matrisen"*.
///
/// This IS a written-down list, and every other oracle in this file is
/// deliberately walked from the manifests instead. The difference is what it is
/// for: the manifest walk answers "does the declaration describe something that
/// exists", which must never be pinned; this answers "is the declaration the set
/// The owner settled", which is exactly a pinned list or it is nothing. A guard
/// that derived the expected set FROM the declaration it is checking would be the
/// round-trip this file's header warns about.
const MATRIX: [(&str, Role, &str); 5] = [
("nornir-server", Role::Server, "nornir-server"),
("nornir", Role::Client, "nornir"),
("nornir-cli", Role::Client, "nornir-cli"),
("nornir-ui", Role::Client, "nornir-ui"),
("nornir-solo", Role::Solo, "nornir-solo"),
];
/// ★ **EXACTLY THE MATRIX — no more, no less.**
///
/// RED in both directions, which is the point: a MISSING row fails, and so does
/// an EXTRA one. The extra-row half is what `nornir-watch` and `nornir-mcp`
/// tripped on 2026-08-03; without it a sixth atom could be added back tomorrow
/// and every other test in this file would still pass, because all of them ask
/// "is each declared atom well-formed" and none asked "is this atom supposed to
/// exist at all".
#[test]
fn the_declared_set_is_exactly_the_matrix() {
let set = declared();
let got: Vec<&str> = set.variants.iter().map(|v| v.name.as_str()).collect();
let want: Vec<&str> = MATRIX.iter().map(|(a, _, _)| *a).collect();
assert_eq!(
got, want,
"the declared atom set is not the matrix (order included)"
);
for (atom, role, bin) in MATRIX {
let v = set
.get(atom)
.unwrap_or_else(|| panic!("atom '{atom}' is not declared"));
assert_eq!(v.role, Some(role), "atom '{atom}' has the wrong role");
assert_eq!(
v.bin.as_deref(),
Some(bin),
"atom '{atom}' points at the wrong bin"
);
}
// The two that left, named so a re-add is a deliberate act with a test to
// change and not an accident nothing notices.
for gone in ["nornir-watch", "nornir-mcp", "nornir-thin"] {
assert!(
set.get(gone).is_none(),
"atom '{gone}' is declared again. It is not in the build matrix: \
`nornir-watch` is one feature flag on the CLI, `nornir-mcp` is a \
capability of the fat server (`nornir-server mcp`), and `nornir-thin` \
was renamed `nornir-cli`. Re-adding one means changing MATRIX above, \
which means asking the owner."
);
}
}
/// ★ **ONLY `ORIGINAL` EXISTS.** The owner: *"det skall bara finnas suite=ORIGINAL"*.
///
/// holger is the ONE member allowed two deliveries (`MVP` + `ORIGINAL`); nornir,
/// korp, dwarves and modgunn ship one. Carrying a suite tag is what makes an atom
/// a Build Thing, so "every atom carries exactly `["ORIGINAL"]`" and "no other
/// suite name appears" are the same assertion from both ends, and both are made.
///
/// **Seen RED before being trusted** (LAW 2): the seeded case below hands
/// `check_suites` a set with a second delivery and requires it to say so.
#[test]
fn only_the_original_suite_exists() {
let set = declared();
assert_eq!(
set.suites(),
vec![SUITE],
"nornir declares a suite other than {SUITE}"
);
for v in &set.variants {
assert_eq!(
v.suites,
vec![SUITE.to_string()],
"atom '{}' does not carry exactly `suites = [\"{SUITE}\"]` — carrying the \
tag is what makes an atom a Build Thing, so an atom tagged differently \
(or not at all) is not one",
v.name
);
}
assert_eq!(
set.base_suite.as_deref(),
Some(SUITE),
"base_suite must be {SUITE}"
);
// NON-VACUOUS ANCHOR: the check can still say no. Seed a second delivery and
// require `suites()` to report both — if it cannot see a second suite, the
// equality above is proving nothing.
let mut seeded = declared();
seeded.variants[0].suites = vec![SUITE.to_string(), "SMUGGLED".to_string()];
let seen = seeded.suites();
assert!(
seen.contains(&"SMUGGLED"),
"BuildVariantSet::suites() did not report a second delivery that was planted \
in it, so its agreement with [{SUITE}] above is vacuous. Got: {seen:?}"
);
}
/// ★ **EVERY ATOM IS REACHABLE THROUGH THE DECLARED DOOR.**
///
/// A `[[bin]]` with `required-features` is SILENTLY SKIPPED by
/// `cargo build --all-targets` — it is not an error, the bin simply is not built
/// — so an atom whose declared feature set does not satisfy its bin's
/// `required-features` produces NOTHING and cargo exits 0. That is the exact
/// silent-green shape: `xtask build --suite ORIGINAL` would report success having
/// emitted no file.
///
/// The oracle is the MANIFEST (`[[bin]] required-features`), expanded through the
/// package's real `[features]` table via `BuildVariantSet::cargo_feature_tables`
/// + `FeatureTable::closure`, so a composite feature that pulls the marker in
/// transitively counts, as it does for cargo.
///
/// ⚠ **`closure`, NOT `leaf_closure` — and this guard was RED on its first run
/// for exactly that reason.** `leaf_closure` is `closure` with every composite
/// ALIAS dropped, which is right for the suite-subset rule it was written for (a
/// tier label compiles nothing by itself) and WRONG here. Cargo matches
/// `required-features` against the enabled feature NAMES, composites included: a
/// bin requiring `viz` is built when `viz` is enabled, whether or not `viz` is
/// itself a composite. Measured RED 2026-08-03 — the first version reported four
/// perfectly reachable atoms as unreachable:
///
/// ```text
/// atom 'nornir-server' builds bin 'nornir-server', whose `required-features`
/// include 'server' — and 'server' is NOT in the atom's declared feature closure.
/// atom 'nornir-ui' … include 'viz' — and 'viz' is NOT in the closure.
/// atom 'nornir-solo' … include 'solo' — and 'solo' is NOT in the closure.
/// ```
///
/// All four declarations were correct; the guard was reading the wrong closure.
/// Recorded rather than quietly fixed, because "the new guard's own first red was
/// its own bug" is the most useful thing this test has to say about itself.
///
/// ⚠ FOLLOW-UP, recorded rather than silently absorbed: the `required-features`
/// READER below is nornir's own. It belongs beside
/// `BuildVariantSet::cargo_bin_targets` in `nornir-build-thing` — the one writer
/// of "what counts as this repo's manifests" — so korp, dwarves and modgunn get
/// the same guard. It is here and not there only because edda is a shared crate
/// this pass did not own.
#[test]
fn every_atom_satisfies_its_bins_required_features() {
let set = declared();
let tables = BuildVariantSet::cargo_feature_tables(&repo_root())
.expect("read every package's [features] table");
let required = bin_required_features(&repo_root());
// NON-VACUOUS ANCHOR (a): the reader must have found the gates we know exist.
// An empty/blind reader would make every check below trivially pass.
for (bin, feat) in [
("nornir-cli", "atom-cli"),
("nornir-ui", "viz"),
("nornir-solo", "solo"),
] {
let got = required.get(bin).unwrap_or_else(|| {
panic!("no `required-features` read for [[bin]] {bin}: {required:?}")
});
assert!(
got.iter().any(|f| f == feat),
"the manifest reader did not see `{feat}` on [[bin]] {bin} — it is not \
reading Cargo.toml, so its verdict is vacuous. Got: {got:?}"
);
}
let mut problems: Vec<String> = Vec::new();
for v in &set.variants {
let Some(bin) = v.bin.as_deref() else {
continue;
};
let Some(need) = required.get(bin) else {
continue;
};
let pkg = v
.package
.as_deref()
.expect("every nornir atom names its package");
let table = tables
.get(pkg)
.unwrap_or_else(|| panic!("no [features] table for package {pkg}"));
let have = table.closure(&v.features, !v.no_default_features);
for f in need {
if !have.contains(f) {
problems.push(format!(
"atom '{}' builds bin '{bin}', whose `required-features` include \
'{f}' — and '{f}' is NOT in the atom's declared feature closure. \
cargo SKIPS such a bin silently, so this atom builds nothing and \
still exits 0.",
v.name
));
}
}
}
// NON-VACUOUS ANCHOR (b): the comparison itself can say no. Take a real atom,
// strip the feature its bin requires, and require the same logic to catch it.
{
let v = set.get("nornir-ui").expect("nornir-ui is declared");
let table = tables.get("nornir").expect("nornir [features]");
let stripped: Vec<String> = v.features.iter().filter(|f| *f != "viz").cloned().collect();
let have = table.closure(&stripped, !v.no_default_features);
assert!(
!have.contains("viz"),
"removing `viz` from nornir-ui's declared features still left it in the \
closure, so the reachability comparison above cannot fail and its green \
means nothing"
);
}
assert!(
problems.is_empty(),
"unreachable atom(s):\n{}",
problems.join("\n")
);
}
/// Every `[[bin]]`'s `required-features`, keyed by bin name, walked from the
/// workspace manifests. Mirrors the walk in
/// `BuildVariantSet::cargo_bin_targets` (see the follow-up note above).
fn bin_required_features(repo_root: &Path) -> std::collections::BTreeMap<String, Vec<String>> {
fn walk(dir: &Path, out: &mut std::collections::BTreeMap<String, Vec<String>>, depth: usize) {
if depth > 8 {
return;
}
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.flatten() {
let p = e.path();
let name = e.file_name();
let name = name.to_string_lossy();
if p.is_dir() {
if matches!(name.as_ref(), "target" | ".git" | "vendor" | "node_modules")
|| name.starts_with('.')
{
continue;
}
walk(&p, out, depth + 1);
} else if name == "Cargo.toml" {
let Ok(text) = std::fs::read_to_string(&p) else {
continue;
};
let Ok(doc) = toml::from_str::<toml::Value>(&text) else {
continue;
};
let Some(bins) = doc.get("bin").and_then(|b| b.as_array()) else {
continue;
};
for b in bins {
let Some(n) = b.get("name").and_then(|n| n.as_str()) else {
continue;
};
let rf: Vec<String> = b
.get("required-features")
.and_then(|r| r.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
out.insert(n.to_string(), rf);
}
}
}
}
let mut out = std::collections::BTreeMap::new();
walk(repo_root, &mut out, 0);
out
}