#[cfg(test)] #[cfg(not(sd_cross_compile))] // Cross-compilation does not allow to spawn threads but `command.assert()` would do. mod cli { use anyhow::Result; use assert_cmd::Command; use std::io::prelude::*; fn sd() -> Command { Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("Error invoking sd") } fn assert_file(path: &std::path::Path, content: &str) { assert_eq!(content, std::fs::read_to_string(path).unwrap()); } fn create_soft_link>( 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(()) } #[test] fn in_place() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"abc123def")?; let path = file.into_temp_path(); sd().args(["abc\\d+", "", path.to_str().unwrap()]) .assert() .success(); assert_file(&path, "def"); Ok(()) } #[test] fn in_place_with_empty_result_file() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"a7c")?; let path = file.into_temp_path(); sd().args(["a\\dc", "", path.to_str().unwrap()]) .assert() .success(); assert_file(&path, ""); Ok(()) } #[test] fn in_place_following_symlink() -> Result<()> { let dir = tempfile::tempdir()?; let path = dir.path(); let file = path.join("file"); let link = path.join("link"); create_soft_link(&file, &link)?; std::fs::write(&file, "abc123def")?; sd().args(["abc\\d+", "", link.to_str().unwrap()]) .assert() .success(); assert_file(&file, "def"); assert!(std::fs::symlink_metadata(link)?.file_type().is_symlink()); Ok(()) } #[test] fn replace_into_stdout() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"abc123def")?; 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() )); assert_file(file.path(), "abc123def"); Ok(()) } #[test] fn stdin() -> Result<()> { sd().args(["abc\\d+", ""]) .write_stdin("abc123def") .assert() .success() .stdout("def"); Ok(()) } fn bad_replace_helper_styled(replace: &str) -> String { let err = sd() .args(["find", replace]) .write_stdin("stdin") .unwrap_err(); String::from_utf8(err.as_output().unwrap().stderr.clone()).unwrap() } 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 ` // 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() } #[test] fn fixed_strings_ambiguous_replace_is_fine() { sd().args([ "--fixed-strings", "foo", "inner_before $1fine inner_after", ]) .write_stdin("outer_before foo outer_after") .assert() .success() .stdout("outer_before inner_before $1fine inner_after outer_after"); } #[test] fn ambiguous_replace_basic() { let plain_stderr = bad_replace_helper_plain("before $1bad after"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. before $1bad after ^^^^ "###); } #[test] fn ambiguous_replace_variable_width() { let plain_stderr = bad_replace_helper_plain("\r\n\t$1bad\r"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. ␍␊␉$1bad␍ ^^^^ "###); } #[test] fn ambiguous_replace_multibyte_char() { let plain_stderr = bad_replace_helper_plain("😈$1bad😇"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. 😈$1bad😇 ^^^^ "###); } #[test] fn ambiguous_replace_issue_44() { let plain_stderr = bad_replace_helper_plain("$1Call $2($5, GetFM20ReturnKey(), $6)"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}Call`. $1Call $2($5, GetFM20ReturnKey(), $6) ^^^^^ "###); } // NOTE: styled terminal output is platform dependent, so convert to a // common format, in this case HTML, to check #[test] fn ambiguous_replace_ensure_styling() { let styled_stderr = bad_replace_helper_styled("\t$1bad after"); let html_stderr = ansi_to_html::convert(&styled_stderr, true, true).unwrap(); insta::assert_snapshot!(html_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. $1bad after ^^^^ "###); } #[test] fn limit_replacements_file() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"foo\nfoo\nfoo")?; let path = file.into_temp_path(); sd().args(["-n", "1", "foo", "bar", path.to_str().unwrap()]) .assert() .success(); assert_file(&path, "bar\nfoo\nfoo"); Ok(()) } #[test] fn limit_replacements_file_preview() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"foo\nfoo\nfoo")?; let path = file.into_temp_path(); sd().args([ "--preview", "-n", "1", "foo", "bar", path.to_str().unwrap(), ]) .assert() .success() .stdout(format!( "{}\nfoo\nfoo\n", ansi_term::Color::Blue.paint("bar") )); Ok(()) } #[test] fn limit_replacements_stdin() { sd().args(["-n", "1", "foo", "bar"]) .write_stdin("foo\nfoo\nfoo") .assert() .success() .stdout("bar\nfoo\nfoo"); } #[test] fn limit_replacements_stdin_preview() { sd().args(["--preview", "-n", "1", "foo", "bar"]) .write_stdin("foo\nfoo\nfoo") .assert() .success() .stdout("bar\nfoo\nfoo"); } }