tests/clean_out.rs 6.4 KiB 182 lines Raw
1 //! f0rge #67: a reused `-o DIR` must not keep serving pages whose source2 //! (issue, file, release) was deleted. Every build starts from an empty3 //! output dir, and the guard refuses to delete the repo or its ancestors.4 5 use std::sync::atomic::{AtomicU64, Ordering};6 7 static SEQ: AtomicU64 = AtomicU64::new(0);8 9 use std::path::{Path, PathBuf};10 use std::process::Command;11 12 fn git(repo: &Path, args: &[&str]) {13     let st = Command::new("git")14         .arg("-C")15         .arg(repo)16         .args(args)17         .status()18         .expect("spawn git");19     assert!(st.success(), "git {args:?} failed");20 }21 22 fn fresh_dir(prefix: &str) -> PathBuf {23     let dir = std::env::temp_dir().join(format!(24         "hu13-clean-{prefix}-{}-{}",25         std::process::id(),26         std::time::SystemTime::now()27             .duration_since(std::time::UNIX_EPOCH)28             .map(|d| d.as_nanos())29             .unwrap_or(0)30             .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))31     ));32     std::fs::create_dir_all(&dir).unwrap();33     dir34 }35 36 fn build(repo: &Path, out: &Path) -> std::process::Output {37     Command::new(env!("CARGO_BIN_EXE_hu13"))38         .arg("build")39         .arg(repo)40         .arg("-o")41         .arg(out)42         .output()43         .expect("spawn hu13 build")44 }45 46 #[test]47 fn rebuild_removes_stale_pages() {48     let repo = fresh_dir("repo");49     let out = fresh_dir("out");50     git(&repo, &["init", "-b", "main"]);51     git(&repo, &["config", "user.name", "hu13-test"]);52     git(&repo, &["config", "user.email", "[email protected]"]);53     git(&repo, &["config", "commit.gpgsign", "false"]);54     std::fs::write(repo.join("README.md"), "# clean\n").unwrap();55     std::fs::write(repo.join("keep.txt"), "keep\n").unwrap();56     let issue = repo.join(".forge/issues/99999999-9999-4999-8999-999999999999");57     std::fs::create_dir_all(&issue).unwrap();58     std::fs::write(59         issue.join("issue.md"),60         "---\nid: 99999999-9999-4999-8999-999999999999\ntitle: gone soon\n\61          state: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",62     )63     .unwrap();64     git(&repo, &["add", "-A"]);65     git(&repo, &["commit", "-m", "one"]);66 67     let first = build(&repo, &out);68     assert!(69         first.status.success(),70         "first build: {}",71         String::from_utf8_lossy(&first.stderr)72     );73     let page = out.join("issues/99999999-9999-4999-8999-999999999999.html");74     assert!(page.is_file(), "issue page written");75     assert!(out.join("raw/keep.txt").is_file());76 77     // Delete the issue + a file, rebuild into the same out dir.78     std::fs::remove_dir_all(repo.join(".forge/issues/99999999-9999-4999-8999-999999999999"))79         .unwrap();80     std::fs::remove_file(repo.join("keep.txt")).unwrap();81     std::fs::write(repo.join("README.md"), "# clean two\n").unwrap();82     git(&repo, &["add", "-A"]);83     git(&repo, &["commit", "-m", "two"]);84 85     let second = build(&repo, &out);86     assert!(87         second.status.success(),88         "second build: {}",89         String::from_utf8_lossy(&second.stderr)90     );91     assert!(!page.exists(), "stale issue page must be gone");92     assert!(93         !out.join("raw/keep.txt").exists(),94         "stale raw file must be gone"95     );96     assert!(out.join("raw/README.md").is_file(), "fresh output present");97 }98 99 #[test]100 fn refuses_to_clean_repo_or_its_parent() {101     let repo = fresh_dir("repo");102     git(&repo, &["init", "-b", "main"]);103     git(&repo, &["config", "user.name", "hu13-test"]);104     git(&repo, &["config", "user.email", "[email protected]"]);105     git(&repo, &["config", "commit.gpgsign", "false"]);106     std::fs::write(repo.join("README.md"), "# keep\n").unwrap();107     git(&repo, &["add", "-A"]);108     git(&repo, &["commit", "-m", "one"]);109     let sender = repo.join("src.txt");110     std::fs::write(&sender, "must survive\n").unwrap();111 112     for out in [repo.clone(), repo.parent().unwrap().to_path_buf()] {113         let res = build(&repo, &out);114         assert!(115             !res.status.success(),116             "must refuse to clean {}",117             out.display()118         );119         let err = String::from_utf8_lossy(&res.stderr);120         assert!(err.contains("refusing"), "stderr: {err}");121         assert!(repo.join("README.md").is_file(), "repo untouched");122         assert!(sender.is_file(), "repo untouched");123     }124 }125 126 #[test]127 fn refuses_relative_dotdot_output_and_keeps_parent() {128     let root = fresh_dir("dotdot");129     let repo = root.join("repo");130     std::fs::create_dir_all(repo.join("src")).unwrap();131     git(&repo, &["init", "-b", "main"]);132     git(&repo, &["config", "user.name", "hu13-test"]);133     git(&repo, &["config", "user.email", "[email protected]"]);134     git(&repo, &["config", "commit.gpgsign", "false"]);135     std::fs::write(repo.join("README.md"), "# keep\n").unwrap();136     std::fs::write(repo.join("src/main.rs"), "fn main() {}\n").unwrap();137     git(&repo, &["add", "-A"]);138     git(&repo, &["commit", "-m", "one"]);139     let canary = root.join("canary.txt");140     std::fs::write(&canary, "must survive\n").unwrap();141 142     let res = Command::new(env!("CARGO_BIN_EXE_hu13"))143         .arg("build")144         .arg(&repo)145         .arg("-o")146         .arg("..")147         .current_dir(&repo)148         .output()149         .expect("spawn hu13 build");150     assert!(!res.status.success(), "relative .. must be refused");151     let err = String::from_utf8_lossy(&res.stderr);152     assert!(err.contains("refusing"), "stderr: {err}");153     assert!(canary.is_file(), "parent canary must survive");154     assert!(repo.join("README.md").is_file(), "repo must survive");155 }156 157 #[cfg(unix)]158 #[test]159 fn refuses_symlinked_ancestor_into_repo() {160     let root = fresh_dir("symlink");161     let repo = root.join("repo");162     std::fs::create_dir_all(repo.join("src")).unwrap();163     git(&repo, &["init", "-b", "main"]);164     git(&repo, &["config", "user.name", "hu13-test"]);165     git(&repo, &["config", "user.email", "[email protected]"]);166     git(&repo, &["config", "commit.gpgsign", "false"]);167     std::fs::write(repo.join("README.md"), "# keep\n").unwrap();168     std::fs::write(repo.join("src/main.rs"), "fn main() {}\n").unwrap();169     git(&repo, &["add", "-A"]);170     git(&repo, &["commit", "-m", "one"]);171     let alias = root.join("alias");172     std::os::unix::fs::symlink(&repo, &alias).unwrap();173 174     let res = build(&repo, &alias.join("src"));175     assert!(!res.status.success(), "symlinked src must be refused");176     let err = String::from_utf8_lossy(&res.stderr);177     assert!(err.contains("refusing"), "stderr: {err}");178     assert!(179         repo.join("src/main.rs").is_file(),180         "tracked source must survive"181     );182 }