#![allow(
clippy::too_many_lines,
clippy::similar_names,
clippy::items_after_statements,
clippy::doc_markdown,
clippy::manual_let_else,
clippy::map_unwrap_or,
clippy::uninlined_format_args,
clippy::manual_assert
)]
use std::path::PathBuf;
use std::process::{Command, Output};
fn snapdir_bin() -> PathBuf {
assert_cmd::cargo::cargo_bin("snapdir")
}
fn run(args: &[&str]) -> Output {
Command::new(snapdir_bin())
.args(args)
.output()
.expect("failed to run snapdir")
}
fn stdout_of(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr_of(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn shell_on_path(shell: &str) -> bool {
Command::new("which")
.arg(shell)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn autocomplete_bash_exit0_non_empty_mentions_snapdir() {
let out = run(&["autocomplete", "bash"]);
assert!(
out.status.success(),
"autocomplete bash: expected exit 0, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stdout = stdout_of(&out);
assert!(
!stdout.trim().is_empty(),
"autocomplete bash: stdout was empty"
);
assert!(
stdout.contains("snapdir"),
"autocomplete bash: script does not mention `snapdir`\nstdout (first 200 chars): {}",
&stdout[..stdout.len().min(200)],
);
}
#[test]
fn autocomplete_zsh_exit0_non_empty_mentions_snapdir() {
let out = run(&["autocomplete", "zsh"]);
assert!(
out.status.success(),
"autocomplete zsh: expected exit 0, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stdout = stdout_of(&out);
assert!(
!stdout.trim().is_empty(),
"autocomplete zsh: stdout was empty"
);
assert!(
stdout.contains("snapdir"),
"autocomplete zsh: script does not mention `snapdir`",
);
}
#[test]
fn autocomplete_fish_exit0_non_empty_mentions_snapdir() {
let out = run(&["autocomplete", "fish"]);
assert!(
out.status.success(),
"autocomplete fish: expected exit 0, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stdout = stdout_of(&out);
assert!(
!stdout.trim().is_empty(),
"autocomplete fish: stdout was empty"
);
assert!(
stdout.contains("snapdir"),
"autocomplete fish: script does not mention `snapdir`",
);
}
#[test]
fn autocomplete_powershell_exit0_non_empty_mentions_snapdir() {
let out = run(&["autocomplete", "powershell"]);
assert!(
out.status.success(),
"autocomplete powershell: expected exit 0, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stdout = stdout_of(&out);
assert!(
!stdout.trim().is_empty(),
"autocomplete powershell: stdout was empty"
);
assert!(
stdout.contains("snapdir"),
"autocomplete powershell: script does not mention `snapdir`",
);
}
#[test]
fn autocomplete_elvish_exit0_non_empty_mentions_snapdir() {
let out = run(&["autocomplete", "elvish"]);
assert!(
out.status.success(),
"autocomplete elvish: expected exit 0, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stdout = stdout_of(&out);
assert!(
!stdout.trim().is_empty(),
"autocomplete elvish: stdout was empty"
);
assert!(
stdout.contains("snapdir"),
"autocomplete elvish: script does not mention `snapdir`",
);
}
#[test]
fn autocomplete_visible_in_top_level_help() {
let out = run(&["--help"]);
let stdout = stdout_of(&out);
assert!(
stdout.contains("autocomplete"),
"`autocomplete` does not appear in `snapdir --help` output\n\
(it is hidden or not yet present)\nstdout:\n{}",
stdout,
);
}
#[test]
fn autocomplete_help_shows_bash_eval_example() {
let out = run(&["autocomplete", "--help"]);
let stdout = stdout_of(&out);
let stderr = stderr_of(&out);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("eval") || combined.contains("source"),
"`snapdir autocomplete --help` does not show an eval/source wiring example\n\
combined output:\n{}",
combined,
);
assert!(
combined.to_lowercase().contains("bash"),
"`snapdir autocomplete --help` wiring example does not mention bash\n\
combined output:\n{}",
combined,
);
}
#[test]
fn autocomplete_help_shows_zsh_eval_example() {
let out = run(&["autocomplete", "--help"]);
let stdout = stdout_of(&out);
let stderr = stderr_of(&out);
let combined = format!("{stdout}{stderr}");
assert!(
combined.to_lowercase().contains("zsh"),
"`snapdir autocomplete --help` does not mention zsh in a wiring example\n\
combined output:\n{}",
combined,
);
}
#[test]
fn autocomplete_unknown_shell_tcsh_exit2_lists_valid() {
let out = run(&["autocomplete", "tcsh"]);
assert_eq!(
out.status.code(),
Some(2),
"autocomplete tcsh: expected exit 2 (clap usage error), got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stderr = stderr_of(&out);
let lists_valid = stderr.contains("bash")
|| stderr.contains("zsh")
|| stderr.contains("fish")
|| stderr.contains("possible")
|| stderr.contains("valid");
assert!(
lists_valid,
"autocomplete tcsh: exit 2 but stderr does not list valid shells\nstderr: {}",
stderr,
);
}
#[test]
fn autocomplete_unknown_shell_notashell_exit2_lists_valid() {
let out = run(&["autocomplete", "notashell"]);
assert_eq!(
out.status.code(),
Some(2),
"autocomplete notashell: expected exit 2, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stderr = stderr_of(&out);
let lists_valid = stderr.contains("bash")
|| stderr.contains("zsh")
|| stderr.contains("fish")
|| stderr.contains("possible")
|| stderr.contains("valid");
assert!(
lists_valid,
"autocomplete notashell: exit 2 but stderr does not list valid shells\nstderr: {}",
stderr,
);
}
#[test]
fn completions_alias_still_works_and_identical_to_autocomplete() {
let new_out = run(&["autocomplete", "bash"]);
assert!(
new_out.status.success(),
"autocomplete bash: failed (prerequisite for alias check)\nstderr: {}",
stderr_of(&new_out),
);
let old_out = run(&["completions", "bash"]);
assert!(
old_out.status.success(),
"completions bash (hidden alias): expected exit 0, got {:?}\nstderr: {}",
old_out.status.code(),
stderr_of(&old_out),
);
assert_eq!(
new_out.stdout, old_out.stdout,
"completions bash and autocomplete bash differ in stdout\n\
(they must be byte-identical for release.yml back-compat)",
);
}
#[test]
fn completions_alias_zsh_identical_to_autocomplete_zsh() {
let new_out = run(&["autocomplete", "zsh"]);
assert!(
new_out.status.success(),
"autocomplete zsh: failed (prerequisite for alias check)\nstderr: {}",
stderr_of(&new_out),
);
let old_out = run(&["completions", "zsh"]);
assert!(
old_out.status.success(),
"completions zsh (hidden alias): expected exit 0, got {:?}\nstderr: {}",
old_out.status.code(),
stderr_of(&old_out),
);
assert_eq!(
new_out.stdout, old_out.stdout,
"completions zsh and autocomplete zsh differ in stdout\n\
(they must be byte-identical for release.yml back-compat)",
);
}
#[test]
fn completions_alias_is_hidden_from_top_level_help() {
let out = run(&["--help"]);
let stdout = stdout_of(&out);
assert!(
!stdout.contains("completions"),
"`completions` (hidden alias) APPEARS in `snapdir --help` — it should be hidden\n\
If visible, users see two commands for the same thing (breaks the documented surface).\n\
stdout:\n{}",
stdout,
);
}
#[test]
fn autocomplete_bash_output_passes_bash_syntax_check() {
let completion_out = run(&["autocomplete", "bash"]);
assert!(
completion_out.status.success(),
"autocomplete bash: prerequisite failed\nstderr: {}",
stderr_of(&completion_out),
);
let script = &completion_out.stdout;
let bash_result = Command::new("bash")
.arg("-n")
.arg("/dev/stdin")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
stdin.write_all(script).ok();
}
child.wait_with_output()
});
match bash_result {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(
"SKIP autocomplete_bash_output_passes_bash_syntax_check: bash not found on PATH"
);
}
Err(e) => panic!("failed to run bash -n: {e}"),
Ok(out) => {
assert!(
out.status.success(),
"bash -n rejected the autocomplete bash output (syntax error)\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
}
}
}
#[test]
fn autocomplete_zsh_output_sources_cleanly_if_zsh_present() {
if !shell_on_path("zsh") {
eprintln!(
"SKIP autocomplete_zsh_output_sources_cleanly_if_zsh_present: zsh not found on PATH"
);
return;
}
let completion_out = run(&["autocomplete", "zsh"]);
assert!(
completion_out.status.success(),
"autocomplete zsh: prerequisite failed\nstderr: {}",
stderr_of(&completion_out),
);
let script = &completion_out.stdout;
let zsh_result = Command::new("zsh")
.args([
"-c",
"autoload -Uz compinit && compinit -u && source /dev/stdin",
])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
stdin.write_all(script).ok();
}
child.wait_with_output()
});
match zsh_result {
Err(e) => panic!("failed to run zsh -c 'source /dev/stdin': {e}"),
Ok(out) => {
assert!(
out.status.success(),
"zsh source check failed for autocomplete zsh output\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
}
}
}
#[test]
fn autocomplete_fish_output_syntax_check_if_fish_present() {
if !shell_on_path("fish") {
eprintln!(
"SKIP autocomplete_fish_output_syntax_check_if_fish_present: fish not found on PATH"
);
return;
}
let completion_out = run(&["autocomplete", "fish"]);
assert!(
completion_out.status.success(),
"autocomplete fish: prerequisite failed\nstderr: {}",
stderr_of(&completion_out),
);
let tmp = std::env::temp_dir().join(format!(
"snapdir-fish-completion-{}-check.fish",
std::process::id()
));
std::fs::write(&tmp, &completion_out.stdout).expect("write fish completion to tempfile");
let fish_result = Command::new("fish")
.args(["--no-execute", tmp.to_str().unwrap()])
.output();
let _ = std::fs::remove_file(&tmp);
match fish_result {
Err(e) => panic!("failed to run fish --no-execute: {e}"),
Ok(out) => {
assert!(
out.status.success(),
"fish --no-execute rejected the autocomplete fish output\nstderr: {}",
String::from_utf8_lossy(&out.stderr),
);
}
}
}
#[test]
fn autocomplete_no_shell_arg_exit2_names_required_arg() {
let out = run(&["autocomplete"]);
assert_eq!(
out.status.code(),
Some(2),
"autocomplete with no arg: expected exit 2, got {:?}\nstderr: {}",
out.status.code(),
stderr_of(&out),
);
let stderr = stderr_of(&out);
assert!(
!stderr.trim().is_empty(),
"autocomplete with no arg: exit 2 but stderr was empty (should name missing arg)",
);
}
#[test]
fn autocomplete_uppercase_bash_deterministic_behavior() {
let out = run(&["autocomplete", "BASH"]);
let code = out.status.code();
let stdout = stdout_of(&out);
let stderr = stderr_of(&out);
match code {
Some(0) => {
assert!(
!stdout.trim().is_empty(),
"autocomplete BASH: exit 0 but stdout was empty (invalid: must emit a script or exit 2)",
);
assert!(
stdout.contains("snapdir"),
"autocomplete BASH: exit 0 with non-empty stdout but output never mentions `snapdir`\nstdout (first 200): {}",
&stdout[..stdout.len().min(200)],
);
}
Some(2) => {
let lists_valid = stderr.contains("bash")
|| stderr.contains("zsh")
|| stderr.contains("fish")
|| stderr.contains("possible")
|| stderr.contains("valid");
assert!(
lists_valid,
"autocomplete BASH: exit 2 but stderr does not list valid shells\nstderr: {}",
stderr,
);
}
other => {
panic!(
"autocomplete BASH: unexpected exit code {other:?} (must be 0 or 2)\nstdout: {stdout}\nstderr: {stderr}",
);
}
}
}
#[test]
fn autocomplete_unknown_shell_error_lists_exactly_five_shells() {
let out = run(&["autocomplete", "tcsh"]);
assert_eq!(
out.status.code(),
Some(2),
"autocomplete tcsh: expected exit 2\nstderr: {}",
stderr_of(&out),
);
let stderr = stderr_of(&out);
for shell in &["bash", "elvish", "fish", "powershell", "zsh"] {
assert!(
stderr.contains(shell),
"autocomplete tcsh: error message does not list expected shell `{shell}`\nstderr: {stderr}",
);
}
}
#[test]
fn autocomplete_bash_is_deterministic() {
let out1 = run(&["autocomplete", "bash"]);
let out2 = run(&["autocomplete", "bash"]);
assert!(
out1.status.success() && out2.status.success(),
"autocomplete bash: one or both runs failed (cannot test determinism)\n\
run1 exit={:?}, run2 exit={:?}",
out1.status.code(),
out2.status.code(),
);
assert_eq!(
out1.stdout, out2.stdout,
"autocomplete bash: two consecutive runs produced different stdout (non-deterministic!)",
);
}
#[test]
fn autocomplete_bash_script_targets_snapdir_binary() {
let out = run(&["autocomplete", "bash"]);
assert!(
out.status.success(),
"autocomplete bash: prerequisite failed\nstderr: {}",
stderr_of(&out),
);
let stdout = stdout_of(&out);
assert!(
stdout.contains("snapdir"),
"autocomplete bash script does not mention `snapdir` at all",
);
let has_bash_marker = stdout.contains("complete ") || stdout.contains("_snapdir");
assert!(
has_bash_marker,
"autocomplete bash script missing expected bash-completion marker \
(`complete ` or `_snapdir`)\nstdout (first 400 chars):\n{}",
&stdout[..stdout.len().min(400)],
);
}
#[test]
fn autocomplete_bash_happy_path_stderr_clean() {
let out = run(&["autocomplete", "bash"]);
assert!(
out.status.success(),
"autocomplete bash: expected exit 0\nstderr: {}",
stderr_of(&out),
);
let stdout = stdout_of(&out);
let stderr = stderr_of(&out);
assert!(
!stdout.trim().is_empty(),
"autocomplete bash: stdout was empty (script must go to stdout)",
);
assert!(
stdout.contains("snapdir"),
"autocomplete bash: script content not on stdout\nstdout (first 200): {}\nstderr (first 200): {}",
&stdout[..stdout.len().min(200)],
&stderr[..stderr.len().min(200)],
);
if stdout.trim().is_empty() {
panic!(
"autocomplete bash: stdout empty while stderr has content — script written to wrong fd\nstderr: {}",
stderr,
);
}
}