use super::*;
use std::fs;
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"reveal-test-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&dir).unwrap();
dir
}
fn run_with(args: &[&str], cwd: &Path, rec: &RecordingRevealer) -> (i32, String) {
let mut err = Vec::new();
let owned: Vec<OsString> = args.iter().map(OsString::from).collect();
let code = run(&owned, cwd, rec, &mut err);
(code, String::from_utf8_lossy(&err).into_owned())
}
#[test]
fn missing_argument_is_usage_error() {
let rec = RecordingRevealer::default();
let (code, err) = run_with(&[], Path::new("C:\\"), &rec);
assert_eq!(code, EXIT_USAGE);
assert!(err.contains("missing file path argument"), "{err}");
assert!(err.contains("usage: reveal"), "{err}");
assert!(rec.recorded().is_empty());
}
#[test]
fn extra_arguments_are_usage_error() {
let rec = RecordingRevealer::default();
let (code, err) = run_with(&["a.txt", "b.txt"], Path::new("C:\\"), &rec);
assert_eq!(code, EXIT_USAGE);
assert!(err.contains("expected exactly 1 file path argument, got 2"), "{err}");
assert!(rec.recorded().is_empty());
}
#[test]
fn empty_argument_is_usage_error() {
let rec = RecordingRevealer::default();
let (code, err) = run_with(&[""], Path::new("C:\\"), &rec);
assert_eq!(code, EXIT_USAGE);
assert!(err.contains("empty"), "{err}");
assert!(rec.recorded().is_empty());
}
#[test]
fn nonexistent_path_is_usage_error_and_not_revealed() {
let dir = temp_dir("missing");
let rec = RecordingRevealer::default();
let missing = dir.join("nope.txt");
let (code, err) = run_with(&[missing.to_str().unwrap()], &dir, &rec);
assert_eq!(code, EXIT_USAGE);
assert!(err.contains("path does not exist"), "{err}");
assert!(rec.recorded().is_empty());
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn relative_path_is_resolved_against_given_cwd() {
let dir = temp_dir("relative");
let file = dir.join("inner.txt");
fs::write(&file, "x").unwrap();
let rec = RecordingRevealer::default();
let (code, _) = run_with(&["inner.txt"], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![file]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn spaces_and_cjk_survive_unchanged() {
let dir = temp_dir("cjk");
let nested = dir.join("some dir 中文目录");
fs::create_dir_all(&nested).unwrap();
let file = nested.join("空格 文件 中文.txt");
fs::write(&file, "x").unwrap();
let rec = RecordingRevealer::default();
let (code, _) = run_with(&[file.to_str().unwrap()], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![file]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn comma_and_ampersand_in_name_survive_unchanged() {
let dir = temp_dir("meta");
let file = dir.join("a,b & c.txt");
fs::write(&file, "x").unwrap();
let rec = RecordingRevealer::default();
let (code, _) = run_with(&[file.to_str().unwrap()], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![file]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn directory_input_is_revealed_as_itself() {
let dir = temp_dir("dirinput");
let inner = dir.join("subdir");
fs::create_dir_all(&inner).unwrap();
let rec = RecordingRevealer::default();
let (code, _) = run_with(&[inner.to_str().unwrap()], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![inner]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn trailing_separator_and_dotdot_are_accepted() {
let dir = temp_dir("dotdot");
let nested = dir.join("nested");
fs::create_dir_all(&nested).unwrap();
let file = nested.join("f.txt");
fs::write(&file, "x").unwrap();
let rec = RecordingRevealer::default();
let with_slash = format!("{}\\", file.to_str().unwrap());
let (code, _) = run_with(&[&with_slash], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![file]);
let rec2 = RecordingRevealer::default();
let with_dotdot = dir.join("nested").join("..").join("nested").join("f.txt");
let (code2, _) = run_with(&[with_dotdot.to_str().unwrap()], &dir, &rec2);
assert_eq!(code2, EXIT_OK);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn drive_root_has_no_parent_and_is_rejected() {
let rec = RecordingRevealer::default();
let (code, err) = run_with(&["C:\\"], Path::new("C:\\"), &rec);
assert_eq!(code, EXIT_USAGE);
assert!(err.contains("no containing folder"), "{err}");
assert!(rec.recorded().is_empty());
}
#[test]
fn shell_failure_maps_to_exit_code_one() {
struct Failing;
impl Revealer for Failing {
fn reveal(&self, _path: &Path) -> Result<(), Error> {
Err(Error::Shell {
step: "SHOpenFolderAndSelectItems",
hresult: 0x8000_4005,
})
}
}
let dir = temp_dir("shellfail");
let file = dir.join("f.txt");
fs::write(&file, "x").unwrap();
let mut err = Vec::new();
let code = run(
&[OsString::from(file.as_os_str())],
&dir,
&Failing,
&mut err,
);
assert_eq!(code, EXIT_SHELL);
let err = String::from_utf8_lossy(&err);
assert!(err.contains("SHOpenFolderAndSelectItems failed"), "{err}");
assert!(err.contains("0x80004005"), "{err}");
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn resolve_keeps_absolute_path_verbatim() {
let dir = temp_dir("abs");
let file = dir.join("f.txt");
fs::write(&file, "x").unwrap();
assert_eq!(resolve(file.as_os_str(), Path::new("C:\\elsewhere")).unwrap(), file);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn verbatim_disk_path_is_rewritten_before_the_shell_sees_it() {
let dir = temp_dir("verbatim");
let file = dir.join("目标 file.txt");
fs::write(&file, "x").unwrap();
let extended = PathBuf::from(OsString::from(format!(r"\\?\{}", file.display())));
assert_eq!(&*normalize(&extended), file.as_path());
let rec = RecordingRevealer::default();
let (code, _) = run_with(&[extended.to_str().unwrap()], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![file]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn verbatim_unc_path_is_rewritten_to_its_unc_form() {
let unc = Path::new(r"\\?\UNC\fileserver\share\dir\报告, v2.txt");
assert_eq!(&*normalize(unc), Path::new(r"\\fileserver\share\dir\报告, v2.txt"));
assert_eq!(&*normalize(Path::new(r"\\?\C:\")), Path::new(r"C:\"));
}
#[test]
fn drive_relative_path_is_anchored_before_the_shell_sees_it() {
assert_eq!(&*normalize(Path::new("C:Windows\\notepad.exe")), Path::new(r"C:\Windows\notepad.exe"));
assert_eq!(&*normalize(Path::new("C:a.txt")), Path::new(r"C:\a.txt"));
let dir = temp_dir("drive-relative");
let file = dir.join("报告 file.txt");
fs::write(&file, "x").unwrap();
let absolute = file.display().to_string();
assert_eq!(&absolute.as_bytes()[1..3], b":\\", "{absolute} is not a drive-lettered path");
let drive_relative = format!("{}:{}", &absolute[..1], &absolute[3..]);
let rec = RecordingRevealer::default();
let (code, _) = run_with(&[&drive_relative], &dir, &rec);
assert_eq!(code, EXIT_OK);
assert_eq!(rec.recorded(), vec![file]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn paths_the_namespace_cannot_represent_are_left_as_they_are() {
for untouched in [
r"\\?\Device\Harddisk0\Partition1",
r"\\.\GLOBAL\RPC",
r"C:\Windows\notepad.exe",
r"\\server\share\a.txt",
r"relative\name.txt",
] {
assert_eq!(
&*normalize(Path::new(untouched)),
Path::new(untouched),
"{untouched}"
);
}
}
#[test]
fn tab_policy_defaults_to_a_new_tab() {
assert_eq!(tab_policy(None).unwrap(), Switch::NewTab);
assert_eq!(tab_policy(Some(OsString::from("newtab"))).unwrap(), Switch::NewTab);
}
#[test]
fn tab_policy_reads_overwrite_case_insensitively() {
assert_eq!(tab_policy(Some(OsString::from("Overwrite"))).unwrap(), Switch::Overwrite);
assert_eq!(tab_policy(Some(OsString::from("OVERWRITE"))).unwrap(), Switch::Overwrite);
}
#[test]
fn tab_policy_rejects_unknown_spelling_with_both_values_named() {
let error = tab_policy(Some(OsString::from("sideways"))).unwrap_err();
let message = error.to_string();
assert!(message.contains("REVEAL_TAB_POLICY must be 'newtab' or 'overwrite'"), "{message}");
assert!(message.contains("sideways"), "{message}");
}
#[test]
fn dry_run_is_off_unless_a_value_says_otherwise() {
for off in [None, Some(""), Some("0"), Some("false"), Some("NO"), Some("False")] {
assert!(!dry_run_setting(off.map(OsString::from)).unwrap(), "{off:?}");
}
for on in [Some("1"), Some("true"), Some("YES"), Some("True")] {
assert!(dry_run_setting(on.map(OsString::from)).unwrap(), "{on:?}");
}
}
#[test]
fn dry_run_rejects_a_value_that_means_neither_side() {
let error = dry_run_setting(Some(OsString::from("maybe"))).unwrap_err();
assert_eq!(
error,
Error::BadSetting {
variable: DRY_RUN_VAR,
value: OsString::from("maybe"),
allowed: "'1'/'0', 'true'/'false' or 'yes'/'no'",
}
);
let message = error.to_string();
assert!(
message.contains("REVEAL_DRY_RUN must be '1'/'0', 'true'/'false' or 'yes'/'no', got 'maybe'"),
"{message}"
);
}
#[test]
fn every_usage_error_ends_with_the_usage_constant() {
let dir = temp_dir("usage");
let rec = RecordingRevealer::default();
for args in [&[] as &[&str], &["a.txt", "b.txt"], &[""]] {
let (code, err) = run_with(args, &dir, &rec);
assert_eq!(code, EXIT_USAGE, "{args:?}");
assert_eq!(err.lines().last(), Some(USAGE), "{args:?}");
}
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn error_type_plugs_into_a_callers_error_chain() {
fn takes<E: std::error::Error>() {}
takes::<Error>();
}
#[test]
fn an_apartment_the_host_already_owns_is_not_reported_as_failure() {
let host = unsafe { ffi::CoInitializeEx(ptr::null(), 0) }; assert!(host == 0 || host == 1, "MTA init failed: {host:#010x}");
assert_eq!(
init_apartment(),
Ok(false),
"a host-owned apartment must be adopted, not refused"
);
unsafe { ffi::CoUninitialize() };
}
#[test]
fn the_revealers_can_be_shared_across_threads() {
fn takes<T: Send + Sync + Revealer>() {}
takes::<RecordingRevealer>();
takes::<ShellRevealer>();
}