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

nordisk/holger-ui

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