tests/provenance.rs 5.3 KiB 166 lines Raw
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 }