1use std::{
2 collections::HashMap,
3 fs::{self, File},
4 io::{BufReader, BufWriter, Write},
5 path::Path,
6};
7use tempfile::NamedTempFile;
8
9use crate::search::{SearchResult, SearchResultWithReplacement, SearchType};
10use crate::{line_reader::BufReadExt, search};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum ReplaceResult {
14 Success,
15 Error(String),
16}
17
18pub fn replace_in_file(results: &mut [SearchResultWithReplacement]) -> anyhow::Result<()> {
21 let file_path = match results {
22 [r, ..] => r.search_result.path.clone(),
23 [] => return Ok(()),
24 };
25 debug_assert!(results.iter().all(|r| r.search_result.path == file_path));
26
27 let mut line_map = results
28 .iter_mut()
29 .map(|res| (res.search_result.line_number, res))
30 .collect::<HashMap<_, _>>();
31
32 let file_path = file_path.expect("File path must be present when searching in files");
33 let parent_dir = file_path.parent().unwrap_or(Path::new("."));
34 let temp_output_file = NamedTempFile::new_in(parent_dir)?;
35
36 {
38 let input = File::open(file_path.clone())?;
39 let reader = BufReader::new(input);
40
41 let output = File::create(temp_output_file.path())?;
42 let mut writer = BufWriter::new(output);
43
44 for (idx, line_result) in reader.lines_with_endings().enumerate() {
45 let line_number = idx + 1; let (mut line, line_ending) = line_result?;
47 if let Some(res) = line_map.get_mut(&line_number) {
48 if line == res.search_result.line.as_bytes() {
49 line = res.replacement.as_bytes().to_vec();
50 res.replace_result = Some(ReplaceResult::Success);
51 } else {
52 res.replace_result = Some(ReplaceResult::Error(
53 "File changed since last search".to_owned(),
54 ));
55 }
56 }
57 line.extend(line_ending.as_bytes());
58 writer.write_all(&line)?;
59 }
60
61 writer.flush()?;
62 }
63
64 temp_output_file.persist(file_path)?;
65 Ok(())
66}
67
68const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; fn should_replace_in_memory(path: &Path) -> Result<bool, std::io::Error> {
71 let file_size = fs::metadata(path)?.len();
72 Ok(file_size <= MAX_FILE_SIZE)
73}
74
75pub fn replace_all_in_file(
95 file_path: &Path,
96 search: &SearchType,
97 replace: &str,
98) -> anyhow::Result<bool> {
99 if matches!(should_replace_in_memory(file_path), Ok(true)) {
101 match replace_in_memory(file_path, search, replace) {
102 Ok(replaced) => return Ok(replaced),
103 Err(e) => {
104 log::error!(
105 "Found error when attempting to replace in memory for file {path_display}: {e}",
106 path_display = file_path.display(),
107 );
108 }
109 }
110 }
111
112 replace_chunked(file_path, search, replace)
113}
114
115pub fn add_replacement(
116 search_result: SearchResult,
117 search: &SearchType,
118 replace: &str,
119) -> Option<SearchResultWithReplacement> {
120 let replacement = replacement_if_match(&search_result.line, search, replace)?;
121 Some(SearchResultWithReplacement {
122 search_result,
123 replacement,
124 replace_result: None,
125 })
126}
127
128fn replace_chunked(file_path: &Path, search: &SearchType, replace: &str) -> anyhow::Result<bool> {
129 let search_results = search::search_file(file_path, search)?;
130 if !search_results.is_empty() {
131 let mut replacement_results = search_results
132 .into_iter()
133 .map(|r| {
134 add_replacement(r, search, replace).unwrap_or_else(|| {
135 panic!("Called add_replacement with non-matching search result")
136 })
137 })
138 .collect::<Vec<_>>();
139 replace_in_file(&mut replacement_results)?;
140 return Ok(true);
141 }
142
143 Ok(false)
144}
145
146fn replace_in_memory(file_path: &Path, search: &SearchType, replace: &str) -> anyhow::Result<bool> {
147 let content = fs::read_to_string(file_path)?;
148 if let Some(new_content) = replacement_if_match(&content, search, replace) {
149 let parent_dir = file_path.parent().unwrap_or(Path::new("."));
150 let mut temp_file = NamedTempFile::new_in(parent_dir)?;
151 temp_file.write_all(new_content.as_bytes())?;
152 temp_file.persist(file_path)?;
153 Ok(true)
154 } else {
155 Ok(false)
156 }
157}
158
159pub fn replacement_if_match(line: &str, search: &SearchType, replace: &str) -> Option<String> {
172 if line.is_empty() || search.is_empty() {
173 return None;
174 }
175
176 if search::contains_search(line, search) {
177 let replacement = match search {
178 SearchType::Fixed(fixed_str) => line.replace(fixed_str, replace),
179 SearchType::Pattern(pattern) => pattern.replace_all(line, replace).to_string(),
180 SearchType::PatternAdvanced(pattern) => pattern.replace_all(line, replace).to_string(),
181 };
182 Some(replacement)
183 } else {
184 None
185 }
186}
187
188#[derive(Clone, Debug, Eq, PartialEq)]
189pub struct ReplaceStats {
190 pub num_successes: usize,
191 pub errors: Vec<SearchResultWithReplacement>,
192}
193
194pub fn calculate_statistics<I>(results: I) -> ReplaceStats
195where
196 I: IntoIterator<Item = SearchResultWithReplacement>,
197{
198 let mut num_successes = 0;
199 let mut errors = vec![];
200
201 results.into_iter().for_each(|res| {
202 assert!(
203 res.search_result.included,
204 "Expected only included results, found {res:?}"
205 );
206 match &res.replace_result {
207 Some(ReplaceResult::Success) => {
208 num_successes += 1;
209 }
210 None => {
211 let mut res = res.clone();
212 res.replace_result = Some(ReplaceResult::Error(
213 "Failed to find search result in file".to_owned(),
214 ));
215 errors.push(res);
216 }
217 Some(ReplaceResult::Error(_)) => {
218 errors.push(res.clone());
219 }
220 }
221 });
222
223 ReplaceStats {
224 num_successes,
225 errors,
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::line_reader::LineEnding;
233 use crate::search::{SearchResult, SearchType, search_file};
234 use regex::Regex;
235 use std::path::PathBuf;
236 use tempfile::TempDir;
237
238 mod test_helpers {
239 use crate::search::SearchType;
240
241 pub fn create_fixed_search(term: &str) -> SearchType {
242 SearchType::Fixed(term.to_string())
243 }
244 }
245
246 fn create_search_result_with_replacement(
248 path: &str,
249 line_number: usize,
250 line: &str,
251 replacement: &str,
252 included: bool,
253 replace_result: Option<ReplaceResult>,
254 ) -> SearchResultWithReplacement {
255 SearchResultWithReplacement {
256 search_result: SearchResult {
257 path: Some(PathBuf::from(path)),
258 line_number,
259 line: line.to_string(),
260 line_ending: LineEnding::Lf,
261 included,
262 },
263 replacement: replacement.to_string(),
264 replace_result,
265 }
266 }
267
268 fn create_test_file(temp_dir: &TempDir, name: &str, content: &str) -> PathBuf {
269 let file_path = temp_dir.path().join(name);
270 std::fs::write(&file_path, content).unwrap();
271 file_path
272 }
273
274 fn assert_file_content(file_path: &Path, expected_content: &str) {
275 let content = std::fs::read_to_string(file_path).unwrap();
276 assert_eq!(content, expected_content);
277 }
278
279 fn fixed_search(pattern: &str) -> SearchType {
280 SearchType::Fixed(pattern.to_string())
281 }
282
283 fn regex_search(pattern: &str) -> SearchType {
284 SearchType::Pattern(Regex::new(pattern).unwrap())
285 }
286
287 #[test]
289 fn test_replace_in_file_success() {
290 let temp_dir = TempDir::new().unwrap();
291 let file_path = create_test_file(
292 &temp_dir,
293 "test.txt",
294 "line 1\nold text\nline 3\nold text\nline 5\n",
295 );
296
297 let mut results = vec![
299 create_search_result_with_replacement(
300 file_path.to_str().unwrap(),
301 2,
302 "old text",
303 "new text",
304 true,
305 None,
306 ),
307 create_search_result_with_replacement(
308 file_path.to_str().unwrap(),
309 4,
310 "old text",
311 "new text",
312 true,
313 None,
314 ),
315 ];
316
317 let result = replace_in_file(&mut results);
319 assert!(result.is_ok());
320
321 assert_eq!(results.len(), 2);
323 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
324 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
325
326 assert_file_content(&file_path, "line 1\nnew text\nline 3\nnew text\nline 5\n");
328 }
329
330 #[test]
331 fn test_replace_in_file_success_no_final_newline() {
332 let temp_dir = TempDir::new().unwrap();
333 let file_path = create_test_file(
334 &temp_dir,
335 "test.txt",
336 "line 1\nold text\nline 3\nold text\nline 5",
337 );
338
339 let mut results = vec![
341 create_search_result_with_replacement(
342 file_path.to_str().unwrap(),
343 2,
344 "old text",
345 "new text",
346 true,
347 None,
348 ),
349 create_search_result_with_replacement(
350 file_path.to_str().unwrap(),
351 4,
352 "old text",
353 "new text",
354 true,
355 None,
356 ),
357 ];
358
359 let result = replace_in_file(&mut results);
361 assert!(result.is_ok());
362
363 assert_eq!(results.len(), 2);
365 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
366 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
367
368 let new_content = std::fs::read_to_string(&file_path).unwrap();
370 assert_eq!(new_content, "line 1\nnew text\nline 3\nnew text\nline 5");
371 }
372
373 #[test]
374 fn test_replace_in_file_success_windows_newlines() {
375 let temp_dir = TempDir::new().unwrap();
376 let file_path = create_test_file(
377 &temp_dir,
378 "test.txt",
379 "line 1\r\nold text\r\nline 3\r\nold text\r\nline 5\r\n",
380 );
381
382 let mut results = vec![
384 create_search_result_with_replacement(
385 file_path.to_str().unwrap(),
386 2,
387 "old text",
388 "new text",
389 true,
390 None,
391 ),
392 create_search_result_with_replacement(
393 file_path.to_str().unwrap(),
394 4,
395 "old text",
396 "new text",
397 true,
398 None,
399 ),
400 ];
401
402 let result = replace_in_file(&mut results);
404 assert!(result.is_ok());
405
406 assert_eq!(results.len(), 2);
408 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
409 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
410
411 let new_content = std::fs::read_to_string(&file_path).unwrap();
413 assert_eq!(
414 new_content,
415 "line 1\r\nnew text\r\nline 3\r\nnew text\r\nline 5\r\n"
416 );
417 }
418
419 #[test]
420 fn test_replace_in_file_success_mixed_newlines() {
421 let temp_dir = TempDir::new().unwrap();
422 let file_path = create_test_file(
423 &temp_dir,
424 "test.txt",
425 "\n\r\nline 1\nold text\r\nline 3\nline 4\r\nline 5\r\n\n\n",
426 );
427
428 let mut results = vec![
430 create_search_result_with_replacement(
431 file_path.to_str().unwrap(),
432 4,
433 "old text",
434 "new text",
435 true,
436 None,
437 ),
438 create_search_result_with_replacement(
439 file_path.to_str().unwrap(),
440 7,
441 "line 5",
442 "updated line 5",
443 true,
444 None,
445 ),
446 ];
447
448 let result = replace_in_file(&mut results);
450 assert!(result.is_ok());
451
452 assert_eq!(results.len(), 2);
454 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
455 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
456
457 let new_content = std::fs::read_to_string(&file_path).unwrap();
459 assert_eq!(
460 new_content,
461 "\n\r\nline 1\nnew text\r\nline 3\nline 4\r\nupdated line 5\r\n\n\n"
462 );
463 }
464
465 #[test]
466 fn test_replace_in_file_line_mismatch() {
467 let temp_dir = TempDir::new().unwrap();
468 let file_path = create_test_file(&temp_dir, "test.txt", "line 1\nactual text\nline 3\n");
469
470 let mut results = vec![create_search_result_with_replacement(
472 file_path.to_str().unwrap(),
473 2,
474 "expected text",
475 "new text",
476 true,
477 None,
478 )];
479
480 let result = replace_in_file(&mut results);
482 assert!(result.is_ok());
483
484 assert_eq!(
486 results[0].replace_result,
487 Some(ReplaceResult::Error(
488 "File changed since last search".to_owned()
489 ))
490 );
491
492 let new_content = std::fs::read_to_string(&file_path).unwrap();
494 assert_eq!(new_content, "line 1\nactual text\nline 3\n");
495 }
496
497 #[test]
498 fn test_replace_in_file_nonexistent_file() {
499 let mut results = vec![create_search_result_with_replacement(
500 "/nonexistent/path/file.txt",
501 1,
502 "old",
503 "new",
504 true,
505 None,
506 )];
507
508 let result = replace_in_file(&mut results);
509 assert!(result.is_err());
510 }
511
512 #[test]
513 fn test_replace_directory_errors() {
514 let mut results = vec![create_search_result_with_replacement(
515 "/", 0, "foo", "bar", true, None,
516 )];
517
518 let result = replace_in_file(&mut results);
519 assert!(result.is_err());
520 }
521
522 #[test]
524 fn test_replace_in_memory() {
525 let temp_dir = TempDir::new().unwrap();
526
527 let file_path = create_test_file(
529 &temp_dir,
530 "test.txt",
531 "This is a test.\nIt contains search_term that should be replaced.\nMultiple lines with search_term here.",
532 );
533
534 let result = replace_in_memory(&file_path, &fixed_search("search_term"), "replacement");
535 assert!(result.is_ok());
536 assert!(result.unwrap()); assert_file_content(
539 &file_path,
540 "This is a test.\nIt contains replacement that should be replaced.\nMultiple lines with replacement here.",
541 );
542
543 let regex_path = create_test_file(
545 &temp_dir,
546 "regex_test.txt",
547 "Number: 123, Code: 456, ID: 789",
548 );
549
550 let result = replace_in_memory(®ex_path, ®ex_search(r"\d{3}"), "XXX");
551 assert!(result.is_ok());
552 assert!(result.unwrap());
553
554 assert_file_content(®ex_path, "Number: XXX, Code: XXX, ID: XXX");
555 }
556
557 #[test]
558 fn test_replace_in_memory_no_match() {
559 let temp_dir = TempDir::new().unwrap();
560 let file_path = create_test_file(
561 &temp_dir,
562 "no_match.txt",
563 "This is a test file with no matches.",
564 );
565
566 let result = replace_in_memory(&file_path, &fixed_search("nonexistent"), "replacement");
567 assert!(result.is_ok());
568 assert!(!result.unwrap()); assert_file_content(&file_path, "This is a test file with no matches.");
572 }
573
574 #[test]
575 fn test_replace_in_memory_empty_file() {
576 let temp_dir = TempDir::new().unwrap();
577 let file_path = create_test_file(&temp_dir, "empty.txt", "");
578
579 let result = replace_in_memory(&file_path, &fixed_search("anything"), "replacement");
580 assert!(result.is_ok());
581 assert!(!result.unwrap());
582
583 assert_file_content(&file_path, "");
585 }
586
587 #[test]
588 fn test_replace_in_memory_nonexistent_file() {
589 let result = replace_in_memory(
590 Path::new("/nonexistent/path/file.txt"),
591 &fixed_search("test"),
592 "replacement",
593 );
594 assert!(result.is_err());
595 }
596
597 #[test]
599 fn test_replace_chunked() {
600 let temp_dir = TempDir::new().unwrap();
601
602 let file_path = create_test_file(
604 &temp_dir,
605 "test.txt",
606 "This is line one.\nThis contains search_pattern to replace.\nAnother line with search_pattern here.\nFinal line.",
607 );
608
609 let result = replace_chunked(&file_path, &fixed_search("search_pattern"), "replacement");
610 assert!(result.is_ok());
611 assert!(result.unwrap()); assert_file_content(
614 &file_path,
615 "This is line one.\nThis contains replacement to replace.\nAnother line with replacement here.\nFinal line.",
616 );
617
618 let regex_path = create_test_file(
620 &temp_dir,
621 "regex.txt",
622 "Line with numbers: 123 and 456.\nAnother line with 789.",
623 );
624
625 let result = replace_chunked(®ex_path, ®ex_search(r"\d{3}"), "XXX");
626 assert!(result.is_ok());
627 assert!(result.unwrap());
628
629 assert_file_content(
630 ®ex_path,
631 "Line with numbers: XXX and XXX.\nAnother line with XXX.",
632 );
633 }
634
635 #[test]
636 fn test_replace_chunked_no_match() {
637 let temp_dir = TempDir::new().unwrap();
638 let file_path = create_test_file(
639 &temp_dir,
640 "test.txt",
641 "This is a test file with no matching patterns.",
642 );
643
644 let result = replace_chunked(&file_path, &fixed_search("nonexistent"), "replacement");
645 assert!(result.is_ok());
646 assert!(!result.unwrap());
647
648 assert_file_content(&file_path, "This is a test file with no matching patterns.");
650 }
651
652 #[test]
653 fn test_replace_chunked_empty_file() {
654 let temp_dir = TempDir::new().unwrap();
655 let file_path = create_test_file(&temp_dir, "empty.txt", "");
656
657 let result = replace_chunked(&file_path, &fixed_search("anything"), "replacement");
658 assert!(result.is_ok());
659 assert!(!result.unwrap());
660
661 assert_file_content(&file_path, "");
663 }
664
665 #[test]
666 fn test_replace_chunked_nonexistent_file() {
667 let result = replace_chunked(
668 Path::new("/nonexistent/path/file.txt"),
669 &fixed_search("test"),
670 "replacement",
671 );
672 assert!(result.is_err());
673 }
674
675 #[test]
677 fn test_replace_all_in_file() {
678 let temp_dir = TempDir::new().unwrap();
679 let file_path = create_test_file(
680 &temp_dir,
681 "test.txt",
682 "This is a test file.\nIt has some content to replace.\nThe word replace should be replaced.",
683 );
684
685 let result = replace_all_in_file(&file_path, &fixed_search("replace"), "modify");
686 assert!(result.is_ok());
687 assert!(result.unwrap());
688
689 assert_file_content(
690 &file_path,
691 "This is a test file.\nIt has some content to modify.\nThe word modify should be modifyd.",
692 );
693 }
694
695 #[test]
696 fn test_unicode_in_file() {
697 let mut temp_file = NamedTempFile::new().unwrap();
698 writeln!(temp_file, "Line with Greek: αβγδε").unwrap();
699 write!(temp_file, "Line with Emoji: 😀 🚀 🌍\r\n").unwrap();
700 write!(temp_file, "Line with Arabic: مرحبا بالعالم").unwrap();
701 temp_file.flush().unwrap();
702
703 let search = SearchType::Pattern(Regex::new(r"\p{Greek}+").unwrap());
704 let replacement = "GREEK";
705 let results = search_file(temp_file.path(), &search)
706 .unwrap()
707 .into_iter()
708 .filter_map(|r| add_replacement(r, &search, replacement))
709 .collect::<Vec<_>>();
710
711 assert_eq!(results.len(), 1);
712 assert_eq!(results[0].replacement, "Line with Greek: GREEK");
713
714 let search = SearchType::Pattern(Regex::new(r"🚀").unwrap());
715 let replacement = "ROCKET";
716 let results = search_file(temp_file.path(), &search)
717 .unwrap()
718 .into_iter()
719 .filter_map(|r| add_replacement(r, &search, replacement))
720 .collect::<Vec<_>>();
721
722 assert_eq!(results.len(), 1);
723 assert_eq!(results[0].replacement, "Line with Emoji: 😀 ROCKET 🌍");
724 assert_eq!(results[0].search_result.line_ending, LineEnding::CrLf);
725 }
726
727 mod search_file_tests {
728 use super::*;
729 use fancy_regex::Regex as FancyRegex;
730 use regex::Regex;
731 use std::io::Write;
732 use tempfile::NamedTempFile;
733
734 #[test]
735 fn test_search_file_simple_match() {
736 let mut temp_file = NamedTempFile::new().unwrap();
737 writeln!(temp_file, "line 1").unwrap();
738 writeln!(temp_file, "search target").unwrap();
739 writeln!(temp_file, "line 3").unwrap();
740 temp_file.flush().unwrap();
741
742 let search = test_helpers::create_fixed_search("search");
743 let replacement = "replace";
744 let results = search_file(temp_file.path(), &search)
745 .unwrap()
746 .into_iter()
747 .filter_map(|r| add_replacement(r, &search, replacement))
748 .collect::<Vec<_>>();
749
750 assert_eq!(results.len(), 1);
751 assert_eq!(results[0].search_result.line_number, 2);
752 assert_eq!(results[0].search_result.line, "search target");
753 assert_eq!(results[0].replacement, "replace target");
754 assert!(results[0].search_result.included);
755 }
756
757 #[test]
758 fn test_search_file_multiple_matches() {
759 let mut temp_file = NamedTempFile::new().unwrap();
760 writeln!(temp_file, "test line 1").unwrap();
761 writeln!(temp_file, "test line 2").unwrap();
762 writeln!(temp_file, "no match here").unwrap();
763 writeln!(temp_file, "test line 4").unwrap();
764 temp_file.flush().unwrap();
765
766 let search = test_helpers::create_fixed_search("test");
767 let replacement = "replaced";
768 let results = search_file(temp_file.path(), &search)
769 .unwrap()
770 .into_iter()
771 .filter_map(|r| add_replacement(r, &search, replacement))
772 .collect::<Vec<_>>();
773
774 assert_eq!(results.len(), 3);
775 assert_eq!(results[0].search_result.line_number, 1);
776 assert_eq!(results[0].replacement, "replaced line 1");
777 assert_eq!(results[1].search_result.line_number, 2);
778 assert_eq!(results[1].replacement, "replaced line 2");
779 assert_eq!(results[2].search_result.line_number, 4);
780 assert_eq!(results[2].replacement, "replaced line 4");
781 }
782
783 #[test]
784 fn test_search_file_no_matches() {
785 let mut temp_file = NamedTempFile::new().unwrap();
786 writeln!(temp_file, "line 1").unwrap();
787 writeln!(temp_file, "line 2").unwrap();
788 writeln!(temp_file, "line 3").unwrap();
789 temp_file.flush().unwrap();
790
791 let search = SearchType::Fixed("nonexistent".to_string());
792 let replacement = "replace";
793 let results = search_file(temp_file.path(), &search)
794 .unwrap()
795 .into_iter()
796 .filter_map(|r| add_replacement(r, &search, replacement))
797 .collect::<Vec<_>>();
798
799 assert_eq!(results.len(), 0);
800 }
801
802 #[test]
803 fn test_search_file_regex_pattern() {
804 let mut temp_file = NamedTempFile::new().unwrap();
805 writeln!(temp_file, "number: 123").unwrap();
806 writeln!(temp_file, "text without numbers").unwrap();
807 writeln!(temp_file, "another number: 456").unwrap();
808 temp_file.flush().unwrap();
809
810 let search = SearchType::Pattern(Regex::new(r"\d+").unwrap());
811 let replacement = "XXX";
812 let results = search_file(temp_file.path(), &search)
813 .unwrap()
814 .into_iter()
815 .filter_map(|r| add_replacement(r, &search, replacement))
816 .collect::<Vec<_>>();
817
818 assert_eq!(results.len(), 2);
819 assert_eq!(results[0].replacement, "number: XXX");
820 assert_eq!(results[1].replacement, "another number: XXX");
821 }
822
823 #[test]
824 fn test_search_file_advanced_regex_pattern() {
825 let mut temp_file = NamedTempFile::new().unwrap();
826 writeln!(temp_file, "123abc456").unwrap();
827 writeln!(temp_file, "abc").unwrap();
828 writeln!(temp_file, "789xyz123").unwrap();
829 writeln!(temp_file, "no match").unwrap();
830 temp_file.flush().unwrap();
831
832 let search =
834 SearchType::PatternAdvanced(FancyRegex::new(r"(?<=\d{3})abc(?=\d{3})").unwrap());
835 let replacement = "REPLACED";
836 let results = search_file(temp_file.path(), &search)
837 .unwrap()
838 .into_iter()
839 .filter_map(|r| add_replacement(r, &search, replacement))
840 .collect::<Vec<_>>();
841
842 assert_eq!(results.len(), 1);
843 assert_eq!(results[0].replacement, "123REPLACED456");
844 assert_eq!(results[0].search_result.line_number, 1);
845 }
846
847 #[test]
848 fn test_search_file_empty_search() {
849 let mut temp_file = NamedTempFile::new().unwrap();
850 writeln!(temp_file, "some content").unwrap();
851 temp_file.flush().unwrap();
852
853 let search = SearchType::Fixed("".to_string());
854 let replacement = "replace";
855 let results = search_file(temp_file.path(), &search)
856 .unwrap()
857 .into_iter()
858 .filter_map(|r| add_replacement(r, &search, replacement))
859 .collect::<Vec<_>>();
860
861 assert_eq!(results.len(), 0);
862 }
863
864 #[test]
865 fn test_search_file_preserves_line_endings() {
866 let mut temp_file = NamedTempFile::new().unwrap();
867 write!(temp_file, "line1\nline2\r\nline3").unwrap();
868 temp_file.flush().unwrap();
869
870 let search = SearchType::Fixed("line".to_string());
871 let replacement = "X";
872 let results = search_file(temp_file.path(), &search)
873 .unwrap()
874 .into_iter()
875 .filter_map(|r| add_replacement(r, &search, replacement))
876 .collect::<Vec<_>>();
877
878 assert_eq!(results.len(), 3);
879 assert_eq!(results[0].search_result.line_ending, LineEnding::Lf);
880 assert_eq!(results[1].search_result.line_ending, LineEnding::CrLf);
881 assert_eq!(results[2].search_result.line_ending, LineEnding::None);
882 }
883
884 #[test]
885 fn test_search_file_nonexistent() {
886 let nonexistent_path = PathBuf::from("/this/file/does/not/exist.txt");
887 let search = test_helpers::create_fixed_search("test");
888 let results = search_file(&nonexistent_path, &search);
889 assert!(results.is_err());
890 }
891
892 #[test]
893 fn test_search_file_unicode_content() {
894 let mut temp_file = NamedTempFile::new().unwrap();
895 writeln!(temp_file, "Hello 世界!").unwrap();
896 writeln!(temp_file, "Здравствуй мир!").unwrap();
897 writeln!(temp_file, "🚀 Rocket").unwrap();
898 temp_file.flush().unwrap();
899
900 let search = SearchType::Fixed("世界".to_string());
901 let replacement = "World";
902 let results = search_file(temp_file.path(), &search)
903 .unwrap()
904 .into_iter()
905 .filter_map(|r| add_replacement(r, &search, replacement))
906 .collect::<Vec<_>>();
907
908 assert_eq!(results.len(), 1);
909 assert_eq!(results[0].replacement, "Hello World!");
910 }
911
912 #[test]
913 fn test_search_file_with_binary_content() {
914 let mut temp_file = NamedTempFile::new().unwrap();
915 let binary_data = [0x00, 0x01, 0x02, 0xFF, 0xFE];
917 temp_file.write_all(&binary_data).unwrap();
918 temp_file.flush().unwrap();
919
920 let search = test_helpers::create_fixed_search("test");
921 let replacement = "replace";
922 let results = search_file(temp_file.path(), &search)
923 .unwrap()
924 .into_iter()
925 .filter_map(|r| add_replacement(r, &search, replacement))
926 .collect::<Vec<_>>();
927
928 assert_eq!(results.len(), 0);
929 }
930
931 #[test]
932 fn test_search_file_large_content() {
933 let mut temp_file = NamedTempFile::new().unwrap();
934
935 for i in 0..1000 {
937 if i % 100 == 0 {
938 writeln!(temp_file, "target line {i}").unwrap();
939 } else {
940 writeln!(temp_file, "normal line {i}").unwrap();
941 }
942 }
943 temp_file.flush().unwrap();
944
945 let search = SearchType::Fixed("target".to_string());
946 let replacement = "found";
947 let results = search_file(temp_file.path(), &search)
948 .unwrap()
949 .into_iter()
950 .filter_map(|r| add_replacement(r, &search, replacement))
951 .collect::<Vec<_>>();
952
953 assert_eq!(results.len(), 10); assert_eq!(results[0].search_result.line_number, 1); assert_eq!(results[1].search_result.line_number, 101);
956 assert_eq!(results[9].search_result.line_number, 901);
957 }
958 }
959
960 mod replace_if_match_tests {
961 use crate::validation::SearchConfig;
962
963 use super::*;
964
965 mod test_helpers {
966 use crate::{
967 search::ParsedSearchConfig,
968 validation::{
969 SearchConfig, SimpleErrorHandler, ValidationResult,
970 validate_search_configuration,
971 },
972 };
973
974 pub fn must_parse_search_config(search_config: SearchConfig<'_>) -> ParsedSearchConfig {
975 let mut error_handler = SimpleErrorHandler::new();
976 let (search_config, _dir_config) =
977 match validate_search_configuration(search_config, None, &mut error_handler)
978 .unwrap()
979 {
980 ValidationResult::Success(search_config) => search_config,
981 ValidationResult::ValidationErrors => {
982 panic!("{}", error_handler.errors_str().unwrap());
983 }
984 };
985 search_config
986 }
987 }
988
989 mod fixed_string_tests {
990 use super::*;
991
992 mod whole_word_true_match_case_true {
993
994 use super::*;
995
996 #[test]
997 fn test_basic_replacement() {
998 let search_config = SearchConfig {
999 search_text: "world",
1000 fixed_strings: true,
1001 match_whole_word: true,
1002 match_case: true,
1003 replacement_text: "earth",
1004 advanced_regex: false,
1005 };
1006 let parsed = test_helpers::must_parse_search_config(search_config);
1007
1008 assert_eq!(
1009 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1010 Some("hello earth".to_string())
1011 );
1012 }
1013
1014 #[test]
1015 fn test_case_sensitivity() {
1016 let search_config = SearchConfig {
1017 search_text: "world",
1018 fixed_strings: true,
1019 match_whole_word: true,
1020 match_case: true,
1021 replacement_text: "earth",
1022 advanced_regex: false,
1023 };
1024 let parsed = test_helpers::must_parse_search_config(search_config);
1025
1026 assert_eq!(
1027 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1028 None
1029 );
1030 }
1031
1032 #[test]
1033 fn test_word_boundaries() {
1034 let search_config = SearchConfig {
1035 search_text: "world",
1036 fixed_strings: true,
1037 match_whole_word: true,
1038 match_case: true,
1039 replacement_text: "earth",
1040 advanced_regex: false,
1041 };
1042 let parsed = test_helpers::must_parse_search_config(search_config);
1043
1044 assert_eq!(
1045 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1046 None
1047 );
1048 }
1049 }
1050
1051 mod whole_word_true_match_case_false {
1052 use super::*;
1053
1054 #[test]
1055 fn test_basic_replacement() {
1056 let search_config = SearchConfig {
1057 search_text: "world",
1058 fixed_strings: true,
1059 match_whole_word: true,
1060 match_case: false,
1061 replacement_text: "earth",
1062 advanced_regex: false,
1063 };
1064 let parsed = test_helpers::must_parse_search_config(search_config);
1065
1066 assert_eq!(
1067 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1068 Some("hello earth".to_string())
1069 );
1070 }
1071
1072 #[test]
1073 fn test_case_insensitivity() {
1074 let search_config = SearchConfig {
1075 search_text: "world",
1076 fixed_strings: true,
1077 match_whole_word: true,
1078 match_case: false,
1079 replacement_text: "earth",
1080 advanced_regex: false,
1081 };
1082 let parsed = test_helpers::must_parse_search_config(search_config);
1083
1084 assert_eq!(
1085 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1086 Some("hello earth".to_string())
1087 );
1088 }
1089
1090 #[test]
1091 fn test_word_boundaries() {
1092 let search_config = SearchConfig {
1093 search_text: "world",
1094 fixed_strings: true,
1095 match_whole_word: true,
1096 match_case: false,
1097 replacement_text: "earth",
1098 advanced_regex: false,
1099 };
1100 let parsed = test_helpers::must_parse_search_config(search_config);
1101
1102 assert_eq!(
1103 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1104 None
1105 );
1106 }
1107
1108 #[test]
1109 fn test_unicode() {
1110 let search_config = SearchConfig {
1111 search_text: "café",
1112 fixed_strings: true,
1113 match_whole_word: true,
1114 match_case: false,
1115 replacement_text: "restaurant",
1116 advanced_regex: false,
1117 };
1118 let parsed = test_helpers::must_parse_search_config(search_config);
1119
1120 assert_eq!(
1121 replacement_if_match("Hello CAFÉ table", &parsed.search, &parsed.replace),
1122 Some("Hello restaurant table".to_string())
1123 );
1124 }
1125 }
1126
1127 mod whole_word_false_match_case_true {
1128 use super::*;
1129
1130 #[test]
1131 fn test_basic_replacement() {
1132 let search_config = SearchConfig {
1133 search_text: "world",
1134 fixed_strings: true,
1135 match_whole_word: false,
1136 match_case: true,
1137 replacement_text: "earth",
1138 advanced_regex: false,
1139 };
1140 let parsed = test_helpers::must_parse_search_config(search_config);
1141
1142 assert_eq!(
1143 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1144 Some("hello earth".to_string())
1145 );
1146 }
1147
1148 #[test]
1149 fn test_case_sensitivity() {
1150 let search_config = SearchConfig {
1151 search_text: "world",
1152 fixed_strings: true,
1153 match_whole_word: false,
1154 match_case: true,
1155 replacement_text: "earth",
1156 advanced_regex: false,
1157 };
1158 let parsed = test_helpers::must_parse_search_config(search_config);
1159
1160 assert_eq!(
1161 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1162 None
1163 );
1164 }
1165
1166 #[test]
1167 fn test_substring_matches() {
1168 let search_config = SearchConfig {
1169 search_text: "world",
1170 fixed_strings: true,
1171 match_whole_word: false,
1172 match_case: true,
1173 replacement_text: "earth",
1174 advanced_regex: false,
1175 };
1176 let parsed = test_helpers::must_parse_search_config(search_config);
1177
1178 assert_eq!(
1179 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1180 Some("earthwide".to_string())
1181 );
1182 }
1183 }
1184
1185 mod whole_word_false_match_case_false {
1186 use super::*;
1187
1188 #[test]
1189 fn test_basic_replacement() {
1190 let search_config = SearchConfig {
1191 search_text: "world",
1192 fixed_strings: true,
1193 match_whole_word: false,
1194 match_case: false,
1195 replacement_text: "earth",
1196 advanced_regex: false,
1197 };
1198 let parsed = test_helpers::must_parse_search_config(search_config);
1199
1200 assert_eq!(
1201 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1202 Some("hello earth".to_string())
1203 );
1204 }
1205
1206 #[test]
1207 fn test_case_insensitivity() {
1208 let search_config = SearchConfig {
1209 search_text: "world",
1210 fixed_strings: true,
1211 match_whole_word: false,
1212 match_case: false,
1213 replacement_text: "earth",
1214 advanced_regex: false,
1215 };
1216 let parsed = test_helpers::must_parse_search_config(search_config);
1217
1218 assert_eq!(
1219 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1220 Some("hello earth".to_string())
1221 );
1222 }
1223
1224 #[test]
1225 fn test_substring_matches() {
1226 let search_config = SearchConfig {
1227 search_text: "world",
1228 fixed_strings: true,
1229 match_whole_word: false,
1230 match_case: false,
1231 replacement_text: "earth",
1232 advanced_regex: false,
1233 };
1234 let parsed = test_helpers::must_parse_search_config(search_config);
1235
1236 assert_eq!(
1237 replacement_if_match("WORLDWIDE", &parsed.search, &parsed.replace),
1238 Some("earthWIDE".to_string())
1239 );
1240 }
1241 }
1242 }
1243
1244 mod regex_pattern_tests {
1245 use super::*;
1246
1247 mod whole_word_true_match_case_true {
1248 use crate::validation::SearchConfig;
1249
1250 use super::*;
1251
1252 #[test]
1253 fn test_basic_regex() {
1254 let re_str = r"w\w+d";
1255 let search_config = SearchConfig {
1256 search_text: re_str,
1257 fixed_strings: false,
1258 match_whole_word: true,
1259 match_case: true,
1260 replacement_text: "earth",
1261 advanced_regex: false,
1262 };
1263 let parsed = test_helpers::must_parse_search_config(search_config);
1264
1265 assert_eq!(
1266 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1267 Some("hello earth".to_string())
1268 );
1269 }
1270
1271 #[test]
1272 fn test_case_sensitivity() {
1273 let re_str = r"world";
1274 let search_config = SearchConfig {
1275 search_text: re_str,
1276 fixed_strings: false,
1277 match_whole_word: true,
1278 match_case: true,
1279 replacement_text: "earth",
1280 advanced_regex: false,
1281 };
1282 let parsed = test_helpers::must_parse_search_config(search_config);
1283
1284 assert_eq!(
1285 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1286 None
1287 );
1288 }
1289
1290 #[test]
1291 fn test_word_boundaries() {
1292 let re_str = r"world";
1293 let search_config = SearchConfig {
1294 search_text: re_str,
1295 fixed_strings: false,
1296 match_whole_word: true,
1297 match_case: true,
1298 replacement_text: "earth",
1299 advanced_regex: false,
1300 };
1301 let parsed = test_helpers::must_parse_search_config(search_config);
1302
1303 assert_eq!(
1304 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1305 None
1306 );
1307 }
1308 }
1309
1310 mod whole_word_true_match_case_false {
1311 use super::*;
1312
1313 #[test]
1314 fn test_basic_regex() {
1315 let re_str = r"w\w+d";
1316 let search_config = SearchConfig {
1317 search_text: re_str,
1318 fixed_strings: false,
1319 match_whole_word: true,
1320 match_case: false,
1321 replacement_text: "earth",
1322 advanced_regex: false,
1323 };
1324 let parsed = test_helpers::must_parse_search_config(search_config);
1325
1326 assert_eq!(
1327 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1328 Some("hello earth".to_string())
1329 );
1330 }
1331
1332 #[test]
1333 fn test_word_boundaries() {
1334 let re_str = r"world";
1335 let search_config = SearchConfig {
1336 search_text: re_str,
1337 fixed_strings: false,
1338 match_whole_word: true,
1339 match_case: false,
1340 replacement_text: "earth",
1341 advanced_regex: false,
1342 };
1343 let parsed = test_helpers::must_parse_search_config(search_config);
1344
1345 assert_eq!(
1346 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1347 None
1348 );
1349 }
1350
1351 #[test]
1352 fn test_special_characters() {
1353 let re_str = r"\d+";
1354 let search_config = SearchConfig {
1355 search_text: re_str,
1356 fixed_strings: false,
1357 match_whole_word: true,
1358 match_case: false,
1359 replacement_text: "NUM",
1360 advanced_regex: false,
1361 };
1362 let parsed = test_helpers::must_parse_search_config(search_config);
1363
1364 assert_eq!(
1365 replacement_if_match("test 123 number", &parsed.search, &parsed.replace),
1366 Some("test NUM number".to_string())
1367 );
1368 }
1369
1370 #[test]
1371 fn test_unicode_word_boundaries() {
1372 let re_str = r"\b\p{Script=Han}{2}\b";
1373 let search_config = SearchConfig {
1374 search_text: re_str,
1375 fixed_strings: false,
1376 match_whole_word: true,
1377 match_case: false,
1378 replacement_text: "XX",
1379 advanced_regex: false,
1380 };
1381 let parsed = test_helpers::must_parse_search_config(search_config);
1382
1383 assert!(
1384 replacement_if_match("Text 世界 more", &parsed.search, &parsed.replace)
1385 .is_some()
1386 );
1387 assert!(replacement_if_match("Text世界more", &parsed.search, "XX").is_none());
1388 }
1389 }
1390
1391 mod whole_word_false_match_case_true {
1392 use super::*;
1393
1394 #[test]
1395 fn test_basic_regex() {
1396 let re_str = r"w\w+d";
1397 let search_config = SearchConfig {
1398 search_text: re_str,
1399 fixed_strings: false,
1400 match_whole_word: false,
1401 match_case: true,
1402 replacement_text: "earth",
1403 advanced_regex: false,
1404 };
1405 let parsed = test_helpers::must_parse_search_config(search_config);
1406
1407 assert_eq!(
1408 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1409 Some("hello earth".to_string())
1410 );
1411 }
1412
1413 #[test]
1414 fn test_case_sensitivity() {
1415 let re_str = r"world";
1416 let search_config = SearchConfig {
1417 search_text: re_str,
1418 fixed_strings: false,
1419 match_whole_word: false,
1420 match_case: true,
1421 replacement_text: "earth",
1422 advanced_regex: false,
1423 };
1424 let parsed = test_helpers::must_parse_search_config(search_config);
1425
1426 assert_eq!(
1427 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1428 None
1429 );
1430 }
1431
1432 #[test]
1433 fn test_substring_matches() {
1434 let re_str = r"world";
1435 let search_config = SearchConfig {
1436 search_text: re_str,
1437 fixed_strings: false,
1438 match_whole_word: false,
1439 match_case: true,
1440 replacement_text: "earth",
1441 advanced_regex: false,
1442 };
1443 let parsed = test_helpers::must_parse_search_config(search_config);
1444
1445 assert_eq!(
1446 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1447 Some("earthwide".to_string())
1448 );
1449 }
1450 }
1451
1452 mod whole_word_false_match_case_false {
1453 use super::*;
1454
1455 #[test]
1456 fn test_basic_regex() {
1457 let re_str = r"w\w+d";
1458 let search_config = SearchConfig {
1459 search_text: re_str,
1460 fixed_strings: false,
1461 match_whole_word: false,
1462 match_case: false,
1463 replacement_text: "earth",
1464 advanced_regex: false,
1465 };
1466 let parsed = test_helpers::must_parse_search_config(search_config);
1467
1468 assert_eq!(
1469 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1470 Some("hello earth".to_string())
1471 );
1472 }
1473
1474 #[test]
1475 fn test_substring_matches() {
1476 let re_str = r"world";
1477 let search_config = SearchConfig {
1478 search_text: re_str,
1479 fixed_strings: false,
1480 match_whole_word: false,
1481 match_case: false,
1482 replacement_text: "earth",
1483 advanced_regex: false,
1484 };
1485 let parsed = test_helpers::must_parse_search_config(search_config);
1486
1487 assert_eq!(
1488 replacement_if_match("WORLDWIDE", &parsed.search, &parsed.replace),
1489 Some("earthWIDE".to_string())
1490 );
1491 }
1492
1493 #[test]
1494 fn test_complex_pattern() {
1495 let re_str = r"\d{3}-\d{2}-\d{4}";
1496 let search_config = SearchConfig {
1497 search_text: re_str,
1498 fixed_strings: false,
1499 match_whole_word: false,
1500 match_case: false,
1501 replacement_text: "XXX-XX-XXXX",
1502 advanced_regex: false,
1503 };
1504 let parsed = test_helpers::must_parse_search_config(search_config);
1505
1506 assert_eq!(
1507 replacement_if_match("SSN: 123-45-6789", &parsed.search, &parsed.replace),
1508 Some("SSN: XXX-XX-XXXX".to_string())
1509 );
1510 }
1511 }
1512 }
1513
1514 mod fancy_regex_pattern_tests {
1515 use super::*;
1516
1517 mod whole_word_true_match_case_true {
1518
1519 use super::*;
1520
1521 #[test]
1522 fn test_lookbehind() {
1523 let re_str = r"(?<=@)\w+";
1524 let search_config = SearchConfig {
1525 search_text: re_str,
1526 match_whole_word: true,
1527 fixed_strings: false,
1528 advanced_regex: true,
1529 match_case: true,
1530 replacement_text: "domain",
1531 };
1532 let parsed = test_helpers::must_parse_search_config(search_config);
1533
1534 assert_eq!(
1535 replacement_if_match(
1536 "email: user@example.com",
1537 &parsed.search,
1538 &parsed.replace
1539 ),
1540 Some("email: user@domain.com".to_string())
1541 );
1542 }
1543
1544 #[test]
1545 fn test_lookahead() {
1546 let re_str = r"\w+(?=\.\w+$)";
1547 let search_config = SearchConfig {
1548 search_text: re_str,
1549 match_whole_word: true,
1550 fixed_strings: false,
1551 advanced_regex: true,
1552 match_case: true,
1553 replacement_text: "report",
1554 };
1555 let parsed = test_helpers::must_parse_search_config(search_config);
1556
1557 assert_eq!(
1558 replacement_if_match("file: document.pdf", &parsed.search, &parsed.replace),
1559 Some("file: report.pdf".to_string())
1560 );
1561 }
1562
1563 #[test]
1564 fn test_case_sensitivity() {
1565 let re_str = r"world";
1566 let search_config = SearchConfig {
1567 search_text: re_str,
1568 match_whole_word: true,
1569 fixed_strings: false,
1570 advanced_regex: true,
1571 match_case: true,
1572 replacement_text: "earth",
1573 };
1574 let parsed = test_helpers::must_parse_search_config(search_config);
1575
1576 assert_eq!(
1577 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1578 None
1579 );
1580 }
1581 }
1582
1583 mod whole_word_true_match_case_false {
1584 use super::*;
1585
1586 #[test]
1587 fn test_lookbehind_case_insensitive() {
1588 let re_str = r"(?<=@)\w+";
1589 let search_config = SearchConfig {
1590 search_text: re_str,
1591 match_whole_word: true,
1592 fixed_strings: false,
1593 advanced_regex: true,
1594 match_case: false,
1595 replacement_text: "domain",
1596 };
1597 let parsed = test_helpers::must_parse_search_config(search_config);
1598
1599 assert_eq!(
1600 replacement_if_match(
1601 "email: user@EXAMPLE.com",
1602 &parsed.search,
1603 &parsed.replace
1604 ),
1605 Some("email: user@domain.com".to_string())
1606 );
1607 }
1608
1609 #[test]
1610 fn test_word_boundaries() {
1611 let re_str = r"world";
1612 let search_config = SearchConfig {
1613 search_text: re_str,
1614 match_whole_word: true,
1615 fixed_strings: false,
1616 advanced_regex: true,
1617 match_case: false,
1618 replacement_text: "earth",
1619 };
1620 let parsed = test_helpers::must_parse_search_config(search_config);
1621
1622 assert_eq!(
1623 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1624 None
1625 );
1626 }
1627 }
1628
1629 mod whole_word_false_match_case_true {
1630 use super::*;
1631
1632 #[test]
1633 fn test_complex_pattern() {
1634 let re_str = r"(?<=\d{4}-\d{2}-\d{2}T)\d{2}:\d{2}";
1635 let search_config = SearchConfig {
1636 search_text: re_str,
1637 match_whole_word: false,
1638 fixed_strings: false,
1639 advanced_regex: true,
1640 match_case: true,
1641 replacement_text: "XX:XX",
1642 };
1643 let parsed = test_helpers::must_parse_search_config(search_config);
1644
1645 assert_eq!(
1646 replacement_if_match(
1647 "Timestamp: 2023-01-15T14:30:00Z",
1648 &parsed.search,
1649 &parsed.replace
1650 ),
1651 Some("Timestamp: 2023-01-15TXX:XX:00Z".to_string())
1652 );
1653 }
1654
1655 #[test]
1656 fn test_case_sensitivity() {
1657 let re_str = r"WORLD";
1658 let search_config = SearchConfig {
1659 search_text: re_str,
1660 match_whole_word: false,
1661 fixed_strings: false,
1662 advanced_regex: true,
1663 match_case: true,
1664 replacement_text: "earth",
1665 };
1666 let parsed = test_helpers::must_parse_search_config(search_config);
1667
1668 assert_eq!(
1669 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1670 None
1671 );
1672 }
1673 }
1674
1675 mod whole_word_false_match_case_false {
1676 use super::*;
1677
1678 #[test]
1679 fn test_complex_pattern_case_insensitive() {
1680 let re_str = r"(?<=\[)\w+(?=\])";
1681 let search_config = SearchConfig {
1682 search_text: re_str,
1683 match_whole_word: false,
1684 fixed_strings: false,
1685 advanced_regex: true,
1686 match_case: false,
1687 replacement_text: "ERROR",
1688 };
1689 let parsed = test_helpers::must_parse_search_config(search_config);
1690
1691 assert_eq!(
1692 replacement_if_match(
1693 "Tag: [WARNING] message",
1694 &parsed.search,
1695 &parsed.replace
1696 ),
1697 Some("Tag: [ERROR] message".to_string())
1698 );
1699 }
1700
1701 #[test]
1702 fn test_unicode_support() {
1703 let re_str = r"\p{Greek}+";
1704 let search_config = SearchConfig {
1705 search_text: re_str,
1706 match_whole_word: false,
1707 fixed_strings: false,
1708 advanced_regex: true,
1709 match_case: false,
1710 replacement_text: "GREEK",
1711 };
1712 let parsed = test_helpers::must_parse_search_config(search_config);
1713
1714 assert_eq!(
1715 replacement_if_match("Symbol: αβγδ", &parsed.search, &parsed.replace),
1716 Some("Symbol: GREEK".to_string())
1717 );
1718 }
1719 }
1720 }
1721
1722 #[test]
1723 fn test_multiple_replacements() {
1724 let search_config = SearchConfig {
1725 search_text: "world",
1726 fixed_strings: true,
1727 match_whole_word: true,
1728 match_case: false,
1729 replacement_text: "earth",
1730 advanced_regex: false,
1731 };
1732 let parsed = test_helpers::must_parse_search_config(search_config);
1733 assert_eq!(
1734 replacement_if_match("world hello world", &parsed.search, &parsed.replace),
1735 Some("earth hello earth".to_string())
1736 );
1737 }
1738
1739 #[test]
1740 fn test_no_match() {
1741 let search_config = SearchConfig {
1742 search_text: "world",
1743 fixed_strings: true,
1744 match_whole_word: true,
1745 match_case: false,
1746 replacement_text: "earth",
1747 advanced_regex: false,
1748 };
1749 let parsed = test_helpers::must_parse_search_config(search_config);
1750 assert_eq!(
1751 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1752 None
1753 );
1754 let search_config = SearchConfig {
1755 search_text: "world",
1756 fixed_strings: true,
1757 match_whole_word: true,
1758 match_case: false,
1759 replacement_text: "earth",
1760 advanced_regex: false,
1761 };
1762 let parsed = test_helpers::must_parse_search_config(search_config);
1763 assert_eq!(
1764 replacement_if_match("_world_", &parsed.search, &parsed.replace),
1765 None
1766 );
1767 }
1768
1769 #[test]
1770 fn test_word_boundaries() {
1771 let search_config = SearchConfig {
1772 search_text: "world",
1773 fixed_strings: true,
1774 match_whole_word: true,
1775 match_case: false,
1776 replacement_text: "earth",
1777 advanced_regex: false,
1778 };
1779 let parsed = test_helpers::must_parse_search_config(search_config);
1780 assert_eq!(
1781 replacement_if_match(",world-", &parsed.search, &parsed.replace),
1782 Some(",earth-".to_string())
1783 );
1784 let search_config = SearchConfig {
1785 search_text: "world",
1786 fixed_strings: true,
1787 match_whole_word: true,
1788 match_case: false,
1789 replacement_text: "earth",
1790 advanced_regex: false,
1791 };
1792 let parsed = test_helpers::must_parse_search_config(search_config);
1793 assert_eq!(
1794 replacement_if_match("world-word", &parsed.search, &parsed.replace),
1795 Some("earth-word".to_string())
1796 );
1797 let search_config = SearchConfig {
1798 search_text: "world",
1799 fixed_strings: true,
1800 match_whole_word: true,
1801 match_case: false,
1802 replacement_text: "earth",
1803 advanced_regex: false,
1804 };
1805 let parsed = test_helpers::must_parse_search_config(search_config);
1806 assert_eq!(
1807 replacement_if_match("Hello-world!", &parsed.search, &parsed.replace),
1808 Some("Hello-earth!".to_string())
1809 );
1810 }
1811
1812 #[test]
1813 fn test_case_sensitive() {
1814 let search_config = SearchConfig {
1815 search_text: "world",
1816 fixed_strings: true,
1817 match_whole_word: true,
1818 match_case: true,
1819 replacement_text: "earth",
1820 advanced_regex: false,
1821 };
1822 let parsed = test_helpers::must_parse_search_config(search_config);
1823 assert_eq!(
1824 replacement_if_match("Hello WORLD", &parsed.search, &parsed.replace),
1825 None
1826 );
1827 let search_config = SearchConfig {
1828 search_text: "wOrld",
1829 fixed_strings: true,
1830 match_whole_word: true,
1831 match_case: true,
1832 replacement_text: "earth",
1833 advanced_regex: false,
1834 };
1835 let parsed = test_helpers::must_parse_search_config(search_config);
1836 assert_eq!(
1837 replacement_if_match("Hello world", &parsed.search, &parsed.replace),
1838 None
1839 );
1840 }
1841
1842 #[test]
1843 fn test_empty_strings() {
1844 let search_config = SearchConfig {
1845 search_text: "world",
1846 fixed_strings: true,
1847 match_whole_word: true,
1848 match_case: false,
1849 replacement_text: "earth",
1850 advanced_regex: false,
1851 };
1852 let parsed = test_helpers::must_parse_search_config(search_config);
1853 assert_eq!(
1854 replacement_if_match("", &parsed.search, &parsed.replace),
1855 None
1856 );
1857 let search_config = SearchConfig {
1858 search_text: "",
1859 fixed_strings: true,
1860 match_whole_word: true,
1861 match_case: false,
1862 replacement_text: "earth",
1863 advanced_regex: false,
1864 };
1865 let parsed = test_helpers::must_parse_search_config(search_config);
1866 assert_eq!(
1867 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1868 None
1869 );
1870 }
1871
1872 #[test]
1873 fn test_substring_no_match() {
1874 let search_config = SearchConfig {
1875 search_text: "world",
1876 fixed_strings: true,
1877 match_whole_word: true,
1878 match_case: false,
1879 replacement_text: "earth",
1880 advanced_regex: false,
1881 };
1882 let parsed = test_helpers::must_parse_search_config(search_config);
1883 assert_eq!(
1884 replacement_if_match("worldwide web", &parsed.search, &parsed.replace),
1885 None
1886 );
1887 let search_config = SearchConfig {
1888 search_text: "world",
1889 fixed_strings: true,
1890 match_whole_word: true,
1891 match_case: false,
1892 replacement_text: "earth",
1893 advanced_regex: false,
1894 };
1895 let parsed = test_helpers::must_parse_search_config(search_config);
1896 assert_eq!(
1897 replacement_if_match("underworld", &parsed.search, &parsed.replace),
1898 None
1899 );
1900 }
1901
1902 #[test]
1903 fn test_special_regex_chars() {
1904 let search_config = SearchConfig {
1905 search_text: "(world)",
1906 fixed_strings: true,
1907 match_whole_word: true,
1908 match_case: false,
1909 replacement_text: "earth",
1910 advanced_regex: false,
1911 };
1912 let parsed = test_helpers::must_parse_search_config(search_config);
1913 assert_eq!(
1914 replacement_if_match("hello (world)", &parsed.search, &parsed.replace),
1915 Some("hello earth".to_string())
1916 );
1917 let search_config = SearchConfig {
1918 search_text: "world.*",
1919 fixed_strings: true,
1920 match_whole_word: true,
1921 match_case: false,
1922 replacement_text: "ea+rth",
1923 advanced_regex: false,
1924 };
1925 let parsed = test_helpers::must_parse_search_config(search_config);
1926 assert_eq!(
1927 replacement_if_match("hello world.*", &parsed.search, &parsed.replace),
1928 Some("hello ea+rth".to_string())
1929 );
1930 }
1931
1932 #[test]
1933 fn test_basic_regex_patterns() {
1934 let re_str = r"ax*b";
1935 let search_config = SearchConfig {
1936 search_text: re_str,
1937 fixed_strings: false,
1938 match_whole_word: true,
1939 match_case: false,
1940 replacement_text: "NEW",
1941 advanced_regex: false,
1942 };
1943 let parsed = test_helpers::must_parse_search_config(search_config);
1944 assert_eq!(
1945 replacement_if_match("foo axxxxb bar", &parsed.search, &parsed.replace),
1946 Some("foo NEW bar".to_string())
1947 );
1948 let search_config = SearchConfig {
1949 search_text: re_str,
1950 fixed_strings: false,
1951 match_whole_word: true,
1952 match_case: false,
1953 replacement_text: "NEW",
1954 advanced_regex: false,
1955 };
1956 let parsed = test_helpers::must_parse_search_config(search_config);
1957 assert_eq!(
1958 replacement_if_match("fooaxxxxb bar", &parsed.search, &parsed.replace),
1959 None
1960 );
1961 }
1962
1963 #[test]
1964 fn test_patterns_with_spaces() {
1965 let re_str = r"hel+o world";
1966 let search_config = SearchConfig {
1967 search_text: re_str,
1968 fixed_strings: false,
1969 match_whole_word: true,
1970 match_case: false,
1971 replacement_text: "hi earth",
1972 advanced_regex: false,
1973 };
1974 let parsed = test_helpers::must_parse_search_config(search_config);
1975 assert_eq!(
1976 replacement_if_match("say hello world!", &parsed.search, &parsed.replace),
1977 Some("say hi earth!".to_string())
1978 );
1979 let search_config = SearchConfig {
1980 search_text: re_str,
1981 fixed_strings: false,
1982 match_whole_word: true,
1983 match_case: false,
1984 replacement_text: "hi earth",
1985 advanced_regex: false,
1986 };
1987 let parsed = test_helpers::must_parse_search_config(search_config);
1988 assert_eq!(
1989 replacement_if_match("helloworld", &parsed.search, &parsed.replace),
1990 None
1991 );
1992 }
1993
1994 #[test]
1995 fn test_multiple_matches() {
1996 let re_str = r"a+b+";
1997 let search_config = SearchConfig {
1998 search_text: re_str,
1999 fixed_strings: false,
2000 match_whole_word: true,
2001 match_case: false,
2002 replacement_text: "X",
2003 advanced_regex: false,
2004 };
2005 let parsed = test_helpers::must_parse_search_config(search_config);
2006 assert_eq!(
2007 replacement_if_match("foo aab abb", &parsed.search, &parsed.replace),
2008 Some("foo X X".to_string())
2009 );
2010 let search_config = SearchConfig {
2011 search_text: re_str,
2012 fixed_strings: false,
2013 match_whole_word: true,
2014 match_case: false,
2015 replacement_text: "X",
2016 advanced_regex: false,
2017 };
2018 let parsed = test_helpers::must_parse_search_config(search_config);
2019 assert_eq!(
2020 replacement_if_match("ab abaab abb", &parsed.search, &parsed.replace),
2021 Some("X abaab X".to_string())
2022 );
2023 let search_config = SearchConfig {
2024 search_text: re_str,
2025 fixed_strings: false,
2026 match_whole_word: true,
2027 match_case: false,
2028 replacement_text: "X",
2029 advanced_regex: false,
2030 };
2031 let parsed = test_helpers::must_parse_search_config(search_config);
2032 assert_eq!(
2033 replacement_if_match("ababaababb", &parsed.search, &parsed.replace),
2034 None
2035 );
2036 let search_config = SearchConfig {
2037 search_text: re_str,
2038 fixed_strings: false,
2039 match_whole_word: true,
2040 match_case: false,
2041 replacement_text: "X",
2042 advanced_regex: false,
2043 };
2044 let parsed = test_helpers::must_parse_search_config(search_config);
2045 assert_eq!(
2046 replacement_if_match("ab ab aab abb", &parsed.search, &parsed.replace),
2047 Some("X X X X".to_string())
2048 );
2049 }
2050
2051 #[test]
2052 fn test_boundary_cases() {
2053 let re_str = r"foo\s*bar";
2054 let search_config = SearchConfig {
2056 search_text: re_str,
2057 fixed_strings: false,
2058 match_whole_word: true,
2059 match_case: false,
2060 replacement_text: "TEST",
2061 advanced_regex: false,
2062 };
2063 let parsed = test_helpers::must_parse_search_config(search_config);
2064 assert_eq!(
2065 replacement_if_match("foo bar baz", &parsed.search, &parsed.replace),
2066 Some("TEST baz".to_string())
2067 );
2068 let search_config = SearchConfig {
2070 search_text: re_str,
2071 fixed_strings: false,
2072 match_whole_word: true,
2073 match_case: false,
2074 replacement_text: "TEST",
2075 advanced_regex: false,
2076 };
2077 let parsed = test_helpers::must_parse_search_config(search_config);
2078 assert_eq!(
2079 replacement_if_match("baz foo bar", &parsed.search, &parsed.replace),
2080 Some("baz TEST".to_string())
2081 );
2082 let search_config = SearchConfig {
2084 search_text: re_str,
2085 fixed_strings: false,
2086 match_whole_word: true,
2087 match_case: false,
2088 replacement_text: "TEST",
2089 advanced_regex: false,
2090 };
2091 let parsed = test_helpers::must_parse_search_config(search_config);
2092 assert_eq!(
2093 replacement_if_match("a (?( foo bar)", &parsed.search, &parsed.replace),
2094 Some("a (?( TEST)".to_string())
2095 );
2096 }
2097
2098 #[test]
2099 fn test_with_punctuation() {
2100 let re_str = r"a\d+b";
2101 let search_config = SearchConfig {
2102 search_text: re_str,
2103 fixed_strings: false,
2104 match_whole_word: true,
2105 match_case: false,
2106 replacement_text: "X",
2107 advanced_regex: false,
2108 };
2109 let parsed = test_helpers::must_parse_search_config(search_config);
2110 assert_eq!(
2111 replacement_if_match("(a42b)", &parsed.search, &parsed.replace),
2112 Some("(X)".to_string())
2113 );
2114 let search_config = SearchConfig {
2115 search_text: re_str,
2116 fixed_strings: false,
2117 match_whole_word: true,
2118 match_case: false,
2119 replacement_text: "X",
2120 advanced_regex: false,
2121 };
2122 let parsed = test_helpers::must_parse_search_config(search_config);
2123 assert_eq!(
2124 replacement_if_match("foo.a123b!bar", &parsed.search, &parsed.replace),
2125 Some("foo.X!bar".to_string())
2126 );
2127 }
2128
2129 #[test]
2130 fn test_complex_patterns() {
2131 let re_str = r"[a-z]+\d+[a-z]+";
2132 let search_config = SearchConfig {
2133 search_text: re_str,
2134 fixed_strings: false,
2135 match_whole_word: true,
2136 match_case: false,
2137 replacement_text: "NEW",
2138 advanced_regex: false,
2139 };
2140 let parsed = test_helpers::must_parse_search_config(search_config);
2141 assert_eq!(
2142 replacement_if_match("test9 abc123def 8xyz", &parsed.search, &parsed.replace),
2143 Some("test9 NEW 8xyz".to_string())
2144 );
2145 let search_config = SearchConfig {
2146 search_text: re_str,
2147 fixed_strings: false,
2148 match_whole_word: true,
2149 match_case: false,
2150 replacement_text: "NEW",
2151 advanced_regex: false,
2152 };
2153 let parsed = test_helpers::must_parse_search_config(search_config);
2154 assert_eq!(
2155 replacement_if_match("test9abc123def8xyz", &parsed.search, &parsed.replace),
2156 None
2157 );
2158 }
2159
2160 #[test]
2161 fn test_optional_patterns() {
2162 let re_str = r"colou?r";
2163 let search_config = SearchConfig {
2164 search_text: re_str,
2165 fixed_strings: false,
2166 match_whole_word: true,
2167 match_case: false,
2168 replacement_text: "X",
2169 advanced_regex: false,
2170 };
2171 let parsed = test_helpers::must_parse_search_config(search_config);
2172 assert_eq!(
2173 replacement_if_match("my color and colour", &parsed.search, &parsed.replace),
2174 Some("my X and X".to_string())
2175 );
2176 }
2177
2178 #[test]
2179 fn test_empty_haystack() {
2180 let re_str = r"test";
2181 let search_config = SearchConfig {
2182 search_text: re_str,
2183 fixed_strings: false,
2184 match_whole_word: true,
2185 match_case: false,
2186 replacement_text: "NEW",
2187 advanced_regex: false,
2188 };
2189 let parsed = test_helpers::must_parse_search_config(search_config);
2190 assert_eq!(
2191 replacement_if_match("", &parsed.search, &parsed.replace),
2192 None
2193 );
2194 }
2195
2196 #[test]
2197 fn test_empty_search_regex() {
2198 let re_str = r"";
2199 let search_config = SearchConfig {
2200 search_text: re_str,
2201 fixed_strings: false,
2202 match_whole_word: true,
2203 match_case: false,
2204 replacement_text: "NEW",
2205 advanced_regex: false,
2206 };
2207 let parsed = test_helpers::must_parse_search_config(search_config);
2208 assert_eq!(
2209 replacement_if_match("search", &parsed.search, &parsed.replace),
2210 None
2211 );
2212 }
2213
2214 #[test]
2215 fn test_single_char() {
2216 let re_str = r"a";
2217 let search_config = SearchConfig {
2218 search_text: re_str,
2219 fixed_strings: false,
2220 match_whole_word: true,
2221 match_case: false,
2222 replacement_text: "X",
2223 advanced_regex: false,
2224 };
2225 let parsed = test_helpers::must_parse_search_config(search_config);
2226 assert_eq!(
2227 replacement_if_match("b a c", &parsed.search, &parsed.replace),
2228 Some("b X c".to_string())
2229 );
2230 let search_config = SearchConfig {
2231 search_text: re_str,
2232 fixed_strings: false,
2233 match_whole_word: true,
2234 match_case: false,
2235 replacement_text: "X",
2236 advanced_regex: false,
2237 };
2238 let parsed = test_helpers::must_parse_search_config(search_config);
2239 assert_eq!(
2240 replacement_if_match("bac", &parsed.search, &parsed.replace),
2241 None
2242 );
2243 }
2244
2245 #[test]
2246 fn test_escaped_chars() {
2247 let re_str = r"\(\d+\)";
2248 let search_config = SearchConfig {
2249 search_text: re_str,
2250 fixed_strings: false,
2251 match_whole_word: true,
2252 match_case: false,
2253 replacement_text: "X",
2254 advanced_regex: false,
2255 };
2256 let parsed = test_helpers::must_parse_search_config(search_config);
2257 assert_eq!(
2258 replacement_if_match("test (123) foo", &parsed.search, &parsed.replace),
2259 Some("test X foo".to_string())
2260 );
2261 }
2262
2263 #[test]
2264 fn test_with_unicode() {
2265 let re_str = r"λ\d+";
2266 let search_config = SearchConfig {
2267 search_text: re_str,
2268 fixed_strings: false,
2269 match_whole_word: true,
2270 match_case: false,
2271 replacement_text: "X",
2272 advanced_regex: false,
2273 };
2274 let parsed = test_helpers::must_parse_search_config(search_config);
2275 assert_eq!(
2276 replacement_if_match("calc λ123 β", &parsed.search, &parsed.replace),
2277 Some("calc X β".to_string())
2278 );
2279 let search_config = SearchConfig {
2280 search_text: re_str,
2281 fixed_strings: false,
2282 match_whole_word: true,
2283 match_case: false,
2284 replacement_text: "X",
2285 advanced_regex: false,
2286 };
2287 let parsed = test_helpers::must_parse_search_config(search_config);
2288 assert_eq!(
2289 replacement_if_match("calcλ123", &parsed.search, &parsed.replace),
2290 None
2291 );
2292 }
2293 }
2294}