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;
fn snapdir(cache: &Path, catalog: &Path) -> Command {
let mut cmd = Command::cargo_bin("snapdir").expect("snapdir binary built");
cmd.env("SNAPDIR_CACHE_DIR", cache);
cmd.env("SNAPDIR_CATALOG", catalog);
cmd
}
fn build_tree(dir: &TempDir, leaf: &str) {
dir.child("a.txt").write_str(leaf).unwrap();
std::fs::set_permissions(dir.child("a.txt").path(), PermissionsExt::from_mode(0o644)).unwrap();
std::fs::set_permissions(dir.path(), PermissionsExt::from_mode(0o755)).unwrap();
}
fn stdout_ok(cache: &Path, catalog: &Path, args: &[&str]) -> String {
let out = snapdir(cache, catalog)
.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()
}
fn file_store(dir: &Path) -> String {
format!("file://{}", dir.display())
}
fn json_field<'a>(line: &'a str, key: &str) -> Option<&'a str> {
let needle = format!("\"{key}\":");
let start = line.find(&needle)? + needle.len();
let rest = &line[start..];
if let Some(stripped) = rest.strip_prefix('"') {
let end = stripped.find('"')?;
Some(&stripped[..end])
} else {
let end = rest.find([',', '}']).unwrap_or(rest.len());
Some(rest[..end].trim())
}
}
#[test]
fn catalog_commands_push_records_location_and_revision() {
let cache = TempDir::new().unwrap();
let catalog = cache.child("catalog.redb");
let store_dir = TempDir::new().unwrap();
let src = TempDir::new().unwrap();
build_tree(&src, "hello");
let src_str = src.path().to_string_lossy().into_owned();
let store = file_store(store_dir.path());
let id = stdout_ok(
cache.path(),
catalog.path(),
&["push", "--store", &store, &src_str],
);
assert_eq!(id.len(), 64, "push must print a 64-hex id: {id:?}");
let bare_id = stdout_ok(cache.path(), catalog.path(), &["id", &src_str]);
assert_eq!(id, bare_id, "pushed id must equal `snapdir id`");
let revisions = stdout_ok(
cache.path(),
catalog.path(),
&["revisions", "--location", &store],
);
let lines: Vec<&str> = revisions.lines().collect();
assert_eq!(lines.len(), 1, "exactly one revision: {revisions:?}");
assert_eq!(json_field(lines[0], "id"), Some(id.as_str()));
assert_eq!(
json_field(lines[0], "previous_id"),
Some("null"),
"first revision's previous_id is null: {:?}",
lines[0]
);
assert!(
json_field(lines[0], "created_at").is_some(),
"revision has a created_at: {:?}",
lines[0]
);
let locations = stdout_ok(cache.path(), catalog.path(), &["locations"]);
let loc_lines: Vec<&str> = locations.lines().collect();
assert_eq!(loc_lines.len(), 1, "exactly one location: {locations:?}");
assert_eq!(json_field(loc_lines[0], "location"), Some(store.as_str()));
assert_eq!(json_field(loc_lines[0], "id"), Some(id.as_str()));
}
#[test]
fn catalog_commands_second_push_lists_both_and_ancestors_walks_back() {
let cache = TempDir::new().unwrap();
let catalog = cache.child("catalog.redb");
let store_dir = TempDir::new().unwrap();
let store = file_store(store_dir.path());
let src1 = TempDir::new().unwrap();
build_tree(&src1, "first revision");
let id1 = stdout_ok(
cache.path(),
catalog.path(),
&["push", "--store", &store, &src1.path().to_string_lossy()],
);
let src2 = TempDir::new().unwrap();
build_tree(&src2, "second revision (changed)");
let id2 = stdout_ok(
cache.path(),
catalog.path(),
&["push", "--store", &store, &src2.path().to_string_lossy()],
);
assert_ne!(id1, id2, "the two trees must have distinct ids");
let revisions = stdout_ok(
cache.path(),
catalog.path(),
&["revisions", "--location", &store],
);
let lines: Vec<&str> = revisions.lines().collect();
assert_eq!(lines.len(), 2, "two revisions: {revisions:?}");
assert_eq!(
json_field(lines[0], "id"),
Some(id2.as_str()),
"newest revision first"
);
assert_eq!(json_field(lines[1], "id"), Some(id1.as_str()));
assert_eq!(json_field(lines[0], "previous_id"), Some(id1.as_str()));
let ancestors = stdout_ok(cache.path(), catalog.path(), &["ancestors", "--id", &id2]);
let anc_lines: Vec<&str> = ancestors.lines().collect();
assert_eq!(anc_lines.len(), 1, "one ancestor: {ancestors:?}");
assert_eq!(
json_field(anc_lines[0], "id"),
Some(id1.as_str()),
"ancestor id is the previous id"
);
assert_eq!(json_field(anc_lines[0], "location"), Some(store.as_str()));
}
#[test]
fn catalog_commands_unknown_location_is_empty_and_exits_zero() {
let cache = TempDir::new().unwrap();
let catalog = cache.child("catalog.redb");
let revisions = stdout_ok(
cache.path(),
catalog.path(),
&["revisions", "--location", "file:///nope/does-not-exist"],
);
assert_eq!(revisions, "", "unknown location → empty revisions");
let locations = stdout_ok(cache.path(), catalog.path(), &["locations"]);
assert_eq!(locations, "", "empty catalog → empty locations");
let ancestors = stdout_ok(
cache.path(),
catalog.path(),
&["ancestors", "--id", &"0".repeat(64)],
);
assert_eq!(ancestors, "", "unknown id → empty ancestors");
}