crates/nornir-testmatrix/examples/target_compile.rs
//! **The thin caller** — `cargo run --example target_compile -- <repo> [<repo> …]`.
//!
//! Answers, for each repo named on the command line: *does every declared test
//! target COMPILE, in every feature configuration this repo itself declares?*
//! See [`nornir_testmatrix::targetcheck`] for why that question is not answered
//! by `cargo check` and not answered by `cargo test` either.
//!
//! It lives here, as one example, rather than as a copy of the same forty lines
//! in eleven repos' xtasks (LAW 5). The only per-repo input is the path.
//!
//! # The `build-variants.toml` bridge
//!
//! `nornir-testmatrix` cannot read that file: its dependency budget is
//! `serde` + `anyhow` with no TOML parser, and the edge to
//! `nornir-build-thing` — which is the one writer of that format — closes a
//! `cargo metadata` cycle through `nornir-git` → `nornir-hash`, so it can only
//! ever be a **dev**-dependency. An example links dev-dependencies, so the
//! mapping `BuildVariant` → [`VariantSpec`] lives here, once, in six lines.
//!
//! That bridge is the half that matters: korp-ui's failing feature combination
//! is declared in `build-variants.toml` and in no test, no suite and no CI job,
//! so nothing but this enumeration would ever have compiled it.
//!
//! # Flags
//!
//! * `--variant <name>` — check ONLY this declared build variant (repeatable).
//! The cheap way to ask about one shipped artifact: a full variant sweep is
//! one whole workspace build per variant.
//! * `--no-variants` — skip `.nornir/build-variants.toml`.
//! * `--no-arms` — skip `.nornir/testmatrix-arms.json`.
//! * `--no-all-features` — drop the `--all-features` corner (a repo with
//! mutually-exclusive features cannot build it, and says so noisily).
//! * `--only-default` — the cheap run: one workspace-wide pass, nothing else.
//! * `--build` — compile all the way through codegen and the linker instead of
//! only type-checking. Measured on nornir (oden, cold): same 26 broken
//! targets, 9 min 51 s / ~170 GB instead of 2 min 20 s / 2.8 GB. Ask for it
//! when the artifact matters (monomorphisation, const-eval at codegen, a
//! missing native library); the default answers everything else.
//! * `--json <path>` — also write the rows a [`TestSink`] would take.
//!
//! Exit status is 0 only when every repo is green. **A repo that examined zero
//! test targets exits non-zero**, because a run that looked at nothing is shaped
//! exactly like a clean one.
use std::path::{Path, PathBuf};
use nornir_build_thing::variant::BuildVariantSet;
use nornir_testmatrix::{
check_targets, configs_from_arms, configs_from_metadata, configs_from_variants,
dedup_configs, rows_to_json, workspace_crates, CompileConfig, ConfigSource, Depth,
MatrixArms, VariantSpec,
};
/// Map the variants a repo SHIPS onto compile configurations. The whole bridge.
fn variant_specs(repo_root: &Path, only: &[String]) -> Vec<VariantSpec> {
match BuildVariantSet::discover(repo_root) {
Ok(Some(set)) => set
.variants
.iter()
.filter(|v| only.is_empty() || only.contains(&v.name))
.map(|v| VariantSpec {
name: v.name.clone(),
package: v.package.clone(),
features: v.features.clone(),
no_default_features: v.no_default_features,
// These two used to be DROPPED right here, which is why every
// detached-workspace and cross-compile-only variant in the suite came
// back as a "dead configuration" — a detector gap reported as a repo
// defect. `member` is deliberately NOT read: it is the suite member
// name, not a path (see `VariantSpec::manifest_dir`).
target: v.target.clone(),
manifest_dir: v.manifest_dir.clone(),
})
.collect(),
Ok(None) => Vec::new(),
Err(e) => {
eprintln!(" note: could not read build-variants.toml: {e}");
Vec::new()
}
}
}
fn main() {
let mut roots: Vec<PathBuf> = Vec::new();
let mut want_variants = true;
let mut want_arms = true;
let mut want_all_features = true;
let mut only_default = false;
let mut json_out: Option<PathBuf> = None;
let mut only_variants: Vec<String> = Vec::new();
let mut depth = Depth::Check;
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--no-variants" => want_variants = false,
"--no-arms" => want_arms = false,
"--no-all-features" => want_all_features = false,
"--only-default" => only_default = true,
"--build" => depth = Depth::Build,
"--json" => json_out = args.next().map(PathBuf::from),
"--variant" => {
if let Some(v) = args.next() {
only_variants.push(v);
}
}
other => roots.push(PathBuf::from(other)),
}
}
if roots.is_empty() {
eprintln!(
"usage: target_compile <repo-root> [<repo-root> …] \
[--only-default] [--build] [--variant <name>] [--no-variants] \
[--no-arms] [--no-all-features] [--json <path>]"
);
std::process::exit(2);
}
let mut all_rows = Vec::new();
let mut failed = 0usize;
for root in &roots {
println!("\n═══ {} ═══", root.display());
let crates = match workspace_crates(root) {
Ok(c) => c,
Err(e) => {
println!(" cargo metadata FAILED: {e}");
failed += 1;
continue;
}
};
let mut configs = configs_from_metadata(&crates);
if only_default {
configs.retain(|c| c.source == ConfigSource::Default);
} else {
if !want_all_features {
configs.retain(|c| c.source != ConfigSource::AllFeatures);
}
if want_arms {
if let Ok(Some(arms)) = MatrixArms::read(root) {
configs.extend(configs_from_arms(&arms));
}
}
if want_variants {
let specs = variant_specs(root, &only_variants);
if !only_variants.is_empty() && specs.is_empty() {
// Naming a variant that does not exist must not quietly
// degrade into "checked the default and called it fine".
println!(" no declared variant matched {only_variants:?} — nothing to check");
failed += 1;
continue;
}
configs.extend(configs_from_variants(&specs));
}
}
let configs: Vec<CompileConfig> = dedup_configs(
configs
.into_iter()
.map(|mut c| {
c.depth = depth;
c
})
.collect(),
);
println!(
" {} declared test target(s) in {} package(s); {} configuration(s):",
crates.iter().map(|k| k.tests.len()).sum::<usize>(),
crates.len(),
configs.len()
);
for c in &configs {
println!(" [{}] {}", c.source.tag(), c.label);
}
let t0 = std::time::Instant::now();
let report = check_targets(root, &crates, &configs);
let secs = t0.elapsed().as_secs_f64();
print!("{}", report.summary());
println!(
" → {} of {} declared test target(s) compile somewhere; {} broken, \
{} unaccounted for, {} dead configuration(s); {:.1}s wall-clock",
report.compiled_targets(),
report.declared_test_targets,
report.broken().len(),
report.unaccounted().len(),
report.dead_configs().len(),
secs
);
for c in &report.configs {
println!(" {:>8.1}s {}", c.duration_ms / 1000.0, c.label);
}
all_rows.extend(report.rows());
if !report.is_green() {
failed += 1;
}
}
if let Some(p) = json_out {
if let Err(e) = std::fs::write(&p, rows_to_json(&all_rows)) {
eprintln!("could not write {}: {e}", p.display());
} else {
println!("\nwrote {} row(s) to {}", all_rows.len(), p.display());
}
}
println!("\n{} of {} repo(s) green", roots.len() - failed, roots.len());
if failed > 0 {
std::process::exit(1);
}
}