browser-tests/src/bundle.rs
//! **The console bundles the browser loads**, each built once per test run.
//!
//! The same three steps as the Build Thing's web-app step (nornir-build-thing `web_app`):
//! the wasm32 cargo build of `holger-ui-web`, wasm-bindgen as a library (`--target web`),
//! and the page assembled by `web_app::assemble_dist`. Two bundles (A28 M13), as the recipe
//! declares them:
//!
//! * [`dist`]: the METERED bundle (row `mvp-ui-web-metered`: `--features metered`, the page
//! `index-metered.html`), the one holger's paid front embeds and these tests drive.
//! `HOLGER_UI_DIST` names one already built (a release `dist-metered/`), and then nothing
//! is built.
//! * [`dist_open`]: the OPEN bundle (row `mvp-ui-web`: no feature, the page `index.html`).
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use anyhow::{anyhow, bail, Context as _, Result};
/// holger-ui's repository root (this crate's parent).
pub fn repo() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("browser-tests sits in holger-ui").to_path_buf()
}
/// One bundle: the features of its wasm32 build and the page it is assembled from.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Bundle {
pub features: &'static str,
pub page: &'static str,
/// Its own target directory under the outer one, so the two builds never share one.
pub target: &'static str,
}
/// The metered bundle (`mvp-ui-web-metered`).
pub const METERED: Bundle = Bundle { features: "metered", page: "index-metered.html", target: "browser-bundle" };
/// The open bundle (`mvp-ui-web`).
pub const OPEN: Bundle = Bundle { features: "", page: "index.html", target: "browser-bundle-open" };
/// **The metered bundle's directory**, built on first use; every later caller gets the same one.
pub fn dist() -> &'static Path {
static DIST: OnceLock<PathBuf> = OnceLock::new();
DIST.get_or_init(|| {
if let Some(d) = std::env::var_os("HOLGER_UI_DIST").filter(|d| !d.is_empty()) {
let d = PathBuf::from(d);
assert!(d.join("index.html").is_file(), "HOLGER_UI_DIST={} holds no index.html", d.display());
return d;
}
build(METERED).unwrap_or_else(|e| panic!("the metered console bundle could not be built: {e:#}"))
})
}
/// **The open bundle's directory**, built on first use.
pub fn dist_open() -> &'static Path {
static DIST: OnceLock<PathBuf> = OnceLock::new();
DIST.get_or_init(|| build(OPEN).unwrap_or_else(|e| panic!("the open console bundle could not be built: {e:#}")))
}
/// Pure: **the directory one process assembles a bundle into**: its own, so two runs over
/// one target directory never clear or fill each other's.
pub fn dist_dir(target: &Path, pid: u32) -> PathBuf {
target.join(format!("dist-{pid}"))
}
/// Pure: the cargo arguments of a bundle's wasm32 build.
pub fn cargo_args(b: Bundle) -> Vec<&'static str> {
let mut a = vec!["build", "--locked", "--target", "wasm32-unknown-unknown", "-p", "holger-ui-web"];
if !b.features.is_empty() {
a.extend(["--features", b.features]);
}
a
}
fn build(b: Bundle) -> Result<PathBuf> {
let repo = repo();
// holger-ui's lock is the estate's (its sibling patches on). These tests run only beside
// the siblings, so the patches are switched on as the CI runner switches them on.
let siblings = repo.join(".cargo/siblings.toml");
if !siblings.exists() {
std::fs::copy(repo.join(".cargo/estate-siblings.toml"), &siblings).context("switch the sibling patches on")?;
}
// Its own target directory: the outer `cargo test` holds the lock of its own.
let base = std::env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| repo.join("browser-tests/target"));
let target = base.join(b.target);
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let status = Command::new(cargo)
.args(cargo_args(b))
.arg("--manifest-path")
.arg(repo.join("Cargo.toml"))
.env("CARGO_TARGET_DIR", &target)
.status()
.context("start the wasm32 cargo build")?;
if !status.success() {
bail!("the wasm32 cargo build of holger-ui-web ({b:?}) exited {status}");
}
let wasm = target.join("wasm32-unknown-unknown/debug/holger-ui-web.wasm");
let out = dist_dir(&target, std::process::id());
if out.exists() {
std::fs::remove_dir_all(&out).with_context(|| format!("clear {}", out.display()))?;
}
std::fs::create_dir_all(&out)?;
let name = "holger-ui-web";
let mut bg = wasm_bindgen_cli_support::Bindgen::new();
bg.input_path(&wasm).out_name(name).typescript(false);
bg.web(true).map_err(|e| anyhow!("wasm-bindgen --target web: {e:#}"))?;
bg.generate(&out).map_err(|e| anyhow!("wasm-bindgen over {}: {e:#}", wasm.display()))?;
nornir_build_thing::web_app::assemble_dist(&repo.join(b.page), &out.join(format!("{name}.js")), &out.join(format!("{name}_bg.wasm")), &out)?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
/// ★ Two runs over one target directory (two CI runs at once, as PAID-WORDS' log shows:
/// "two assets land at …/browser-bundle/dist/console-<hash>.css") never assemble into
/// one directory: each process has its own.
#[test]
fn two_processes_never_assemble_one_bundle_directory() {
let t = Path::new("/t/browser-bundle");
assert_ne!(dist_dir(t, 1), dist_dir(t, 2));
assert!(dist_dir(t, 7).starts_with(t));
}
#[test]
fn each_bundle_is_built_as_the_recipe_declares_it() {
assert_eq!(cargo_args(OPEN), ["build", "--locked", "--target", "wasm32-unknown-unknown", "-p", "holger-ui-web"]);
assert_eq!(cargo_args(METERED)[6..], ["--features", "metered"]);
assert_ne!(OPEN.target, METERED.target, "never one target directory for both");
for b in [OPEN, METERED] {
assert!(repo().join(b.page).is_file(), "{} is committed", b.page);
}
}
}