// 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::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 struct Case { root: 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); let case = Self { repo: root.join("repo"), store: root .join("xdg/ahab/git.example.org/acme/proj") .to_path_buf(), outside: root.join("outside"), root, }; for dir in [&case.repo, &case.outside] { fs::create_dir_all(dir).expect("creating the case directories"); } 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", "https://git.example.org/acme/proj.git", ]); 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.root.join("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 } }