xtask/tests/fast_console_two_runs.rs
//! ★ **Two Gunnar fast runs of ONE checkout build their consoles apart** (FAST-REL, 2026-10-25).
//!
//! The fast verb built gunnar-ui's paid console inside gunnar-ui's SOURCE tree
//! (`crates/gunnar-ui-web/dist-metered`). Two runs of one checkout that started together both
//! found it missing and ran trunk into that one directory: one run's finalize removed the other's
//! staging ("error from asset pipeline: No such file or directory"; "Text file busy" on the
//! CI-green lane's runs). Each run now builds into its OWN directory, under its `--out`, and the
//! front row embeds that one.
//!
//! The real build, on the host: trunk and a gunnar-ui checkout beside gunnar are needed. Without
//! them (the toolchain container has no trunk) the test says so by name and checks nothing.
use std::path::{Path, PathBuf};
fn skip(why: &str) {
eprintln!("[skip: fast-console-two-runs] {why}");
}
#[test]
fn two_runs_of_one_checkout_build_their_consoles_apart() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let ui = root.parent().unwrap().join("gunnar-ui");
if Path::new("/run/.containerenv").exists() || Path::new("/.dockerenv").exists() {
return skip("in a container: the console is a host trunk build");
}
if !ui.join("crates/gunnar-ui-web/Trunk.toml").is_file() {
return skip("no gunnar-ui checkout beside gunnar");
}
if std::process::Command::new("trunk")
.arg("--version")
.output()
.map(|o| !o.status.success())
.unwrap_or(true)
{
return skip("no trunk on PATH");
}
std::env::remove_var("GUNNAR_FRONT_CONSOLE_DIST"); // this test binary runs this one test
let base =
std::env::temp_dir().join(format!("gunnar-fast-two-consoles-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let (out_a, out_b) = (base.join("a"), base.join("b"));
let (a, b): (Result<PathBuf, String>, Result<PathBuf, String>) = std::thread::scope(|s| {
let ta = s.spawn(|| xtask::fast::build_console(&root, &out_a));
let tb = s.spawn(|| xtask::fast::build_console(&root, &out_b));
(ta.join().unwrap(), tb.join().unwrap())
});
let (a, b) = (
a.expect("run a's console builds while run b builds its own"),
b.expect("run b's console builds while run a builds its own"),
);
assert_ne!(a, b, "two runs, two console directories");
for (d, out) in [(&a, &out_a), (&b, &out_b)] {
assert!(
d.starts_with(out),
"{} is the run's own (under {})",
d.display(),
out.display()
);
assert!(
!d.starts_with(&ui),
"{} is not in gunnar-ui's source tree",
d.display()
);
assert!(
d.join("index.html").is_file(),
"{} holds a whole console",
d.display()
);
}
let _ = std::fs::remove_dir_all(&base);
}