// enough scaffolding to run ahab against a throwaway repository, hand-rolled // rather than pulled in: the crate has no dependencies it does not need, and a // dev-dependency is still something to trust and keep current. // each integration test file compiles this module separately, so a helper only // one of them needs looks unused to the others #![allow(dead_code)] use std::ffi::OsStr; use std::fmt::Write as _; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::sync::atomic::{AtomicU32, Ordering}; use std::{env, fs}; // unique per case even with the suite running in parallel static NEXT: AtomicU32 = AtomicU32::new(0); pub const REMOTE: &str = "https://git.example.org/acme/proj.git"; pub struct Case { root: PathBuf, // the data home every checkout of a case shares, as one user's would xdg: PathBuf, pub repo: PathBuf, pub store: PathBuf, pub outside: PathBuf, } impl Case { // a repository with an origin remote, a store of its own, and one commit pub fn new(label: &str) -> Self { let root = env::temp_dir().join(format!( "ahab-{label}-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); let _ = fs::remove_dir_all(&root); Self::checkout(root.clone(), root.join("xdg")) } // a second clone of the same remote, sharing the first one's data home pub fn second_checkout(&self) -> Self { Self::checkout(self.root.join("second"), self.xdg.clone()) } fn checkout(root: PathBuf, xdg: PathBuf) -> Self { let repo = root.join("repo"); let outside = root.join("outside"); for dir in [&repo, &outside] { fs::create_dir_all(dir).expect("creating the case directories"); } // `-` of the canonical checkout, as ahab spells it let canonical = fs::canonicalize(&repo).expect("resolving the repository"); let store = xdg.join("ahab/git.example.org/acme/proj").join(format!( "repo-{}", fingerprint(canonical.as_os_str().as_bytes()) )); let case = Self { root, xdg, repo, store, outside, }; case.git(&["init", "-q", "."]); case.git(&["config", "user.email", "test@example.org"]); case.git(&["config", "user.name", "test"]); case.git(&["config", "commit.gpgsign", "false"]); case.git(&["remote", "add", "origin", REMOTE]); case.write("tracked.txt", b"tracked\n"); case.git(&["add", "tracked.txt"]); case.git(&["commit", "-qm", "init"]); case } pub fn git>(&self, args: &[S]) -> Output { let out = Command::new("git") .current_dir(&self.repo) .args(args) .output() .expect("running git"); assert!( out.status.success(), "git {:?} failed: {}", args.iter() .map(|a| a.as_ref().to_string_lossy()) .collect::>(), String::from_utf8_lossy(&out.stderr) ); out } // ahab, in the repository, with a store nothing else shares pub fn ahab>(&self, args: &[S]) -> Run { let out = Command::new(env!("CARGO_BIN_EXE_ahab")) .current_dir(&self.repo) .env("XDG_DATA_HOME", &self.xdg) // the git commands ahab runs must not read the developer's own config .env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_CONFIG_SYSTEM", "/dev/null") .args(args) .output() .expect("running ahab"); Run { code: out.status.code(), stdout: out.stdout, stderr: String::from_utf8_lossy(&out.stderr).into_owned(), } } pub fn path(&self, rel: &str) -> PathBuf { self.repo.join(rel) } pub fn write(&self, rel: &str, contents: &[u8]) { let path = self.path(rel); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("creating a parent directory"); } fs::write(path, contents).expect("writing a file"); } pub fn mkdir(&self, rel: &str) { fs::create_dir_all(self.path(rel)).expect("creating a directory"); } pub fn link(&self, at: &Path, to: &Path) { if let Some(parent) = at.parent() { fs::create_dir_all(parent).expect("creating a parent directory"); } std::os::unix::fs::symlink(to, at).expect("creating a symlink"); } pub fn gitignore(&self, lines: &str) { self.write(".gitignore", lines.as_bytes()); self.git(&["add", ".gitignore"]); self.git(&["commit", "-qm", "ignore"]); } pub fn is_symlink(&self, rel: &str) -> bool { fs::symlink_metadata(self.path(rel)).is_ok_and(|meta| meta.is_symlink()) } pub fn mode(&self, path: &Path) -> u32 { use std::os::unix::fs::PermissionsExt; fs::symlink_metadata(path) .expect("reading a mode") .permissions() .mode() & 0o777 } } impl Drop for Case { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.root); } } pub struct Run { pub code: Option, pub stdout: Vec, pub stderr: String, } impl Run { pub fn out(&self) -> String { String::from_utf8_lossy(&self.stdout).into_owned() } pub fn ok(&self) -> &Self { assert_eq!(self.code, Some(0), "{}", self.report()); self } pub fn failed(&self) -> &Self { assert_ne!(self.code, Some(0), "{}", self.report()); self } pub fn code_is(&self, want: i32) -> &Self { assert_eq!(self.code, Some(want), "{}", self.report()); self } pub fn says(&self, needle: &str) -> &Self { assert!( self.out().contains(needle) || self.stderr.contains(needle), "expected {needle:?}\n{}", self.report() ); self } pub fn silent_about(&self, needle: &str) -> &Self { assert!( !self.out().contains(needle) && !self.stderr.contains(needle), "did not expect {needle:?}\n{}", self.report() ); self } fn report(&self) -> String { let mut out = String::new(); let _ = write!( out, "exit: {:?}\n--- stdout\n{}--- stderr\n{}", self.code, self.out(), self.stderr ); out } } // fnv-1a, as the store spells a checkout pub fn fingerprint(bytes: &[u8]) -> String { let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in bytes { hash ^= u64::from(*byte); hash = hash.wrapping_mul(0x100_0000_01b3); } format!("{:08x}", hash as u32) }