test: cover the store and the exit codes before moving any of it

This commit is contained in:
2026-09-09 12:27:51 +00:00
parent 2563d56595
commit d9439253fd
3 changed files with 568 additions and 0 deletions

207
tests/support/mod.rs Normal file
View File

@@ -0,0 +1,207 @@
// 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<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.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<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
}
}