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: HashMap<_, _> = results
28 .iter_mut()
29 .map(|res| (res.search_result.line_number, res))
30 .collect();
31
32 let parent_dir = file_path.parent().unwrap_or(Path::new("."));
33 let temp_output_file = NamedTempFile::new_in(parent_dir)?;
34
35 {
37 let input = File::open(file_path.clone())?;
38 let reader = BufReader::new(input);
39
40 let output = File::create(temp_output_file.path())?;
41 let mut writer = BufWriter::new(output);
42
43 for (mut line_number, line_result) in reader.lines_with_endings().enumerate() {
44 line_number += 1; let (mut line, line_ending) = line_result?;
46 if let Some(res) = line_map.get_mut(&line_number) {
47 if line == res.search_result.line.as_bytes() {
48 line = res.replacement.as_bytes().to_vec();
49 res.replace_result = Some(ReplaceResult::Success);
50 } else {
51 res.replace_result = Some(ReplaceResult::Error(
52 "File changed since last search".to_owned(),
53 ));
54 }
55 }
56 line.extend(line_ending.as_bytes());
57 writer.write_all(&line)?;
58 }
59
60 writer.flush()?;
61 }
62
63 temp_output_file.persist(file_path)?;
64 Ok(())
65}
66
67const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; fn should_replace_in_memory(path: &Path) -> Result<bool, std::io::Error> {
70 let file_size = fs::metadata(path)?.len();
71 Ok(file_size <= MAX_FILE_SIZE)
72}
73
74pub fn replace_all_in_file(
94 file_path: &Path,
95 search: &SearchType,
96 replace: &str,
97) -> anyhow::Result<bool> {
98 if matches!(should_replace_in_memory(file_path), Ok(true)) {
100 match replace_in_memory(file_path, search, replace) {
101 Ok(replaced) => return Ok(replaced),
102 Err(e) => {
103 log::error!(
104 "Found error when attempting to replace in memory for file {path_display}: {e}",
105 path_display = file_path.display(),
106 );
107 }
108 }
109 }
110
111 replace_chunked(file_path, search, replace)
112}
113
114pub fn add_replacement(
115 search_result: SearchResult,
116 search: &SearchType,
117 replace: &str,
118) -> Option<SearchResultWithReplacement> {
119 let replacement = replacement_if_match(&search_result.line, search, replace)?;
120 Some(SearchResultWithReplacement {
121 search_result,
122 replacement,
123 replace_result: None,
124 })
125}
126
127fn replace_chunked(file_path: &Path, search: &SearchType, replace: &str) -> anyhow::Result<bool> {
128 let search_results = search::search_file(file_path, search)?;
129 if !search_results.is_empty() {
130 let mut replacement_results = search_results
131 .into_iter()
132 .map(|r| {
133 add_replacement(r, search, replace).unwrap_or_else(|| {
134 panic!("Called add_replacement with non-matching search result")
135 })
136 })
137 .collect::<Vec<_>>();
138 replace_in_file(&mut replacement_results)?;
139 return Ok(true);
140 }
141
142 Ok(false)
143}
144
145fn replace_in_memory(file_path: &Path, search: &SearchType, replace: &str) -> anyhow::Result<bool> {
146 let content = fs::read_to_string(file_path)?;
147 if let Some(new_content) = replacement_if_match(&content, search, replace) {
148 let parent_dir = file_path.parent().unwrap_or(Path::new("."));
149 let mut temp_file = NamedTempFile::new_in(parent_dir)?;
150 temp_file.write_all(new_content.as_bytes())?;
151 temp_file.persist(file_path)?;
152 Ok(true)
153 } else {
154 Ok(false)
155 }
156}
157
158pub fn replacement_if_match(line: &str, search: &SearchType, replace: &str) -> Option<String> {
171 if line.is_empty() || search.is_empty() {
172 return None;
173 }
174
175 if search::contains_search(line, search) {
176 let replacement = match search {
177 SearchType::Fixed(fixed_str) => line.replace(fixed_str, replace),
178 SearchType::Pattern(pattern) => pattern.replace_all(line, replace).to_string(),
179 SearchType::PatternAdvanced(pattern) => pattern.replace_all(line, replace).to_string(),
180 };
181 Some(replacement)
182 } else {
183 None
184 }
185}
186
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct ReplaceStats {
189 pub num_successes: usize,
190 pub errors: Vec<SearchResultWithReplacement>,
191}
192
193pub fn calculate_statistics<I>(results: I) -> ReplaceStats
194where
195 I: IntoIterator<Item = SearchResultWithReplacement>,
196{
197 let mut num_successes = 0;
198 let mut errors = vec![];
199
200 results.into_iter().for_each(|res| {
201 assert!(
202 res.search_result.included,
203 "Expected only included results, found {res:?}"
204 );
205 match &res.replace_result {
206 Some(ReplaceResult::Success) => {
207 num_successes += 1;
208 }
209 None => {
210 let mut res = res.clone();
211 res.replace_result = Some(ReplaceResult::Error(
212 "Failed to find search result in file".to_owned(),
213 ));
214 errors.push(res);
215 }
216 Some(ReplaceResult::Error(_)) => {
217 errors.push(res.clone());
218 }
219 }
220 });
221
222 ReplaceStats {
223 num_successes,
224 errors,
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::line_reader::LineEnding;
232 use crate::search::{SearchResult, SearchType, search_file};
233 use regex::Regex;
234 use std::path::PathBuf;
235 use tempfile::TempDir;
236
237 mod test_helpers {
238 use crate::search::SearchType;
239
240 pub fn create_fixed_search(term: &str) -> SearchType {
241 SearchType::Fixed(term.to_string())
242 }
243 }
244
245 fn create_search_result_with_replacement(
247 path: &str,
248 line_number: usize,
249 line: &str,
250 replacement: &str,
251 included: bool,
252 replace_result: Option<ReplaceResult>,
253 ) -> SearchResultWithReplacement {
254 SearchResultWithReplacement {
255 search_result: SearchResult {
256 path: PathBuf::from(path),
257 line_number,
258 line: line.to_string(),
259 line_ending: LineEnding::Lf,
260 included,
261 },
262 replacement: replacement.to_string(),
263 replace_result,
264 }
265 }
266
267 fn create_test_file(temp_dir: &TempDir, name: &str, content: &str) -> PathBuf {
268 let file_path = temp_dir.path().join(name);
269 std::fs::write(&file_path, content).unwrap();
270 file_path
271 }
272
273 fn assert_file_content(file_path: &Path, expected_content: &str) {
274 let content = std::fs::read_to_string(file_path).unwrap();
275 assert_eq!(content, expected_content);
276 }
277
278 fn fixed_search(pattern: &str) -> SearchType {
279 SearchType::Fixed(pattern.to_string())
280 }
281
282 fn regex_search(pattern: &str) -> SearchType {
283 SearchType::Pattern(Regex::new(pattern).unwrap())
284 }
285
286 #[test]
288 fn test_replace_in_file_success() {
289 let temp_dir = TempDir::new().unwrap();
290 let file_path = create_test_file(
291 &temp_dir,
292 "test.txt",
293 "line 1\nold text\nline 3\nold text\nline 5\n",
294 );
295
296 let mut results = vec![
298 create_search_result_with_replacement(
299 file_path.to_str().unwrap(),
300 2,
301 "old text",
302 "new text",
303 true,
304 None,
305 ),
306 create_search_result_with_replacement(
307 file_path.to_str().unwrap(),
308 4,
309 "old text",
310 "new text",
311 true,
312 None,
313 ),
314 ];
315
316 let result = replace_in_file(&mut results);
318 assert!(result.is_ok());
319
320 assert_eq!(results.len(), 2);
322 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
323 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
324
325 assert_file_content(&file_path, "line 1\nnew text\nline 3\nnew text\nline 5\n");
327 }
328
329 #[test]
330 fn test_replace_in_file_success_no_final_newline() {
331 let temp_dir = TempDir::new().unwrap();
332 let file_path = create_test_file(
333 &temp_dir,
334 "test.txt",
335 "line 1\nold text\nline 3\nold text\nline 5",
336 );
337
338 let mut results = vec![
340 create_search_result_with_replacement(
341 file_path.to_str().unwrap(),
342 2,
343 "old text",
344 "new text",
345 true,
346 None,
347 ),
348 create_search_result_with_replacement(
349 file_path.to_str().unwrap(),
350 4,
351 "old text",
352 "new text",
353 true,
354 None,
355 ),
356 ];
357
358 let result = replace_in_file(&mut results);
360 assert!(result.is_ok());
361
362 assert_eq!(results.len(), 2);
364 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
365 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
366
367 let new_content = std::fs::read_to_string(&file_path).unwrap();
369 assert_eq!(new_content, "line 1\nnew text\nline 3\nnew text\nline 5");
370 }
371
372 #[test]
373 fn test_replace_in_file_success_windows_newlines() {
374 let temp_dir = TempDir::new().unwrap();
375 let file_path = create_test_file(
376 &temp_dir,
377 "test.txt",
378 "line 1\r\nold text\r\nline 3\r\nold text\r\nline 5\r\n",
379 );
380
381 let mut results = vec![
383 create_search_result_with_replacement(
384 file_path.to_str().unwrap(),
385 2,
386 "old text",
387 "new text",
388 true,
389 None,
390 ),
391 create_search_result_with_replacement(
392 file_path.to_str().unwrap(),
393 4,
394 "old text",
395 "new text",
396 true,
397 None,
398 ),
399 ];
400
401 let result = replace_in_file(&mut results);
403 assert!(result.is_ok());
404
405 assert_eq!(results.len(), 2);
407 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
408 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
409
410 let new_content = std::fs::read_to_string(&file_path).unwrap();
412 assert_eq!(
413 new_content,
414 "line 1\r\nnew text\r\nline 3\r\nnew text\r\nline 5\r\n"
415 );
416 }
417
418 #[test]
419 fn test_replace_in_file_success_mixed_newlines() {
420 let temp_dir = TempDir::new().unwrap();
421 let file_path = create_test_file(
422 &temp_dir,
423 "test.txt",
424 "\n\r\nline 1\nold text\r\nline 3\nline 4\r\nline 5\r\n\n\n",
425 );
426
427 let mut results = vec![
429 create_search_result_with_replacement(
430 file_path.to_str().unwrap(),
431 4,
432 "old text",
433 "new text",
434 true,
435 None,
436 ),
437 create_search_result_with_replacement(
438 file_path.to_str().unwrap(),
439 7,
440 "line 5",
441 "updated line 5",
442 true,
443 None,
444 ),
445 ];
446
447 let result = replace_in_file(&mut results);
449 assert!(result.is_ok());
450
451 assert_eq!(results.len(), 2);
453 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
454 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
455
456 let new_content = std::fs::read_to_string(&file_path).unwrap();
458 assert_eq!(
459 new_content,
460 "\n\r\nline 1\nnew text\r\nline 3\nline 4\r\nupdated line 5\r\n\n\n"
461 );
462 }
463
464 #[test]
465 fn test_replace_in_file_line_mismatch() {
466 let temp_dir = TempDir::new().unwrap();
467 let file_path = create_test_file(&temp_dir, "test.txt", "line 1\nactual text\nline 3\n");
468
469 let mut results = vec![create_search_result_with_replacement(
471 file_path.to_str().unwrap(),
472 2,
473 "expected text",
474 "new text",
475 true,
476 None,
477 )];
478
479 let result = replace_in_file(&mut results);
481 assert!(result.is_ok());
482
483 assert_eq!(
485 results[0].replace_result,
486 Some(ReplaceResult::Error(
487 "File changed since last search".to_owned()
488 ))
489 );
490
491 let new_content = std::fs::read_to_string(&file_path).unwrap();
493 assert_eq!(new_content, "line 1\nactual text\nline 3\n");
494 }
495
496 #[test]
497 fn test_replace_in_file_nonexistent_file() {
498 let mut results = vec![create_search_result_with_replacement(
499 "/nonexistent/path/file.txt",
500 1,
501 "old",
502 "new",
503 true,
504 None,
505 )];
506
507 let result = replace_in_file(&mut results);
508 assert!(result.is_err());
509 }
510
511 #[test]
512 fn test_replace_directory_errors() {
513 let mut results = vec![create_search_result_with_replacement(
514 "/", 0, "foo", "bar", true, None,
515 )];
516
517 let result = replace_in_file(&mut results);
518 assert!(result.is_err());
519 }
520
521 #[test]
523 fn test_replace_in_memory() {
524 let temp_dir = TempDir::new().unwrap();
525
526 let file_path = create_test_file(
528 &temp_dir,
529 "test.txt",
530 "This is a test.\nIt contains search_term that should be replaced.\nMultiple lines with search_term here.",
531 );
532
533 let result = replace_in_memory(&file_path, &fixed_search("search_term"), "replacement");
534 assert!(result.is_ok());
535 assert!(result.unwrap()); assert_file_content(
538 &file_path,
539 "This is a test.\nIt contains replacement that should be replaced.\nMultiple lines with replacement here.",
540 );
541
542 let regex_path = create_test_file(
544 &temp_dir,
545 "regex_test.txt",
546 "Number: 123, Code: 456, ID: 789",
547 );
548
549 let result = replace_in_memory(®ex_path, ®ex_search(r"\d{3}"), "XXX");
550 assert!(result.is_ok());
551 assert!(result.unwrap());
552
553 assert_file_content(®ex_path, "Number: XXX, Code: XXX, ID: XXX");
554 }
555
556 #[test]
557 fn test_replace_in_memory_no_match() {
558 let temp_dir = TempDir::new().unwrap();
559 let file_path = create_test_file(
560 &temp_dir,
561 "no_match.txt",
562 "This is a test file with no matches.",
563 );
564
565 let result = replace_in_memory(&file_path, &fixed_search("nonexistent"), "replacement");
566 assert!(result.is_ok());
567 assert!(!result.unwrap()); assert_file_content(&file_path, "This is a test file with no matches.");
571 }
572
573 #[test]
574 fn test_replace_in_memory_empty_file() {
575 let temp_dir = TempDir::new().unwrap();
576 let file_path = create_test_file(&temp_dir, "empty.txt", "");
577
578 let result = replace_in_memory(&file_path, &fixed_search("anything"), "replacement");
579 assert!(result.is_ok());
580 assert!(!result.unwrap());
581
582 assert_file_content(&file_path, "");
584 }
585
586 #[test]
587 fn test_replace_in_memory_nonexistent_file() {
588 let result = replace_in_memory(
589 Path::new("/nonexistent/path/file.txt"),
590 &fixed_search("test"),
591 "replacement",
592 );
593 assert!(result.is_err());
594 }
595
596 #[test]
598 fn test_replace_chunked() {
599 let temp_dir = TempDir::new().unwrap();
600
601 let file_path = create_test_file(
603 &temp_dir,
604 "test.txt",
605 "This is line one.\nThis contains search_pattern to replace.\nAnother line with search_pattern here.\nFinal line.",
606 );
607
608 let result = replace_chunked(&file_path, &fixed_search("search_pattern"), "replacement");
609 assert!(result.is_ok());
610 assert!(result.unwrap()); assert_file_content(
613 &file_path,
614 "This is line one.\nThis contains replacement to replace.\nAnother line with replacement here.\nFinal line.",
615 );
616
617 let regex_path = create_test_file(
619 &temp_dir,
620 "regex.txt",
621 "Line with numbers: 123 and 456.\nAnother line with 789.",
622 );
623
624 let result = replace_chunked(®ex_path, ®ex_search(r"\d{3}"), "XXX");
625 assert!(result.is_ok());
626 assert!(result.unwrap());
627
628 assert_file_content(
629 ®ex_path,
630 "Line with numbers: XXX and XXX.\nAnother line with XXX.",
631 );
632 }
633
634 #[test]
635 fn test_replace_chunked_no_match() {
636 let temp_dir = TempDir::new().unwrap();
637 let file_path = create_test_file(
638 &temp_dir,
639 "test.txt",
640 "This is a test file with no matching patterns.",
641 );
642
643 let result = replace_chunked(&file_path, &fixed_search("nonexistent"), "replacement");
644 assert!(result.is_ok());
645 assert!(!result.unwrap());
646
647 assert_file_content(&file_path, "This is a test file with no matching patterns.");
649 }
650
651 #[test]
652 fn test_replace_chunked_empty_file() {
653 let temp_dir = TempDir::new().unwrap();
654 let file_path = create_test_file(&temp_dir, "empty.txt", "");
655
656 let result = replace_chunked(&file_path, &fixed_search("anything"), "replacement");
657 assert!(result.is_ok());
658 assert!(!result.unwrap());
659
660 assert_file_content(&file_path, "");
662 }
663
664 #[test]
665 fn test_replace_chunked_nonexistent_file() {
666 let result = replace_chunked(
667 Path::new("/nonexistent/path/file.txt"),
668 &fixed_search("test"),
669 "replacement",
670 );
671 assert!(result.is_err());
672 }
673
674 #[test]
676 fn test_replace_all_in_file() {
677 let temp_dir = TempDir::new().unwrap();
678 let file_path = create_test_file(
679 &temp_dir,
680 "test.txt",
681 "This is a test file.\nIt has some content to replace.\nThe word replace should be replaced.",
682 );
683
684 let result = replace_all_in_file(&file_path, &fixed_search("replace"), "modify");
685 assert!(result.is_ok());
686 assert!(result.unwrap());
687
688 assert_file_content(
689 &file_path,
690 "This is a test file.\nIt has some content to modify.\nThe word modify should be modifyd.",
691 );
692 }
693
694 #[test]
695 fn test_unicode_in_file() {
696 let mut temp_file = NamedTempFile::new().unwrap();
697 writeln!(temp_file, "Line with Greek: αβγδε").unwrap();
698 write!(temp_file, "Line with Emoji: 😀 🚀 🌍\r\n").unwrap();
699 write!(temp_file, "Line with Arabic: مرحبا بالعالم").unwrap();
700 temp_file.flush().unwrap();
701
702 let search = SearchType::Pattern(Regex::new(r"\p{Greek}+").unwrap());
703 let replacement = "GREEK";
704 let results = search_file(temp_file.path(), &search)
705 .unwrap()
706 .into_iter()
707 .filter_map(|r| add_replacement(r, &search, replacement))
708 .collect::<Vec<_>>();
709
710 assert_eq!(results.len(), 1);
711 assert_eq!(results[0].replacement, "Line with Greek: GREEK");
712
713 let search = SearchType::Pattern(Regex::new(r"🚀").unwrap());
714 let replacement = "ROCKET";
715 let results = search_file(temp_file.path(), &search)
716 .unwrap()
717 .into_iter()
718 .filter_map(|r| add_replacement(r, &search, replacement))
719 .collect::<Vec<_>>();
720
721 assert_eq!(results.len(), 1);
722 assert_eq!(results[0].replacement, "Line with Emoji: 😀 ROCKET 🌍");
723 assert_eq!(results[0].search_result.line_ending, LineEnding::CrLf);
724 }
725
726 mod search_file_tests {
727 use super::*;
728 use fancy_regex::Regex as FancyRegex;
729 use regex::Regex;
730 use std::io::Write;
731 use tempfile::NamedTempFile;
732
733 #[test]
734 fn test_search_file_simple_match() {
735 let mut temp_file = NamedTempFile::new().unwrap();
736 writeln!(temp_file, "line 1").unwrap();
737 writeln!(temp_file, "search target").unwrap();
738 writeln!(temp_file, "line 3").unwrap();
739 temp_file.flush().unwrap();
740
741 let search = test_helpers::create_fixed_search("search");
742 let replacement = "replace";
743 let results = search_file(temp_file.path(), &search)
744 .unwrap()
745 .into_iter()
746 .filter_map(|r| add_replacement(r, &search, replacement))
747 .collect::<Vec<_>>();
748
749 assert_eq!(results.len(), 1);
750 assert_eq!(results[0].search_result.line_number, 2);
751 assert_eq!(results[0].search_result.line, "search target");
752 assert_eq!(results[0].replacement, "replace target");
753 assert!(results[0].search_result.included);
754 }
755
756 #[test]
757 fn test_search_file_multiple_matches() {
758 let mut temp_file = NamedTempFile::new().unwrap();
759 writeln!(temp_file, "test line 1").unwrap();
760 writeln!(temp_file, "test line 2").unwrap();
761 writeln!(temp_file, "no match here").unwrap();
762 writeln!(temp_file, "test line 4").unwrap();
763 temp_file.flush().unwrap();
764
765 let search = test_helpers::create_fixed_search("test");
766 let replacement = "replaced";
767 let results = search_file(temp_file.path(), &search)
768 .unwrap()
769 .into_iter()
770 .filter_map(|r| add_replacement(r, &search, replacement))
771 .collect::<Vec<_>>();
772
773 assert_eq!(results.len(), 3);
774 assert_eq!(results[0].search_result.line_number, 1);
775 assert_eq!(results[0].replacement, "replaced line 1");
776 assert_eq!(results[1].search_result.line_number, 2);
777 assert_eq!(results[1].replacement, "replaced line 2");
778 assert_eq!(results[2].search_result.line_number, 4);
779 assert_eq!(results[2].replacement, "replaced line 4");
780 }
781
782 #[test]
783 fn test_search_file_no_matches() {
784 let mut temp_file = NamedTempFile::new().unwrap();
785 writeln!(temp_file, "line 1").unwrap();
786 writeln!(temp_file, "line 2").unwrap();
787 writeln!(temp_file, "line 3").unwrap();
788 temp_file.flush().unwrap();
789
790 let search = SearchType::Fixed("nonexistent".to_string());
791 let replacement = "replace";
792 let results = search_file(temp_file.path(), &search)
793 .unwrap()
794 .into_iter()
795 .filter_map(|r| add_replacement(r, &search, replacement))
796 .collect::<Vec<_>>();
797
798 assert_eq!(results.len(), 0);
799 }
800
801 #[test]
802 fn test_search_file_regex_pattern() {
803 let mut temp_file = NamedTempFile::new().unwrap();
804 writeln!(temp_file, "number: 123").unwrap();
805 writeln!(temp_file, "text without numbers").unwrap();
806 writeln!(temp_file, "another number: 456").unwrap();
807 temp_file.flush().unwrap();
808
809 let search = SearchType::Pattern(Regex::new(r"\d+").unwrap());
810 let replacement = "XXX";
811 let results = search_file(temp_file.path(), &search)
812 .unwrap()
813 .into_iter()
814 .filter_map(|r| add_replacement(r, &search, replacement))
815 .collect::<Vec<_>>();
816
817 assert_eq!(results.len(), 2);
818 assert_eq!(results[0].replacement, "number: XXX");
819 assert_eq!(results[1].replacement, "another number: XXX");
820 }
821
822 #[test]
823 fn test_search_file_advanced_regex_pattern() {
824 let mut temp_file = NamedTempFile::new().unwrap();
825 writeln!(temp_file, "123abc456").unwrap();
826 writeln!(temp_file, "abc").unwrap();
827 writeln!(temp_file, "789xyz123").unwrap();
828 writeln!(temp_file, "no match").unwrap();
829 temp_file.flush().unwrap();
830
831 let search =
833 SearchType::PatternAdvanced(FancyRegex::new(r"(?<=\d{3})abc(?=\d{3})").unwrap());
834 let replacement = "REPLACED";
835 let results = search_file(temp_file.path(), &search)
836 .unwrap()
837 .into_iter()
838 .filter_map(|r| add_replacement(r, &search, replacement))
839 .collect::<Vec<_>>();
840
841 assert_eq!(results.len(), 1);
842 assert_eq!(results[0].replacement, "123REPLACED456");
843 assert_eq!(results[0].search_result.line_number, 1);
844 }
845
846 #[test]
847 fn test_search_file_empty_search() {
848 let mut temp_file = NamedTempFile::new().unwrap();
849 writeln!(temp_file, "some content").unwrap();
850 temp_file.flush().unwrap();
851
852 let search = SearchType::Fixed("".to_string());
853 let replacement = "replace";
854 let results = search_file(temp_file.path(), &search)
855 .unwrap()
856 .into_iter()
857 .filter_map(|r| add_replacement(r, &search, replacement))
858 .collect::<Vec<_>>();
859
860 assert_eq!(results.len(), 0);
861 }
862
863 #[test]
864 fn test_search_file_preserves_line_endings() {
865 let mut temp_file = NamedTempFile::new().unwrap();
866 write!(temp_file, "line1\nline2\r\nline3").unwrap();
867 temp_file.flush().unwrap();
868
869 let search = SearchType::Fixed("line".to_string());
870 let replacement = "X";
871 let results = search_file(temp_file.path(), &search)
872 .unwrap()
873 .into_iter()
874 .filter_map(|r| add_replacement(r, &search, replacement))
875 .collect::<Vec<_>>();
876
877 assert_eq!(results.len(), 3);
878 assert_eq!(results[0].search_result.line_ending, LineEnding::Lf);
879 assert_eq!(results[1].search_result.line_ending, LineEnding::CrLf);
880 assert_eq!(results[2].search_result.line_ending, LineEnding::None);
881 }
882
883 #[test]
884 fn test_search_file_nonexistent() {
885 let nonexistent_path = PathBuf::from("/this/file/does/not/exist.txt");
886 let search = test_helpers::create_fixed_search("test");
887 let results = search_file(&nonexistent_path, &search);
888 assert!(results.is_err());
889 }
890
891 #[test]
892 fn test_search_file_unicode_content() {
893 let mut temp_file = NamedTempFile::new().unwrap();
894 writeln!(temp_file, "Hello 世界!").unwrap();
895 writeln!(temp_file, "Здравствуй мир!").unwrap();
896 writeln!(temp_file, "🚀 Rocket").unwrap();
897 temp_file.flush().unwrap();
898
899 let search = SearchType::Fixed("世界".to_string());
900 let replacement = "World";
901 let results = search_file(temp_file.path(), &search)
902 .unwrap()
903 .into_iter()
904 .filter_map(|r| add_replacement(r, &search, replacement))
905 .collect::<Vec<_>>();
906
907 assert_eq!(results.len(), 1);
908 assert_eq!(results[0].replacement, "Hello World!");
909 }
910
911 #[test]
912 fn test_search_file_with_binary_content() {
913 let mut temp_file = NamedTempFile::new().unwrap();
914 let binary_data = [0x00, 0x01, 0x02, 0xFF, 0xFE];
916 temp_file.write_all(&binary_data).unwrap();
917 temp_file.flush().unwrap();
918
919 let search = test_helpers::create_fixed_search("test");
920 let replacement = "replace";
921 let results = search_file(temp_file.path(), &search)
922 .unwrap()
923 .into_iter()
924 .filter_map(|r| add_replacement(r, &search, replacement))
925 .collect::<Vec<_>>();
926
927 assert_eq!(results.len(), 0);
928 }
929
930 #[test]
931 fn test_search_file_large_content() {
932 let mut temp_file = NamedTempFile::new().unwrap();
933
934 for i in 0..1000 {
936 if i % 100 == 0 {
937 writeln!(temp_file, "target line {i}").unwrap();
938 } else {
939 writeln!(temp_file, "normal line {i}").unwrap();
940 }
941 }
942 temp_file.flush().unwrap();
943
944 let search = SearchType::Fixed("target".to_string());
945 let replacement = "found";
946 let results = search_file(temp_file.path(), &search)
947 .unwrap()
948 .into_iter()
949 .filter_map(|r| add_replacement(r, &search, replacement))
950 .collect::<Vec<_>>();
951
952 assert_eq!(results.len(), 10); assert_eq!(results[0].search_result.line_number, 1); assert_eq!(results[1].search_result.line_number, 101);
955 assert_eq!(results[9].search_result.line_number, 901);
956 }
957 }
958}