diff options
Diffstat (limited to 'tests/cli.rs')
-rw-r--r-- | tests/cli.rs | 188 |
1 files changed, 169 insertions, 19 deletions
diff --git a/tests/cli.rs b/tests/cli.rs index 6bf78cc..1030dc1 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -3,7 +3,7 @@ mod cli { use anyhow::Result; use assert_cmd::Command; - use std::io::prelude::*; + use std::{fs, io::prelude::*, path::Path}; fn sd() -> Command { Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("Error invoking sd") @@ -13,14 +13,18 @@ mod cli { assert_eq!(content, std::fs::read_to_string(path).unwrap()); } + // This should really be cfg_attr(target_family = "windows"), but wasi impl + // is nightly for now, and other impls are not part of std + #[cfg_attr( + not(target_family = "unix"), + ignore = "Windows symlinks are privileged" + )] fn create_soft_link<P: AsRef<std::path::Path>>( src: &P, dst: &P, ) -> Result<()> { #[cfg(target_family = "unix")] std::os::unix::fs::symlink(src, dst)?; - #[cfg(target_family = "windows")] - std::os::windows::fs::symlink_file(src, dst)?; Ok(()) } @@ -53,6 +57,10 @@ mod cli { Ok(()) } + #[cfg_attr( + target_family = "windows", + ignore = "Windows symlinks are privileged" + )] #[test] fn in_place_following_symlink() -> Result<()> { let dir = tempfile::tempdir()?; @@ -81,11 +89,7 @@ mod cli { sd().args(["-p", "abc\\d+", "", file.path().to_str().unwrap()]) .assert() .success() - .stdout(format!( - "{}{}def\n", - ansi_term::Color::Blue.prefix(), - ansi_term::Color::Blue.suffix() - )); + .stdout("def"); assert_file(file.path(), "abc123def"); @@ -113,13 +117,7 @@ mod cli { fn bad_replace_helper_plain(replace: &str) -> String { let stderr = bad_replace_helper_styled(replace); - - // TODO: no easy way to toggle off styling yet. Add a `--color <when>` - // flag, and respect things like `$NO_COLOR`. `ansi_term` is - // unmaintained, so we should migrate off of it anyways - console::AnsiCodeIterator::new(&stderr) - .filter_map(|(s, is_ansi)| (!is_ansi).then_some(s)) - .collect() + stderr } #[test] @@ -182,6 +180,7 @@ mod cli { // NOTE: styled terminal output is platform dependent, so convert to a // common format, in this case HTML, to check + #[ignore = "TODO: wait for proper colorization"] #[test] fn ambiguous_replace_ensure_styling() { let styled_stderr = bad_replace_helper_styled("\t$1bad after"); @@ -225,10 +224,7 @@ mod cli { ]) .assert() .success() - .stdout(format!( - "{}\nfoo\nfoo\n", - ansi_term::Color::Blue.paint("bar") - )); + .stdout("bar\nfoo\nfoo"); Ok(()) } @@ -250,4 +246,158 @@ mod cli { .success() .stdout("bar\nfoo\nfoo"); } + + const UNTOUCHED_CONTENTS: &str = "untouched"; + + fn assert_fails_correctly( + command: &mut Command, + valid: &Path, + test_home: &Path, + snap_name: &str, + ) { + let failed_command = command.assert().failure().code(1); + + assert_eq!(fs::read_to_string(&valid).unwrap(), UNTOUCHED_CONTENTS); + + let stderr_orig = + std::str::from_utf8(&failed_command.get_output().stderr).unwrap(); + // Normalize unstable path bits + let stderr_norm = stderr_orig + .replace(test_home.to_str().unwrap(), "<test_home>") + .replace('\\', "/"); + insta::assert_snapshot!(snap_name, stderr_norm); + } + + #[test] + fn correctly_fails_on_missing_file() -> Result<()> { + let test_dir = tempfile::Builder::new().prefix("sd-test-").tempdir()?; + let test_home = test_dir.path(); + + let valid = test_home.join("valid"); + fs::write(&valid, UNTOUCHED_CONTENTS)?; + let missing = test_home.join("missing"); + + assert_fails_correctly( + sd().args([".*", ""]).arg(&valid).arg(&missing), + &valid, + test_home, + "correctly_fails_on_missing_file", + ); + + Ok(()) + } + + #[cfg_attr(not(target_family = "unix"), ignore = "only runs on unix")] + #[test] + fn correctly_fails_on_unreadable_file() -> Result<()> { + #[cfg(not(target_family = "unix"))] + { + unreachable!("This test should be ignored"); + } + #[cfg(target_family = "unix")] + { + use std::os::unix::fs::OpenOptionsExt; + + let test_dir = + tempfile::Builder::new().prefix("sd-test-").tempdir()?; + let test_home = test_dir.path(); + + let valid = test_home.join("valid"); + fs::write(&valid, UNTOUCHED_CONTENTS)?; + let write_only = { + let path = test_home.join("write_only"); + let mut write_only_file = std::fs::OpenOptions::new() + .mode(0o333) + .create(true) + .write(true) + .open(&path)?; + write!(write_only_file, "unreadable")?; + path + }; + + assert_fails_correctly( + sd().args([".*", ""]).arg(&valid).arg(&write_only), + &valid, + test_home, + "correctly_fails_on_unreadable_file", + ); + + Ok(()) + } + } + + // Failing to create a temporary file in the same directory as the input is + // one of the failure cases that is past the "point of no return" (after we + // already start making replacements). This means that any files that could + // be modified are, and we report any failure cases + #[cfg_attr(not(target_family = "unix"), ignore = "only runs on unix")] + #[test] + fn reports_errors_on_atomic_file_swap_creation_failure() -> Result<()> { + #[cfg(not(target_family = "unix"))] + { + unreachable!("This test should be ignored"); + } + #[cfg(target_family = "unix")] + { + use std::os::unix::fs::PermissionsExt; + + const FIND_REPLACE: [&str; 2] = ["able", "ed"]; + const ORIG_TEXT: &str = "modifiable"; + const MODIFIED_TEXT: &str = "modified"; + + let test_dir = + tempfile::Builder::new().prefix("sd-test-").tempdir()?; + let test_home = test_dir.path().canonicalize()?; + + let writable_dir = test_home.join("writable"); + fs::create_dir(&writable_dir)?; + let writable_dir_file = writable_dir.join("foo"); + fs::write(&writable_dir_file, ORIG_TEXT)?; + + let unwritable_dir = test_home.join("unwritable"); + fs::create_dir(&unwritable_dir)?; + let unwritable_dir_file1 = unwritable_dir.join("bar"); + fs::write(&unwritable_dir_file1, ORIG_TEXT)?; + let unwritable_dir_file2 = unwritable_dir.join("baz"); + fs::write(&unwritable_dir_file2, ORIG_TEXT)?; + let mut perms = fs::metadata(&unwritable_dir)?.permissions(); + perms.set_mode(0o555); + fs::set_permissions(&unwritable_dir, perms)?; + + let failed_command = sd() + .args(FIND_REPLACE) + .arg(&writable_dir_file) + .arg(&unwritable_dir_file1) + .arg(&unwritable_dir_file2) + .assert() + .failure() + .code(1); + + // Confirm that we modified the one file that we were able to + assert_eq!(fs::read_to_string(&writable_dir_file)?, MODIFIED_TEXT); + assert_eq!(fs::read_to_string(&unwritable_dir_file1)?, ORIG_TEXT); + assert_eq!(fs::read_to_string(&unwritable_dir_file2)?, ORIG_TEXT); + + let stderr_orig = + std::str::from_utf8(&failed_command.get_output().stderr) + .unwrap(); + // Normalize unstable path bits + let stderr_partial_norm = stderr_orig + .replace(test_home.to_str().unwrap(), "<test_home>") + .replace('\\', "/"); + let tmp_file_rep = regex::Regex::new(r"\.tmp\w+")?; + let stderr_norm = + tmp_file_rep.replace_all(&stderr_partial_norm, "<tmp_file>"); + insta::assert_snapshot!(stderr_norm); + + // Make the unwritable dir writable again, so it can be cleaned up + // when dropping the temp dir + let mut perms = fs::metadata(&unwritable_dir)?.permissions(); + perms.set_mode(0o777); + fs::set_permissions(&unwritable_dir, perms)?; + test_dir.close()?; + + Ok(()) + } + } } |