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

nordisk/gunnar

xtask/tests/ci_quick.rs

//! **`cargo xtask ci --quick` runs exactly the quick rows, and builds one graph.**
//!
//! dwarf's CI forge runs `cargo xtask ci --quick` on every new head of `master`, with a
//! 40 GB target cap. `task_ci` ignored `--quick` and ran the full set, which filled 138 GB
//! of target on dwarf (2026-10-29). This runs the real verb with `CARGO` pointed at a
//! recorder, so no cargo line builds anything, and pins:
//!
//! * the rows it announces, in order ([`xtask::ci_quick::ROWS`], and the literal list);
//! * the cargo lines that build: exactly clippy, the test build and the doc tests, all of the
//!   default feature set, all into the caller's `CARGO_TARGET_DIR`, all with the quick
//!   environment.
//!
//! `fmt` (the whole tree, every package) and the five no-cargo checks run for real
//! (seconds); their verdict does not matter here, only that they are the rows. Zero
//! dependencies, like the rest of `xtask`.

use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

const EXE: &str = env!("CARGO_BIN_EXE_xtask");

struct Scratch(PathBuf);
impl Drop for Scratch {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

/// A `cargo` that writes one line per call (its target dir, the quick env and its argv)
/// and succeeds.
fn recorder(dir: &std::path::Path) -> (PathBuf, PathBuf) {
    let log = dir.join("calls.log");
    let cargo = dir.join("cargo");
    let script = format!(
        "#!/bin/sh\nprintf 'TARGET=%s INC=%s DEV=%s TEST=%s STRIP=%s/%s ARGV=%s\\n' \"$CARGO_TARGET_DIR\" \"$CARGO_INCREMENTAL\" \"$CARGO_PROFILE_DEV_DEBUG\" \"$CARGO_PROFILE_TEST_DEBUG\" \"$CARGO_PROFILE_DEV_STRIP\" \"$CARGO_PROFILE_TEST_STRIP\" \"$*\" >> '{}'\nexit 0\n",
        log.display()
    );
    std::fs::write(&cargo, script).expect("write the recorder");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&cargo, std::fs::Permissions::from_mode(0o755)).expect("chmod");
    }
    (cargo, log)
}

#[test]
fn quick_runs_the_quick_rows_and_one_graph_into_one_target() {
    let dir = std::env::temp_dir().join(format!("gunnar-ci-quick-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).expect("scratch");
    let scratch = Scratch(dir.clone());
    let (cargo, log) = recorder(&scratch.0);
    let target = scratch.0.join("target");

    let mut child = Command::new(EXE)
        .args(["ci", "--quick"])
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .env("CARGO", &cargo)
        .env("CARGO_TARGET_DIR", &target)
        .env_remove("CARGO_INCREMENTAL")
        .env_remove("CARGO_PROFILE_DEV_DEBUG")
        .env_remove("CARGO_PROFILE_TEST_DEBUG")
        .env_remove("CARGO_PROFILE_DEV_STRIP")
        .env_remove("CARGO_PROFILE_TEST_STRIP")
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::from(
            std::fs::File::create(scratch.0.join("stderr")).expect("stderr file"),
        ))
        .spawn()
        .expect("spawn xtask");
    // The full set, which is what an ignored `--quick` runs, takes minutes even with a
    // recording cargo (Keycloak, cargo-deny, clones); the quick set takes seconds.
    let deadline = Instant::now() + Duration::from_secs(300);
    loop {
        if child.try_wait().expect("wait").is_some() {
            break;
        }
        if Instant::now() > deadline {
            let _ = child.kill();
            let _ = child.wait();
            let err = std::fs::read_to_string(scratch.0.join("stderr")).unwrap_or_default();
            panic!("`cargo xtask ci --quick` ran past 300 s: it is not the quick set. Its rows so far:\n{}", rows(&err).join("\n"));
        }
        std::thread::sleep(Duration::from_millis(200));
    }
    let err = std::fs::read_to_string(scratch.0.join("stderr")).unwrap_or_default();

    assert_eq!(
        rows(&err),
        ["fmt", "deploy-check", "installer-pin", "one-clone", "ci-check", "key-census", "clippy", "test", "front"],
        "the quick rows changed; this list and xtask::ci_quick::ROWS change together, and every new \
         row must fit dwarf's CI budget (40 GB of target, about 20 minutes)"
    );
    assert_eq!(rows(&err), xtask::ci_quick::ROWS);
    // OSSF-SILVER-CODE (2026-10-31): SC-GUNNAR-FMT formatted the whole tree (3b91e79a), so the
    // quick set's `fmt` is the whole-tree check (`cargo xtask fmt --check`, every package),
    // no longer a ratchet over the files the head changed.
    assert!(
        err.contains("xtask fmt: ") && err.contains("package(s)"),
        "the quick set's fmt did not check the whole tree:\n{err}"
    );
    assert!(
        !err.contains("the head changed"),
        "the quick set's fmt is still the ratchet over the head's changed files:\n{err}"
    );

    let calls = std::fs::read_to_string(&log).unwrap_or_default();
    // The no-cargo checks resolve (`metadata`, `tree`); a resolve builds nothing. What is
    // pinned is every cargo line that BUILDS.
    let calls: Vec<&str> = calls
        .lines()
        .filter(|l| {
            let sub = l
                .split("ARGV=")
                .nth(1)
                .unwrap_or("")
                .split(' ')
                .next()
                .unwrap_or("");
            !matches!(sub, "metadata" | "tree" | "locate-project" | "pkgid")
        })
        .collect();
    let t = target.display().to_string();
    let quick_env = format!("TARGET={t} INC=0 DEV=0 TEST=0 STRIP=symbols/symbols ARGV=");
    // The front (GUN-FIELD-TEST, 2026-10-31): gunnar-front is a detached workspace, so no
    // `--workspace` line above builds a line of it; the quick set runs its tests at its
    // defaults, by manifest path, into the same target with the same environment. Its
    // feature-gated tests (`front-features`) stay in the full set.
    let front: &[&str] = &[
        "test",
        "--manifest-path",
        "crates/gunnar-front/Cargo.toml",
        "--no-fail-fast",
    ];
    let want: Vec<String> = [
        xtask::ci_quick::CLIPPY,
        xtask::ci_quick::BUILD_TESTS,
        xtask::ci_quick::DOC_TESTS,
        front,
    ]
    .iter()
    .map(|argv| format!("{quick_env}{}", argv.join(" ")))
    .collect();
    assert_eq!(calls, want, "the quick set's cargo lines: clippy, the test build and the doc tests of the root, one target, one feature set; then the front's tests");
    assert!(want[0].ends_with("ARGV=clippy --workspace --all-targets -- -D warnings"));
    assert!(want[1]
        .ends_with("ARGV=test --workspace --no-run --message-format=json-render-diagnostics"));
    assert!(want[2].ends_with("ARGV=test --workspace --doc --no-fail-fast"));
    // The recorder lists no test binary, and the row says so rather than passing.
    assert!(
        err.contains("the build listed no test binary"),
        "a test row that ran nothing was not red:\n{err}"
    );
}

/// The `── xtask ci: <row> ──` headers, in order.
fn rows(stderr: &str) -> Vec<String> {
    stderr
        .lines()
        .filter_map(|l| l.strip_prefix("── xtask ci: "))
        .map(|l| l.trim_end_matches(" ──").to_string())
        .collect()
}