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