xtask/src/browser.rs
//! **`cargo xtask browser`** (OX-WIRED 2026-10-28): monetize-site-buy's buy pages in a real
//! browser — the `browser_*` tests, which say NOT RUN without `CHROME` — in the browser image
//! (`browser/Containerfile`: the toolchain image plus Debian's Chromium, gunnar-ui's recipe),
//! built once per content of that file and tagged by its hash. [`NEEDS`]`=1` makes a missing
//! Chromium red in every one of them. Inside a container (the CI runner's, or
//! `NORNIR_CI_HOST=1`) the tests run right here and `$CHROME` must name a Chromium.
use std::path::Path;
use std::process::{Command, ExitCode};
/// The browser tests of monetize-site-buy (`crates/monetize-site-buy/tests/<name>.rs`).
pub const TESTS: &[&str] = &["browser_paid_page", "browser_paid_return", "browser_plans_order", "browser_welcome_settle"];
/// Set (to `1`): a browser test with no `CHROME` FAILS by name instead of saying NOT RUN.
pub const NEEDS: &str = "MONETIZE_NEEDS_BROWSER";
/// The browser image's recipe, from the repository's root.
pub const RECIPE: &str = "browser/Containerfile";
/// The cargo arguments that run [`TESTS`].
pub fn args() -> Vec<String> {
let mut a: Vec<String> = ["test", "--locked", "-p", "monetize-site-buy", "--features", "testkit"].iter().map(|s| s.to_string()).collect();
for t in TESTS {
a.push("--test".into());
a.push((*t).into());
}
a.extend(["--".into(), "--test-threads".into(), "1".into()]);
a
}
/// The browser image's tag for a `Containerfile`'s bytes: 64-bit FNV-1a, as 16 hex digits
/// (gunnar-ui's `recipe_tag`: stable across Rust versions, and xtask takes no hash crate).
pub fn recipe_tag(recipe: &[u8]) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in recipe {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{h:016x}")
}
/// **`cargo xtask browser`**: [`TESTS`] with [`NEEDS`]`=1`, here when the toolchain is here,
/// else in the browser image (built from [`RECIPE`] when its tag is not present yet).
pub fn run(root: &Path) -> ExitCode {
let args = args();
let env = vec![(NEEDS.to_string(), "1".to_string())];
let go = |mut c: Command, what: &str| -> ExitCode {
match c.status() {
Ok(s) if s.success() => ExitCode::SUCCESS,
Ok(s) => {
eprintln!("xtask browser: {what} exited {s}");
ExitCode::FAILURE
}
Err(e) => {
eprintln!("xtask browser: could not start {what}: {e}");
ExitCode::FAILURE
}
}
};
if nornir_xtask_support::ci::toolchain_is_here() {
let mut c = Command::new("cargo");
c.args(&args).envs(env.iter().map(|(k, v)| (k, v))).current_dir(root);
return go(c, "the browser tests");
}
let mut image = nornir_xtask_support::ci::Container::from_env(root);
let file = root.join(RECIPE);
let recipe = match std::fs::read(&file) {
Ok(r) => r,
Err(e) => {
eprintln!("xtask browser: read {}: {e}", file.display());
return ExitCode::FAILURE;
}
};
let tag = format!("localhost/monetize-browser:{}", recipe_tag(&recipe));
let present = Command::new(&image.engine).args(["image", "exists", &tag]).status().is_ok_and(|s| s.success());
if !present {
let mut c = Command::new(&image.engine);
c.args(["build", "-t", &tag, "--build-arg", &format!("BASE={}", image.image), "-f"]).arg(&file).arg(root.join("browser"));
let built = go(c, "the browser image's build");
if built != ExitCode::SUCCESS {
return built;
}
}
image.image = tag;
if let Err(e) = std::fs::create_dir_all(&image.target) {
eprintln!("xtask browser: create {}: {e}", image.target.display());
return ExitCode::FAILURE;
}
let argv = image.argv(None, &env, "cargo", &args);
let mut c = Command::new(&argv[0]);
c.args(&argv[1..]);
go(c, "the browser tests in their image")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_verb_runs_every_browser_test_one_at_a_time() {
let a = args();
for t in TESTS {
assert!(a.windows(2).any(|w| w[0] == "--test" && w[1] == *t), "{t}: {a:?}");
}
assert!(a.starts_with(&["test".into(), "--locked".into(), "-p".into(), "monetize-site-buy".into(), "--features".into(), "testkit".into()]), "{a:?}");
assert_eq!(recipe_tag(b"x"), recipe_tag(b"x"));
assert_ne!(recipe_tag(b"x"), recipe_tag(b"y"), "a changed recipe is a new image");
}
}