237 lines
6.8 KiB
Rust
237 lines
6.8 KiB
Rust
// 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");
|
|
}
|
|
|
|
// `<dir>-<hash>` 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<S: AsRef<OsStr>>(&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::<Vec<_>>(),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
out
|
|
}
|
|
|
|
// ahab, in the repository, with a store nothing else shares
|
|
pub fn ahab<S: AsRef<OsStr>>(&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<i32>,
|
|
pub stdout: Vec<u8>,
|
|
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)
|
|
}
|