use super::*;
#[test]
fn usage_help_explains_defaults_and_exclusive_choices() {
let dir = TempDir::new().unwrap();
let refresh = cli(dir.path(), &["cache", "refresh", "--help"], "");
assert!(refresh.status.success());
let refresh_usage = "Usage: cockup cache refresh [NAME|--ingredients|--catalog]";
assert!(text(&refresh).contains(refresh_usage), "{}", text(&refresh));
assert!(
text(&refresh)
.contains("With no choice, refresh all downloaded ingredients and the catalog.")
);
let conflict = cli(
dir.path(),
&["cache", "refresh", "--ingredients", "--catalog"],
"",
);
assert_eq!(conflict.status.code(), Some(2));
assert!(
text(&conflict).contains(refresh_usage),
"{}",
text(&conflict)
);
let restore = cli(dir.path(), &["restore", "--help"], "");
assert!(restore.status.success());
let restore_usage =
"Usage: cockup restore [OPTIONS] [--override|--skip-existing] <CONFIG_FILE>";
assert!(text(&restore).contains(restore_usage), "{}", text(&restore));
let conflict = cli(
dir.path(),
&["restore", "config.yaml", "--override", "--skip-existing"],
"",
);
assert_eq!(conflict.status.code(), Some(2));
assert!(
text(&conflict).contains(restore_usage),
"{}",
text(&conflict)
);
assert!(
text(&restore)
.contains("By default, ask before replacing conflicting local files and symlinks.")
);
for (args, description) in [
(&["backup", "--help"][..], "Path to the YAML configuration"),
(&["verify", "--help"][..], "Path to the YAML configuration"),
(&["diff", "--help"][..], "Path to the YAML configuration"),
(
&["hook", "--help"][..],
"Select hooks interactively when omitted",
),
(
&["detect", "--help"][..],
"Detect all installed casks when omitted",
),
(
&["ingredient", "search", "--help"][..],
"Case-insensitive ingredient name search",
),
(
&["cache", "show", "--help"][..],
"Name of the cached ingredient",
),
(
&["cache", "delete", "--help"][..],
"One or more cached ingredient names",
),
] {
let out = cli(dir.path(), args, "");
assert!(out.status.success());
assert!(text(&out).contains(description), "{}", text(&out));
}
}
#[test]
fn cli_exposes_commands_and_version() {
let dir = TempDir::new().unwrap();
let out = cli(dir.path(), &["--help"], "");
assert!(out.status.success());
for word in [
"backup",
"restore",
"diff",
"hook",
"detect",
"template",
"completions",
] {
assert!(text(&out).contains(word), "{}", text(&out));
}
let version = cli(dir.path(), &["--version"], "");
assert!(version.status.success(), "{}", text(&version));
assert_eq!(
String::from_utf8_lossy(&version.stdout).trim(),
concat!("cockup ", env!("CARGO_PKG_VERSION"))
);
assert_eq!(cli(dir.path(), &["list"], "").status.code(), Some(2));
assert_eq!(cli(dir.path(), &["unknown"], "").status.code(), Some(2));
}
#[test]
fn completions_print_scripts_for_supported_shells() {
let dir = TempDir::new().unwrap();
for shell in ["bash", "zsh", "fish", "powershell", "elvish"] {
let out = cli(dir.path(), &["completions", shell], "");
assert!(out.status.success(), "{shell}: {}", text(&out));
assert!(out.stderr.is_empty(), "{shell}: {}", text(&out));
let script = String::from_utf8(out.stdout).unwrap();
assert!(script.contains("cockup"), "{shell}: {script}");
assert!(script.contains("backup"), "{shell}: {script}");
}
let unsupported = cli(dir.path(), &["completions", "unknown"], "");
assert_eq!(unsupported.status.code(), Some(2));
}
#[test]
fn template_prints_commented_complete_valid_yaml() {
let dir = TempDir::new().unwrap();
let out = cli(dir.path(), &["template"], "");
assert!(out.status.success(), "{}", text(&out));
assert!(out.stderr.is_empty());
let yaml = String::from_utf8(out.stdout).unwrap();
for field in [
"symlinks:",
"destination:",
"rules:",
"clean:",
"metadata:",
"include:",
"file:",
"wrap:",
"from:",
"targets:",
"to:",
"on-start:",
"on-end:",
"hooks:",
"pre-backup:",
"post-backup:",
"pre-restore:",
"post-restore:",
"name:",
"command:",
"check:",
"output:",
"timeout:",
"env:",
] {
assert!(yaml.contains(field), "missing {field}");
}
assert!(yaml.contains("Required:"));
assert!(yaml.contains("Optional:"));
write(dir.path(), "config.yaml", &yaml);
let loaded = cli(dir.path(), &["backup", "config.yaml", "-a"], "");
assert!(loaded.status.success(), "{}", text(&loaded));
}
#[test]
fn approve_hooks_accepts_only_approve_hooks_and_a_flags() {
let dir = TempDir::new().unwrap();
write(
dir.path(),
"config.yaml",
"symlinks: reference\ndestination: backup\nrules: []\n",
);
for flag in ["--approve-hooks", "-a"] {
let out = cli(dir.path(), &["backup", "config.yaml", flag], "");
assert!(out.status.success(), "{}", text(&out));
}
let help = cli(dir.path(), &["backup", "--help"], "");
assert!(text(&help).contains("--approve-hooks"));
assert!(text(&help).contains("Run configured hooks without asking"));
for flag in ["--quiet", "-q", "--yes", "-y"] {
let out = cli(dir.path(), &["backup", "config.yaml", flag], "");
assert_eq!(out.status.code(), Some(2), "{}", text(&out));
}
}
#[test]
fn no_arguments_shows_help_successfully() {
let d = TempDir::new().unwrap();
let out = cli(d.path(), &[], "");
assert!(out.status.success(), "{}", text(&out));
assert!(text(&out).contains("backup"));
}
#[test]
fn documented_sample_checks_have_expected_exit_statuses() {
let d = TempDir::new().unwrap();
let p = d.path();
write(p, "config.yaml", include_str!("../../sample/hooks.yaml"));
let pass = cli(
p,
&["hook", "config.yaml", "-a", "--name", "Check created file"],
"",
);
assert!(pass.status.success(), "{}", text(&pass));
assert!(text(&pass).contains("Check passed"));
assert!(!p.join(".hook-check-result.txt").exists());
let fail = cli(
p,
&["hook", "config.yaml", "-a", "--name", "Check failure"],
"",
);
assert_eq!(fail.status.code(), Some(1));
assert!(text(&fail).contains("Completed 0/1 hook. Error: 1 hook failed."));
}
#[test]
fn backup_and_restore_progress_use_green_with_color_enabled() {
let d = TempDir::new().unwrap();
let p = d.path();
write(p, "source/file", "content");
write(
p,
"config.yaml",
"symlinks: reference\ndestination: backup\nrules:\n - from: source\n targets: [file]\n to: files\n",
);
for command in ["backup", "restore"] {
let out = Command::new(env!("CARGO_BIN_EXE_cockup"))
.current_dir(p)
.args([command, "config.yaml", "-a"])
.env_remove("NO_COLOR")
.env("FORCE_COLOR", "1")
.output()
.unwrap();
assert!(out.status.success(), "{}", text(&out));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("\x1b[36;1m=> \x1b[0m\x1b[32;1m"),
"{stdout}"
);
assert!(
stdout.contains("\x1b[1mFile copied:\x1b[0m ")
|| stdout.contains("\x1b[1mFile existed, updating:\x1b[0m "),
"{stdout}"
);
let summary = if command == "backup" {
"Backup completed."
} else {
"Restore completed."
};
assert!(stdout.contains(summary), "{stdout}");
assert!(stdout.contains("\x1b[0m"), "{stdout}");
}
}
#[test]
fn backup_and_restore_report_original_operation_progress() {
let d = TempDir::new().unwrap();
let p = d.path();
write(p, "source/one.txt", "one");
write(p, "source/two.txt", "two");
write(p, "source/folder/item", "nested");
write(p, "backup/stale", "old");
write(
p,
"config.yaml",
"symlinks: reference\nclean: true\nmetadata: false\ndestination: backup\nrules:\n - from: source\n targets: ['*.txt', 'folder*']\n to: files\nhooks:\n post-backup:\n - name: done\n command: 'true'\n",
);
let out = cli(p, &["backup", "config.yaml", "-a"], "");
assert!(out.status.success(), "{}", text(&out));
let stdout = String::from_utf8_lossy(&out.stdout);
for expected in [
"Starting backup...",
"Clean mode enabled, will remove backup folder first if exists.",
"Found existing backup folder, removing...",
"Metadata preservation disabled.",
"Target pattern matched (2 found):",
"File copied:",
"Folder copied:",
"Running post-backup hooks...",
"Running hook (1/1): done",
"Completed 1/1 hook.",
"Backup completed.",
] {
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
}
assert!(stdout.find("Starting backup...").unwrap() < stdout.find("File copied:").unwrap());
assert!(
stdout.find("File copied:").unwrap() < stdout.find("Running post-backup hooks...").unwrap()
);
assert!(!p.join("backup/stale").exists());
let out = cli(p, &["restore", "config.yaml", "-a"], "");
assert!(out.status.success(), "{}", text(&out));
assert!(text(&out).contains("Starting restore..."));
}