use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Command, Stdio};
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use assert_fs::TempDir;
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();
}
struct Fixture {
cache: TempDir,
tree: TempDir,
}
impl Fixture {
fn new() -> Self {
let cache = TempDir::new().unwrap();
let tree = TempDir::new().unwrap();
build_tree(&tree);
Fixture { cache, tree }
}
fn cmd(&self) -> Command {
snapdir(self.cache.path())
}
fn tree_path(&self) -> &Path {
self.tree.path()
}
}
fn run(fx: &Fixture, args: &[&str]) -> std::process::Output {
fx.cmd().args(args).output().expect("run snapdir")
}
fn stderr_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn stdout_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn id_rejects_transfer_flag_jobs() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["id", "--jobs", "4", &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`id --jobs` must exit 2; stderr:\n{}",
stderr_of(&out)
);
let err = stderr_of(&out).to_lowercase();
assert!(
err.contains("--jobs"),
"error must name the offending flag `--jobs`:\n{}",
stderr_of(&out)
);
assert!(
err.contains("id"),
"error must name the command `id`:\n{}",
stderr_of(&out)
);
}
#[test]
fn manifest_rejects_transfer_flag_limit_rate() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["manifest", "--limit-rate", "1M", &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`manifest --limit-rate` must exit 2; stderr:\n{}",
stderr_of(&out)
);
let err = stderr_of(&out).to_lowercase();
assert!(
err.contains("--limit-rate"),
"error must name `--limit-rate`:\n{}",
stderr_of(&out)
);
assert!(
err.contains("manifest"),
"error must name `manifest`:\n{}",
stderr_of(&out)
);
}
#[test]
fn defaults_rejects_staging_flag_keep() {
let fx = Fixture::new();
let out = run(&fx, &["defaults", "--keep"]);
assert_eq!(
out.status.code(),
Some(2),
"`defaults --keep` must exit 2; stderr:\n{}",
stderr_of(&out)
);
let err = stderr_of(&out).to_lowercase();
assert!(
err.contains("--keep"),
"error must name `--keep`:\n{}",
stderr_of(&out)
);
assert!(
err.contains("defaults"),
"error must name `defaults`:\n{}",
stderr_of(&out)
);
}
#[test]
fn diff_rejects_walk_jobs_flag() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["diff", "--walk-jobs", "2", &dir, &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`diff --walk-jobs` must exit 2; stderr:\n{}",
stderr_of(&out)
);
let err = stderr_of(&out).to_lowercase();
assert!(
err.contains("--walk-jobs"),
"error must name `--walk-jobs`:\n{}",
stderr_of(&out)
);
assert!(
err.contains("diff"),
"error must name `diff`:\n{}",
stderr_of(&out)
);
}
#[test]
fn id_help_is_scoped_no_transfer_flags() {
let fx = Fixture::new();
let out = run(&fx, &["id", "--help"]);
assert!(
out.status.success(),
"`id --help` must succeed; stderr:\n{}",
stderr_of(&out)
);
let help = stdout_of(&out);
for forbidden in ["--limit-rate", "--store", "--jobs"] {
assert!(
!help.contains(forbidden),
"`id --help` must NOT advertise `{forbidden}`:\n{help}"
);
}
}
#[test]
fn manifest_help_is_scoped_no_limit_rate() {
let fx = Fixture::new();
let out = run(&fx, &["manifest", "--help"]);
assert!(
out.status.success(),
"`manifest --help` must succeed; stderr:\n{}",
stderr_of(&out)
);
let help = stdout_of(&out);
assert!(
!help.contains("--limit-rate"),
"`manifest --help` must NOT advertise `--limit-rate`:\n{help}"
);
}
#[test]
fn push_help_advertises_transfer_flags() {
let fx = Fixture::new();
let out = run(&fx, &["push", "--help"]);
assert!(
out.status.success(),
"`push --help` must succeed; stderr:\n{}",
stderr_of(&out)
);
let help = stdout_of(&out);
for expected in ["--jobs", "--limit-rate"] {
assert!(
help.contains(expected),
"`push --help` must advertise the applicable flag `{expected}`:\n{help}"
);
}
}
#[test]
fn debug_removed_on_id() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["id", "--debug", &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`id --debug` must be an unknown-arg error (exit 2); stderr:\n{}",
stderr_of(&out)
);
assert!(
stderr_of(&out).to_lowercase().contains("--debug"),
"error must name the removed `--debug` flag:\n{}",
stderr_of(&out)
);
}
#[test]
fn debug_removed_on_manifest() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["manifest", "--debug", &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`manifest --debug` must be an unknown-arg error (exit 2); stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn debug_removed_on_defaults() {
let fx = Fixture::new();
let out = run(&fx, &["defaults", "--debug"]);
assert_eq!(
out.status.code(),
Some(2),
"`defaults --debug` must be an unknown-arg error (exit 2); stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn env_set_inapplicable_flag_is_silent_and_inert() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let baseline = fx
.cmd()
.args(["manifest", &dir])
.output()
.expect("baseline manifest");
assert!(
baseline.status.success(),
"baseline `manifest` must succeed; stderr:\n{}",
stderr_of(&baseline)
);
let with_env = fx
.cmd()
.env("SNAPDIR_LIMIT_RATE", "1M")
.args(["manifest", &dir])
.output()
.expect("manifest with SNAPDIR_LIMIT_RATE");
assert!(
with_env.status.success(),
"`manifest` with an inapplicable SNAPDIR_LIMIT_RATE env must STILL exit 0; stderr:\n{}",
stderr_of(&with_env)
);
assert_eq!(
with_env.stdout,
baseline.stdout,
"an exported inapplicable env var must not change manifest output\n\
baseline:\n{}\nwith env:\n{}",
stdout_of(&baseline),
stdout_of(&with_env)
);
}
#[test]
fn universal_flags_accepted_on_id() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
for flag_args in [
vec!["--quiet"],
vec!["--color", "auto"],
vec!["--no-progress"],
vec!["--verbose"],
] {
let mut args = vec!["id"];
args.extend(flag_args.iter().copied());
args.push(&dir);
let out = run(&fx, &args);
assert!(
out.status.success(),
"`id {flag_args:?}` must be accepted (exit 0); stderr:\n{}",
stderr_of(&out)
);
}
}
#[test]
fn universal_flags_accepted_on_manifest() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
for flag_args in [
vec!["--quiet"],
vec!["--color", "auto"],
vec!["--no-progress"],
vec!["--verbose"],
] {
let mut args = vec!["manifest"];
args.extend(flag_args.iter().copied());
args.push(&dir);
let out = run(&fx, &args);
assert!(
out.status.success(),
"`manifest {flag_args:?}` must be accepted (exit 0); stderr:\n{}",
stderr_of(&out)
);
}
}
#[test]
fn universal_flags_accepted_on_defaults() {
let fx = Fixture::new();
for flag_args in [
vec!["--quiet"],
vec!["--color", "auto"],
vec!["--no-progress"],
vec!["--verbose"],
] {
let mut args = vec!["defaults"];
args.extend(flag_args.iter().copied());
let out = run(&fx, &args);
assert!(
out.status.success(),
"`defaults {flag_args:?}` must be accepted (exit 0); stderr:\n{}",
stderr_of(&out)
);
}
}
#[test]
fn keystone_id_is_deterministic_and_unchanged() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let first = run(&fx, &["id", &dir]);
assert!(
first.status.success(),
"`id` must succeed; stderr:\n{}",
stderr_of(&first)
);
let baseline = first.stdout.clone();
let second = run(&fx, &["id", &dir]);
assert!(second.status.success(), "second `id` must succeed");
assert_eq!(
second.stdout,
baseline,
"`id` must be deterministic across runs\nfirst:\n{}\nsecond:\n{}",
stdout_of(&first),
stdout_of(&second)
);
let id = stdout_of(&first).trim().to_owned();
assert_eq!(id.len(), 64, "id must be 64 hex chars, got {id:?}");
assert!(
id.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"id must be lowercase hex: {id:?}"
);
}
#[test]
fn keystone_manifest_valid_use_still_works() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["manifest", &dir]);
assert!(
out.status.success(),
"valid `manifest <dir>` must succeed; stderr:\n{}",
stderr_of(&out)
);
assert!(
!stdout_of(&out).trim().is_empty(),
"valid `manifest <dir>` must emit a non-empty manifest"
);
}
#[test]
fn manifest_id_flag_is_not_silently_ignored() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let some_id = "0".repeat(64);
let plain = run(&fx, &["manifest", &dir]);
assert!(
plain.status.success(),
"plain `manifest <dir>` must succeed for the comparison baseline; stderr:\n{}",
stderr_of(&plain)
);
let with_id = run(&fx, &["manifest", "--id", &some_id, &dir]);
if with_id.status.success() {
assert_ne!(
with_id.stdout, plain.stdout,
"`manifest --id <ID> <dir>` silently re-walked the dir (output identical \
to plain `manifest <dir>`); --id was ignored, which is forbidden"
);
} else {
assert_ne!(
with_id.status.code(),
Some(0),
"`manifest --id` must be honored or rejected, never silently ignored"
);
}
}
#[test]
fn limit_rate_bogus_value_is_rejected() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let store = TempDir::new().unwrap();
let store_url = format!("file://{}", store.path().to_str().unwrap());
let out = run(
&fx,
&["push", "--store", &store_url, "--limit-rate", "bogus", &dir],
);
assert_ne!(
out.status.code(),
Some(0),
"`--limit-rate bogus` must be rejected (nonzero), not silently accepted; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn color_bogus_value_is_rejected() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["id", "--color", "bogus", &dir]);
assert_ne!(
out.status.code(),
Some(0),
"`--color bogus` must be rejected (nonzero), not silently accepted; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn paths_nomatch_is_not_a_silent_noop() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let full = run(&fx, &["id", &dir]);
assert!(
full.status.success(),
"unfiltered `id <dir>` must succeed for the baseline; stderr:\n{}",
stderr_of(&full)
);
let full_id = stdout_of(&full).trim().to_owned();
let filtered = run(&fx, &["id", "--paths", "does/not/match", &dir]);
if filtered.status.success() {
let filtered_id = stdout_of(&filtered).trim().to_owned();
assert_ne!(
filtered_id, full_id,
"`id --paths <nomatch>` returned the unchanged full-tree hash — \
the filter was a silent no-op, which is forbidden"
);
} else {
assert_ne!(
filtered.status.code(),
Some(0),
"`id --paths <nomatch>` must filter or be rejected, never a silent no-op"
);
}
}
fn is_unexpected_arg(stderr: &str, flag: &str) -> bool {
let s = stderr.to_lowercase();
(s.contains("unexpected argument") || s.contains("unknown argument"))
&& s.contains(&flag.to_lowercase())
}
fn file_store() -> (TempDir, String) {
let dir = TempDir::new().unwrap();
let uri = format!("file://{}", dir.path().to_str().unwrap());
(dir, uri)
}
#[test]
fn manifest_accepts_and_honors_catalog() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let catalog = TempDir::new().unwrap();
let cat = catalog
.child("catalog.redb")
.path()
.to_str()
.unwrap()
.to_owned();
let out = run(&fx, &["manifest", "--catalog", &cat, &dir]);
assert!(
out.status.success(),
"`manifest --catalog` must be accepted (exit 0); stderr:\n{}",
stderr_of(&out)
);
let locs = run(&fx, &["locations", "--catalog", &cat]);
assert!(
locs.status.success(),
"`locations --catalog` must succeed; stderr:\n{}",
stderr_of(&locs)
);
assert!(
stdout_of(&locs).contains(&dir),
"`manifest --catalog` must LOG the manifested dir; locations:\n{}",
stdout_of(&locs)
);
}
#[test]
fn stage_accepts_catalog() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let catalog = TempDir::new().unwrap();
let cat = catalog
.child("catalog.redb")
.path()
.to_str()
.unwrap()
.to_owned();
let out = run(&fx, &["stage", "--catalog", &cat, &dir]);
assert!(
out.status.success(),
"`stage --catalog` must be accepted (exit 0); stderr:\n{}",
stderr_of(&out)
);
let locs = run(&fx, &["locations", "--catalog", &cat]);
assert!(
stdout_of(&locs).contains(&dir),
"`stage --catalog` must LOG the staged dir; locations:\n{}",
stdout_of(&locs)
);
}
#[test]
fn push_accepts_catalog_flag() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let catalog = TempDir::new().unwrap();
let cat = catalog
.child("catalog.redb")
.path()
.to_str()
.unwrap()
.to_owned();
let (_store, store_uri) = file_store();
let out = run(
&fx,
&["push", "--store", &store_uri, "--catalog", &cat, &dir],
);
assert!(
!is_unexpected_arg(&stderr_of(&out), "--catalog"),
"`push --catalog` must be a known flag (TransferArgs); stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn id_rejects_catalog_flag() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let catalog = TempDir::new().unwrap();
let cat = catalog.path().to_str().unwrap().to_owned();
let out = run(&fx, &["id", "--catalog", &cat, &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`id --catalog` must be rejected (exit 2); stderr:\n{}",
stderr_of(&out)
);
assert!(
is_unexpected_arg(&stderr_of(&out), "--catalog"),
"`id --catalog` rejection must name the unexpected `--catalog`; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn id_does_not_log_even_with_catalog_env() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let catalog = TempDir::new().unwrap();
let cat = catalog
.child("catalog.redb")
.path()
.to_str()
.unwrap()
.to_owned();
let id_out = fx
.cmd()
.env("SNAPDIR_CATALOG", &cat)
.args(["id", &dir])
.output()
.expect("run id with SNAPDIR_CATALOG");
assert!(
id_out.status.success(),
"`id` with SNAPDIR_CATALOG must still exit 0; stderr:\n{}",
stderr_of(&id_out)
);
let locs = run(&fx, &["locations", "--catalog", &cat]);
assert!(
locs.status.success(),
"`locations --catalog` must succeed; stderr:\n{}",
stderr_of(&locs)
);
assert_eq!(
stdout_of(&locs).trim(),
"",
"`id` must NEVER log to the catalog; locations:\n{}",
stdout_of(&locs)
);
}
#[test]
fn objects_needed_accepts_store_flag() {
let fx = Fixture::new();
let (_store, store_uri) = file_store();
let mut child = fx
.cmd()
.args(["objects-needed", "--store", &store_uri])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn objects-needed");
child.stdin.take().unwrap().write_all(b"").unwrap();
let out = child.wait_with_output().expect("wait objects-needed");
assert!(
!is_unexpected_arg(&stderr_of(&out), "--store"),
"`objects-needed --store` must be a known flag (PlumbingArgs); stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn send_pack_accepts_store_flag() {
let fx = Fixture::new();
let (_store, store_uri) = file_store();
let mut child = fx
.cmd()
.args(["send-pack", "--store", &store_uri, "--ids", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn send-pack");
child.stdin.take().unwrap().write_all(b"").unwrap();
let out = child.wait_with_output().expect("wait send-pack");
assert!(
!is_unexpected_arg(&stderr_of(&out), "--store"),
"`send-pack --store` must be a known flag (PlumbingArgs); stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn receive_pack_accepts_store_flag() {
let fx = Fixture::new();
let (_store, store_uri) = file_store();
let mut child = fx
.cmd()
.args(["receive-pack", "--store", &store_uri])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn receive-pack");
child.stdin.take().unwrap().write_all(b"").unwrap();
let out = child.wait_with_output().expect("wait receive-pack");
assert!(
!is_unexpected_arg(&stderr_of(&out), "--store"),
"`receive-pack --store` must be a known flag (PlumbingArgs); stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn objects_needed_accepts_store_env() {
let fx = Fixture::new();
let (_store, store_uri) = file_store();
let mut child = fx
.cmd()
.env("SNAPDIR_STORE", &store_uri)
.args(["objects-needed"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn objects-needed (env store)");
child.stdin.take().unwrap().write_all(b"").unwrap();
let out = child.wait_with_output().expect("wait objects-needed");
assert!(
out.status.success(),
"`SNAPDIR_STORE=… objects-needed` (empty stdin) must exit 0; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn sync_from_falls_back_to_env() {
let fx = Fixture::new();
let (_from, from_uri) = file_store();
let (_to, to_uri) = file_store();
let out = fx
.cmd()
.env("SNAPDIR_STORE", &from_uri)
.args(["sync", "--to", &to_uri])
.output()
.expect("run sync with SNAPDIR_STORE");
let err = stderr_of(&out).to_lowercase();
assert!(
!(err.contains("--from") && (err.contains("required") || err.contains("provided"))),
"`SNAPDIR_STORE` must satisfy `sync --from`; it must not demand `--from`; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn sync_from_required_without_env() {
let fx = Fixture::new();
let (_to, to_uri) = file_store();
let out = fx
.cmd()
.env_remove("SNAPDIR_STORE")
.args(["sync", "--to", &to_uri])
.output()
.expect("run sync without store");
assert_eq!(
out.status.code(),
Some(2),
"`sync --to` with no `--from`/`SNAPDIR_STORE` must be a parse error (exit 2); stderr:\n{}",
stderr_of(&out)
);
assert!(
stderr_of(&out).to_lowercase().contains("--from"),
"the rejection must name the required `--from`; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn push_accepts_jobs_flag() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let (_store, store_uri) = file_store();
let out = run(&fx, &["push", "--store", &store_uri, "--jobs", "2", &dir]);
assert!(
!is_unexpected_arg(&stderr_of(&out), "--jobs"),
"`push --jobs` must be a known flag (TransferArgs); stderr:\n{}",
stderr_of(&out)
);
assert!(
out.status.success(),
"`push --store file://… --jobs 2 <dir>` must succeed; stderr:\n{}",
stderr_of(&out)
);
}
#[test]
fn manifest_rejects_jobs_flag() {
let fx = Fixture::new();
let dir = fx.tree_path().to_str().unwrap().to_owned();
let out = run(&fx, &["manifest", "--jobs", "2", &dir]);
assert_eq!(
out.status.code(),
Some(2),
"`manifest --jobs` must be rejected (exit 2); stderr:\n{}",
stderr_of(&out)
);
assert!(
is_unexpected_arg(&stderr_of(&out), "--jobs"),
"`manifest --jobs` rejection must name the unexpected `--jobs`; stderr:\n{}",
stderr_of(&out)
);
}