use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_reveal")
}
fn command(args: &[&str], cwd: Option<&Path>) -> Command {
let mut cmd = Command::new(bin());
cmd.args(args);
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
cmd.env_remove("REVEAL_DRY_RUN");
cmd.env_remove("REVEAL_TAB_POLICY");
cmd
}
fn run(args: &[&str], cwd: Option<&Path>, dry_run: bool) -> Output {
let mut cmd = command(args, cwd);
if dry_run {
cmd.env("REVEAL_DRY_RUN", "1");
}
cmd.output().expect("reveal binary should run")
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
fn fixture(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"reveal-cli-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn no_arguments_exits_two_with_usage() {
let out = run(&[], None, true);
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(err.contains("missing file path argument"), "{err}");
assert!(err.contains("usage: reveal"), "{err}");
}
#[test]
fn two_arguments_exit_two_with_usage() {
let out = run(&["a.txt", "b.txt"], None, true);
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(err.contains("expected exactly 1 file path argument, got 2"), "{err}");
}
#[test]
fn empty_argument_exits_two() {
let out = run(&[""], None, true);
assert_eq!(out.status.code(), Some(2));
assert!(text(&out.stderr).contains("empty"), "{}", text(&out.stderr));
}
#[test]
fn nonexistent_path_exits_two_and_names_the_path() {
let dir = fixture("missing");
let missing = dir.join("nope.txt");
let out = run(&[missing.to_str().unwrap()], Some(&dir), true);
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(err.contains("path does not exist"), "{err}");
assert!(err.contains("nope.txt"), "{err}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn nonexistent_path_without_dry_run_also_exits_two_before_touching_shell() {
let dir = fixture("missing-real");
let missing = dir.join("nope.txt");
let out = run(&[missing.to_str().unwrap()], Some(&dir), false);
assert_eq!(out.status.code(), Some(2));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn dry_run_reports_exact_path_with_spaces_and_cjk() {
let dir = fixture("cjk");
let nested = dir.join("some dir 中文目录");
std::fs::create_dir_all(&nested).unwrap();
let file = nested.join("空格 文件 中文.txt");
std::fs::write(&file, "x").unwrap();
let out = run(&[file.to_str().unwrap()], Some(&dir), true);
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
let stdout = text(&out.stdout);
assert!(stdout.contains(&format!("would reveal: {}", file.display())), "{stdout}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn dry_run_reports_exact_path_with_comma_and_ampersand() {
let dir = fixture("meta");
let file = dir.join("a,b & c.txt");
std::fs::write(&file, "x").unwrap();
let out = run(&[file.to_str().unwrap()], Some(&dir), true);
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
assert!(text(&out.stdout).contains("a,b & c.txt"), "{}", text(&out.stdout));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn dry_run_resolves_relative_path_against_process_cwd() {
let dir = fixture("relative");
let file = dir.join("inner.txt");
std::fs::write(&file, "x").unwrap();
let out = run(&["inner.txt"], Some(&dir), true);
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
let stdout = text(&out.stdout);
assert!(stdout.contains(&format!("would reveal: {}", file.display())), "{stdout}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn dry_run_accepts_directory_and_trailing_separator() {
let dir = fixture("dir");
let inner = dir.join("subdir");
std::fs::create_dir_all(&inner).unwrap();
let out = run(&[inner.to_str().unwrap()], Some(&dir), true);
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
let with_sep = format!("{}\\", inner.to_str().unwrap());
let out = run(&[&with_sep], Some(&dir), true);
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn drive_root_is_rejected_with_clear_message() {
let out = run(&["C:\\"], None, true);
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(err.contains("no containing folder"), "{err}");
}
#[test]
fn success_writes_nothing_to_stderr() {
let dir = fixture("quiet");
let file = dir.join("f.txt");
std::fs::write(&file, "x").unwrap();
let out = run(&[file.to_str().unwrap()], Some(&dir), true);
assert_eq!(out.status.code(), Some(0));
assert!(out.stderr.is_empty(), "{}", text(&out.stderr));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn unknown_tab_policy_exits_two_before_touching_the_shell() {
let dir = fixture("badpolicy");
let file = dir.join("f.txt");
std::fs::write(&file, "x").unwrap();
let out = command(&[], Some(&dir))
.arg(&file)
.env("REVEAL_TAB_POLICY", "sideways")
.output()
.expect("reveal binary should run");
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(err.contains("REVEAL_TAB_POLICY must be 'newtab' or 'overwrite'"), "{err}");
assert!(err.contains("sideways"), "{err}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn help_goes_to_stdout_and_exits_zero() {
for flag in ["--help", "-h"] {
let out = run(&[flag], None, false);
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
let stdout = text(&out.stdout);
assert!(stdout.starts_with("reveal: open a path's containing folder"), "{stdout}");
assert!(stdout.contains("REVEAL_DRY_RUN"), "{stdout}");
assert!(stdout.contains("REVEAL_TAB_POLICY"), "{stdout}");
assert!(stdout.contains("newtab"), "{stdout}");
assert!(out.stderr.is_empty(), "{}", text(&out.stderr));
}
}
#[test]
fn help_and_the_error_path_quote_the_same_usage_line() {
let help = run(&["--help"], None, false);
let usage = text(&help.stdout)
.lines()
.find(|line| line.starts_with("usage: "))
.expect("help states a usage line")
.to_owned();
let out = run(&[], None, false);
assert_eq!(out.status.code(), Some(2));
assert_eq!(
text(&out.stderr).lines().last(),
Some(usage.as_str()),
"usage in help and in errors must be one string"
);
}
#[test]
fn help_only_helps_as_the_first_argument() {
let out = run(&["--helpme"], None, true);
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(err.contains("path does not exist"), "{err}");
assert!(err.contains("--helpme"), "{err}");
let out = run(&["Cargo.toml", "--help"], None, true);
assert_eq!(out.status.code(), Some(2));
assert!(
text(&out.stderr).contains("expected exactly 1 file path argument, got 2"),
"{}",
text(&out.stderr)
);
}
#[test]
fn help_survives_a_broken_setting() {
let out = command(&["--help"], None)
.env("REVEAL_DRY_RUN", "maybe")
.env("REVEAL_TAB_POLICY", "sideways")
.output()
.expect("reveal binary should run");
assert_eq!(out.status.code(), Some(0), "{}", text(&out.stderr));
assert!(text(&out.stdout).starts_with("reveal: open a path's"), "{}", text(&out.stdout));
}
#[test]
fn a_bad_dry_run_value_is_refused_by_name() {
let dir = fixture("dryrun-bad");
let file = dir.join("f.txt");
std::fs::write(&file, "x").unwrap();
let out = command(&[], Some(&dir))
.arg(&file)
.env("REVEAL_DRY_RUN", "maybe")
.output()
.expect("reveal binary should run");
assert_eq!(out.status.code(), Some(2));
let err = text(&out.stderr);
assert!(
err.contains("REVEAL_DRY_RUN must be '1'/'0', 'true'/'false' or 'yes'/'no', got 'maybe'"),
"{err}"
);
assert!(err.contains("usage: reveal"), "{err}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn dry_run_off_spellings_reach_the_shell_instead_of_printing() {
let dir = fixture("dryrun-off");
let file = dir.join("f.txt");
std::fs::write(&file, "x").unwrap();
for off in ["0", "false", "NO", ""] {
let out = command(&[], Some(&dir))
.arg(&file)
.env("REVEAL_DRY_RUN", off)
.env("REVEAL_TAB_POLICY", "sideways")
.output()
.expect("reveal binary should run");
assert_eq!(out.status.code(), Some(2), "REVEAL_DRY_RUN={off:?}");
assert!(out.stdout.is_empty(), "REVEAL_DRY_RUN={off:?} printed a dry-run line");
assert!(text(&out.stderr).contains("REVEAL_TAB_POLICY"), "REVEAL_DRY_RUN={off:?}");
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn dry_run_on_spellings_skip_the_shell_entirely() {
let dir = fixture("dryrun-on");
let file = dir.join("f.txt");
std::fs::write(&file, "x").unwrap();
for on in ["1", "true", "YES"] {
let out = command(&[], Some(&dir))
.arg(&file)
.env("REVEAL_DRY_RUN", on)
.env("REVEAL_TAB_POLICY", "sideways")
.output()
.expect("reveal binary should run");
assert_eq!(out.status.code(), Some(0), "REVEAL_DRY_RUN={on:?}: {}", text(&out.stderr));
assert!(text(&out.stdout).contains("would reveal:"), "REVEAL_DRY_RUN={on:?}");
assert!(out.stderr.is_empty(), "REVEAL_DRY_RUN={on:?}");
}
std::fs::remove_dir_all(&dir).unwrap();
}