//! Configuration, templating and discovery — the surface that decides whether this is a
//! generator for one site or for anyone's.
//!
//! The theme running through these tests is that **the zero-config path has to work**.
//! A directory of `.org` files with no `orgo.toml`, no templates and no knowledge of
//! this tool must build into a real site; configuration is how you change the output,
//! never how you make it work at all.
use std::sync::atomic::{AtomicU32, Ordering};
use camino::Utf8PathBuf;
use orgo::config::{Config, NavMode};
use orgo::site::{build_site, BuildOptions};
fn tmpdir(tag: &str) -> Utf8PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
.expect("utf-8 temp dir")
.join(format!("orgo-cfg-{}-{tag}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
base
}
/// A site with a root page, a second root page, and one nested page.
fn write_site(src: &Utf8PathBuf) {
std::fs::create_dir_all(src.join("blog")).unwrap();
std::fs::write(src.join("index.org"), "#+TITLE: Home\n\nWelcome.\n").unwrap();
std::fs::write(src.join("about.org"), "#+TITLE: About\n\nAbout.\n").unwrap();
std::fs::write(
src.join("blog/post.org"),
"#+TITLE: A Post\n#+DATE: 2024-05-01\n#+FILETAGS: :rust:web:\n\nBody.\n",
)
.unwrap();
}
fn build(src: &Utf8PathBuf, out: &Utf8PathBuf) -> orgo::site::SiteReport {
build_site(src, out, &BuildOptions::default()).expect("build")
}
fn page(out: &Utf8PathBuf, rel: &str) -> String {
std::fs::read_to_string(out.join(rel)).unwrap_or_else(|e| panic!("reading {rel}: {e}"))
}
// ---------------------------------------------------------------------------
// Zero config
// ---------------------------------------------------------------------------
/// The headline promise: point it at a directory of org files and get a site.
#[test]
fn a_bare_directory_of_org_files_builds_with_no_config() {
let root = tmpdir("bare");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
write_site(&src);
let out = root.join("out");
let report = build(&src, &out);
assert_eq!(report.pages.len(), 3);
let home = page(&out, "index.html");
assert!(home.contains(""), "a full page, not a fragment");
assert!(home.contains("Welcome."), "the content is there");
assert!(
out.join("syntax.css").exists(),
"the stylesheet the highlighter needs is emitted too"
);
}
/// A missing config is normal. A *malformed* one is not: someone who wrote a config
/// meant it, and quietly building the default site would hide their typo behind
/// plausible-looking output.
#[test]
fn a_malformed_config_is_an_error_but_a_missing_one_is_not() {
let root = tmpdir("malformed");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
write_site(&src);
assert_eq!(Config::load(&src).unwrap(), Config::default());
std::fs::write(src.join("orgo.toml"), "[site\ntitle = broken").unwrap();
let err = Config::load(&src).expect_err("malformed config must fail");
assert!(format!("{err:#}").contains("orgo.toml"), "names the file: {err:#}");
}
/// A misspelled key is a silent no-op in most config formats, which is exactly how
/// someone spends an afternoon wondering why a setting does nothing.
#[test]
fn an_unknown_config_key_is_rejected() {
let root = tmpdir("unknownkey");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("orgo.toml"), "[site]\ntittle = \"typo\"\n").unwrap();
let err = Config::load(&src).expect_err("unknown key must fail");
assert!(
format!("{err:#}").contains("tittle"),
"the error names the offending key: {err:#}"
);
}
// ---------------------------------------------------------------------------
// Nav modes
// ---------------------------------------------------------------------------
fn nav_of(html: &str) -> String {
html.split("").next())
.unwrap_or("")
.to_string()
}
#[test]
fn nav_modes_select_different_pages() {
for (mode, expect_post, expect_about) in [
("top-level", false, true),
("all", true, true),
("none", false, false),
] {
let root = tmpdir(&format!("nav-{mode}"));
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
write_site(&src);
std::fs::write(
src.join("orgo.toml"),
format!("[nav]\nmode = \"{mode}\"\n"),
)
.unwrap();
let out = root.join("out");
build(&src, &out);
let nav = nav_of(&page(&out, "index.html"));
assert_eq!(
nav.contains("A Post"),
expect_post,
"mode {mode} nested page presence, nav was:\n{nav}"
);
assert_eq!(
nav.contains("About"),
expect_about,
"mode {mode} root page presence, nav was:\n{nav}"
);
}
}
/// An explicit nav is a designed sequence, so configured order beats discovery order.
#[test]
fn explicit_nav_uses_the_configured_order() {
let root = tmpdir("navexplicit");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
write_site(&src);
std::fs::write(
src.join("orgo.toml"),
"[nav]\nmode = \"explicit\"\npages = [\"blog/post.org\", \"index.org\"]\n",
)
.unwrap();
let out = root.join("out");
build(&src, &out);
let nav = nav_of(&page(&out, "index.html"));
let post = nav.find("A Post").expect("post in nav");
let home = nav.find("Home").expect("home in nav");
assert!(post < home, "configured order wins:\n{nav}");
assert!(!nav.contains("About"), "unlisted pages stay out:\n{nav}");
}
/// A nav entry naming a page that does not exist is a typo, and a silently shorter nav
/// is a poor way to find out.
#[test]
fn explicit_nav_rejects_a_page_that_does_not_exist() {
let root = tmpdir("navmissing");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
write_site(&src);
std::fs::write(
src.join("orgo.toml"),
"[nav]\nmode = \"explicit\"\npages = [\"nope.org\"]\n",
)
.unwrap();
let err = build_site(&src, &root.join("out"), &BuildOptions::default())
.expect_err("missing nav page must fail");
assert!(format!("{err:#}").contains("nope.org"), "names it: {err:#}");
}
/// `mode` and `pages` disagreeing means one of them is being ignored.
#[test]
fn contradictory_nav_settings_are_rejected() {
let mut config = Config::default();
config.nav.pages = vec![Utf8PathBuf::from("index.org")];
assert!(config.validate().is_err(), "pages without explicit mode");
let mut config = Config::default();
config.nav.mode = NavMode::Explicit;
assert!(config.validate().is_err(), "explicit mode without pages");
}
// ---------------------------------------------------------------------------
// Templates
// ---------------------------------------------------------------------------
/// The single biggest blocker to general use: without this every site built with this
/// tool looks identical.
#[test]
fn a_user_template_replaces_the_built_in_layout() {
let root = tmpdir("template");
let src = root.join("src");
std::fs::create_dir_all(src.join("templates")).unwrap();
write_site(&src);
std::fs::write(
src.join("templates/base.html"),
"
{{ page.title }}
{{ body | safe }}",
)
.unwrap();
let out = root.join("out");
build(&src, &out);
let home = page(&out, "index.html");
assert!(home.contains("class=\"mine\""), "the user layout is used:\n{home}");
assert!(!home.contains("