1 //! Footer provenance line (m6, f0rge issue #41):2 //! every page extends the existing footer block with a single muted line3 //! (`generated from a snapshot of a repo and its f0rge` + branch@sha).4 //!5 //! Builds a small fixture repo with the real binary and string-asserts on6 //! the emitted HTML + `static/style.css` (no rendering engine needed).7 8 use std::sync::atomic::{AtomicU64, Ordering};9 10 static SEQ: AtomicU64 = AtomicU64::new(0);11 12 use std::path::{Path, PathBuf};13 use std::process::Command;14 15 fn bin() -> &'static str {16 env!("CARGO_BIN_EXE_hu13")17 }18 19 fn manifest() -> PathBuf {20 PathBuf::from(env!("CARGO_MANIFEST_DIR"))21 }22 23 fn fresh(prefix: &str) -> PathBuf {24 let dir = std::env::temp_dir().join(format!(25 "hu13-provenance-{prefix}-{}-{}",26 std::process::id(),27 std::time::SystemTime::now()28 .duration_since(std::time::UNIX_EPOCH)29 .map(|d| d.as_nanos())30 .unwrap_or(0)31 .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))32 ));33 std::fs::create_dir_all(&dir).unwrap();34 dir35 }36 37 fn git(repo: &Path, args: &[&str]) {38 let st = Command::new("git")39 .arg("-C")40 .arg(repo)41 .args(args)42 .status()43 .expect("spawn git");44 assert!(st.success(), "git {args:?} failed");45 }46 47 fn git_out(repo: &Path, args: &[&str]) -> String {48 let out = Command::new("git")49 .arg("-C")50 .arg(repo)51 .args(args)52 .output()53 .expect("spawn git");54 assert!(out.status.success(), "git {args:?} failed");55 String::from_utf8_lossy(&out.stdout).trim().to_string()56 }57 58 /// Fixture with two commits so list + detail pages exist.59 fn fixture_repo() -> PathBuf {60 let dir = fresh("repo");61 git(&dir, &["init", "-b", "main"]);62 git(&dir, &["config", "user.name", "hu13-test"]);63 git(&dir, &["config", "user.email", "[email protected]"]);64 git(&dir, &["config", "commit.gpgsign", "false"]);65 std::fs::write(dir.join("README.md"), "# provenance fixture\n\nhello\n").unwrap();66 std::fs::write(dir.join("hello.txt"), "hello\n").unwrap();67 git(&dir, &["add", "-A"]);68 git(&dir, &["commit", "-m", "first"]);69 std::fs::write(dir.join("hello.txt"), "hello again\n").unwrap();70 git(&dir, &["add", "-A"]);71 git(&dir, &["commit", "-m", "second"]);72 dir73 }74 75 fn footer_of(html: &str) -> &str {76 let start = html.find("<footer").expect("footer open");77 let end = html.find("</footer>").expect("footer close") + "</footer>".len();78 &html[start..end]79 }80 81 #[test]82 fn footer_provenance_on_index_and_detail_pages() {83 let repo = fixture_repo();84 let sha = git_out(&repo, &["rev-parse", "--short", "HEAD"]);85 assert!(!sha.is_empty(), "fixture has a short sha");86 let out = fresh("out");87 let st = Command::new(bin())88 .arg("build")89 .arg(&repo)90 .arg("-o")91 .arg(&out)92 .status()93 .expect("spawn hu13 build");94 assert!(st.success(), "fixture build exits 0");95 96 // One commit-detail page exists.97 let commit_page = std::fs::read_dir(out.join("commit"))98 .expect("commit dir")99 .filter_map(|e| e.ok())100 .map(|e| e.path())101 .find(|p| p.extension().is_some_and(|x| x == "html"))102 .expect("one commit page");103 104 // Index + a detail page (commit detail) + breadth across tabs.105 let mut pages = vec![106 out.join("index.html"),107 commit_page,108 out.join("commits.html"),109 out.join("files").join("hello.txt.html"),110 out.join("issues/index.html"),111 out.join("agents.html"),112 out.join("new-issue.html"),113 ];114 // Wiki/releases tabs render even when empty (deep links keep working).115 pages.push(out.join("wiki/index.html"));116 pages.push(out.join("releases/index.html"));117 for page in &pages {118 let html =119 std::fs::read_to_string(page).unwrap_or_else(|_| panic!("read {}", page.display()));120 assert_eq!(121 html.matches("<footer").count(),122 1,123 "{} extends the existing footer (exactly one)",124 page.display()125 );126 assert!(127 html.contains("<footer class=\"site-footer\">"),128 "{} keeps the footer block",129 page.display()130 );131 assert!(132 html.contains("generated from a snapshot of a repo and its f0rge"),133 "{} carries the provenance line",134 page.display()135 );136 assert!(137 html.contains(&format!("main @ {sha}")),138 "{} names the built snapshot",139 page.display()140 );141 // Muted, single line: no spacers inside the footer.142 let footer = footer_of(&html);143 assert!(144 footer.contains("muted"),145 "{} footer is muted",146 page.display()147 );148 assert!(149 !footer.contains("<br"),150 "{} footer is a single line",151 page.display()152 );153 }154 }155 156 #[test]157 fn footer_styling_is_muted_and_tiny() {158 let css = std::fs::read_to_string(manifest().join("static").join("style.css"))159 .expect("read style.css");160 assert!(css.contains("footer.site-footer"), "footer block styled");161 assert!(162 css.contains(".footer-provenance"),163 "provenance line has its own rule"164 );165 assert!(css.contains(".muted"), "muted tone reused, not a new color");166 }