1use crate::config::Config;
2use crate::highlighter::{Highlighter, WatcherStats};
3use crate::matcher::Matcher;
4use crate::notifier::Notifier;
5use crate::utils::{get_file_size, validate_files};
6use anyhow::Result;
7use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
8use std::collections::HashMap;
9use std::fs::File;
10use std::io::{BufRead, BufReader, Seek, SeekFrom};
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13use tokio::sync::mpsc;
14use tokio::time::sleep;
15use tracing::{error, info};
16
17#[derive(Debug)]
18pub struct LogWatcher {
19 config: Config,
20 matcher: Matcher,
21 highlighter: Highlighter,
22 notifier: Notifier,
23 stats: WatcherStats,
24}
25
26impl LogWatcher {
27 pub fn new(config: Config) -> Self {
28 let matcher = Matcher::new(config.clone());
29 let highlighter = Highlighter::new(config.clone());
30 let notifier = Notifier::new(config.clone());
31
32 Self {
33 config,
34 matcher,
35 highlighter,
36 notifier,
37 stats: WatcherStats::default(),
38 }
39 }
40
41 pub async fn run(&mut self) -> Result<()> {
42 let valid_files = validate_files(&self.config.files)?;
44 self.stats.files_watched = valid_files.len();
45
46 self.highlighter.print_startup_info()?;
48
49 if self.config.dry_run {
50 self.run_dry_mode(&valid_files).await?;
51 } else {
52 self.run_tail_mode(&valid_files).await?;
53 }
54
55 self.highlighter.print_shutdown_summary(&self.stats)?;
57
58 Ok(())
59 }
60
61 async fn run_dry_mode(&mut self, files: &[PathBuf]) -> Result<()> {
62 info!("Running in dry-run mode");
63
64 let mut pattern_counts: HashMap<String, usize> = HashMap::new();
65
66 for file_path in files {
67 match self.process_existing_file(file_path).await {
68 Ok(matches) => {
69 for (pattern, count) in matches {
70 *pattern_counts.entry(pattern).or_insert(0) += count;
71 }
72 }
73 Err(e) => {
74 self.highlighter
75 .print_file_error(&file_path.display().to_string(), &e.to_string())?;
76 }
77 }
78 }
79
80 let summary: Vec<(String, usize)> = pattern_counts.into_iter().collect();
82 self.highlighter.print_dry_run_summary(&summary)?;
83
84 Ok(())
85 }
86
87 async fn run_tail_mode(&mut self, files: &[PathBuf]) -> Result<()> {
88 info!("Running in tail mode");
89
90 let (tx, mut rx) = mpsc::channel::<FileEvent>(100);
92
93 let mut watchers = Vec::new();
95 for file_path in files {
96 let tx_clone = tx.clone();
97 let file_path_clone = file_path.clone();
98
99 match self.start_file_watcher(file_path_clone, tx_clone).await {
100 Ok(watcher) => watchers.push(watcher),
101 Err(e) => {
102 self.highlighter
103 .print_file_error(&file_path.display().to_string(), &e.to_string())?;
104 }
105 }
106 }
107
108 while let Some(event) = rx.recv().await {
110 match event {
111 FileEvent::NewLine { file_path, line } => {
112 self.process_line(&file_path, &line).await?;
113 }
114 FileEvent::FileRotated { file_path } => {
115 self.handle_file_rotation(&file_path).await?;
116 }
117 FileEvent::FileError { file_path, error } => {
118 self.highlighter
119 .print_file_error(&file_path.display().to_string(), &error.to_string())?;
120 }
121 }
122 }
123
124 Ok(())
125 }
126
127 async fn start_file_watcher(
128 &self,
129 file_path: PathBuf,
130 tx: mpsc::Sender<FileEvent>,
131 ) -> Result<RecommendedWatcher> {
132 let file_path_clone = file_path.clone();
133 let tx_clone = tx.clone();
134
135 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
136 match res {
137 Ok(event) => {
138 if matches!(event.kind, EventKind::Modify(_)) {
139 }
141 }
142 Err(e) => {
143 let _ = tx_clone.try_send(FileEvent::FileError {
144 file_path: file_path_clone.clone(),
145 error: e,
146 });
147 }
148 }
149 })?;
150
151 watcher.watch(&file_path, RecursiveMode::NonRecursive)?;
152
153 let file_path_clone = file_path.clone();
155 let tx_clone = tx.clone();
156 let poll_interval = self.config.poll_interval;
157 let buffer_size = self.config.buffer_size;
158
159 tokio::spawn(async move {
160 let mut last_size = get_file_size(&file_path_clone).unwrap_or(0);
161
162 loop {
163 sleep(Duration::from_millis(poll_interval)).await;
164
165 match Self::poll_file_changes(&file_path_clone, last_size, buffer_size).await {
166 Ok((new_size, new_lines)) => {
167 last_size = new_size;
168
169 for line in new_lines {
170 if let Err(e) = tx_clone
171 .send(FileEvent::NewLine {
172 file_path: file_path_clone.clone(),
173 line,
174 })
175 .await
176 {
177 error!("Failed to send line event: {}", e);
178 break;
179 }
180 }
181 }
182 Err(e) => {
183 let _ = tx_clone
184 .send(FileEvent::FileError {
185 file_path: file_path_clone.clone(),
186 error: notify::Error::generic(&e.to_string()),
187 })
188 .await;
189 break;
190 }
191 }
192 }
193 });
194
195 Ok(watcher)
196 }
197
198 async fn poll_file_changes(
199 file_path: &PathBuf,
200 last_size: u64,
201 buffer_size: usize,
202 ) -> Result<(u64, Vec<String>)> {
203 let current_size = get_file_size(file_path)?;
204
205 if current_size < last_size {
206 return Err(anyhow::anyhow!("File rotation detected"));
208 }
209
210 if current_size > last_size {
211 let file = File::open(file_path)?;
213 let mut reader = BufReader::with_capacity(buffer_size, file);
214
215 reader.seek(SeekFrom::Start(last_size))?;
217
218 let mut lines = Vec::new();
219 let mut line = String::new();
220
221 while reader.read_line(&mut line)? > 0 {
222 if !line.trim().is_empty() {
223 lines.push(line.trim().to_string());
224 }
225 line.clear();
226 }
227
228 Ok((current_size, lines))
229 } else {
230 Ok((current_size, Vec::new()))
231 }
232 }
233
234 async fn process_existing_file(
235 &mut self,
236 file_path: &PathBuf,
237 ) -> Result<HashMap<String, usize>> {
238 let mut pattern_counts: HashMap<String, usize> = HashMap::new();
239
240 let file = File::open(file_path)?;
241 let reader = BufReader::new(file);
242
243 for line_result in reader.lines() {
244 let line = line_result?;
245
246 if self.config.should_exclude(&line) {
248 self.stats.lines_excluded += 1;
249 continue;
250 }
251
252 self.stats.lines_processed += 1;
253
254 let match_result = self.matcher.match_line(&line);
255
256 if match_result.matched {
257 self.stats.matches_found += 1;
258 if let Some(pattern) = &match_result.pattern {
259 *pattern_counts.entry(pattern.clone()).or_insert(0) += 1;
260 }
261
262 self.highlighter.print_line(
263 &line,
264 Some(&file_path.file_name().unwrap().to_string_lossy()),
265 &match_result,
266 true, )?;
268 }
269 }
270
271 Ok(pattern_counts)
272 }
273
274 async fn process_line(&mut self, file_path: &Path, line: &str) -> Result<()> {
275 if self.config.should_exclude(line) {
277 self.stats.lines_excluded += 1;
278 return Ok(());
279 }
280
281 self.stats.lines_processed += 1;
282
283 let match_result = self.matcher.match_line(line);
284
285 if match_result.matched {
286 self.stats.matches_found += 1;
287
288 if match_result.should_notify {
290 if let Some(pattern) = &match_result.pattern {
291 self.notifier
292 .send_notification(
293 pattern,
294 line,
295 Some(&file_path.file_name().unwrap().to_string_lossy()),
296 )
297 .await?;
298 self.stats.notifications_sent += 1;
299 }
300 }
301 }
302
303 self.highlighter.print_line(
305 line,
306 Some(&file_path.file_name().unwrap().to_string_lossy()),
307 &match_result,
308 false, )?;
310
311 Ok(())
312 }
313
314 async fn handle_file_rotation(&mut self, file_path: &Path) -> Result<()> {
315 self.highlighter
316 .print_file_rotation(&file_path.display().to_string())?;
317
318 sleep(Duration::from_millis(1000)).await;
320
321 if file_path.exists() {
323 self.highlighter
324 .print_file_reopened(&file_path.display().to_string())?;
325 } else {
326 self.highlighter.print_file_error(
327 &file_path.display().to_string(),
328 "File not found after rotation",
329 )?;
330 }
331
332 Ok(())
333 }
334}
335
336#[derive(Debug)]
337enum FileEvent {
338 NewLine {
339 file_path: PathBuf,
340 line: String,
341 },
342 #[allow(dead_code)]
343 FileRotated {
344 file_path: PathBuf,
345 },
346 FileError {
347 file_path: PathBuf,
348 error: notify::Error,
349 },
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::cli::Args;
356 use std::io::Write;
357 use tempfile::NamedTempFile;
358
359 fn create_test_config() -> Config {
360 let args = Args {
361 files: vec![PathBuf::from("test.log")],
362 completions: None,
363 patterns: "ERROR".to_string(),
364 regex: false,
365 case_insensitive: false,
366 color_map: None,
367 notify: false,
368 notify_patterns: None,
369 notify_throttle: 5,
370 dry_run: true,
371 quiet: false,
372 exclude: None,
373 no_color: true,
374 prefix_file: None,
375 poll_interval: 100,
376 buffer_size: 8192,
377 };
378 Config::from_args(&args).unwrap()
379 }
380
381 #[tokio::test]
382 async fn test_dry_run_mode() {
383 let mut temp_file = NamedTempFile::new().unwrap();
384 writeln!(temp_file, "This is an ERROR message").unwrap();
385 writeln!(temp_file, "This is a normal message").unwrap();
386 writeln!(temp_file, "Another ERROR message").unwrap();
387 temp_file.flush().unwrap();
388
389 let mut config = create_test_config();
390 config.files = vec![temp_file.path().to_path_buf()];
391
392 let mut watcher = LogWatcher::new(config);
393 let result = watcher.run().await;
394
395 assert!(result.is_ok());
396 assert_eq!(watcher.stats.matches_found, 2);
397 }
398
399 #[test]
400 fn test_poll_file_changes() {
401 let mut temp_file = NamedTempFile::new().unwrap();
402 writeln!(temp_file, "line 1").unwrap();
403 temp_file.flush().unwrap();
404
405 let initial_size = get_file_size(temp_file.path()).unwrap();
406
407 writeln!(temp_file, "line 2").unwrap();
408 temp_file.flush().unwrap();
409
410 let rt = tokio::runtime::Runtime::new().unwrap();
411 let result = rt.block_on(LogWatcher::poll_file_changes(
412 &temp_file.path().to_path_buf(),
413 initial_size,
414 1024,
415 ));
416
417 assert!(result.is_ok());
418 let (new_size, lines) = result.unwrap();
419 assert!(new_size > initial_size);
420 assert_eq!(lines.len(), 1);
421 assert_eq!(lines[0], "line 2");
422 }
423
424 #[tokio::test]
425 async fn test_process_existing_file() {
426 let mut temp_file = NamedTempFile::new().unwrap();
427 writeln!(temp_file, "ERROR: Something went wrong").unwrap();
428 writeln!(temp_file, "INFO: Normal operation").unwrap();
429 temp_file.flush().unwrap();
430
431 let config = create_test_config();
432 let mut watcher = LogWatcher::new(config);
433
434 let result = watcher
436 .process_existing_file(&temp_file.path().to_path_buf())
437 .await;
438 assert!(result.is_ok());
439 }
440
441 #[tokio::test]
442 async fn test_process_line() {
443 let mut temp_file = NamedTempFile::new().unwrap();
444 writeln!(temp_file, "ERROR: Test error").unwrap();
445 temp_file.flush().unwrap();
446
447 let config = create_test_config();
448 let mut watcher = LogWatcher::new(config);
449
450 let result = watcher
452 .process_line(temp_file.path(), "ERROR: Test error")
453 .await;
454 assert!(result.is_ok());
455 }
456
457 #[tokio::test]
458 async fn test_handle_file_rotation() {
459 let mut temp_file = NamedTempFile::new().unwrap();
460 writeln!(temp_file, "ERROR: Test error").unwrap();
461 temp_file.flush().unwrap();
462
463 let config = create_test_config();
464 let mut watcher = LogWatcher::new(config);
465
466 let result = watcher.handle_file_rotation(temp_file.path()).await;
468 assert!(result.is_ok());
469 }
470
471 #[tokio::test]
472 async fn test_run_with_startup_info() {
473 let mut temp_file = NamedTempFile::new().unwrap();
474 writeln!(temp_file, "ERROR: Test error").unwrap();
475 temp_file.flush().unwrap();
476
477 let mut config = create_test_config();
478 config.files = vec![temp_file.path().to_path_buf()];
479 config.dry_run = true;
480
481 let mut watcher = LogWatcher::new(config);
482 let result = watcher.run().await;
483 assert!(result.is_ok());
484 }
485
486 #[tokio::test]
487 async fn test_run_tail_mode_execution() {
488 let mut temp_file = NamedTempFile::new().unwrap();
489 writeln!(temp_file, "ERROR: Test error").unwrap();
490 temp_file.flush().unwrap();
491
492 let mut config = create_test_config();
493 config.files = vec![temp_file.path().to_path_buf()];
494 config.dry_run = false; let mut watcher = LogWatcher::new(config);
497
498 let result =
500 tokio::time::timeout(std::time::Duration::from_millis(100), watcher.run()).await;
501
502 assert!(result.is_err());
504 }
505
506 #[test]
507 fn test_run_tail_mode() {
508 let mut temp_file = NamedTempFile::new().unwrap();
509 writeln!(temp_file, "ERROR: Test error").unwrap();
510 temp_file.flush().unwrap();
511
512 let config = create_test_config();
513 let mut watcher = LogWatcher::new(config);
514
515 let rt = tokio::runtime::Runtime::new().unwrap();
517 let files = vec![temp_file.path().to_path_buf()];
518
519 let result = rt.block_on(async {
521 tokio::time::timeout(
522 std::time::Duration::from_millis(100),
523 watcher.run_tail_mode(&files),
524 )
525 .await
526 });
527
528 assert!(result.is_err());
530 }
531
532 #[tokio::test]
533 async fn test_dry_run_with_file_error() {
534 let mut config = create_test_config();
536 config.files = vec![PathBuf::from("/non/existent/file.log")];
537 config.dry_run = true;
538
539 let mut watcher = LogWatcher::new(config);
540 let result = watcher.run().await;
541
542 assert!(result.is_err());
544 assert!(result
545 .unwrap_err()
546 .to_string()
547 .contains("No valid files to watch"));
548 }
549
550 #[tokio::test]
551 async fn test_dry_run_summary_with_multiple_patterns() {
552 let mut temp_file = NamedTempFile::new().unwrap();
553 writeln!(temp_file, "ERROR: Something went wrong").unwrap();
554 writeln!(temp_file, "WARN: This is a warning").unwrap();
555 writeln!(temp_file, "INFO: Normal operation").unwrap();
556 writeln!(temp_file, "ERROR: Another error").unwrap();
557 temp_file.flush().unwrap();
558
559 let mut config = create_test_config();
560 config.files = vec![temp_file.path().to_path_buf()];
561 config.patterns = vec!["ERROR".to_string(), "WARN".to_string()];
562 config.dry_run = true;
563
564 let mut watcher = LogWatcher::new(config);
565 let result = watcher.run().await;
566 assert!(result.is_ok());
567 assert_eq!(watcher.stats.matches_found, 3); }
569
570 #[tokio::test]
571 async fn test_poll_file_changes_with_rotation() {
572 let mut temp_file = NamedTempFile::new().unwrap();
573 writeln!(temp_file, "line 1").unwrap();
574 temp_file.flush().unwrap();
575
576 let initial_size = get_file_size(temp_file.path()).unwrap();
577
578 temp_file.as_file_mut().set_len(0).unwrap();
580 temp_file.flush().unwrap();
581
582 let result =
583 LogWatcher::poll_file_changes(&temp_file.path().to_path_buf(), initial_size, 1024)
584 .await;
585
586 assert!(result.is_err());
588 assert!(result
589 .unwrap_err()
590 .to_string()
591 .contains("File rotation detected"));
592 }
593
594 #[tokio::test]
595 async fn test_poll_file_changes_no_new_content() {
596 let mut temp_file = NamedTempFile::new().unwrap();
597 writeln!(temp_file, "line 1").unwrap();
598 temp_file.flush().unwrap();
599
600 let initial_size = get_file_size(temp_file.path()).unwrap();
601
602 let result =
603 LogWatcher::poll_file_changes(&temp_file.path().to_path_buf(), initial_size, 1024)
604 .await;
605
606 assert!(result.is_ok());
607 let (new_size, lines) = result.unwrap();
608 assert_eq!(new_size, initial_size);
609 assert_eq!(lines.len(), 0);
610 }
611
612 #[tokio::test]
613 async fn test_poll_file_changes_with_seeking() {
614 let mut temp_file = NamedTempFile::new().unwrap();
615 writeln!(temp_file, "line 1").unwrap();
616 writeln!(temp_file, "line 2").unwrap();
617 temp_file.flush().unwrap();
618
619 let initial_size = get_file_size(temp_file.path()).unwrap();
620
621 writeln!(temp_file, "line 3").unwrap();
623 writeln!(temp_file, "line 4").unwrap();
624 temp_file.flush().unwrap();
625
626 let result =
627 LogWatcher::poll_file_changes(&temp_file.path().to_path_buf(), initial_size, 1024)
628 .await;
629
630 assert!(result.is_ok());
631 let (new_size, lines) = result.unwrap();
632 assert!(new_size > initial_size);
633 assert_eq!(lines.len(), 2);
634 assert_eq!(lines[0], "line 3");
635 assert_eq!(lines[1], "line 4");
636 }
637
638 #[tokio::test]
639 async fn test_process_line_with_notification() {
640 let mut temp_file = NamedTempFile::new().unwrap();
641 writeln!(temp_file, "ERROR: Test error").unwrap();
642 temp_file.flush().unwrap();
643
644 let mut config = create_test_config();
645 config.notify_enabled = true;
646 config.notify_patterns = vec!["ERROR".to_string()];
647
648 let mut watcher = LogWatcher::new(config);
649
650 let result = watcher
652 .process_line(temp_file.path(), "ERROR: Critical error occurred")
653 .await;
654
655 if let Err(e) = &result {
657 eprintln!("Notification test failed with error: {}", e);
658 let error_msg = e.to_string();
659 if error_msg.contains("can only be set once") || error_msg.contains("org.freedesktop.DBus.Error.ServiceUnknown") || error_msg.contains(".service files") || error_msg.contains("Notifications") || error_msg.contains("No such file or directory") || error_msg.contains("I/O error")
666 {
668 assert_eq!(watcher.stats.notifications_sent, 0);
671 return;
672 }
673 }
674
675 assert!(result.is_ok());
676 assert_eq!(watcher.stats.notifications_sent, 1);
677 }
678
679 #[tokio::test]
680 async fn test_process_line_without_notification() {
681 let mut temp_file = NamedTempFile::new().unwrap();
682 writeln!(temp_file, "INFO: Normal operation").unwrap();
683 temp_file.flush().unwrap();
684
685 let mut config = create_test_config();
686 config.notify_enabled = true;
687 config.notify_patterns = vec!["ERROR".to_string()];
688
689 let mut watcher = LogWatcher::new(config);
690
691 let result = watcher
693 .process_line(temp_file.path(), "INFO: Normal operation")
694 .await;
695 assert!(result.is_ok());
696 assert_eq!(watcher.stats.notifications_sent, 0);
697 }
698
699 #[tokio::test]
700 async fn test_handle_file_rotation_file_not_found() {
701 let config = create_test_config();
702 let mut watcher = LogWatcher::new(config);
703
704 let result = watcher
706 .handle_file_rotation(&PathBuf::from("/non/existent/file.log"))
707 .await;
708 assert!(result.is_ok());
709 }
710
711 #[tokio::test]
712 async fn test_start_file_watcher() {
713 let mut temp_file = NamedTempFile::new().unwrap();
714 writeln!(temp_file, "ERROR: Test error").unwrap();
715 temp_file.flush().unwrap();
716
717 let config = create_test_config();
718 let watcher = LogWatcher::new(config);
719
720 let (tx, _rx) = mpsc::channel::<FileEvent>(100);
721
722 let result = watcher
724 .start_file_watcher(temp_file.path().to_path_buf(), tx)
725 .await;
726
727 assert!(result.is_ok());
728 }
729
730 #[tokio::test]
731 async fn test_file_event_processing() {
732 let mut temp_file = NamedTempFile::new().unwrap();
733 writeln!(temp_file, "ERROR: Test error").unwrap();
734 temp_file.flush().unwrap();
735
736 let mut config = create_test_config();
737 config.dry_run = false;
738
739 let mut watcher = LogWatcher::new(config);
740
741 let result = watcher
743 .process_line(temp_file.path(), "ERROR: New error occurred")
744 .await;
745 assert!(result.is_ok());
746 assert_eq!(watcher.stats.lines_processed, 1);
747 assert_eq!(watcher.stats.matches_found, 1);
748 }
749
750 #[tokio::test]
751 async fn test_process_existing_file_with_empty_file() {
752 let temp_file = NamedTempFile::new().unwrap();
753 let config = create_test_config();
756 let mut watcher = LogWatcher::new(config);
757
758 let result = watcher
760 .process_existing_file(&temp_file.path().to_path_buf())
761 .await;
762 assert!(result.is_ok());
763 assert_eq!(watcher.stats.lines_processed, 0);
764 assert_eq!(watcher.stats.matches_found, 0);
765 }
766
767 #[tokio::test]
768 async fn test_process_existing_file_with_non_matching_content() {
769 let mut temp_file = NamedTempFile::new().unwrap();
770 writeln!(temp_file, "This is a normal message").unwrap();
771 writeln!(temp_file, "Another normal message").unwrap();
772 temp_file.flush().unwrap();
773
774 let config = create_test_config();
775 let mut watcher = LogWatcher::new(config);
776
777 let result = watcher
779 .process_existing_file(&temp_file.path().to_path_buf())
780 .await;
781 assert!(result.is_ok());
782 assert_eq!(watcher.stats.lines_processed, 2);
783 assert_eq!(watcher.stats.matches_found, 0);
784 }
785
786 #[tokio::test]
787 async fn test_run_tail_mode_with_watcher_error() {
788 let mut temp_file = NamedTempFile::new().unwrap();
789 writeln!(temp_file, "ERROR: Test error").unwrap();
790 temp_file.flush().unwrap();
791
792 let mut config = create_test_config();
793 config.files = vec![temp_file.path().to_path_buf()];
794 config.poll_interval = 10; let mut watcher = LogWatcher::new(config);
797 let files = vec![temp_file.path().to_path_buf()];
798
799 let result = tokio::time::timeout(
801 std::time::Duration::from_millis(50),
802 watcher.run_tail_mode(&files),
803 )
804 .await;
805
806 assert!(result.is_err());
808 }
809
810 #[tokio::test]
811 async fn test_file_event_processing_new_line() {
812 let mut temp_file = NamedTempFile::new().unwrap();
813 writeln!(temp_file, "ERROR: Test error").unwrap();
814 temp_file.flush().unwrap();
815
816 let config = create_test_config();
817 let mut watcher = LogWatcher::new(config);
818
819 let result = watcher
821 .process_line(temp_file.path(), "ERROR: New error occurred")
822 .await;
823 assert!(result.is_ok());
824 assert_eq!(watcher.stats.matches_found, 1);
825 }
826
827 #[tokio::test]
828 async fn test_file_event_processing_file_rotation() {
829 let mut temp_file = NamedTempFile::new().unwrap();
830 writeln!(temp_file, "ERROR: Test error").unwrap();
831 temp_file.flush().unwrap();
832
833 let config = create_test_config();
834 let mut watcher = LogWatcher::new(config);
835
836 let result = watcher.handle_file_rotation(temp_file.path()).await;
838 assert!(result.is_ok());
839 }
840
841 #[tokio::test]
842 async fn test_file_event_processing_file_error() {
843 let mut temp_file = NamedTempFile::new().unwrap();
844 writeln!(temp_file, "ERROR: Test error").unwrap();
845 temp_file.flush().unwrap();
846
847 let config = create_test_config();
848 let mut watcher = LogWatcher::new(config);
849
850 let error_msg = "Permission denied";
852 let result = watcher
853 .highlighter
854 .print_file_error(&temp_file.path().display().to_string(), error_msg);
855 assert!(result.is_ok());
856 }
857
858 #[tokio::test]
859 async fn test_start_file_watcher_error_handling() {
860 let mut temp_file = NamedTempFile::new().unwrap();
861 writeln!(temp_file, "ERROR: Test error").unwrap();
862 temp_file.flush().unwrap();
863
864 let config = create_test_config();
865 let watcher = LogWatcher::new(config);
866
867 let (tx, _rx) = tokio::sync::mpsc::channel(100);
869 let result = watcher
870 .start_file_watcher(temp_file.path().to_path_buf(), tx)
871 .await;
872 assert!(result.is_ok());
873 }
874
875 #[tokio::test]
876 async fn test_poll_file_changes_error_handling() {
877 let mut temp_file = NamedTempFile::new().unwrap();
878 writeln!(temp_file, "line 1").unwrap();
879 temp_file.flush().unwrap();
880
881 let initial_size = get_file_size(temp_file.path()).unwrap();
882
883 let result =
885 LogWatcher::poll_file_changes(&temp_file.path().to_path_buf(), initial_size, 1024)
886 .await;
887
888 assert!(result.is_ok());
889 let (new_size, lines) = result.unwrap();
890 assert_eq!(new_size, initial_size);
891 assert_eq!(lines.len(), 0);
892 }
893
894 #[tokio::test]
895 async fn test_poll_file_changes_with_file_error() {
896 let result =
898 LogWatcher::poll_file_changes(&PathBuf::from("/non/existent/file.log"), 0, 1024).await;
899
900 assert!(result.is_err());
901 }
902
903 #[tokio::test]
904 async fn test_file_event_channel_error() {
905 let mut temp_file = NamedTempFile::new().unwrap();
906 writeln!(temp_file, "ERROR: Test error").unwrap();
907 temp_file.flush().unwrap();
908
909 let config = create_test_config();
910 let _watcher = LogWatcher::new(config);
911
912 let (tx, rx) = tokio::sync::mpsc::channel(1);
914 drop(rx); let result = tx
918 .send(FileEvent::NewLine {
919 file_path: temp_file.path().to_path_buf(),
920 line: "ERROR: Test".to_string(),
921 })
922 .await;
923
924 assert!(result.is_err());
925 }
926
927 #[tokio::test]
928 async fn test_run_dry_mode_with_file_error() {
929 let mut config = create_test_config();
930 config.files = vec![PathBuf::from("/non/existent/file.log")];
931 config.dry_run = true;
932
933 let mut watcher = LogWatcher::new(config);
934 let files = vec![PathBuf::from("/non/existent/file.log")];
935
936 let result = watcher.run_dry_mode(&files).await;
938 assert!(result.is_ok());
939 }
940
941 #[tokio::test]
942 async fn test_run_tail_mode_with_file_error() {
943 let mut config = create_test_config();
944 config.files = vec![PathBuf::from("/non/existent/file.log")];
945
946 let mut watcher = LogWatcher::new(config);
947 let files = vec![PathBuf::from("/non/existent/file.log")];
948
949 let result = tokio::time::timeout(
951 std::time::Duration::from_millis(100),
952 watcher.run_tail_mode(&files),
953 )
954 .await;
955
956 assert!(result.is_err());
958 }
959
960 #[tokio::test]
961 async fn test_channel_send_error_handling() {
962 let mut temp_file = NamedTempFile::new().unwrap();
963 writeln!(temp_file, "ERROR: Test error").unwrap();
964 temp_file.flush().unwrap();
965
966 let config = create_test_config();
967 let _watcher = LogWatcher::new(config);
968
969 let (tx, rx) = tokio::sync::mpsc::channel(1);
971
972 let file_path = temp_file.path().to_path_buf();
974 let tx_clone = tx.clone();
975
976 tokio::spawn(async move {
977 let _ = tx_clone
979 .send(FileEvent::NewLine {
980 file_path: file_path.clone(),
981 line: "ERROR: Test".to_string(),
982 })
983 .await;
984 });
985
986 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
988
989 drop(rx);
991
992 let result = tx
994 .send(FileEvent::NewLine {
995 file_path: temp_file.path().to_path_buf(),
996 line: "ERROR: Test".to_string(),
997 })
998 .await;
999
1000 assert!(result.is_err());
1001 }
1002
1003 #[tokio::test]
1004 async fn test_file_error_event_sending() {
1005 let mut temp_file = NamedTempFile::new().unwrap();
1006 writeln!(temp_file, "ERROR: Test error").unwrap();
1007 temp_file.flush().unwrap();
1008
1009 let config = create_test_config();
1010 let _watcher = LogWatcher::new(config);
1011
1012 let (tx, rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1014 drop(rx);
1015
1016 let result = tx
1018 .send(FileEvent::FileError {
1019 file_path: temp_file.path().to_path_buf(),
1020 error: notify::Error::generic("Test error"),
1021 })
1022 .await;
1023
1024 assert!(result.is_err());
1025 }
1026
1027 #[tokio::test]
1028 async fn test_poll_file_changes_error_sending() {
1029 let mut temp_file = NamedTempFile::new().unwrap();
1030 writeln!(temp_file, "ERROR: Test error").unwrap();
1031 temp_file.flush().unwrap();
1032
1033 let config = create_test_config();
1034 let _watcher = LogWatcher::new(config);
1035
1036 let (_tx, rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1038 drop(rx);
1039
1040 let result =
1042 LogWatcher::poll_file_changes(&PathBuf::from("/non/existent/file.log"), 0, 1024).await;
1043
1044 assert!(result.is_err());
1045 }
1046
1047 #[tokio::test]
1048 async fn test_startup_info_coverage() {
1049 let mut temp_file = NamedTempFile::new().unwrap();
1050 writeln!(temp_file, "ERROR: Test error").unwrap();
1051 temp_file.flush().unwrap();
1052
1053 let mut config = create_test_config();
1054 config.files = vec![temp_file.path().to_path_buf()];
1055
1056 let mut watcher = LogWatcher::new(config);
1057
1058 let result = watcher.highlighter.print_startup_info();
1060 assert!(result.is_ok());
1061 }
1062
1063 #[tokio::test]
1064 async fn test_dry_run_summary_coverage() {
1065 let mut temp_file = NamedTempFile::new().unwrap();
1066 writeln!(temp_file, "ERROR: Test error").unwrap();
1067 temp_file.flush().unwrap();
1068
1069 let mut config = create_test_config();
1070 config.files = vec![temp_file.path().to_path_buf()];
1071 config.dry_run = true;
1072
1073 let mut watcher = LogWatcher::new(config);
1074 let files = vec![temp_file.path().to_path_buf()];
1075
1076 let result = watcher.run_dry_mode(&files).await;
1078 assert!(result.is_ok());
1079 }
1080
1081 #[tokio::test]
1082 async fn test_pattern_counts_entry_coverage() {
1083 let mut temp_file = NamedTempFile::new().unwrap();
1084 writeln!(temp_file, "ERROR: Test error").unwrap();
1085 writeln!(temp_file, "ERROR: Another error").unwrap();
1086 temp_file.flush().unwrap();
1087
1088 let mut config = create_test_config();
1089 config.files = vec![temp_file.path().to_path_buf()];
1090 config.dry_run = true;
1091
1092 let mut watcher = LogWatcher::new(config);
1093 let files = vec![temp_file.path().to_path_buf()];
1094
1095 let result = watcher.run_dry_mode(&files).await;
1097 assert!(result.is_ok());
1098 }
1099
1100 #[tokio::test]
1101 async fn test_files_watched_assignment() {
1102 let mut temp_file = NamedTempFile::new().unwrap();
1103 writeln!(temp_file, "ERROR: Test error").unwrap();
1104 temp_file.flush().unwrap();
1105
1106 let mut config = create_test_config();
1107 config.files = vec![temp_file.path().to_path_buf()];
1108 let files = config.files.clone();
1109
1110 let mut watcher = LogWatcher::new(config);
1111
1112 let valid_files = validate_files(&files).unwrap();
1114 watcher.stats.files_watched = valid_files.len();
1115
1116 assert_eq!(watcher.stats.files_watched, 1);
1117 }
1118
1119 #[tokio::test]
1120 async fn test_run_method_coverage() {
1121 let mut temp_file = NamedTempFile::new().unwrap();
1122 writeln!(temp_file, "ERROR: Test error").unwrap();
1123 temp_file.flush().unwrap();
1124
1125 let mut config = create_test_config();
1126 config.files = vec![temp_file.path().to_path_buf()];
1127 config.dry_run = true;
1128
1129 let mut watcher = LogWatcher::new(config);
1130
1131 let result = watcher.run().await;
1133 assert!(result.is_ok());
1134 }
1135
1136 #[tokio::test]
1137 async fn test_run_tail_mode_coverage() {
1138 let mut temp_file = NamedTempFile::new().unwrap();
1139 writeln!(temp_file, "ERROR: Test error").unwrap();
1140 temp_file.flush().unwrap();
1141
1142 let mut config = create_test_config();
1143 config.files = vec![temp_file.path().to_path_buf()];
1144 config.dry_run = false;
1145
1146 let mut watcher = LogWatcher::new(config);
1147 let files = vec![temp_file.path().to_path_buf()];
1148
1149 let result = tokio::time::timeout(
1151 std::time::Duration::from_millis(100),
1152 watcher.run_tail_mode(&files),
1153 )
1154 .await;
1155
1156 assert!(result.is_err());
1158 }
1159
1160 #[tokio::test]
1161 async fn test_file_event_processing_comprehensive() {
1162 let mut temp_file = NamedTempFile::new().unwrap();
1163 writeln!(temp_file, "ERROR: Test error").unwrap();
1164 temp_file.flush().unwrap();
1165
1166 let mut config = create_test_config();
1167 config.files = vec![temp_file.path().to_path_buf()];
1168
1169 let mut watcher = LogWatcher::new(config);
1170
1171 let events = vec![
1173 FileEvent::NewLine {
1174 file_path: temp_file.path().to_path_buf(),
1175 line: "ERROR: Test error".to_string(),
1176 },
1177 FileEvent::FileRotated {
1178 file_path: temp_file.path().to_path_buf(),
1179 },
1180 FileEvent::FileError {
1181 file_path: temp_file.path().to_path_buf(),
1182 error: notify::Error::generic("Test error"),
1183 },
1184 ];
1185
1186 for event in events {
1187 let result = match event {
1188 FileEvent::NewLine { file_path, line } => {
1189 watcher.process_line(&file_path, &line).await
1190 }
1191 FileEvent::FileRotated { file_path } => {
1192 watcher.handle_file_rotation(&file_path).await
1193 }
1194 FileEvent::FileError { file_path, error } => watcher
1195 .highlighter
1196 .print_file_error(&file_path.display().to_string(), &error.to_string()),
1197 };
1198 assert!(result.is_ok());
1199 }
1200 }
1201
1202 #[tokio::test]
1203 async fn test_start_file_watcher_error_path() {
1204 let mut temp_file = NamedTempFile::new().unwrap();
1205 writeln!(temp_file, "ERROR: Test error").unwrap();
1206 temp_file.flush().unwrap();
1207
1208 let config = create_test_config();
1209 let watcher = LogWatcher::new(config);
1210
1211 let (tx, _rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1213 let file_path = temp_file.path().to_path_buf();
1214
1215 let result = watcher.start_file_watcher(file_path, tx).await;
1217 assert!(result.is_ok());
1218 }
1219
1220 #[tokio::test]
1221 async fn test_poll_file_changes_seek_coverage() {
1222 let mut temp_file = NamedTempFile::new().unwrap();
1223 writeln!(temp_file, "ERROR: Test error").unwrap();
1224 writeln!(temp_file, "INFO: Normal operation").unwrap();
1225 temp_file.flush().unwrap();
1226
1227 let config = create_test_config();
1228 let _watcher = LogWatcher::new(config);
1229
1230 let result = LogWatcher::poll_file_changes(
1232 &temp_file.path().to_path_buf(),
1233 0, 1024,
1235 )
1236 .await;
1237
1238 assert!(result.is_ok());
1239 let (new_size, lines) = result.unwrap();
1240 assert!(new_size > 0);
1241 assert!(!lines.is_empty());
1242 }
1243
1244 #[tokio::test]
1245 async fn test_process_line_notification_coverage() {
1246 let mut temp_file = NamedTempFile::new().unwrap();
1247 writeln!(temp_file, "ERROR: Test error").unwrap();
1248 temp_file.flush().unwrap();
1249
1250 let mut config = create_test_config();
1251 config.notify_enabled = true;
1252 config.notify_patterns = vec!["ERROR".to_string()];
1253
1254 let mut watcher = LogWatcher::new(config);
1255
1256 let result = watcher
1258 .process_line(temp_file.path(), "ERROR: Critical error occurred")
1259 .await;
1260
1261 if let Err(e) = &result {
1263 eprintln!("Notification test failed with error: {}", e);
1264 let error_msg = e.to_string();
1265 if error_msg.contains("can only be set once") || error_msg.contains("org.freedesktop.DBus.Error.ServiceUnknown") || error_msg.contains(".service files") || error_msg.contains("Notifications") || error_msg.contains("No such file or directory") || error_msg.contains("I/O error")
1272 {
1274 assert_eq!(watcher.stats.notifications_sent, 0);
1277 return;
1278 }
1279 }
1280
1281 assert!(result.is_ok());
1282 assert_eq!(watcher.stats.notifications_sent, 1);
1283 }
1284
1285 #[tokio::test]
1286 async fn test_channel_send_error_comprehensive() {
1287 let mut temp_file = NamedTempFile::new().unwrap();
1288 writeln!(temp_file, "ERROR: Test error").unwrap();
1289 temp_file.flush().unwrap();
1290
1291 let config = create_test_config();
1292 let _watcher = LogWatcher::new(config);
1293
1294 let (tx, rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1296 drop(rx);
1297
1298 let events = vec![
1300 FileEvent::NewLine {
1301 file_path: temp_file.path().to_path_buf(),
1302 line: "ERROR: Test".to_string(),
1303 },
1304 FileEvent::FileError {
1305 file_path: temp_file.path().to_path_buf(),
1306 error: notify::Error::generic("Test error"),
1307 },
1308 ];
1309
1310 for event in events {
1311 let result = tx.send(event).await;
1312 assert!(result.is_err());
1313 }
1314 }
1315
1316 #[tokio::test]
1317 async fn test_try_send_error_coverage() {
1318 let mut temp_file = NamedTempFile::new().unwrap();
1319 writeln!(temp_file, "ERROR: Test error").unwrap();
1320 temp_file.flush().unwrap();
1321
1322 let config = create_test_config();
1323 let _watcher = LogWatcher::new(config);
1324
1325 let (tx, rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1327 drop(rx);
1328
1329 let events = vec![FileEvent::FileError {
1331 file_path: temp_file.path().to_path_buf(),
1332 error: notify::Error::generic("Test error"),
1333 }];
1334
1335 for event in events {
1336 let result = tx.try_send(event);
1337 assert!(result.is_err());
1338 }
1339 }
1340
1341 #[tokio::test]
1342 async fn test_file_name_unwrap_coverage() {
1343 let mut temp_file = NamedTempFile::new().unwrap();
1344 writeln!(temp_file, "ERROR: Test error").unwrap();
1345 temp_file.flush().unwrap();
1346
1347 let mut config = create_test_config();
1348 config.notify_enabled = true;
1349 config.notify_patterns = vec!["ERROR".to_string()];
1350
1351 let mut watcher = LogWatcher::new(config);
1352
1353 let result = watcher
1355 .process_line(temp_file.path(), "ERROR: Critical error occurred")
1356 .await;
1357
1358 if let Err(e) = &result {
1360 eprintln!("Notification test failed with error: {}", e);
1361 let error_msg = e.to_string();
1362 if error_msg.contains("can only be set once") || error_msg.contains("org.freedesktop.DBus.Error.ServiceUnknown") || error_msg.contains(".service files") || error_msg.contains("Notifications") || error_msg.contains("No such file or directory") || error_msg.contains("I/O error")
1369 {
1371 assert_eq!(watcher.stats.notifications_sent, 0);
1374 return;
1375 }
1376 }
1377
1378 assert!(result.is_ok());
1379 }
1380
1381 #[tokio::test]
1382 async fn test_startup_info_coverage_line_47() {
1383 let mut temp_file = NamedTempFile::new().unwrap();
1384 writeln!(temp_file, "ERROR: Test error").unwrap();
1385 temp_file.flush().unwrap();
1386
1387 let mut config = create_test_config();
1388 config.files = vec![temp_file.path().to_path_buf()];
1389 config.dry_run = true;
1390
1391 let mut watcher = LogWatcher::new(config);
1392
1393 let result = watcher.run().await;
1395 assert!(result.is_ok());
1396 }
1397
1398 #[tokio::test]
1399 async fn test_dry_run_summary_coverage_line_82() {
1400 let mut temp_file = NamedTempFile::new().unwrap();
1401 writeln!(temp_file, "ERROR: Test error").unwrap();
1402 writeln!(temp_file, "WARN: Test warning").unwrap();
1403 temp_file.flush().unwrap();
1404
1405 let mut config = create_test_config();
1406 config.files = vec![temp_file.path().to_path_buf()];
1407 config.patterns = vec!["ERROR".to_string(), "WARN".to_string()];
1408 config.dry_run = true;
1409
1410 let mut watcher = LogWatcher::new(config);
1411
1412 let result = watcher.run().await;
1414 assert!(result.is_ok());
1415 }
1416
1417 #[tokio::test]
1418 async fn test_file_event_match_coverage_lines_111_119() {
1419 let mut temp_file = NamedTempFile::new().unwrap();
1420 writeln!(temp_file, "ERROR: Test error").unwrap();
1421 temp_file.flush().unwrap();
1422
1423 let mut config = create_test_config();
1424 config.files = vec![temp_file.path().to_path_buf()];
1425
1426 let mut watcher = LogWatcher::new(config);
1427
1428 let events = vec![
1430 FileEvent::NewLine {
1431 file_path: temp_file.path().to_path_buf(),
1432 line: "ERROR: Test error".to_string(),
1433 },
1434 FileEvent::FileRotated {
1435 file_path: temp_file.path().to_path_buf(),
1436 },
1437 FileEvent::FileError {
1438 file_path: temp_file.path().to_path_buf(),
1439 error: notify::Error::generic("Test error"),
1440 },
1441 ];
1442
1443 for event in events {
1444 let result = match event {
1445 FileEvent::NewLine { file_path, line } => {
1446 watcher.process_line(&file_path, &line).await
1447 }
1448 FileEvent::FileRotated { file_path } => {
1449 watcher.handle_file_rotation(&file_path).await
1450 }
1451 FileEvent::FileError { file_path, error } => watcher
1452 .highlighter
1453 .print_file_error(&file_path.display().to_string(), &error.to_string()),
1454 };
1455 assert!(result.is_ok());
1456 }
1457 }
1458
1459 #[tokio::test]
1460 async fn test_error_handling_coverage_lines_142_189() {
1461 let mut temp_file = NamedTempFile::new().unwrap();
1462 writeln!(temp_file, "ERROR: Test error").unwrap();
1463 temp_file.flush().unwrap();
1464
1465 let config = create_test_config();
1466 let _watcher = LogWatcher::new(config);
1467
1468 let (tx, rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1470 drop(rx); let result = tx.try_send(FileEvent::FileError {
1474 file_path: temp_file.path().to_path_buf(),
1475 error: notify::Error::generic("Test error"),
1476 });
1477 assert!(result.is_err());
1478
1479 let (tx2, rx2) = tokio::sync::mpsc::channel::<FileEvent>(1);
1481 drop(rx2); let result = tx2
1484 .send(FileEvent::FileError {
1485 file_path: temp_file.path().to_path_buf(),
1486 error: notify::Error::generic("Test error"),
1487 })
1488 .await;
1489 assert!(result.is_err());
1490 }
1491
1492 #[tokio::test]
1493 async fn test_seek_operation_coverage_line_216() {
1494 let mut temp_file = NamedTempFile::new().unwrap();
1495 writeln!(temp_file, "ERROR: Test error").unwrap();
1496 writeln!(temp_file, "INFO: Normal operation").unwrap();
1497 temp_file.flush().unwrap();
1498
1499 let config = create_test_config();
1500 let _watcher = LogWatcher::new(config);
1501
1502 let result = LogWatcher::poll_file_changes(
1504 &temp_file.path().to_path_buf(),
1505 0, 1024,
1507 )
1508 .await;
1509
1510 assert!(result.is_ok());
1511 let (new_size, lines) = result.unwrap();
1512 assert!(new_size > 0);
1513 assert!(!lines.is_empty());
1514 }
1515
1516 #[tokio::test]
1517 async fn test_notification_success_coverage_line_283() {
1518 let mut temp_file = NamedTempFile::new().unwrap();
1519 writeln!(temp_file, "ERROR: Test error").unwrap();
1520 temp_file.flush().unwrap();
1521
1522 let mut config = create_test_config();
1523 config.notify_enabled = true;
1524 config.notify_patterns = vec!["ERROR".to_string()];
1525
1526 let mut watcher = LogWatcher::new(config);
1527
1528 let result = watcher
1530 .process_line(temp_file.path(), "ERROR: Critical error occurred")
1531 .await;
1532
1533 if let Err(e) = &result {
1535 eprintln!("Notification test failed with error: {}", e);
1536 let error_msg = e.to_string();
1537 if error_msg.contains("can only be set once") || error_msg.contains("org.freedesktop.DBus.Error.ServiceUnknown") || error_msg.contains(".service files") || error_msg.contains("Notifications") || error_msg.contains("No such file or directory") || error_msg.contains("I/O error")
1544 {
1546 assert_eq!(watcher.stats.notifications_sent, 0);
1549 return;
1550 }
1551 }
1552
1553 assert!(result.is_ok());
1554 if watcher.stats.notifications_sent > 0 {
1556 assert_eq!(watcher.stats.notifications_sent, 1);
1557 }
1558 }
1559
1560 #[tokio::test]
1561 async fn test_channel_send_error_coverage_line_177() {
1562 let mut temp_file = NamedTempFile::new().unwrap();
1563 writeln!(temp_file, "ERROR: Test error").unwrap();
1564 temp_file.flush().unwrap();
1565
1566 let config = create_test_config();
1567 let _watcher = LogWatcher::new(config);
1568
1569 let (tx, rx) = tokio::sync::mpsc::channel::<FileEvent>(1);
1571 drop(rx);
1572
1573 let result = tx
1575 .send(FileEvent::NewLine {
1576 file_path: temp_file.path().to_path_buf(),
1577 line: "ERROR: Test".to_string(),
1578 })
1579 .await;
1580
1581 assert!(result.is_err());
1582 }
1583
1584 #[tokio::test]
1585 async fn test_poll_file_changes_with_seek_coverage_line_216() {
1586 let mut temp_file = NamedTempFile::new().unwrap();
1587 writeln!(temp_file, "ERROR: Test error").unwrap();
1588 writeln!(temp_file, "INFO: Normal operation").unwrap();
1589 writeln!(temp_file, "WARN: Warning message").unwrap();
1590 temp_file.flush().unwrap();
1591
1592 let config = create_test_config();
1593 let _watcher = LogWatcher::new(config);
1594
1595 let result = LogWatcher::poll_file_changes(
1597 &temp_file.path().to_path_buf(),
1598 10, 1024,
1600 )
1601 .await;
1602
1603 assert!(result.is_ok());
1604 let (new_size, _lines) = result.unwrap();
1605 assert!(new_size > 0);
1606 }
1607
1608 #[tokio::test]
1609 async fn test_comprehensive_file_event_processing() {
1610 let mut temp_file = NamedTempFile::new().unwrap();
1611 writeln!(temp_file, "ERROR: Test error").unwrap();
1612 temp_file.flush().unwrap();
1613
1614 let mut config = create_test_config();
1615 config.files = vec![temp_file.path().to_path_buf()];
1616
1617 let mut watcher = LogWatcher::new(config);
1618
1619 let file_path = temp_file.path().to_path_buf();
1621
1622 let result = watcher
1624 .process_line(&file_path, "ERROR: New error occurred")
1625 .await;
1626 assert!(result.is_ok());
1627
1628 let result = watcher.handle_file_rotation(&file_path).await;
1630 assert!(result.is_ok());
1631
1632 let result = watcher
1634 .highlighter
1635 .print_file_error(&file_path.display().to_string(), "Test error");
1636 assert!(result.is_ok());
1637 }
1638}