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

nordisk/holger-ui

tests/open_bundle_speaks_no_money.rs

//! **The open console bundle speaks no money** (A28 M13, the bundle's half): the twin of
//! `crates/holger-ui-server/tests/open_build_speaks_no_money.rs`, with the same judge and the
//! same lexer (`tests/money/mod.rs`), and gunnar-ui's console half
//! (`gunnar-ui-server/tests/open_build_speaks_no_money.rs`,
//! `the_consoles_own_words_are_free_of_money`) as the pattern.
//!
//! The bundle is built twice, from two pages:
//!
//! * the OPEN bundle, `dist/`: `index.html`, and the wasm console built without features;
//! * the METERED bundle, `dist-metered/`: `index-metered.html`, and the wasm console built
//!   with `metered` (the sign-up, welcome and order pages, "My plan", the buy links).
//!
//! What this file holds:
//!
//! * **the WORDS the open console can render**: every string literal in `src/` an open
//!   `holger-ui-web` compiles. Comments are not scanned, nor is code the open build does not
//!   contain (`#[cfg(feature = "metered")]` items and modules, tests).
//! * **the open PAGE and every file it copies** into `dist/`: words to a visitor, comments
//!   and markup included (view-source is one keystroke).
//! * **the two pages are one page**: the metered page is the open page plus the paid files,
//!   so the paid build keeps every page it had and the open build carries none of them.

mod money;

use std::path::{Path, PathBuf};

use money::{leaks_in, scan_source, sources, Scanned};

/// The open bundle's page (`Trunk.toml` `target`, the recipe's `mvp-ui-web` web app).
const OPEN_PAGE: &str = "index.html";
/// The metered bundle's page (the recipe's `mvp-ui-web-metered` web app).
const METERED_PAGE: &str = "index-metered.html";

/// **The paid half's files**: copied by the metered page only. holger-ui-server serves them
/// under its own `metered` and refuses them by name (404) without it (`OPEN_REFUSES`).
const PAID_FILES: [&str; 3] = ["assets/signup.html", "assets/welcome.html", "assets/paid.html"];

/// **The modules behind `metered`**, exactly. A module gated and not on this list is a file
/// the scanner silently stops watching; one on this list and not gated is a stale entry.
const METERED_MODULES: &[&str] = &["src/pages/plan.rs", "src/purchase.rs", "src/site_switch.rs"];

/// **Deliberate exceptions**, `(file suffix, fragment, reason)`. Each must still match.
const EXCEPTIONS: &[(&str, &str, &str)] = &[];

fn repo() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn rel(p: &Path) -> String {
    p.strip_prefix(repo()).unwrap_or(p).to_string_lossy().replace('\\', "/")
}

/// Pure: the `href`s of a page's `<link data-trunk …>` tags (copy-file, css, icon), resolved
/// against the page's directory, repository-relative.
fn copied(page: &str, html: &str) -> Vec<String> {
    let dir = Path::new(page).parent().unwrap_or(Path::new(""));
    let mut out = Vec::new();
    let mut rest = html;
    while let Some(at) = rest.find("<link") {
        let end = rest[at..].find('>').map_or(rest.len(), |e| at + e + 1);
        let tag = &rest[at..end];
        rest = &rest[end..];
        if !tag.contains("data-trunk") {
            continue;
        }
        let Some(h) = tag.find("href=\"") else { continue };
        let href = &tag[h + 6..];
        let href = &href[..href.find('"').unwrap_or(href.len())];
        let mut parts: Vec<&str> = dir.iter().map(|c| c.to_str().unwrap_or("")).collect();
        for seg in href.split('/') {
            match seg {
                ".." => {
                    parts.pop();
                }
                "." | "" => {}
                s => parts.push(s),
            }
        }
        out.push(parts.join("/"));
    }
    out
}

/// Pure: a page with its `<link data-trunk rel="copy-file" …/>` lines, its comments and the
/// `../` of its hrefs taken out, so two pages that differ only in what they copy compare equal.
fn shape(html: &str) -> String {
    let mut out = String::new();
    let mut rest = html;
    while let Some(c) = rest.find("<!--") {
        out.push_str(&rest[..c]);
        rest = rest[c..].find("-->").map_or("", |e| &rest[c + e + 3..]);
    }
    out.push_str(rest);
    out.lines()
        .filter(|l| !l.contains("rel=\"copy-file\""))
        .map(|l| l.replace("href=\"../", "href=\""))
        .filter(|l| !l.trim().is_empty())
        .collect::<Vec<_>>()
        .join("\n")
}

/// The files of `src/` an open build compiles, with what each yields.
fn open_sources() -> (Vec<(PathBuf, Scanned)>, Vec<PathBuf>) {
    let files: Vec<(PathBuf, Scanned)> = sources(&repo().join("src"))
        .into_iter()
        .map(|p| {
            let text = std::fs::read_to_string(&p).unwrap_or_default();
            let s = scan_source(&text);
            (p, s)
        })
        .collect();
    let mut dead = Vec::new();
    for (p, s) in &files {
        let stem = p.file_stem().unwrap().to_string_lossy();
        let base = if matches!(stem.as_ref(), "lib" | "main" | "mod") { p.parent().unwrap().to_path_buf() } else { p.parent().unwrap().join(stem.as_ref()) };
        for m in &s.skipped_mods {
            dead.push(base.join(format!("{m}.rs")));
            dead.push(base.join(m));
        }
    }
    (files, dead)
}

