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