use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Command;
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use assert_fs::TempDir;
use predicates::prelude::*;
fn snapdir(cache: &Path) -> Command {
let mut cmd = Command::cargo_bin("snapdir").expect("snapdir binary built");
cmd.env("SNAPDIR_CACHE_DIR", cache);
cmd
}
fn build_tree(dir: &TempDir) {
dir.child("a.txt").write_str("hello").unwrap();
std::fs::set_permissions(dir.child("a.txt").path(), PermissionsExt::from_mode(0o644)).unwrap();
dir.child("sub/b.txt").write_str("world!!").unwrap();
std::fs::set_permissions(
dir.child("sub/b.txt").path(),
PermissionsExt::from_mode(0o600),
)
.unwrap();
std::fs::set_permissions(dir.child("sub").path(), PermissionsExt::from_mode(0o755)).unwrap();
std::fs::set_permissions(dir.path(), PermissionsExt::from_mode(0o755)).unwrap();
}
fn stdout_ok(cache: &Path, args: &[&str]) -> String {
let out = snapdir(cache).args(args).output().expect("run snapdir");
assert!(
out.status.success(),
"snapdir {args:?} failed ({:?})\nstderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr),
);
String::from_utf8(out.stdout).unwrap().trim_end().to_owned()
}
#[test]
fn id_is_64_lowercase_hex() {
let cache = TempDir::new().unwrap();
let src = TempDir::new().unwrap();
build_tree(&src);
let src_str = src.path().to_string_lossy().into_owned();
let id = stdout_ok(cache.path(), &["id", &src_str]);
assert_eq!(id.len(), 64, "snapshot id must be 64 hex chars: {id:?}");
assert!(
id.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"snapshot id must be lowercase hex: {id:?}"
);
}
#[test]
fn push_fetch_checkout_roundtrip_reproduces_id() {
let cache = TempDir::new().unwrap();
let src = TempDir::new().unwrap();
let store = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
build_tree(&src);
let src_str = src.path().to_string_lossy().into_owned();
let dest_str = dest.path().to_string_lossy().into_owned();
let store_url = format!("file://{}", store.path().display());
let src_id = stdout_ok(cache.path(), &["id", &src_str]);
let pushed = stdout_ok(cache.path(), &["push", "--store", &store_url, &src_str]);
assert_eq!(pushed, src_id, "push must print the source snapshot id");
snapdir(cache.path())
.args(["fetch", "--store", &store_url, "--id", &src_id])
.assert()
.success();
snapdir(cache.path())
.args(["checkout", "--id", &src_id, &dest_str])
.assert()
.success();
dest.child("a.txt").assert("hello");
dest.child("sub/b.txt").assert("world!!");
assert_eq!(
stdout_ok(cache.path(), &["id", &dest_str]),
src_id,
"checked-out tree must re-manifest to the source id"
);
snapdir(cache.path())
.args(["verify", "--store", &store_url, "--id", &src_id])
.assert()
.success();
}
#[test]
fn verify_purge_is_rejected() {
let cache = TempDir::new().unwrap();
let zeros = "0".repeat(64);
snapdir(cache.path())
.args([
"verify",
"--store",
"file:///tmp/nonexistent-snapdir-verify-purge",
"--id",
&zeros,
"--purge",
])
.assert()
.failure()
.stderr(predicate::str::contains("verify").and(predicate::str::contains("--purge")));
}
#[test]
fn verify_without_purge_does_not_hit_purge_error() {
let cache = TempDir::new().unwrap();
let zeros = "0".repeat(64);
snapdir(cache.path())
.args([
"verify",
"--store",
"file:///tmp/nonexistent-snapdir-verify-purge",
"--id",
&zeros,
])
.assert()
.failure()
.stderr(predicate::str::contains("does not support --purge").not());
}
#[test]
fn pull_is_fetch_plus_checkout() {
let cache = TempDir::new().unwrap();
let src = TempDir::new().unwrap();
let store = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
build_tree(&src);
let src_str = src.path().to_string_lossy().into_owned();
let dest_str = dest.path().to_string_lossy().into_owned();
let store_url = format!("file://{}", store.path().display());
let src_id = stdout_ok(cache.path(), &["push", "--store", &store_url, &src_str]);
snapdir(cache.path())
.args(["pull", "--store", &store_url, "--id", &src_id, &dest_str])
.assert()
.success();
dest.child("a.txt").assert("hello");
dest.child("sub/b.txt").assert("world!!");
assert_eq!(stdout_ok(cache.path(), &["id", &dest_str]), src_id);
}
#[test]
fn fetch_without_store_fails_with_clear_message() {
let cache = TempDir::new().unwrap();
snapdir(cache.path())
.args(["fetch", "--id", &"0".repeat(64)])
.assert()
.failure()
.stderr(predicate::str::contains("missing --store option"));
}
#[test]
fn checkout_unknown_id_fails() {
let cache = TempDir::new().unwrap();
let dest = TempDir::new().unwrap();
let dest_str = dest.path().to_string_lossy().into_owned();
snapdir(cache.path())
.args(["checkout", "--id", &"0".repeat(64), &dest_str])
.assert()
.failure();
}