use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args([
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"-c",
"commit.gpgsign=false",
"-c",
"init.defaultBranch=main",
])
.args(args)
.current_dir(dir)
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed");
}
fn write(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(path, content).expect("write");
}
fn fresh_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("roteiro-render-cli-{tag}-{}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
fn source(marker: &str, lines: usize) -> String {
use std::fmt::Write as _;
let mut s = format!("// {marker}: deferred\n");
for i in 1..lines {
let _ = writeln!(s, "pub const N{i}: u32 = {i};");
}
s
}
#[test]
fn render_docs_builds_site_from_adrs_and_assets() {
let dir = fresh_dir("docs");
git(&dir, &["init", "-q"]);
write(&dir, "website/public/style.css", "body{color:#111}\n");
write(&dir, "website/public/index.html", "<h1>Home</h1>\n");
write(&dir, "website/public/favicon.svg", "<svg/>\n");
write(
&dir,
"docs/adr/0001-example.md",
"---\nadr-id: \"0001\"\nstatus: Accepted\n---\n\n# ADR-0001: Example\n\n## Context\n\n| a | b |\n|---|---|\n| 1 | 2 |\n",
);
write(&dir, "docs/adr/README.md", "index, not an ADR\n");
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let out = Command::new(BIN)
.args(["render", "docs", "--out", "site"])
.current_dir(&dir)
.output()
.expect("run render");
assert!(out.status.success(), "render failed: {out:?}");
let site = dir.join("site");
assert!(site.join("style.css").exists());
assert!(site.join("index.html").exists());
assert!(site.join("favicon.svg").exists());
let page = std::fs::read_to_string(site.join("adr/0001-example.html")).expect("adr page");
assert!(page.starts_with("<!doctype html>"));
assert!(page.contains("<h1 id=\"adr-0001-example\">ADR-0001: Example</h1>"));
assert!(page.contains("<table>"), "GFM table should render");
assert!(!page.contains("adr-id"), "frontmatter should be stripped");
assert!(page.contains("← Back to roteiro.dev"));
assert!(
!site.join("adr/README.html").exists(),
"README is not an ADR page"
);
let index = std::fs::read_to_string(site.join("adr/index.html")).expect("index");
assert!(index.contains("<a href=\"0001-example.html\">ADR-0001: Example</a>"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn render_obsidian_home_scopes_debt_by_the_ignore_config() {
let dir = fresh_dir("obsidian");
git(&dir, &["init", "-q"]);
write(&dir, "src/lib.rs", &source("TODO", 100));
write(&dir, "vendor/dep.rs", &source("FIXME", 100));
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let render = |dir: &Path| -> String {
let out = Command::new(BIN)
.args(["render", "obsidian", "--out", "vault"])
.current_dir(dir)
.env("ROTEIRO_HOME", dir)
.output()
.expect("run render");
assert!(out.status.success(), "render obsidian failed: {out:?}");
std::fs::read_to_string(dir.join("vault/_Home.md")).expect("_Home.md")
};
let home = render(&dir);
assert!(
home.contains("| fixme | 1 |") && home.contains("| todo | 1 |"),
"both markers counted with no ignore config: {home}"
);
assert!(
home.contains("vendor/dep.rs") && home.contains("src/lib.rs"),
"both files ranked with no ignore config: {home}"
);
write(&dir, "roteiro.toml", "[debt]\nignore = [\"vendor/**\"]\n");
let home = render(&dir);
assert!(
!home.contains("fixme"),
"ignored marker must not reach the category totals: {home}"
);
assert!(
home.contains("| todo | 1 |"),
"the marker still in scope is still counted: {home}"
);
assert!(
!home.contains("vendor"),
"ignored file must not reach the density table: {home}"
);
assert!(
home.contains("src/lib.rs"),
"the file still in scope is still ranked: {home}"
);
std::fs::remove_dir_all(&dir).ok();
}
const CURRENT_PAGE: &str = "<marked current, not a link>";
fn bar_entries(html: &str) -> Vec<(String, String)> {
let Some(start) = html.find("<nav class=\"sitenav\">") else {
return Vec::new();
};
let bar = &html[start..];
let bar = &bar[..bar.find("</nav>").map_or(bar.len(), |i| i)];
let mut entries = Vec::new();
let mut rest = bar;
while let Some(open) = rest.find('<') {
let Some((tag, after)) = rest[open + 1..].split_once('>') else {
break;
};
let label: String = after.chars().take_while(|c| *c != '<').collect();
let label = label.trim().to_owned();
if !label.is_empty() {
let dest = tag
.split_once("href=\"")
.and_then(|(_, r)| r.split_once('"'))
.map_or_else(|| CURRENT_PAGE.to_owned(), |(href, _)| href.to_owned());
entries.push((label, dest));
}
rest = after;
}
entries
}
fn bar_as_seen_from(html: &str, own_href: &str) -> Vec<(String, String)> {
let entries = bar_entries(html);
let marked = entries
.iter()
.filter(|(_, dest)| dest == CURRENT_PAGE)
.count();
assert_eq!(
marked, 1,
"the bar on {own_href} marks {marked} pages as current, expected exactly 1: {entries:?}"
);
entries
.into_iter()
.map(|(label, dest)| {
if dest == CURRENT_PAGE {
(label, own_href.to_owned())
} else {
(label, dest)
}
})
.collect()
}
#[test]
fn a_declared_site_page_is_emitted_with_the_shared_bar() {
let dir = fresh_dir("sitepage");
git(&dir, &["init", "-q"]);
write(&dir, "website/public/style.css", "body{color:#111}\n");
write(&dir, "website/public/index.html", "<h1>Home</h1>\n");
write(
&dir,
"docs/adr/0001-example.md",
"---\nadr-id: \"0001\"\nstatus: Accepted\n---\n\n# ADR-0001: Example\n",
);
write(
&dir,
"website/pages/modes.md",
"---\nsite-page: modes\nsite-nav: Modes\nsite-order: 2\n---\n\n\
# The five ways to run it {#modes}\n\n## Offline mode\n\nNo models, no network.\n",
);
write(
&dir,
"docs/GUIDE.md",
"---\nsite-page: guide\nsite-nav: Guide\nsite-order: 1\n---\n\n\
# A guide\n\nSequenced in [the plan](BUILD_PLAN_V2.md).\n",
);
write(
&dir,
"docs/BUILD_PLAN_V2.md",
"---\nsite-page: build-plan-v2\nsite-nav: Roadmap\nsite-order: 3\n---\n\n# Roadmap\n",
);
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let out = Command::new(BIN)
.args(["render", "docs", "--out", "site"])
.current_dir(&dir)
.output()
.expect("run render");
assert!(out.status.success(), "render failed: {out:?}");
let site = dir.join("site");
let modes = std::fs::read_to_string(site.join("modes.html")).expect("modes page");
assert!(
site.join("guide.html").exists(),
"docs/GUIDE.md → guide.html"
);
assert!(site.join("build-plan-v2.html").exists());
assert!(
modes.contains("id=\"modes\""),
"explicit anchor preserved: {modes}"
);
assert!(!modes.contains("site-page"), "frontmatter is not content");
let expected: Vec<(String, String)> = [
("Home", "./"),
("Guide", "guide.html"),
("Modes", "modes.html"),
("Roadmap", "build-plan-v2.html"),
]
.into_iter()
.map(|(label, dest)| (label.to_owned(), dest.to_owned()))
.collect();
for page in ["modes.html", "guide.html", "build-plan-v2.html"] {
let html = std::fs::read_to_string(site.join(page)).expect("page");
assert_eq!(bar_as_seen_from(&html, page), expected, "bar on {page}");
}
assert!(
modes.contains("<span aria-current=\"page\">Modes</span>"),
"current page unlinked: {modes}"
);
let guide = std::fs::read_to_string(site.join("guide.html")).expect("guide page");
assert!(
guide.contains("href=\"build-plan-v2.html\""),
"link resolved to the published slug: {guide}"
);
assert!(
!guide.contains("BUILD_PLAN_V2.html"),
"the file-name guess is gone: {guide}"
);
}
#[test]
fn a_source_link_is_aimed_at_the_repository_at_the_rendered_commit() {
let dir = fresh_dir("sourcelink");
git(&dir, &["init", "-q"]);
write(&dir, "website/public/index.html", "<h1>Home</h1>\n");
write(
&dir,
"docs/adr/0001-example.md",
"---\nadr-id: \"0001\"\nstatus: Accepted\n---\n\n# ADR-0001: Example\n\n\
Root config: [Cargo](../../Cargo.toml).\n",
);
write(
&dir,
"docs/BUILD_PLAN.md",
"# Build Plan\n\nEvidence: [sync](../crates/x/src/sync.rs).\n\n\
Site link: [adrs](adr/).\n",
);
write(&dir, "crates/x/src/sync.rs", "pub fn f() {}\n");
write(&dir, "Cargo.toml", "[workspace]\n");
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
git(&dir, &["remote", "add", "origin", "git@github.com:o/r.git"]);
let sha = String::from_utf8(
Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&dir)
.output()
.expect("rev-parse")
.stdout,
)
.expect("utf8");
let sha = sha.trim();
let out = Command::new(BIN)
.args(["render", "docs", "--out", "site"])
.current_dir(&dir)
.output()
.expect("run render");
assert!(out.status.success(), "render failed: {out:?}");
let plan = std::fs::read_to_string(dir.join("site/build-plan.html")).expect("plan page");
assert!(
plan.contains(&format!(
"href=\"https://github.com/o/r/blob/{sha}/crates/x/src/sync.rs\""
)),
"pinned to the rendered commit, not to a branch: {plan}"
);
assert!(plan.contains("href=\"adr/\""), "{plan}");
let adr = std::fs::read_to_string(dir.join("site/adr/0001-example.html")).expect("adr page");
assert!(
adr.contains(&format!(
"href=\"https://github.com/o/r/blob/{sha}/Cargo.toml\""
)),
"{adr}"
);
}
#[test]
fn without_an_origin_remote_a_source_link_is_left_as_authored() {
let dir = fresh_dir("noorigin");
git(&dir, &["init", "-q"]);
write(&dir, "website/public/index.html", "<h1>Home</h1>\n");
write(
&dir,
"docs/adr/0001-example.md",
"---\nadr-id: \"0001\"\nstatus: Accepted\n---\n\n# ADR-0001: Example\n",
);
write(
&dir,
"docs/BUILD_PLAN.md",
"# Build Plan\n\nEvidence: [sync](../crates/x/src/sync.rs).\n",
);
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let out = Command::new(BIN)
.args(["render", "docs", "--out", "site"])
.current_dir(&dir)
.output()
.expect("run render");
assert!(out.status.success(), "render failed: {out:?}");
let plan = std::fs::read_to_string(dir.join("site/build-plan.html")).expect("plan page");
assert!(!plan.contains("github.com"), "nothing invented: {plan}");
assert!(
plan.contains("href=\"../crates/x/src/sync.rs\""),
"left exactly as authored: {plan}"
);
}
#[test]
fn the_landing_pages_bar_is_rendered_over_whatever_the_file_carried() {
let dir = fresh_dir("landingnav");
git(&dir, &["init", "-q"]);
write(
&dir,
"website/public/index.html",
"<h1>Roteiro</h1>\n<nav class=\"sitenav\">\n<a href=\"gone.html\">Gone</a>\n</nav>\n\
<p>tail</p>\n",
);
write(
&dir,
"docs/adr/0001-example.md",
"---\nadr-id: \"0001\"\nstatus: Accepted\n---\n\n# ADR-0001: Example\n",
);
write(
&dir,
"website/pages/modes.md",
"---\nsite-page: modes\nsite-nav: Modes\nsite-order: 1\n---\n\n# Modes\n",
);
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let out = Command::new(BIN)
.args(["render", "docs", "--out", "site"])
.current_dir(&dir)
.output()
.expect("run render");
assert!(out.status.success(), "render failed: {out:?}");
let landing = std::fs::read_to_string(dir.join("site/index.html")).expect("landing page");
assert!(
!landing.contains("gone.html"),
"the hand-written list is overwritten, not merged: {landing}"
);
assert_eq!(
bar_as_seen_from(&landing, "./"),
vec![
("Home".to_owned(), "./".to_owned()),
("Modes".to_owned(), "modes.html".to_owned()),
],
"the landing page carries the computed bar: {landing}"
);
assert!(landing.starts_with("<h1>Roteiro</h1>\n"), "{landing}");
assert!(landing.ends_with("<p>tail</p>\n"), "{landing}");
}
#[test]
fn the_landing_page_carries_the_bar_the_renderer_emits() {
let out_dir = std::env::temp_dir().join(format!("roteiro-website-bar-{}", std::process::id()));
std::fs::remove_dir_all(&out_dir).ok();
let out = Command::new(BIN)
.args(["render", "docs", "--out"])
.arg(&out_dir)
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.expect("run render");
assert!(out.status.success(), "render failed: {out:?}");
let landing = std::fs::read_to_string(out_dir.join("index.html")).expect("landing page");
let landing_bar = bar_as_seen_from(&landing, "./");
assert!(
!landing_bar.is_empty(),
"the landing page carries a site bar"
);
let rendered = std::fs::read_dir(&out_dir)
.expect("read site")
.filter_map(Result::ok)
.map(|e| e.path())
.find(|p| {
p.extension().and_then(|e| e.to_str()) == Some("html")
&& p.file_name().and_then(|n| n.to_str()) != Some("index.html")
&& std::fs::read_to_string(p).is_ok_and(|h| h.contains("<nav class=\"sitenav\">"))
})
.expect("at least one rendered site page");
let rendered_href = rendered
.file_name()
.and_then(|n| n.to_str())
.expect("rendered page name");
let emitted = bar_as_seen_from(
&std::fs::read_to_string(&rendered).expect("rendered page"),
rendered_href,
);
assert_eq!(
landing_bar,
emitted,
"website/public/index.html's site bar disagrees with the bar {} carries \
(labels *and* destinations are compared)",
rendered.display()
);
std::fs::remove_dir_all(&out_dir).ok();
}