#[test]
fn the_page_reader_resolves_what_a_page_copies() {
    let page = "<link data-trunk rel=\"copy-file\" href=\"../assets/a.html\" />\n<link rel=\"x\" href=\"b\">\n<link data-trunk rel=\"rust\" />";
    assert_eq!(copied("metered/index.html", page), vec!["assets/a.html"]);
    assert_eq!(copied("index.html", "<link data-trunk rel=\"css\" href=\"assets/c.css\"/>"), vec!["assets/c.css"]);
    let a = "<head>\n<!-- one -->\n<link data-trunk rel=\"css\" href=\"assets/c.css\" />\n</head>";
    let b = "<head>\n<!-- two,\n  lines -->\n<link data-trunk rel=\"css\" href=\"../assets/c.css\" />\n<link data-trunk rel=\"copy-file\" href=\"../assets/p.js\" />\n</head>";
    assert_eq!(shape(a), shape(b));
    assert_ne!(shape(a), shape("<head></head>"));
}

/// **Every word the open console can render is free of money.** Never skips: the source
/// scan judges what an OPEN build compiles, whichever features this test was built with.
#[test]
fn the_open_consoles_words_are_free_of_money() {
    let (files, dead) = open_sources();
    let mut leaks = Vec::new();
    let mut used = vec![false; EXCEPTIONS.len()];
    let mut scanned = 0usize;
    for (p, s) in &files {
        if dead.iter().any(|d| p == d || p.starts_with(d)) {
            continue;
        }
        scanned += 1;
        let r = rel(p);
        for lit in &s.literals {
            let hits = leaks_in(&lit.text);
            if hits.is_empty() {
                continue;
            }
            if let Some(k) = EXCEPTIONS.iter().position(|(f, frag, _)| r.ends_with(f) && lit.text.contains(frag)) {
                used[k] = true;
                continue;
            }
            leaks.push(format!("{r}:{}: {hits:?} in {:?}", lit.line, lit.text));
        }
    }
    assert!(scanned > 20, "only {scanned} files scanned; the layout moved");
    let stale: Vec<_> = EXCEPTIONS.iter().zip(&used).filter(|(_, u)| !**u).map(|(e, _)| e.1).collect();
    assert!(stale.is_empty(), "exceptions that no longer match anything: {stale:?}");
    assert!(
        leaks.is_empty(),
        "the OPEN console bundle compiles money vocabulary into what it renders ({} literals). \
         Reword neutrally, or gate the code under `metered`.\n  {}",
        leaks.len(),
        leaks.join("\n  ")
    );
}

/// **The modules behind `metered` are exactly [`METERED_MODULES`]**, stepped over by the
/// attribute and not by luck.
#[test]
fn the_metered_modules_are_the_named_ones() {
    let (_, dead) = open_sources();
    let mut gated: Vec<String> = dead.iter().filter(|d| d.extension().is_some_and(|e| e == "rs") && d.is_file()).map(|d| rel(d)).collect();
    gated.sort();
    let mut want: Vec<String> = METERED_MODULES.iter().map(|s| (*s).to_owned()).collect();
    want.sort();
    assert_eq!(gated, want, "the modules behind `metered` (or `cfg(test)`) are not the named ones");
}

/// **The open page and every file it copies into `dist/` are free of money**, and none of
/// the paid files is among them.
#[test]
fn the_open_page_and_its_files_are_free_of_money() {
    let page = std::fs::read_to_string(repo().join(OPEN_PAGE)).expect("the open page");
    let files = copied(OPEN_PAGE, &page);
    assert!(files.iter().any(|f| f == "assets/login.html"), "the open page copies the login page: {files:?}");
    for paid in PAID_FILES {
        assert!(!files.iter().any(|f| f == paid), "the OPEN page copies {paid}, a page of the paid half");
    }
    let mut leaks = Vec::new();
    let mut texts = vec![(OPEN_PAGE.to_owned(), page.clone())];
    for f in &files {
        if let Ok(text) = std::fs::read_to_string(repo().join(f)) {
            texts.push((f.clone(), text));
        }
    }
    for (name, text) in &texts {
        for (n, para) in text.split("\n\n").enumerate() {
            let hits = leaks_in(para);
            if !hits.is_empty() {
                leaks.push(format!("{name} ¶{}: {hits:?} in {:?}", n + 1, para.trim()));
            }
        }
    }
    assert!(
        leaks.is_empty(),
        "the OPEN bundle's page or a file it copies speaks money ({} paragraphs).\n  {}",
        leaks.len(),
        leaks.join("\n  ")
    );
}

/// **The metered page is the open page plus the paid files**: the same markup and styles,
/// every file the open page copies, and the four paid files on top.
#[test]
fn the_metered_page_is_the_open_page_plus_the_paid_files() {
    let open = std::fs::read_to_string(repo().join(OPEN_PAGE)).expect("the open page");
    let metered = std::fs::read_to_string(repo().join(METERED_PAGE)).expect("the metered page");
    let mut want = copied(OPEN_PAGE, &open);
    want.extend(PAID_FILES.iter().map(|s| (*s).to_owned()));
    want.sort();
    let mut got = copied(METERED_PAGE, &metered);
    got.sort();
    assert_eq!(got, want, "the metered page copies the open page's files and the paid ones");
    assert_eq!(shape(&metered), shape(&open), "the two pages differ in more than what they copy");
}