Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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);
    }
}