1use crate::result::{ProbarError, ProbarResult};
14use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
15use serde::{Deserialize, Serialize};
16use std::collections::HashSet;
17use std::path::{Path, PathBuf};
18use std::sync::mpsc::{channel, Receiver, Sender};
19use std::time::{Duration, Instant};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct WatchConfig {
24 pub patterns: Vec<String>,
26 pub ignore_patterns: Vec<String>,
28 pub debounce_ms: u64,
30 pub clear_screen: bool,
32 pub run_on_start: bool,
34 pub watch_dirs: Vec<PathBuf>,
36}
37
38impl Default for WatchConfig {
39 fn default() -> Self {
40 Self {
41 patterns: vec!["**/*.rs".to_string(), "**/*.toml".to_string()],
42 ignore_patterns: vec![
43 "**/target/**".to_string(),
44 "**/.git/**".to_string(),
45 "**/node_modules/**".to_string(),
46 ],
47 debounce_ms: 300,
48 clear_screen: true,
49 run_on_start: true,
50 watch_dirs: vec![PathBuf::from(".")],
51 }
52 }
53}
54
55impl WatchConfig {
56 #[must_use]
58 pub fn new() -> Self {
59 Self::default()
60 }
61
62 #[must_use]
64 pub fn with_pattern(mut self, pattern: &str) -> Self {
65 self.patterns.push(pattern.to_string());
66 self
67 }
68
69 #[must_use]
71 pub fn with_ignore(mut self, pattern: &str) -> Self {
72 self.ignore_patterns.push(pattern.to_string());
73 self
74 }
75
76 #[must_use]
78 pub const fn with_debounce(mut self, ms: u64) -> Self {
79 self.debounce_ms = ms;
80 self
81 }
82
83 #[must_use]
85 pub const fn with_clear_screen(mut self, clear: bool) -> Self {
86 self.clear_screen = clear;
87 self
88 }
89
90 #[must_use]
92 pub fn with_watch_dir(mut self, dir: &Path) -> Self {
93 self.watch_dirs.push(dir.to_path_buf());
94 self
95 }
96
97 #[must_use]
99 pub fn matches_pattern(&self, path: &Path) -> bool {
100 let path_str = path.to_string_lossy();
101
102 for pattern in &self.ignore_patterns {
104 if Self::glob_matches(pattern, &path_str) {
105 return false;
106 }
107 }
108
109 for pattern in &self.patterns {
111 if Self::glob_matches(pattern, &path_str) {
112 return true;
113 }
114 }
115
116 false
117 }
118
119 fn glob_matches(pattern: &str, path: &str) -> bool {
121 let pattern_parts: Vec<&str> = pattern.split('/').collect();
122 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
123
124 Self::glob_match_parts(&pattern_parts, &path_parts)
125 }
126
127 fn glob_match_parts(pattern_parts: &[&str], path_parts: &[&str]) -> bool {
128 if pattern_parts.is_empty() {
129 return path_parts.is_empty();
130 }
131
132 let first_pattern = pattern_parts[0];
133
134 if first_pattern == "**" {
135 let rest_pattern = &pattern_parts[1..];
137 if rest_pattern.is_empty() {
138 return true;
139 }
140
141 for i in 0..=path_parts.len() {
142 if Self::glob_match_parts(rest_pattern, &path_parts[i..]) {
143 return true;
144 }
145 }
146 return false;
147 }
148
149 if path_parts.is_empty() {
150 return false;
151 }
152
153 let first_path = path_parts[0];
154
155 if Self::glob_match_segment(first_pattern, first_path) {
157 Self::glob_match_parts(&pattern_parts[1..], &path_parts[1..])
158 } else {
159 false
160 }
161 }
162
163 fn glob_match_segment(pattern: &str, segment: &str) -> bool {
164 let mut pattern_chars = pattern.chars().peekable();
166 let mut segment_chars = segment.chars();
167
168 while let Some(p) = pattern_chars.next() {
169 match p {
170 '*' => {
171 if pattern_chars.peek().is_none() {
173 return true;
174 }
175 let remaining: String = pattern_chars.collect();
177 let remaining_segment: String = segment_chars.collect();
178 for i in 0..=remaining_segment.len() {
179 if Self::glob_match_segment(&remaining, &remaining_segment[i..]) {
180 return true;
181 }
182 }
183 return false;
184 }
185 '?' => {
186 if segment_chars.next().is_none() {
188 return false;
189 }
190 }
191 c => {
192 if segment_chars.next() != Some(c) {
193 return false;
194 }
195 }
196 }
197 }
198
199 segment_chars.next().is_none()
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct FileChange {
206 pub path: PathBuf,
208 pub kind: FileChangeKind,
210 pub timestamp: Instant,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216pub enum FileChangeKind {
217 Created,
219 Modified,
221 Deleted,
223 Renamed,
225 Other,
227}
228
229impl From<EventKind> for FileChangeKind {
230 fn from(kind: EventKind) -> Self {
231 match kind {
232 EventKind::Create(_) => Self::Created,
233 EventKind::Modify(_) => Self::Modified,
234 EventKind::Remove(_) => Self::Deleted,
235 EventKind::Other => Self::Other,
236 EventKind::Any | EventKind::Access(_) => Self::Other,
237 }
238 }
239}
240
241pub trait WatchHandler: Send + Sync {
243 fn on_change(&self, changes: &[FileChange]) -> ProbarResult<()>;
245
246 fn on_start(&self) -> ProbarResult<()> {
248 Ok(())
249 }
250
251 fn on_stop(&self) -> ProbarResult<()> {
253 Ok(())
254 }
255}
256
257pub struct FnWatchHandler<F>
259where
260 F: Fn(&[FileChange]) -> ProbarResult<()> + Send + Sync,
261{
262 handler: F,
263}
264
265impl<F> std::fmt::Debug for FnWatchHandler<F>
266where
267 F: Fn(&[FileChange]) -> ProbarResult<()> + Send + Sync,
268{
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 f.debug_struct("FnWatchHandler").finish_non_exhaustive()
271 }
272}
273
274impl<F> FnWatchHandler<F>
275where
276 F: Fn(&[FileChange]) -> ProbarResult<()> + Send + Sync,
277{
278 #[must_use]
280 pub fn new(handler: F) -> Self {
281 Self { handler }
282 }
283}
284
285impl<F> WatchHandler for FnWatchHandler<F>
286where
287 F: Fn(&[FileChange]) -> ProbarResult<()> + Send + Sync,
288{
289 fn on_change(&self, changes: &[FileChange]) -> ProbarResult<()> {
290 (self.handler)(changes)
291 }
292}
293
294pub struct FileWatcher {
296 config: WatchConfig,
297 watcher: Option<RecommendedWatcher>,
298 receiver: Option<Receiver<Result<Event, notify::Error>>>,
299 last_trigger: Option<Instant>,
300 pending_changes: Vec<FileChange>,
301}
302
303impl FileWatcher {
304 pub fn new(config: WatchConfig) -> ProbarResult<Self> {
306 Ok(Self {
307 config,
308 watcher: None,
309 receiver: None,
310 last_trigger: None,
311 pending_changes: Vec::new(),
312 })
313 }
314
315 pub fn start(&mut self) -> ProbarResult<()> {
317 let (tx, rx): (
318 Sender<Result<Event, notify::Error>>,
319 Receiver<Result<Event, notify::Error>>,
320 ) = channel();
321
322 let watcher_config = Config::default().with_poll_interval(Duration::from_millis(100));
323
324 let mut watcher = RecommendedWatcher::new(
325 move |res: Result<Event, notify::Error>| {
326 let _ = tx.send(res);
328 },
329 watcher_config,
330 )
331 .map_err(|e| {
332 ProbarError::Io(std::io::Error::other(format!(
333 "Failed to create watcher: {e}"
334 )))
335 })?;
336
337 for dir in &self.config.watch_dirs {
339 if dir.exists() {
340 watcher.watch(dir, RecursiveMode::Recursive).map_err(|e| {
341 ProbarError::Io(std::io::Error::other(format!(
342 "Failed to watch directory {:?}: {e}",
343 dir
344 )))
345 })?;
346 }
347 }
348
349 self.watcher = Some(watcher);
350 self.receiver = Some(rx);
351 Ok(())
352 }
353
354 pub fn stop(&mut self) {
356 self.watcher = None;
357 self.receiver = None;
358 }
359
360 pub fn check_changes(&mut self) -> Option<Vec<FileChange>> {
362 let receiver = self.receiver.as_ref()?;
363 let now = Instant::now();
364
365 while let Ok(result) = receiver.try_recv() {
367 if let Ok(event) = result {
368 for path in event.paths {
369 if self.config.matches_pattern(&path) {
370 self.pending_changes.push(FileChange {
371 path,
372 kind: event.kind.into(),
373 timestamp: now,
374 });
375 }
376 }
377 }
378 }
379
380 if self.pending_changes.is_empty() {
382 return None;
383 }
384
385 let should_trigger = match self.last_trigger {
386 Some(last) => now.duration_since(last).as_millis() >= self.config.debounce_ms as u128,
387 None => true,
388 };
389
390 if should_trigger {
391 self.last_trigger = Some(now);
392 let changes = std::mem::take(&mut self.pending_changes);
393
394 let unique_paths: HashSet<_> = changes.iter().map(|c| c.path.clone()).collect();
396 let deduped: Vec<FileChange> = unique_paths
397 .into_iter()
398 .filter_map(|path| changes.iter().find(|c| c.path == path).cloned())
399 .collect();
400
401 if deduped.is_empty() {
402 None
403 } else {
404 Some(deduped)
405 }
406 } else {
407 None
408 }
409 }
410
411 #[must_use]
413 pub fn config(&self) -> &WatchConfig {
414 &self.config
415 }
416
417 #[must_use]
419 pub fn is_running(&self) -> bool {
420 self.watcher.is_some()
421 }
422}
423
424impl std::fmt::Debug for FileWatcher {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 f.debug_struct("FileWatcher")
427 .field("config", &self.config)
428 .field("is_running", &self.is_running())
429 .field("pending_changes", &self.pending_changes.len())
430 .finish()
431 }
432}
433
434#[derive(Debug, Clone, Default)]
436pub struct WatchStats {
437 pub trigger_count: u64,
439 pub change_count: u64,
441 pub total_runtime: Duration,
443 pub last_trigger: Option<Instant>,
445}
446
447impl WatchStats {
448 #[must_use]
450 pub fn new() -> Self {
451 Self::default()
452 }
453
454 pub fn record_trigger(&mut self, change_count: usize) {
456 self.trigger_count += 1;
457 self.change_count += change_count as u64;
458 self.last_trigger = Some(Instant::now());
459 }
460}
461
462#[derive(Debug)]
464pub struct WatchBuilder {
465 config: WatchConfig,
466}
467
468impl Default for WatchBuilder {
469 fn default() -> Self {
470 Self {
471 config: WatchConfig {
472 patterns: Vec::new(),
473 ignore_patterns: Vec::new(),
474 debounce_ms: 300,
475 clear_screen: true,
476 run_on_start: true,
477 watch_dirs: vec![std::path::PathBuf::from(".")],
478 },
479 }
480 }
481}
482
483impl WatchBuilder {
484 #[must_use]
486 pub fn new() -> Self {
487 Self::default()
488 }
489
490 #[must_use]
492 pub fn rust_files(mut self) -> Self {
493 self.config.patterns.push("**/*.rs".to_string());
494 self
495 }
496
497 #[must_use]
499 pub fn toml_files(mut self) -> Self {
500 self.config.patterns.push("**/*.toml".to_string());
501 self
502 }
503
504 #[must_use]
506 pub fn test_files(mut self) -> Self {
507 self.config.patterns.push("**/tests/**/*.rs".to_string());
508 self.config.patterns.push("**/*_test.rs".to_string());
509 self.config.patterns.push("**/test_*.rs".to_string());
510 self
511 }
512
513 #[must_use]
515 pub fn src_dir(mut self) -> Self {
516 self.config.watch_dirs.push(PathBuf::from("src"));
517 self
518 }
519
520 #[must_use]
522 pub fn ignore_target(mut self) -> Self {
523 self.config.ignore_patterns.push("**/target/**".to_string());
524 self
525 }
526
527 #[must_use]
529 pub const fn debounce(mut self, ms: u64) -> Self {
530 self.config.debounce_ms = ms;
531 self
532 }
533
534 #[must_use]
536 pub fn build(self) -> WatchConfig {
537 self.config
538 }
539}
540
541#[cfg(test)]
542#[allow(clippy::unwrap_used, clippy::expect_used)]
543mod tests {
544 use super::*;
545
546 mod watch_config_tests {
547 use super::*;
548
549 #[test]
550 fn test_default() {
551 let config = WatchConfig::default();
552 assert!(!config.patterns.is_empty());
553 assert!(!config.ignore_patterns.is_empty());
554 assert_eq!(config.debounce_ms, 300);
555 }
556
557 #[test]
558 fn test_with_pattern() {
559 let config = WatchConfig::new().with_pattern("**/*.js");
560 assert!(config.patterns.contains(&"**/*.js".to_string()));
561 }
562
563 #[test]
564 fn test_with_ignore() {
565 let config = WatchConfig::new().with_ignore("**/dist/**");
566 assert!(config.ignore_patterns.contains(&"**/dist/**".to_string()));
567 }
568
569 #[test]
570 fn test_with_debounce() {
571 let config = WatchConfig::new().with_debounce(500);
572 assert_eq!(config.debounce_ms, 500);
573 }
574
575 #[test]
576 fn test_with_clear_screen() {
577 let config = WatchConfig::new().with_clear_screen(false);
578 assert!(!config.clear_screen);
579 let config = WatchConfig::new().with_clear_screen(true);
580 assert!(config.clear_screen);
581 }
582
583 #[test]
584 fn test_with_watch_dir() {
585 let config = WatchConfig::new().with_watch_dir(Path::new("src"));
586 assert!(config.watch_dirs.contains(&PathBuf::from("src")));
587 }
588
589 #[test]
590 fn test_matches_pattern_rust_file() {
591 let config = WatchConfig::default();
592 assert!(config.matches_pattern(Path::new("src/main.rs")));
593 assert!(config.matches_pattern(Path::new("tests/test.rs")));
594 }
595
596 #[test]
597 fn test_matches_pattern_toml_file() {
598 let config = WatchConfig::default();
599 assert!(config.matches_pattern(Path::new("Cargo.toml")));
600 }
601
602 #[test]
603 fn test_ignores_target() {
604 let config = WatchConfig::default();
605 assert!(!config.matches_pattern(Path::new("target/debug/main.rs")));
606 }
607
608 #[test]
609 fn test_ignores_git() {
610 let config = WatchConfig::default();
611 assert!(!config.matches_pattern(Path::new(".git/config")));
612 }
613 }
614
615 mod glob_matching_tests {
616 use super::*;
617
618 #[test]
619 fn test_exact_match() {
620 assert!(WatchConfig::glob_match_segment("main.rs", "main.rs"));
621 assert!(!WatchConfig::glob_match_segment("main.rs", "test.rs"));
622 }
623
624 #[test]
625 fn test_star_wildcard() {
626 assert!(WatchConfig::glob_match_segment("*.rs", "main.rs"));
627 assert!(WatchConfig::glob_match_segment("*.rs", "test.rs"));
628 assert!(!WatchConfig::glob_match_segment("*.rs", "main.js"));
629 }
630
631 #[test]
632 fn test_question_wildcard() {
633 assert!(WatchConfig::glob_match_segment("?.rs", "a.rs"));
634 assert!(!WatchConfig::glob_match_segment("?.rs", "ab.rs"));
635 }
636
637 #[test]
638 fn test_double_star() {
639 assert!(WatchConfig::glob_matches("**/*.rs", "src/main.rs"));
640 assert!(WatchConfig::glob_matches("**/*.rs", "src/lib/mod.rs"));
641 assert!(WatchConfig::glob_matches(
642 "**/target/**",
643 "crates/probar/target/debug/build"
644 ));
645 }
646
647 #[test]
648 fn test_prefix_pattern() {
649 assert!(WatchConfig::glob_matches("src/**/*.rs", "src/lib.rs"));
650 assert!(WatchConfig::glob_matches(
651 "src/**/*.rs",
652 "src/modules/test.rs"
653 ));
654 assert!(!WatchConfig::glob_matches("src/**/*.rs", "tests/test.rs"));
655 }
656 }
657
658 mod file_change_tests {
659 use super::*;
660
661 #[test]
662 fn test_file_change_kind_from_event() {
663 assert_eq!(
664 FileChangeKind::from(EventKind::Create(notify::event::CreateKind::File)),
665 FileChangeKind::Created
666 );
667 assert_eq!(
668 FileChangeKind::from(EventKind::Modify(notify::event::ModifyKind::Data(
669 notify::event::DataChange::Content
670 ))),
671 FileChangeKind::Modified
672 );
673 assert_eq!(
674 FileChangeKind::from(EventKind::Remove(notify::event::RemoveKind::File)),
675 FileChangeKind::Deleted
676 );
677 }
678 }
679
680 mod file_watcher_tests {
681 use super::*;
682
683 #[test]
684 fn test_new() {
685 let config = WatchConfig::default();
686 let watcher = FileWatcher::new(config);
687 assert!(watcher.is_ok());
688 }
689
690 #[test]
691 fn test_is_running_before_start() {
692 let config = WatchConfig::default();
693 let watcher = FileWatcher::new(config).unwrap();
694 assert!(!watcher.is_running());
695 }
696
697 #[test]
698 fn test_check_changes_before_start() {
699 let config = WatchConfig::default();
700 let mut watcher = FileWatcher::new(config).unwrap();
701 assert!(watcher.check_changes().is_none());
702 }
703
704 #[test]
705 fn test_start_and_stop() {
706 let config = WatchConfig::new().with_watch_dir(Path::new("."));
707 let mut watcher = FileWatcher::new(config).unwrap();
708 assert!(!watcher.is_running());
709
710 watcher.start().unwrap();
711 assert!(watcher.is_running());
712
713 watcher.stop();
714 assert!(!watcher.is_running());
715 }
716
717 #[test]
718 fn test_config_accessor() {
719 let config = WatchConfig::new().with_debounce(500);
720 let watcher = FileWatcher::new(config).unwrap();
721 assert_eq!(watcher.config().debounce_ms, 500);
722 }
723
724 #[test]
725 fn test_debug() {
726 let config = WatchConfig::default();
727 let watcher = FileWatcher::new(config).unwrap();
728 let debug_str = format!("{:?}", watcher);
729 assert!(debug_str.contains("FileWatcher"));
730 assert!(debug_str.contains("is_running"));
731 }
732
733 #[test]
734 fn test_start_stop_multiple_times() {
735 let config = WatchConfig::new().with_watch_dir(Path::new("."));
736 let mut watcher = FileWatcher::new(config).unwrap();
737
738 watcher.start().unwrap();
739 assert!(watcher.is_running());
740 watcher.stop();
741 assert!(!watcher.is_running());
742
743 watcher.start().unwrap();
745 assert!(watcher.is_running());
746 watcher.stop();
747 assert!(!watcher.is_running());
748 }
749
750 #[test]
751 fn test_check_changes_after_start_no_events() {
752 let config = WatchConfig::new().with_watch_dir(Path::new("."));
753 let mut watcher = FileWatcher::new(config).unwrap();
754 watcher.start().unwrap();
755
756 let changes = watcher.check_changes();
758 assert!(changes.is_none());
759
760 watcher.stop();
761 }
762 }
763
764 mod watch_stats_tests {
765 use super::*;
766
767 #[test]
768 fn test_new() {
769 let stats = WatchStats::new();
770 assert_eq!(stats.trigger_count, 0);
771 assert_eq!(stats.change_count, 0);
772 }
773
774 #[test]
775 fn test_record_trigger() {
776 let mut stats = WatchStats::new();
777 stats.record_trigger(3);
778
779 assert_eq!(stats.trigger_count, 1);
780 assert_eq!(stats.change_count, 3);
781 assert!(stats.last_trigger.is_some());
782 }
783
784 #[test]
785 fn test_multiple_triggers() {
786 let mut stats = WatchStats::new();
787 stats.record_trigger(2);
788 stats.record_trigger(5);
789
790 assert_eq!(stats.trigger_count, 2);
791 assert_eq!(stats.change_count, 7);
792 }
793 }
794
795 mod watch_builder_tests {
796 use super::*;
797
798 #[test]
799 fn test_new() {
800 let builder = WatchBuilder::new();
801 let config = builder.build();
802 assert!(config.patterns.is_empty());
803 }
804
805 #[test]
806 fn test_rust_files() {
807 let config = WatchBuilder::new().rust_files().build();
808 assert!(config.patterns.contains(&"**/*.rs".to_string()));
809 }
810
811 #[test]
812 fn test_toml_files() {
813 let config = WatchBuilder::new().toml_files().build();
814 assert!(config.patterns.contains(&"**/*.toml".to_string()));
815 }
816
817 #[test]
818 fn test_test_files() {
819 let config = WatchBuilder::new().test_files().build();
820 assert!(config.patterns.contains(&"**/tests/**/*.rs".to_string()));
821 assert!(config.patterns.contains(&"**/*_test.rs".to_string()));
822 }
823
824 #[test]
825 fn test_ignore_target() {
826 let config = WatchBuilder::new().ignore_target().build();
827 assert!(config.ignore_patterns.contains(&"**/target/**".to_string()));
828 }
829
830 #[test]
831 fn test_src_dir() {
832 let config = WatchBuilder::new().src_dir().build();
833 assert!(config.watch_dirs.contains(&PathBuf::from("src")));
834 }
835
836 #[test]
837 fn test_debounce() {
838 let config = WatchBuilder::new().debounce(500).build();
839 assert_eq!(config.debounce_ms, 500);
840 }
841
842 #[test]
843 fn test_chained_builder() {
844 let config = WatchBuilder::new()
845 .rust_files()
846 .toml_files()
847 .ignore_target()
848 .debounce(200)
849 .build();
850
851 assert!(config.patterns.contains(&"**/*.rs".to_string()));
852 assert!(config.patterns.contains(&"**/*.toml".to_string()));
853 assert!(config.ignore_patterns.contains(&"**/target/**".to_string()));
854 assert_eq!(config.debounce_ms, 200);
855 }
856 }
857
858 mod fn_watch_handler_tests {
859 use super::*;
860 use std::sync::atomic::{AtomicU32, Ordering};
861 use std::sync::Arc;
862
863 #[test]
864 fn test_on_change() {
865 let counter = Arc::new(AtomicU32::new(0));
866 let counter_clone = Arc::clone(&counter);
867
868 let handler = FnWatchHandler::new(move |_changes| {
869 counter_clone.fetch_add(1, Ordering::SeqCst);
870 Ok(())
871 });
872
873 let changes = vec![FileChange {
874 path: PathBuf::from("test.rs"),
875 kind: FileChangeKind::Modified,
876 timestamp: Instant::now(),
877 }];
878
879 handler.on_change(&changes).unwrap();
880 assert_eq!(counter.load(Ordering::SeqCst), 1);
881 }
882
883 #[test]
884 fn test_debug() {
885 let handler = FnWatchHandler::new(|_changes| Ok(()));
886 let debug_str = format!("{:?}", handler);
887 assert!(debug_str.contains("FnWatchHandler"));
888 }
889 }
890
891 mod file_change_kind_tests {
892 use super::*;
893
894 #[test]
895 fn test_other_kind() {
896 let kind = FileChangeKind::from(EventKind::Other);
897 assert_eq!(kind, FileChangeKind::Other);
898 }
899
900 #[test]
901 fn test_access_kind() {
902 let kind = FileChangeKind::from(EventKind::Access(notify::event::AccessKind::Read));
903 assert_eq!(kind, FileChangeKind::Other);
904 }
905 }
906
907 mod file_change_additional_tests {
908 use super::*;
909
910 #[test]
911 fn test_debug() {
912 let change = FileChange {
913 path: PathBuf::from("test.rs"),
914 kind: FileChangeKind::Modified,
915 timestamp: Instant::now(),
916 };
917 let debug_str = format!("{:?}", change);
918 assert!(debug_str.contains("test.rs"));
919 assert!(debug_str.contains("Modified"));
920 }
921
922 #[test]
923 fn test_clone() {
924 let change = FileChange {
925 path: PathBuf::from("test.rs"),
926 kind: FileChangeKind::Created,
927 timestamp: Instant::now(),
928 };
929 let cloned = change.clone();
930 assert_eq!(change.path, cloned.path);
931 assert_eq!(change.kind, cloned.kind);
932 }
933 }
934
935 mod watch_handler_default_tests {
936 use super::*;
937
938 struct TestHandler;
939
940 impl WatchHandler for TestHandler {
941 fn on_change(&self, _changes: &[FileChange]) -> ProbarResult<()> {
942 Ok(())
943 }
944 }
945
946 #[test]
947 fn test_on_start_default() {
948 let handler = TestHandler;
949 assert!(handler.on_start().is_ok());
950 }
951
952 #[test]
953 fn test_on_stop_default() {
954 let handler = TestHandler;
955 assert!(handler.on_stop().is_ok());
956 }
957 }
958
959 mod additional_coverage_tests {
960 use super::*;
961
962 #[test]
964 fn test_file_change_kind_any() {
965 let kind = FileChangeKind::from(EventKind::Any);
966 assert_eq!(kind, FileChangeKind::Other);
967 }
968
969 #[test]
970 fn test_file_change_kind_renamed_variant() {
971 let kind = FileChangeKind::Renamed;
973 assert_eq!(kind, FileChangeKind::Renamed);
974 }
975
976 #[test]
977 fn test_file_change_kind_hash() {
978 use std::collections::HashSet;
980 let mut set = HashSet::new();
981 set.insert(FileChangeKind::Created);
982 set.insert(FileChangeKind::Modified);
983 set.insert(FileChangeKind::Deleted);
984 set.insert(FileChangeKind::Renamed);
985 set.insert(FileChangeKind::Other);
986 assert_eq!(set.len(), 5);
987 }
988
989 #[test]
990 fn test_file_change_kind_copy() {
991 let kind = FileChangeKind::Modified;
992 let copied = kind;
993 assert_eq!(kind, copied);
994 }
995
996 #[test]
998 fn test_watch_config_serialize() {
999 let config = WatchConfig::default();
1000 let json = serde_json::to_string(&config).unwrap();
1001 assert!(json.contains("patterns"));
1002 assert!(json.contains("debounce_ms"));
1003 }
1004
1005 #[test]
1006 fn test_watch_config_deserialize() {
1007 let json = r#"{
1008 "patterns": ["**/*.rs"],
1009 "ignore_patterns": ["**/target/**"],
1010 "debounce_ms": 500,
1011 "clear_screen": false,
1012 "run_on_start": false,
1013 "watch_dirs": ["."]
1014 }"#;
1015 let config: WatchConfig = serde_json::from_str(json).unwrap();
1016 assert_eq!(config.debounce_ms, 500);
1017 assert!(!config.clear_screen);
1018 assert!(!config.run_on_start);
1019 }
1020
1021 #[test]
1022 fn test_watch_config_clone() {
1023 let config = WatchConfig::default();
1024 let cloned = config.clone();
1025 assert_eq!(config.debounce_ms, cloned.debounce_ms);
1026 assert_eq!(config.patterns.len(), cloned.patterns.len());
1027 }
1028
1029 #[test]
1030 fn test_watch_config_debug() {
1031 let config = WatchConfig::default();
1032 let debug_str = format!("{:?}", config);
1033 assert!(debug_str.contains("WatchConfig"));
1034 assert!(debug_str.contains("patterns"));
1035 }
1036
1037 #[test]
1039 fn test_glob_matches_empty_pattern_parts() {
1040 assert!(WatchConfig::glob_matches("**/*.rs", "//src//main.rs"));
1042 }
1043
1044 #[test]
1045 fn test_glob_match_segment_star_at_end() {
1046 assert!(WatchConfig::glob_match_segment("test*", "testing"));
1048 assert!(WatchConfig::glob_match_segment("test*", "test"));
1049 assert!(WatchConfig::glob_match_segment("test*", "test123"));
1050 }
1051
1052 #[test]
1053 fn test_glob_match_segment_star_in_middle() {
1054 assert!(WatchConfig::glob_match_segment("te*st", "test"));
1056 assert!(WatchConfig::glob_match_segment("te*st", "teaast"));
1057 assert!(!WatchConfig::glob_match_segment("te*st", "testing"));
1058 }
1059
1060 #[test]
1061 fn test_glob_match_segment_multiple_stars() {
1062 assert!(WatchConfig::glob_match_segment("*.*", "test.rs"));
1064 assert!(WatchConfig::glob_match_segment("*_*", "test_file"));
1065 }
1066
1067 #[test]
1068 fn test_glob_match_segment_question_at_end() {
1069 assert!(WatchConfig::glob_match_segment("test?", "testX"));
1071 assert!(!WatchConfig::glob_match_segment("test?", "test"));
1072 assert!(!WatchConfig::glob_match_segment("test?", "testXY"));
1073 }
1074
1075 #[test]
1076 fn test_glob_match_segment_question_in_middle() {
1077 assert!(WatchConfig::glob_match_segment("te?t", "test"));
1079 assert!(WatchConfig::glob_match_segment("te?t", "teat"));
1080 assert!(!WatchConfig::glob_match_segment("te?t", "tet"));
1081 }
1082
1083 #[test]
1084 fn test_glob_match_segment_empty_pattern() {
1085 assert!(WatchConfig::glob_match_segment("", ""));
1086 assert!(!WatchConfig::glob_match_segment("", "x"));
1087 }
1088
1089 #[test]
1090 fn test_glob_match_segment_empty_segment() {
1091 assert!(WatchConfig::glob_match_segment("", ""));
1092 assert!(!WatchConfig::glob_match_segment("a", ""));
1093 }
1094
1095 #[test]
1096 fn test_glob_match_parts_empty_both() {
1097 let empty: Vec<&str> = vec![];
1098 assert!(WatchConfig::glob_match_parts(&empty, &empty));
1099 }
1100
1101 #[test]
1102 fn test_glob_match_parts_empty_pattern_non_empty_path() {
1103 let empty: Vec<&str> = vec![];
1104 let path = vec!["src"];
1105 assert!(!WatchConfig::glob_match_parts(&empty, &path));
1106 }
1107
1108 #[test]
1109 fn test_glob_match_parts_double_star_at_end() {
1110 let pattern = vec!["src", "**"];
1112 let path = vec!["src", "lib", "mod.rs"];
1113 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1114 }
1115
1116 #[test]
1117 fn test_glob_match_parts_double_star_matches_zero_segments() {
1118 let pattern = vec!["**", "*.rs"];
1120 let path = vec!["main.rs"];
1121 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1122 }
1123
1124 #[test]
1125 fn test_glob_match_parts_double_star_no_match() {
1126 let pattern = vec!["**", "*.xyz"];
1128 let path = vec!["src", "main.rs"];
1129 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1130 }
1131
1132 #[test]
1133 fn test_glob_matches_node_modules() {
1134 let config = WatchConfig::default();
1135 assert!(!config.matches_pattern(Path::new("node_modules/package/index.js")));
1136 }
1137
1138 #[test]
1139 fn test_matches_pattern_no_watch_patterns() {
1140 let config = WatchConfig {
1142 patterns: vec![],
1143 ignore_patterns: vec![],
1144 debounce_ms: 300,
1145 clear_screen: true,
1146 run_on_start: true,
1147 watch_dirs: vec![],
1148 };
1149 assert!(!config.matches_pattern(Path::new("src/main.rs")));
1150 }
1151
1152 #[test]
1153 fn test_matches_pattern_not_matching_any() {
1154 let config = WatchConfig::default();
1156 assert!(!config.matches_pattern(Path::new("src/main.xyz")));
1157 }
1158
1159 #[test]
1161 fn test_watch_stats_default() {
1162 let stats = WatchStats::default();
1163 assert_eq!(stats.trigger_count, 0);
1164 assert_eq!(stats.change_count, 0);
1165 assert_eq!(stats.total_runtime, Duration::default());
1166 assert!(stats.last_trigger.is_none());
1167 }
1168
1169 #[test]
1170 fn test_watch_stats_clone() {
1171 let mut stats = WatchStats::new();
1172 stats.record_trigger(5);
1173 let cloned = stats.clone();
1174 assert_eq!(stats.trigger_count, cloned.trigger_count);
1175 assert_eq!(stats.change_count, cloned.change_count);
1176 }
1177
1178 #[test]
1179 fn test_watch_stats_debug() {
1180 let stats = WatchStats::new();
1181 let debug_str = format!("{:?}", stats);
1182 assert!(debug_str.contains("WatchStats"));
1183 assert!(debug_str.contains("trigger_count"));
1184 }
1185
1186 #[test]
1188 fn test_watch_builder_default() {
1189 let builder = WatchBuilder::default();
1190 let config = builder.build();
1191 assert!(config.patterns.is_empty());
1192 assert!(config.ignore_patterns.is_empty());
1193 assert_eq!(config.debounce_ms, 300);
1194 assert!(config.clear_screen);
1195 assert!(config.run_on_start);
1196 }
1197
1198 #[test]
1199 fn test_watch_builder_debug() {
1200 let builder = WatchBuilder::new();
1201 let debug_str = format!("{:?}", builder);
1202 assert!(debug_str.contains("WatchBuilder"));
1203 }
1204
1205 #[test]
1207 fn test_file_change_all_kinds() {
1208 let kinds = [
1209 FileChangeKind::Created,
1210 FileChangeKind::Modified,
1211 FileChangeKind::Deleted,
1212 FileChangeKind::Renamed,
1213 FileChangeKind::Other,
1214 ];
1215 for kind in kinds {
1216 let change = FileChange {
1217 path: PathBuf::from("test.rs"),
1218 kind,
1219 timestamp: Instant::now(),
1220 };
1221 let _ = format!("{:?}", change);
1222 }
1223 }
1224
1225 #[test]
1227 fn test_fn_watch_handler_with_error() {
1228 let handler = FnWatchHandler::new(|_changes| {
1229 Err(ProbarError::AssertionFailed {
1230 message: "test error".to_string(),
1231 })
1232 });
1233 let changes = vec![FileChange {
1234 path: PathBuf::from("test.rs"),
1235 kind: FileChangeKind::Modified,
1236 timestamp: Instant::now(),
1237 }];
1238 assert!(handler.on_change(&changes).is_err());
1239 }
1240
1241 #[test]
1242 fn test_fn_watch_handler_access_changes() {
1243 use std::sync::atomic::{AtomicUsize, Ordering};
1244 use std::sync::Arc;
1245
1246 let count = Arc::new(AtomicUsize::new(0));
1247 let count_clone = Arc::clone(&count);
1248
1249 let handler = FnWatchHandler::new(move |changes| {
1250 count_clone.store(changes.len(), Ordering::SeqCst);
1251 Ok(())
1252 });
1253
1254 let changes = vec![
1255 FileChange {
1256 path: PathBuf::from("a.rs"),
1257 kind: FileChangeKind::Created,
1258 timestamp: Instant::now(),
1259 },
1260 FileChange {
1261 path: PathBuf::from("b.rs"),
1262 kind: FileChangeKind::Modified,
1263 timestamp: Instant::now(),
1264 },
1265 ];
1266
1267 handler.on_change(&changes).unwrap();
1268 assert_eq!(count.load(Ordering::SeqCst), 2);
1269 }
1270
1271 #[test]
1273 fn test_file_watcher_with_nonexistent_dir() {
1274 let config =
1275 WatchConfig::new().with_watch_dir(Path::new("/nonexistent/directory/12345"));
1276 let mut watcher = FileWatcher::new(config).unwrap();
1277 let result = watcher.start();
1279 assert!(result.is_ok());
1280 watcher.stop();
1281 }
1282
1283 #[test]
1285 fn test_watch_config_multiple_patterns_chained() {
1286 let config = WatchConfig::new()
1287 .with_pattern("**/*.rs")
1288 .with_pattern("**/*.toml")
1289 .with_pattern("**/*.md")
1290 .with_ignore("**/target/**")
1291 .with_ignore("**/.git/**");
1292
1293 assert!(config.patterns.len() >= 3);
1294 assert!(config.ignore_patterns.len() >= 2);
1295 }
1296
1297 #[test]
1299 fn test_glob_matches_deep_nesting() {
1300 assert!(WatchConfig::glob_matches(
1301 "**/*.rs",
1302 "a/b/c/d/e/f/g/h/i/j/test.rs"
1303 ));
1304 }
1305
1306 #[test]
1307 fn test_glob_matches_single_segment() {
1308 assert!(WatchConfig::glob_matches("*.rs", "main.rs"));
1309 assert!(!WatchConfig::glob_matches("*.rs", "src/main.rs"));
1310 }
1311
1312 #[test]
1313 fn test_glob_match_segment_star_no_match() {
1314 assert!(!WatchConfig::glob_match_segment("*.rs", "main.js"));
1316 }
1317
1318 #[test]
1319 fn test_glob_match_segment_literal_mismatch() {
1320 assert!(!WatchConfig::glob_match_segment("abc", "abd"));
1321 assert!(!WatchConfig::glob_match_segment("abc", "ab"));
1322 }
1323
1324 #[test]
1325 fn test_glob_match_segment_pattern_longer_than_segment() {
1326 assert!(!WatchConfig::glob_match_segment("abcdef", "abc"));
1327 }
1328
1329 #[test]
1331 fn test_watch_config_new() {
1332 let config = WatchConfig::new();
1333 assert!(!config.patterns.is_empty());
1334 assert!(config.run_on_start);
1335 }
1336
1337 #[test]
1339 fn test_glob_match_parts_segment_mismatch() {
1340 let pattern = vec!["src", "lib.rs"];
1341 let path = vec!["src", "main.rs"];
1342 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1343 }
1344
1345 #[test]
1347 fn test_glob_match_parts_single_mismatch() {
1348 let pattern = vec!["foo"];
1349 let path = vec!["bar"];
1350 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1351 }
1352
1353 #[test]
1355 fn test_glob_match_parts_pattern_longer_than_path() {
1356 let pattern = vec!["src", "lib.rs"];
1357 let path = vec!["src"];
1358 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1359 }
1360
1361 #[test]
1363 fn test_glob_match_parts_non_doublestar_empty_path() {
1364 let pattern = vec!["*.rs"];
1365 let empty: Vec<&str> = vec![];
1366 assert!(!WatchConfig::glob_match_parts(&pattern, &empty));
1367 }
1368
1369 #[test]
1371 fn test_glob_match_parts_double_star_exhaustive_search() {
1372 let pattern = vec!["**", "specific.txt"];
1374 let path = vec!["a", "b", "c", "other.txt"];
1375 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1376 }
1377
1378 #[test]
1380 fn test_glob_match_segment_star_backtrack() {
1381 assert!(WatchConfig::glob_match_segment("a*b*c", "aXXbYYc"));
1383 assert!(WatchConfig::glob_match_segment("a*b*c", "abc"));
1384 assert!(!WatchConfig::glob_match_segment("a*b*c", "aXXbYY"));
1385 }
1386
1387 #[test]
1389 fn test_glob_match_segment_question_exhausts_segment() {
1390 assert!(!WatchConfig::glob_match_segment("a??", "ab"));
1391 }
1392
1393 #[test]
1395 fn test_glob_match_segment_literal_char_mismatch() {
1396 assert!(!WatchConfig::glob_match_segment("test", "Test"));
1397 assert!(!WatchConfig::glob_match_segment("abc", "axc"));
1398 }
1399
1400 #[test]
1402 fn test_glob_match_segment_pattern_shorter() {
1403 assert!(!WatchConfig::glob_match_segment("ab", "abc"));
1404 }
1405
1406 #[test]
1408 fn test_file_watcher_pending_changes_init() {
1409 let config = WatchConfig::new();
1410 let watcher = FileWatcher::new(config).unwrap();
1411 assert!(!watcher.is_running());
1413 assert_eq!(watcher.config().debounce_ms, 300);
1414 }
1415
1416 #[test]
1418 fn test_file_watcher_debug_running() {
1419 let config = WatchConfig::new().with_watch_dir(Path::new("."));
1420 let mut watcher = FileWatcher::new(config).unwrap();
1421 watcher.start().unwrap();
1422 let debug_str = format!("{:?}", watcher);
1423 assert!(debug_str.contains("true")); watcher.stop();
1425 }
1426
1427 #[test]
1429 fn test_watch_stats_total_runtime() {
1430 let mut stats = WatchStats::new();
1431 stats.total_runtime = Duration::from_secs(10);
1432 assert_eq!(stats.total_runtime, Duration::from_secs(10));
1433 }
1434
1435 #[test]
1437 fn test_matches_pattern_ignore_takes_precedence() {
1438 let config = WatchConfig {
1439 patterns: vec!["**/*.rs".to_string()],
1440 ignore_patterns: vec!["**/test/**".to_string()],
1441 debounce_ms: 300,
1442 clear_screen: true,
1443 run_on_start: true,
1444 watch_dirs: vec![],
1445 };
1446 assert!(!config.matches_pattern(Path::new("test/main.rs")));
1448 assert!(config.matches_pattern(Path::new("src/main.rs")));
1450 }
1451
1452 #[test]
1454 fn test_glob_matches_root_file() {
1455 assert!(WatchConfig::glob_matches("*.toml", "Cargo.toml"));
1456 assert!(!WatchConfig::glob_matches("*.toml", "src/Cargo.toml"));
1457 }
1458
1459 #[test]
1461 fn test_double_star_various_depths() {
1462 let pattern = vec!["**", "src", "main.rs"];
1464 let path = vec!["src", "main.rs"];
1465 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1466
1467 let path2 = vec!["project", "src", "main.rs"];
1469 assert!(WatchConfig::glob_match_parts(&pattern, &path2));
1470
1471 let path3 = vec!["a", "b", "c", "src", "main.rs"];
1473 assert!(WatchConfig::glob_match_parts(&pattern, &path3));
1474 }
1475
1476 #[test]
1478 fn test_watch_config_run_on_start_default_true() {
1479 let config = WatchConfig::default();
1480 assert!(config.run_on_start);
1481 }
1482
1483 #[test]
1485 fn test_glob_match_segment_star_with_remaining_pattern() {
1486 assert!(WatchConfig::glob_match_segment("foo*bar", "fooXbar"));
1488 assert!(WatchConfig::glob_match_segment("foo*bar", "foobar"));
1489 assert!(WatchConfig::glob_match_segment("foo*bar", "fooXXXbar"));
1490 assert!(!WatchConfig::glob_match_segment("foo*bar", "fooXba"));
1491 }
1492
1493 #[test]
1494 fn test_glob_match_segment_star_matches_empty() {
1495 assert!(WatchConfig::glob_match_segment("a*b", "ab"));
1497 }
1498
1499 #[test]
1500 fn test_glob_match_segment_consecutive_stars() {
1501 assert!(WatchConfig::glob_match_segment("**", "anything"));
1503 assert!(WatchConfig::glob_match_segment("a**b", "aXXXb"));
1504 }
1505
1506 #[test]
1507 fn test_glob_match_segment_star_followed_by_literal() {
1508 assert!(WatchConfig::glob_match_segment("*a", "aaa"));
1510 assert!(WatchConfig::glob_match_segment("*a", "XXXa"));
1511 assert!(!WatchConfig::glob_match_segment("*a", "XXXb"));
1512 }
1513
1514 #[test]
1515 fn test_glob_matches_leading_slash() {
1516 assert!(WatchConfig::glob_matches("**/*.rs", "/src/main.rs"));
1518 }
1519
1520 #[test]
1521 fn test_glob_match_parts_double_star_only() {
1522 let pattern = vec!["**"];
1524 let path = vec!["a", "b", "c"];
1525 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1526
1527 let empty: Vec<&str> = vec![];
1528 assert!(WatchConfig::glob_match_parts(&pattern, &empty));
1529 }
1530
1531 #[test]
1532 fn test_watch_config_watch_dirs_default() {
1533 let config = WatchConfig::default();
1534 assert!(!config.watch_dirs.is_empty());
1535 assert!(config.watch_dirs.contains(&PathBuf::from(".")));
1536 }
1537
1538 #[test]
1540 fn test_file_change_kind_debug() {
1541 let kinds = [
1542 (FileChangeKind::Created, "Created"),
1543 (FileChangeKind::Modified, "Modified"),
1544 (FileChangeKind::Deleted, "Deleted"),
1545 (FileChangeKind::Renamed, "Renamed"),
1546 (FileChangeKind::Other, "Other"),
1547 ];
1548 for (kind, expected) in kinds {
1549 let debug_str = format!("{:?}", kind);
1550 assert!(debug_str.contains(expected));
1551 }
1552 }
1553
1554 #[test]
1556 fn test_file_change_kind_from_create_any() {
1557 let kind = FileChangeKind::from(EventKind::Create(notify::event::CreateKind::Any));
1558 assert_eq!(kind, FileChangeKind::Created);
1559 }
1560
1561 #[test]
1562 fn test_file_change_kind_from_create_folder() {
1563 let kind = FileChangeKind::from(EventKind::Create(notify::event::CreateKind::Folder));
1564 assert_eq!(kind, FileChangeKind::Created);
1565 }
1566
1567 #[test]
1568 fn test_file_change_kind_from_modify_any() {
1569 let kind = FileChangeKind::from(EventKind::Modify(notify::event::ModifyKind::Any));
1570 assert_eq!(kind, FileChangeKind::Modified);
1571 }
1572
1573 #[test]
1574 fn test_file_change_kind_from_modify_name() {
1575 let kind = FileChangeKind::from(EventKind::Modify(notify::event::ModifyKind::Name(
1576 notify::event::RenameMode::Any,
1577 )));
1578 assert_eq!(kind, FileChangeKind::Modified);
1579 }
1580
1581 #[test]
1582 fn test_file_change_kind_from_modify_metadata() {
1583 let kind = FileChangeKind::from(EventKind::Modify(
1584 notify::event::ModifyKind::Metadata(notify::event::MetadataKind::Any),
1585 ));
1586 assert_eq!(kind, FileChangeKind::Modified);
1587 }
1588
1589 #[test]
1590 fn test_file_change_kind_from_remove_any() {
1591 let kind = FileChangeKind::from(EventKind::Remove(notify::event::RemoveKind::Any));
1592 assert_eq!(kind, FileChangeKind::Deleted);
1593 }
1594
1595 #[test]
1596 fn test_file_change_kind_from_remove_folder() {
1597 let kind = FileChangeKind::from(EventKind::Remove(notify::event::RemoveKind::Folder));
1598 assert_eq!(kind, FileChangeKind::Deleted);
1599 }
1600
1601 #[test]
1602 fn test_file_change_kind_from_access_close() {
1603 let kind = FileChangeKind::from(EventKind::Access(notify::event::AccessKind::Close(
1604 notify::event::AccessMode::Any,
1605 )));
1606 assert_eq!(kind, FileChangeKind::Other);
1607 }
1608
1609 #[test]
1611 fn test_watch_builder_all_options() {
1612 let config = WatchBuilder::new()
1613 .rust_files()
1614 .toml_files()
1615 .test_files()
1616 .src_dir()
1617 .ignore_target()
1618 .debounce(100)
1619 .build();
1620
1621 assert!(config.patterns.contains(&"**/*.rs".to_string()));
1622 assert!(config.patterns.contains(&"**/*.toml".to_string()));
1623 assert!(config.patterns.contains(&"**/tests/**/*.rs".to_string()));
1624 assert!(config.patterns.contains(&"**/*_test.rs".to_string()));
1625 assert!(config.patterns.contains(&"**/test_*.rs".to_string()));
1626 assert!(config.watch_dirs.contains(&PathBuf::from("src")));
1627 assert!(config.ignore_patterns.contains(&"**/target/**".to_string()));
1628 assert_eq!(config.debounce_ms, 100);
1629 }
1630
1631 #[test]
1633 fn test_matches_pattern_various_paths() {
1634 let config = WatchConfig::default();
1635
1636 assert!(config.matches_pattern(Path::new("/home/user/project/src/main.rs")));
1638
1639 let _ = config.matches_pattern(Path::new("C:\\Users\\test\\main.rs"));
1642 }
1643
1644 #[test]
1646 fn test_glob_matches_empty_strings() {
1647 assert!(!WatchConfig::glob_matches("", "src"));
1653
1654 assert!(!WatchConfig::glob_matches("", ""));
1657 }
1658
1659 #[test]
1661 fn test_file_change_timestamp() {
1662 let before = Instant::now();
1663 let change = FileChange {
1664 path: PathBuf::from("test.rs"),
1665 kind: FileChangeKind::Modified,
1666 timestamp: Instant::now(),
1667 };
1668 let after = Instant::now();
1669
1670 assert!(change.timestamp >= before);
1671 assert!(change.timestamp <= after);
1672 }
1673
1674 #[test]
1676 fn test_file_watcher_empty_watch_dirs() {
1677 let config = WatchConfig {
1678 patterns: vec!["**/*.rs".to_string()],
1679 ignore_patterns: vec![],
1680 debounce_ms: 300,
1681 clear_screen: true,
1682 run_on_start: true,
1683 watch_dirs: vec![],
1684 };
1685 let mut watcher = FileWatcher::new(config).unwrap();
1686 assert!(watcher.start().is_ok());
1688 assert!(watcher.is_running());
1689 watcher.stop();
1690 }
1691
1692 #[test]
1694 fn test_watch_config_all_custom_values() {
1695 let config = WatchConfig {
1696 patterns: vec!["custom".to_string()],
1697 ignore_patterns: vec!["ignore".to_string()],
1698 debounce_ms: 1000,
1699 clear_screen: false,
1700 run_on_start: false,
1701 watch_dirs: vec![PathBuf::from("/tmp")],
1702 };
1703
1704 assert_eq!(config.patterns, vec!["custom".to_string()]);
1705 assert_eq!(config.ignore_patterns, vec!["ignore".to_string()]);
1706 assert_eq!(config.debounce_ms, 1000);
1707 assert!(!config.clear_screen);
1708 assert!(!config.run_on_start);
1709 assert_eq!(config.watch_dirs, vec![PathBuf::from("/tmp")]);
1710 }
1711
1712 #[test]
1714 fn test_watch_stats_all_fields() {
1715 let mut stats = WatchStats {
1716 trigger_count: 10,
1717 change_count: 25,
1718 total_runtime: Duration::from_secs(60),
1719 last_trigger: Some(Instant::now()),
1720 };
1721
1722 assert_eq!(stats.trigger_count, 10);
1723 assert_eq!(stats.change_count, 25);
1724 assert_eq!(stats.total_runtime.as_secs(), 60);
1725 assert!(stats.last_trigger.is_some());
1726
1727 stats.record_trigger(5);
1729 assert_eq!(stats.trigger_count, 11);
1730 assert_eq!(stats.change_count, 30);
1731 }
1732
1733 #[test]
1735 fn test_glob_match_parts_first_segment_fail() {
1736 let pattern = vec!["foo", "bar"];
1737 let path = vec!["baz", "bar"];
1738 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1739 }
1740
1741 #[test]
1743 fn test_file_change_path_field() {
1744 let change = FileChange {
1745 path: PathBuf::from("/home/user/test.rs"),
1746 kind: FileChangeKind::Created,
1747 timestamp: Instant::now(),
1748 };
1749 assert_eq!(change.path, PathBuf::from("/home/user/test.rs"));
1750 }
1751
1752 #[test]
1754 fn test_fn_watch_handler_empty_changes() {
1755 use std::sync::atomic::{AtomicBool, Ordering};
1756 use std::sync::Arc;
1757
1758 let called = Arc::new(AtomicBool::new(false));
1759 let called_clone = Arc::clone(&called);
1760
1761 let handler = FnWatchHandler::new(move |changes| {
1762 called_clone.store(true, Ordering::SeqCst);
1763 assert!(changes.is_empty());
1764 Ok(())
1765 });
1766
1767 let empty_changes: Vec<FileChange> = vec![];
1768 handler.on_change(&empty_changes).unwrap();
1769 assert!(called.load(Ordering::SeqCst));
1770 }
1771
1772 #[test]
1774 fn test_watch_handler_trait_defaults() {
1775 struct MinimalHandler;
1776 impl WatchHandler for MinimalHandler {
1777 fn on_change(&self, _: &[FileChange]) -> ProbarResult<()> {
1778 Ok(())
1779 }
1780 }
1781
1782 let handler = MinimalHandler;
1783
1784 assert!(handler.on_start().is_ok());
1786 assert!(handler.on_stop().is_ok());
1787 assert!(handler.on_change(&[]).is_ok());
1788 }
1789
1790 #[test]
1792 fn test_matches_pattern_ignore_vs_pattern_priority() {
1793 let config = WatchConfig {
1794 patterns: vec!["**/*.rs".to_string()],
1795 ignore_patterns: vec!["**/*.rs".to_string()], debounce_ms: 300,
1797 clear_screen: true,
1798 run_on_start: true,
1799 watch_dirs: vec![],
1800 };
1801 assert!(!config.matches_pattern(Path::new("src/main.rs")));
1803 }
1804
1805 #[test]
1807 fn test_glob_match_segment_only_questions() {
1808 assert!(WatchConfig::glob_match_segment("???", "abc"));
1809 assert!(!WatchConfig::glob_match_segment("???", "ab"));
1810 assert!(!WatchConfig::glob_match_segment("???", "abcd"));
1811 }
1812
1813 #[test]
1815 fn test_glob_match_segment_only_stars() {
1816 assert!(WatchConfig::glob_match_segment("*", ""));
1817 assert!(WatchConfig::glob_match_segment("*", "anything"));
1818 assert!(WatchConfig::glob_match_segment("***", "test"));
1819 }
1820
1821 #[test]
1823 fn test_glob_match_parts_exact_match() {
1824 let pattern = vec!["src", "lib", "mod.rs"];
1825 let path = vec!["src", "lib", "mod.rs"];
1826 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1827 }
1828
1829 #[test]
1831 fn test_file_watcher_config_reference() {
1832 let original_debounce = 500;
1833 let config = WatchConfig::new().with_debounce(original_debounce);
1834 let watcher = FileWatcher::new(config).unwrap();
1835 let config_ref = watcher.config();
1836 assert_eq!(config_ref.debounce_ms, original_debounce);
1837 }
1838
1839 #[test]
1841 fn test_watch_builder_default_impl() {
1842 let builder1 = WatchBuilder::default();
1843 let builder2 = WatchBuilder::new();
1844 assert_eq!(builder1.build().debounce_ms, builder2.build().debounce_ms);
1846 }
1847 }
1848}