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::new(
333 std::io::ErrorKind::Other,
334 format!("Failed to create watcher: {e}"),
335 ))
336 })?;
337
338 for dir in &self.config.watch_dirs {
340 if dir.exists() {
341 watcher.watch(dir, RecursiveMode::Recursive).map_err(|e| {
342 ProbarError::Io(std::io::Error::new(
343 std::io::ErrorKind::Other,
344 format!("Failed to watch directory {:?}: {e}", dir),
345 ))
346 })?;
347 }
348 }
349
350 self.watcher = Some(watcher);
351 self.receiver = Some(rx);
352 Ok(())
353 }
354
355 pub fn stop(&mut self) {
357 self.watcher = None;
358 self.receiver = None;
359 }
360
361 pub fn check_changes(&mut self) -> Option<Vec<FileChange>> {
363 let receiver = self.receiver.as_ref()?;
364 let now = Instant::now();
365
366 while let Ok(result) = receiver.try_recv() {
368 if let Ok(event) = result {
369 for path in event.paths {
370 if self.config.matches_pattern(&path) {
371 self.pending_changes.push(FileChange {
372 path,
373 kind: event.kind.into(),
374 timestamp: now,
375 });
376 }
377 }
378 }
379 }
380
381 if self.pending_changes.is_empty() {
383 return None;
384 }
385
386 let should_trigger = match self.last_trigger {
387 Some(last) => now.duration_since(last).as_millis() >= self.config.debounce_ms as u128,
388 None => true,
389 };
390
391 if should_trigger {
392 self.last_trigger = Some(now);
393 let changes = std::mem::take(&mut self.pending_changes);
394
395 let unique_paths: HashSet<_> = changes.iter().map(|c| c.path.clone()).collect();
397 let deduped: Vec<FileChange> = unique_paths
398 .into_iter()
399 .filter_map(|path| changes.iter().find(|c| c.path == path).cloned())
400 .collect();
401
402 if deduped.is_empty() {
403 None
404 } else {
405 Some(deduped)
406 }
407 } else {
408 None
409 }
410 }
411
412 #[must_use]
414 pub fn config(&self) -> &WatchConfig {
415 &self.config
416 }
417
418 #[must_use]
420 pub fn is_running(&self) -> bool {
421 self.watcher.is_some()
422 }
423}
424
425impl std::fmt::Debug for FileWatcher {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 f.debug_struct("FileWatcher")
428 .field("config", &self.config)
429 .field("is_running", &self.is_running())
430 .field("pending_changes", &self.pending_changes.len())
431 .finish()
432 }
433}
434
435#[derive(Debug, Clone, Default)]
437pub struct WatchStats {
438 pub trigger_count: u64,
440 pub change_count: u64,
442 pub total_runtime: Duration,
444 pub last_trigger: Option<Instant>,
446}
447
448impl WatchStats {
449 #[must_use]
451 pub fn new() -> Self {
452 Self::default()
453 }
454
455 pub fn record_trigger(&mut self, change_count: usize) {
457 self.trigger_count += 1;
458 self.change_count += change_count as u64;
459 self.last_trigger = Some(Instant::now());
460 }
461}
462
463#[derive(Debug)]
465pub struct WatchBuilder {
466 config: WatchConfig,
467}
468
469impl Default for WatchBuilder {
470 fn default() -> Self {
471 Self {
472 config: WatchConfig {
473 patterns: Vec::new(),
474 ignore_patterns: Vec::new(),
475 debounce_ms: 300,
476 clear_screen: true,
477 run_on_start: true,
478 watch_dirs: vec![std::path::PathBuf::from(".")],
479 },
480 }
481 }
482}
483
484impl WatchBuilder {
485 #[must_use]
487 pub fn new() -> Self {
488 Self::default()
489 }
490
491 #[must_use]
493 pub fn rust_files(mut self) -> Self {
494 self.config.patterns.push("**/*.rs".to_string());
495 self
496 }
497
498 #[must_use]
500 pub fn toml_files(mut self) -> Self {
501 self.config.patterns.push("**/*.toml".to_string());
502 self
503 }
504
505 #[must_use]
507 pub fn test_files(mut self) -> Self {
508 self.config.patterns.push("**/tests/**/*.rs".to_string());
509 self.config.patterns.push("**/*_test.rs".to_string());
510 self.config.patterns.push("**/test_*.rs".to_string());
511 self
512 }
513
514 #[must_use]
516 pub fn src_dir(mut self) -> Self {
517 self.config.watch_dirs.push(PathBuf::from("src"));
518 self
519 }
520
521 #[must_use]
523 pub fn ignore_target(mut self) -> Self {
524 self.config.ignore_patterns.push("**/target/**".to_string());
525 self
526 }
527
528 #[must_use]
530 pub const fn debounce(mut self, ms: u64) -> Self {
531 self.config.debounce_ms = ms;
532 self
533 }
534
535 #[must_use]
537 pub fn build(self) -> WatchConfig {
538 self.config
539 }
540}
541
542#[cfg(test)]
543#[allow(clippy::unwrap_used, clippy::expect_used)]
544mod tests {
545 use super::*;
546
547 mod watch_config_tests {
548 use super::*;
549
550 #[test]
551 fn test_default() {
552 let config = WatchConfig::default();
553 assert!(!config.patterns.is_empty());
554 assert!(!config.ignore_patterns.is_empty());
555 assert_eq!(config.debounce_ms, 300);
556 }
557
558 #[test]
559 fn test_with_pattern() {
560 let config = WatchConfig::new().with_pattern("**/*.js");
561 assert!(config.patterns.contains(&"**/*.js".to_string()));
562 }
563
564 #[test]
565 fn test_with_ignore() {
566 let config = WatchConfig::new().with_ignore("**/dist/**");
567 assert!(config.ignore_patterns.contains(&"**/dist/**".to_string()));
568 }
569
570 #[test]
571 fn test_with_debounce() {
572 let config = WatchConfig::new().with_debounce(500);
573 assert_eq!(config.debounce_ms, 500);
574 }
575
576 #[test]
577 fn test_with_clear_screen() {
578 let config = WatchConfig::new().with_clear_screen(false);
579 assert!(!config.clear_screen);
580 let config = WatchConfig::new().with_clear_screen(true);
581 assert!(config.clear_screen);
582 }
583
584 #[test]
585 fn test_with_watch_dir() {
586 let config = WatchConfig::new().with_watch_dir(Path::new("src"));
587 assert!(config.watch_dirs.contains(&PathBuf::from("src")));
588 }
589
590 #[test]
591 fn test_matches_pattern_rust_file() {
592 let config = WatchConfig::default();
593 assert!(config.matches_pattern(Path::new("src/main.rs")));
594 assert!(config.matches_pattern(Path::new("tests/test.rs")));
595 }
596
597 #[test]
598 fn test_matches_pattern_toml_file() {
599 let config = WatchConfig::default();
600 assert!(config.matches_pattern(Path::new("Cargo.toml")));
601 }
602
603 #[test]
604 fn test_ignores_target() {
605 let config = WatchConfig::default();
606 assert!(!config.matches_pattern(Path::new("target/debug/main.rs")));
607 }
608
609 #[test]
610 fn test_ignores_git() {
611 let config = WatchConfig::default();
612 assert!(!config.matches_pattern(Path::new(".git/config")));
613 }
614 }
615
616 mod glob_matching_tests {
617 use super::*;
618
619 #[test]
620 fn test_exact_match() {
621 assert!(WatchConfig::glob_match_segment("main.rs", "main.rs"));
622 assert!(!WatchConfig::glob_match_segment("main.rs", "test.rs"));
623 }
624
625 #[test]
626 fn test_star_wildcard() {
627 assert!(WatchConfig::glob_match_segment("*.rs", "main.rs"));
628 assert!(WatchConfig::glob_match_segment("*.rs", "test.rs"));
629 assert!(!WatchConfig::glob_match_segment("*.rs", "main.js"));
630 }
631
632 #[test]
633 fn test_question_wildcard() {
634 assert!(WatchConfig::glob_match_segment("?.rs", "a.rs"));
635 assert!(!WatchConfig::glob_match_segment("?.rs", "ab.rs"));
636 }
637
638 #[test]
639 fn test_double_star() {
640 assert!(WatchConfig::glob_matches("**/*.rs", "src/main.rs"));
641 assert!(WatchConfig::glob_matches("**/*.rs", "src/lib/mod.rs"));
642 assert!(WatchConfig::glob_matches(
643 "**/target/**",
644 "crates/probar/target/debug/build"
645 ));
646 }
647
648 #[test]
649 fn test_prefix_pattern() {
650 assert!(WatchConfig::glob_matches("src/**/*.rs", "src/lib.rs"));
651 assert!(WatchConfig::glob_matches(
652 "src/**/*.rs",
653 "src/modules/test.rs"
654 ));
655 assert!(!WatchConfig::glob_matches("src/**/*.rs", "tests/test.rs"));
656 }
657 }
658
659 mod file_change_tests {
660 use super::*;
661
662 #[test]
663 fn test_file_change_kind_from_event() {
664 assert_eq!(
665 FileChangeKind::from(EventKind::Create(notify::event::CreateKind::File)),
666 FileChangeKind::Created
667 );
668 assert_eq!(
669 FileChangeKind::from(EventKind::Modify(notify::event::ModifyKind::Data(
670 notify::event::DataChange::Content
671 ))),
672 FileChangeKind::Modified
673 );
674 assert_eq!(
675 FileChangeKind::from(EventKind::Remove(notify::event::RemoveKind::File)),
676 FileChangeKind::Deleted
677 );
678 }
679 }
680
681 mod file_watcher_tests {
682 use super::*;
683
684 #[test]
685 fn test_new() {
686 let config = WatchConfig::default();
687 let watcher = FileWatcher::new(config);
688 assert!(watcher.is_ok());
689 }
690
691 #[test]
692 fn test_is_running_before_start() {
693 let config = WatchConfig::default();
694 let watcher = FileWatcher::new(config).unwrap();
695 assert!(!watcher.is_running());
696 }
697
698 #[test]
699 fn test_check_changes_before_start() {
700 let config = WatchConfig::default();
701 let mut watcher = FileWatcher::new(config).unwrap();
702 assert!(watcher.check_changes().is_none());
703 }
704
705 #[test]
706 fn test_start_and_stop() {
707 let config = WatchConfig::new().with_watch_dir(Path::new("."));
708 let mut watcher = FileWatcher::new(config).unwrap();
709 assert!(!watcher.is_running());
710
711 watcher.start().unwrap();
712 assert!(watcher.is_running());
713
714 watcher.stop();
715 assert!(!watcher.is_running());
716 }
717
718 #[test]
719 fn test_config_accessor() {
720 let config = WatchConfig::new().with_debounce(500);
721 let watcher = FileWatcher::new(config).unwrap();
722 assert_eq!(watcher.config().debounce_ms, 500);
723 }
724
725 #[test]
726 fn test_debug() {
727 let config = WatchConfig::default();
728 let watcher = FileWatcher::new(config).unwrap();
729 let debug_str = format!("{:?}", watcher);
730 assert!(debug_str.contains("FileWatcher"));
731 assert!(debug_str.contains("is_running"));
732 }
733
734 #[test]
735 fn test_start_stop_multiple_times() {
736 let config = WatchConfig::new().with_watch_dir(Path::new("."));
737 let mut watcher = FileWatcher::new(config).unwrap();
738
739 watcher.start().unwrap();
740 assert!(watcher.is_running());
741 watcher.stop();
742 assert!(!watcher.is_running());
743
744 watcher.start().unwrap();
746 assert!(watcher.is_running());
747 watcher.stop();
748 assert!(!watcher.is_running());
749 }
750
751 #[test]
752 fn test_check_changes_after_start_no_events() {
753 let config = WatchConfig::new().with_watch_dir(Path::new("."));
754 let mut watcher = FileWatcher::new(config).unwrap();
755 watcher.start().unwrap();
756
757 let changes = watcher.check_changes();
759 assert!(changes.is_none());
760
761 watcher.stop();
762 }
763 }
764
765 mod watch_stats_tests {
766 use super::*;
767
768 #[test]
769 fn test_new() {
770 let stats = WatchStats::new();
771 assert_eq!(stats.trigger_count, 0);
772 assert_eq!(stats.change_count, 0);
773 }
774
775 #[test]
776 fn test_record_trigger() {
777 let mut stats = WatchStats::new();
778 stats.record_trigger(3);
779
780 assert_eq!(stats.trigger_count, 1);
781 assert_eq!(stats.change_count, 3);
782 assert!(stats.last_trigger.is_some());
783 }
784
785 #[test]
786 fn test_multiple_triggers() {
787 let mut stats = WatchStats::new();
788 stats.record_trigger(2);
789 stats.record_trigger(5);
790
791 assert_eq!(stats.trigger_count, 2);
792 assert_eq!(stats.change_count, 7);
793 }
794 }
795
796 mod watch_builder_tests {
797 use super::*;
798
799 #[test]
800 fn test_new() {
801 let builder = WatchBuilder::new();
802 let config = builder.build();
803 assert!(config.patterns.is_empty());
804 }
805
806 #[test]
807 fn test_rust_files() {
808 let config = WatchBuilder::new().rust_files().build();
809 assert!(config.patterns.contains(&"**/*.rs".to_string()));
810 }
811
812 #[test]
813 fn test_toml_files() {
814 let config = WatchBuilder::new().toml_files().build();
815 assert!(config.patterns.contains(&"**/*.toml".to_string()));
816 }
817
818 #[test]
819 fn test_test_files() {
820 let config = WatchBuilder::new().test_files().build();
821 assert!(config.patterns.contains(&"**/tests/**/*.rs".to_string()));
822 assert!(config.patterns.contains(&"**/*_test.rs".to_string()));
823 }
824
825 #[test]
826 fn test_ignore_target() {
827 let config = WatchBuilder::new().ignore_target().build();
828 assert!(config.ignore_patterns.contains(&"**/target/**".to_string()));
829 }
830
831 #[test]
832 fn test_src_dir() {
833 let config = WatchBuilder::new().src_dir().build();
834 assert!(config.watch_dirs.contains(&PathBuf::from("src")));
835 }
836
837 #[test]
838 fn test_debounce() {
839 let config = WatchBuilder::new().debounce(500).build();
840 assert_eq!(config.debounce_ms, 500);
841 }
842
843 #[test]
844 fn test_chained_builder() {
845 let config = WatchBuilder::new()
846 .rust_files()
847 .toml_files()
848 .ignore_target()
849 .debounce(200)
850 .build();
851
852 assert!(config.patterns.contains(&"**/*.rs".to_string()));
853 assert!(config.patterns.contains(&"**/*.toml".to_string()));
854 assert!(config.ignore_patterns.contains(&"**/target/**".to_string()));
855 assert_eq!(config.debounce_ms, 200);
856 }
857 }
858
859 mod fn_watch_handler_tests {
860 use super::*;
861 use std::sync::atomic::{AtomicU32, Ordering};
862 use std::sync::Arc;
863
864 #[test]
865 fn test_on_change() {
866 let counter = Arc::new(AtomicU32::new(0));
867 let counter_clone = Arc::clone(&counter);
868
869 let handler = FnWatchHandler::new(move |_changes| {
870 counter_clone.fetch_add(1, Ordering::SeqCst);
871 Ok(())
872 });
873
874 let changes = vec![FileChange {
875 path: PathBuf::from("test.rs"),
876 kind: FileChangeKind::Modified,
877 timestamp: Instant::now(),
878 }];
879
880 handler.on_change(&changes).unwrap();
881 assert_eq!(counter.load(Ordering::SeqCst), 1);
882 }
883
884 #[test]
885 fn test_debug() {
886 let handler = FnWatchHandler::new(|_changes| Ok(()));
887 let debug_str = format!("{:?}", handler);
888 assert!(debug_str.contains("FnWatchHandler"));
889 }
890 }
891
892 mod file_change_kind_tests {
893 use super::*;
894
895 #[test]
896 fn test_other_kind() {
897 let kind = FileChangeKind::from(EventKind::Other);
898 assert_eq!(kind, FileChangeKind::Other);
899 }
900
901 #[test]
902 fn test_access_kind() {
903 let kind = FileChangeKind::from(EventKind::Access(notify::event::AccessKind::Read));
904 assert_eq!(kind, FileChangeKind::Other);
905 }
906 }
907
908 mod file_change_additional_tests {
909 use super::*;
910
911 #[test]
912 fn test_debug() {
913 let change = FileChange {
914 path: PathBuf::from("test.rs"),
915 kind: FileChangeKind::Modified,
916 timestamp: Instant::now(),
917 };
918 let debug_str = format!("{:?}", change);
919 assert!(debug_str.contains("test.rs"));
920 assert!(debug_str.contains("Modified"));
921 }
922
923 #[test]
924 fn test_clone() {
925 let change = FileChange {
926 path: PathBuf::from("test.rs"),
927 kind: FileChangeKind::Created,
928 timestamp: Instant::now(),
929 };
930 let cloned = change.clone();
931 assert_eq!(change.path, cloned.path);
932 assert_eq!(change.kind, cloned.kind);
933 }
934 }
935
936 mod watch_handler_default_tests {
937 use super::*;
938
939 struct TestHandler;
940
941 impl WatchHandler for TestHandler {
942 fn on_change(&self, _changes: &[FileChange]) -> ProbarResult<()> {
943 Ok(())
944 }
945 }
946
947 #[test]
948 fn test_on_start_default() {
949 let handler = TestHandler;
950 assert!(handler.on_start().is_ok());
951 }
952
953 #[test]
954 fn test_on_stop_default() {
955 let handler = TestHandler;
956 assert!(handler.on_stop().is_ok());
957 }
958 }
959
960 mod additional_coverage_tests {
961 use super::*;
962
963 #[test]
965 fn test_file_change_kind_any() {
966 let kind = FileChangeKind::from(EventKind::Any);
967 assert_eq!(kind, FileChangeKind::Other);
968 }
969
970 #[test]
971 fn test_file_change_kind_renamed_variant() {
972 let kind = FileChangeKind::Renamed;
974 assert_eq!(kind, FileChangeKind::Renamed);
975 }
976
977 #[test]
978 fn test_file_change_kind_hash() {
979 use std::collections::HashSet;
981 let mut set = HashSet::new();
982 set.insert(FileChangeKind::Created);
983 set.insert(FileChangeKind::Modified);
984 set.insert(FileChangeKind::Deleted);
985 set.insert(FileChangeKind::Renamed);
986 set.insert(FileChangeKind::Other);
987 assert_eq!(set.len(), 5);
988 }
989
990 #[test]
991 fn test_file_change_kind_copy() {
992 let kind = FileChangeKind::Modified;
993 let copied = kind;
994 assert_eq!(kind, copied);
995 }
996
997 #[test]
999 fn test_watch_config_serialize() {
1000 let config = WatchConfig::default();
1001 let json = serde_json::to_string(&config).unwrap();
1002 assert!(json.contains("patterns"));
1003 assert!(json.contains("debounce_ms"));
1004 }
1005
1006 #[test]
1007 fn test_watch_config_deserialize() {
1008 let json = r#"{
1009 "patterns": ["**/*.rs"],
1010 "ignore_patterns": ["**/target/**"],
1011 "debounce_ms": 500,
1012 "clear_screen": false,
1013 "run_on_start": false,
1014 "watch_dirs": ["."]
1015 }"#;
1016 let config: WatchConfig = serde_json::from_str(json).unwrap();
1017 assert_eq!(config.debounce_ms, 500);
1018 assert!(!config.clear_screen);
1019 assert!(!config.run_on_start);
1020 }
1021
1022 #[test]
1023 fn test_watch_config_clone() {
1024 let config = WatchConfig::default();
1025 let cloned = config.clone();
1026 assert_eq!(config.debounce_ms, cloned.debounce_ms);
1027 assert_eq!(config.patterns.len(), cloned.patterns.len());
1028 }
1029
1030 #[test]
1031 fn test_watch_config_debug() {
1032 let config = WatchConfig::default();
1033 let debug_str = format!("{:?}", config);
1034 assert!(debug_str.contains("WatchConfig"));
1035 assert!(debug_str.contains("patterns"));
1036 }
1037
1038 #[test]
1040 fn test_glob_matches_empty_pattern_parts() {
1041 assert!(WatchConfig::glob_matches("**/*.rs", "//src//main.rs"));
1043 }
1044
1045 #[test]
1046 fn test_glob_match_segment_star_at_end() {
1047 assert!(WatchConfig::glob_match_segment("test*", "testing"));
1049 assert!(WatchConfig::glob_match_segment("test*", "test"));
1050 assert!(WatchConfig::glob_match_segment("test*", "test123"));
1051 }
1052
1053 #[test]
1054 fn test_glob_match_segment_star_in_middle() {
1055 assert!(WatchConfig::glob_match_segment("te*st", "test"));
1057 assert!(WatchConfig::glob_match_segment("te*st", "teaast"));
1058 assert!(!WatchConfig::glob_match_segment("te*st", "testing"));
1059 }
1060
1061 #[test]
1062 fn test_glob_match_segment_multiple_stars() {
1063 assert!(WatchConfig::glob_match_segment("*.*", "test.rs"));
1065 assert!(WatchConfig::glob_match_segment("*_*", "test_file"));
1066 }
1067
1068 #[test]
1069 fn test_glob_match_segment_question_at_end() {
1070 assert!(WatchConfig::glob_match_segment("test?", "testX"));
1072 assert!(!WatchConfig::glob_match_segment("test?", "test"));
1073 assert!(!WatchConfig::glob_match_segment("test?", "testXY"));
1074 }
1075
1076 #[test]
1077 fn test_glob_match_segment_question_in_middle() {
1078 assert!(WatchConfig::glob_match_segment("te?t", "test"));
1080 assert!(WatchConfig::glob_match_segment("te?t", "teat"));
1081 assert!(!WatchConfig::glob_match_segment("te?t", "tet"));
1082 }
1083
1084 #[test]
1085 fn test_glob_match_segment_empty_pattern() {
1086 assert!(WatchConfig::glob_match_segment("", ""));
1087 assert!(!WatchConfig::glob_match_segment("", "x"));
1088 }
1089
1090 #[test]
1091 fn test_glob_match_segment_empty_segment() {
1092 assert!(WatchConfig::glob_match_segment("", ""));
1093 assert!(!WatchConfig::glob_match_segment("a", ""));
1094 }
1095
1096 #[test]
1097 fn test_glob_match_parts_empty_both() {
1098 let empty: Vec<&str> = vec![];
1099 assert!(WatchConfig::glob_match_parts(&empty, &empty));
1100 }
1101
1102 #[test]
1103 fn test_glob_match_parts_empty_pattern_non_empty_path() {
1104 let empty: Vec<&str> = vec![];
1105 let path = vec!["src"];
1106 assert!(!WatchConfig::glob_match_parts(&empty, &path));
1107 }
1108
1109 #[test]
1110 fn test_glob_match_parts_double_star_at_end() {
1111 let pattern = vec!["src", "**"];
1113 let path = vec!["src", "lib", "mod.rs"];
1114 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1115 }
1116
1117 #[test]
1118 fn test_glob_match_parts_double_star_matches_zero_segments() {
1119 let pattern = vec!["**", "*.rs"];
1121 let path = vec!["main.rs"];
1122 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1123 }
1124
1125 #[test]
1126 fn test_glob_match_parts_double_star_no_match() {
1127 let pattern = vec!["**", "*.xyz"];
1129 let path = vec!["src", "main.rs"];
1130 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1131 }
1132
1133 #[test]
1134 fn test_glob_matches_node_modules() {
1135 let config = WatchConfig::default();
1136 assert!(!config.matches_pattern(Path::new("node_modules/package/index.js")));
1137 }
1138
1139 #[test]
1140 fn test_matches_pattern_no_watch_patterns() {
1141 let config = WatchConfig {
1143 patterns: vec![],
1144 ignore_patterns: vec![],
1145 debounce_ms: 300,
1146 clear_screen: true,
1147 run_on_start: true,
1148 watch_dirs: vec![],
1149 };
1150 assert!(!config.matches_pattern(Path::new("src/main.rs")));
1151 }
1152
1153 #[test]
1154 fn test_matches_pattern_not_matching_any() {
1155 let config = WatchConfig::default();
1157 assert!(!config.matches_pattern(Path::new("src/main.xyz")));
1158 }
1159
1160 #[test]
1162 fn test_watch_stats_default() {
1163 let stats = WatchStats::default();
1164 assert_eq!(stats.trigger_count, 0);
1165 assert_eq!(stats.change_count, 0);
1166 assert_eq!(stats.total_runtime, Duration::default());
1167 assert!(stats.last_trigger.is_none());
1168 }
1169
1170 #[test]
1171 fn test_watch_stats_clone() {
1172 let mut stats = WatchStats::new();
1173 stats.record_trigger(5);
1174 let cloned = stats.clone();
1175 assert_eq!(stats.trigger_count, cloned.trigger_count);
1176 assert_eq!(stats.change_count, cloned.change_count);
1177 }
1178
1179 #[test]
1180 fn test_watch_stats_debug() {
1181 let stats = WatchStats::new();
1182 let debug_str = format!("{:?}", stats);
1183 assert!(debug_str.contains("WatchStats"));
1184 assert!(debug_str.contains("trigger_count"));
1185 }
1186
1187 #[test]
1189 fn test_watch_builder_default() {
1190 let builder = WatchBuilder::default();
1191 let config = builder.build();
1192 assert!(config.patterns.is_empty());
1193 assert!(config.ignore_patterns.is_empty());
1194 assert_eq!(config.debounce_ms, 300);
1195 assert!(config.clear_screen);
1196 assert!(config.run_on_start);
1197 }
1198
1199 #[test]
1200 fn test_watch_builder_debug() {
1201 let builder = WatchBuilder::new();
1202 let debug_str = format!("{:?}", builder);
1203 assert!(debug_str.contains("WatchBuilder"));
1204 }
1205
1206 #[test]
1208 fn test_file_change_all_kinds() {
1209 let kinds = [
1210 FileChangeKind::Created,
1211 FileChangeKind::Modified,
1212 FileChangeKind::Deleted,
1213 FileChangeKind::Renamed,
1214 FileChangeKind::Other,
1215 ];
1216 for kind in kinds {
1217 let change = FileChange {
1218 path: PathBuf::from("test.rs"),
1219 kind,
1220 timestamp: Instant::now(),
1221 };
1222 let _ = format!("{:?}", change);
1223 }
1224 }
1225
1226 #[test]
1228 fn test_fn_watch_handler_with_error() {
1229 let handler = FnWatchHandler::new(|_changes| {
1230 Err(ProbarError::AssertionFailed {
1231 message: "test error".to_string(),
1232 })
1233 });
1234 let changes = vec![FileChange {
1235 path: PathBuf::from("test.rs"),
1236 kind: FileChangeKind::Modified,
1237 timestamp: Instant::now(),
1238 }];
1239 assert!(handler.on_change(&changes).is_err());
1240 }
1241
1242 #[test]
1243 fn test_fn_watch_handler_access_changes() {
1244 use std::sync::atomic::{AtomicUsize, Ordering};
1245 use std::sync::Arc;
1246
1247 let count = Arc::new(AtomicUsize::new(0));
1248 let count_clone = Arc::clone(&count);
1249
1250 let handler = FnWatchHandler::new(move |changes| {
1251 count_clone.store(changes.len(), Ordering::SeqCst);
1252 Ok(())
1253 });
1254
1255 let changes = vec![
1256 FileChange {
1257 path: PathBuf::from("a.rs"),
1258 kind: FileChangeKind::Created,
1259 timestamp: Instant::now(),
1260 },
1261 FileChange {
1262 path: PathBuf::from("b.rs"),
1263 kind: FileChangeKind::Modified,
1264 timestamp: Instant::now(),
1265 },
1266 ];
1267
1268 handler.on_change(&changes).unwrap();
1269 assert_eq!(count.load(Ordering::SeqCst), 2);
1270 }
1271
1272 #[test]
1274 fn test_file_watcher_with_nonexistent_dir() {
1275 let config =
1276 WatchConfig::new().with_watch_dir(Path::new("/nonexistent/directory/12345"));
1277 let mut watcher = FileWatcher::new(config).unwrap();
1278 let result = watcher.start();
1280 assert!(result.is_ok());
1281 watcher.stop();
1282 }
1283
1284 #[test]
1286 fn test_watch_config_multiple_patterns_chained() {
1287 let config = WatchConfig::new()
1288 .with_pattern("**/*.rs")
1289 .with_pattern("**/*.toml")
1290 .with_pattern("**/*.md")
1291 .with_ignore("**/target/**")
1292 .with_ignore("**/.git/**");
1293
1294 assert!(config.patterns.len() >= 3);
1295 assert!(config.ignore_patterns.len() >= 2);
1296 }
1297
1298 #[test]
1300 fn test_glob_matches_deep_nesting() {
1301 assert!(WatchConfig::glob_matches(
1302 "**/*.rs",
1303 "a/b/c/d/e/f/g/h/i/j/test.rs"
1304 ));
1305 }
1306
1307 #[test]
1308 fn test_glob_matches_single_segment() {
1309 assert!(WatchConfig::glob_matches("*.rs", "main.rs"));
1310 assert!(!WatchConfig::glob_matches("*.rs", "src/main.rs"));
1311 }
1312
1313 #[test]
1314 fn test_glob_match_segment_star_no_match() {
1315 assert!(!WatchConfig::glob_match_segment("*.rs", "main.js"));
1317 }
1318
1319 #[test]
1320 fn test_glob_match_segment_literal_mismatch() {
1321 assert!(!WatchConfig::glob_match_segment("abc", "abd"));
1322 assert!(!WatchConfig::glob_match_segment("abc", "ab"));
1323 }
1324
1325 #[test]
1326 fn test_glob_match_segment_pattern_longer_than_segment() {
1327 assert!(!WatchConfig::glob_match_segment("abcdef", "abc"));
1328 }
1329
1330 #[test]
1332 fn test_watch_config_new() {
1333 let config = WatchConfig::new();
1334 assert!(!config.patterns.is_empty());
1335 assert!(config.run_on_start);
1336 }
1337
1338 #[test]
1340 fn test_glob_match_parts_segment_mismatch() {
1341 let pattern = vec!["src", "lib.rs"];
1342 let path = vec!["src", "main.rs"];
1343 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1344 }
1345
1346 #[test]
1348 fn test_glob_match_parts_single_mismatch() {
1349 let pattern = vec!["foo"];
1350 let path = vec!["bar"];
1351 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1352 }
1353
1354 #[test]
1356 fn test_glob_match_parts_pattern_longer_than_path() {
1357 let pattern = vec!["src", "lib.rs"];
1358 let path = vec!["src"];
1359 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1360 }
1361
1362 #[test]
1364 fn test_glob_match_parts_non_doublestar_empty_path() {
1365 let pattern = vec!["*.rs"];
1366 let empty: Vec<&str> = vec![];
1367 assert!(!WatchConfig::glob_match_parts(&pattern, &empty));
1368 }
1369
1370 #[test]
1372 fn test_glob_match_parts_double_star_exhaustive_search() {
1373 let pattern = vec!["**", "specific.txt"];
1375 let path = vec!["a", "b", "c", "other.txt"];
1376 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1377 }
1378
1379 #[test]
1381 fn test_glob_match_segment_star_backtrack() {
1382 assert!(WatchConfig::glob_match_segment("a*b*c", "aXXbYYc"));
1384 assert!(WatchConfig::glob_match_segment("a*b*c", "abc"));
1385 assert!(!WatchConfig::glob_match_segment("a*b*c", "aXXbYY"));
1386 }
1387
1388 #[test]
1390 fn test_glob_match_segment_question_exhausts_segment() {
1391 assert!(!WatchConfig::glob_match_segment("a??", "ab"));
1392 }
1393
1394 #[test]
1396 fn test_glob_match_segment_literal_char_mismatch() {
1397 assert!(!WatchConfig::glob_match_segment("test", "Test"));
1398 assert!(!WatchConfig::glob_match_segment("abc", "axc"));
1399 }
1400
1401 #[test]
1403 fn test_glob_match_segment_pattern_shorter() {
1404 assert!(!WatchConfig::glob_match_segment("ab", "abc"));
1405 }
1406
1407 #[test]
1409 fn test_file_watcher_pending_changes_init() {
1410 let config = WatchConfig::new();
1411 let watcher = FileWatcher::new(config).unwrap();
1412 assert!(!watcher.is_running());
1414 assert_eq!(watcher.config().debounce_ms, 300);
1415 }
1416
1417 #[test]
1419 fn test_file_watcher_debug_running() {
1420 let config = WatchConfig::new().with_watch_dir(Path::new("."));
1421 let mut watcher = FileWatcher::new(config).unwrap();
1422 watcher.start().unwrap();
1423 let debug_str = format!("{:?}", watcher);
1424 assert!(debug_str.contains("true")); watcher.stop();
1426 }
1427
1428 #[test]
1430 fn test_watch_stats_total_runtime() {
1431 let mut stats = WatchStats::new();
1432 stats.total_runtime = Duration::from_secs(10);
1433 assert_eq!(stats.total_runtime, Duration::from_secs(10));
1434 }
1435
1436 #[test]
1438 fn test_matches_pattern_ignore_takes_precedence() {
1439 let config = WatchConfig {
1440 patterns: vec!["**/*.rs".to_string()],
1441 ignore_patterns: vec!["**/test/**".to_string()],
1442 debounce_ms: 300,
1443 clear_screen: true,
1444 run_on_start: true,
1445 watch_dirs: vec![],
1446 };
1447 assert!(!config.matches_pattern(Path::new("test/main.rs")));
1449 assert!(config.matches_pattern(Path::new("src/main.rs")));
1451 }
1452
1453 #[test]
1455 fn test_glob_matches_root_file() {
1456 assert!(WatchConfig::glob_matches("*.toml", "Cargo.toml"));
1457 assert!(!WatchConfig::glob_matches("*.toml", "src/Cargo.toml"));
1458 }
1459
1460 #[test]
1462 fn test_double_star_various_depths() {
1463 let pattern = vec!["**", "src", "main.rs"];
1465 let path = vec!["src", "main.rs"];
1466 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1467
1468 let path2 = vec!["project", "src", "main.rs"];
1470 assert!(WatchConfig::glob_match_parts(&pattern, &path2));
1471
1472 let path3 = vec!["a", "b", "c", "src", "main.rs"];
1474 assert!(WatchConfig::glob_match_parts(&pattern, &path3));
1475 }
1476
1477 #[test]
1479 fn test_watch_config_run_on_start_default_true() {
1480 let config = WatchConfig::default();
1481 assert!(config.run_on_start);
1482 }
1483
1484 #[test]
1486 fn test_glob_match_segment_star_with_remaining_pattern() {
1487 assert!(WatchConfig::glob_match_segment("foo*bar", "fooXbar"));
1489 assert!(WatchConfig::glob_match_segment("foo*bar", "foobar"));
1490 assert!(WatchConfig::glob_match_segment("foo*bar", "fooXXXbar"));
1491 assert!(!WatchConfig::glob_match_segment("foo*bar", "fooXba"));
1492 }
1493
1494 #[test]
1495 fn test_glob_match_segment_star_matches_empty() {
1496 assert!(WatchConfig::glob_match_segment("a*b", "ab"));
1498 }
1499
1500 #[test]
1501 fn test_glob_match_segment_consecutive_stars() {
1502 assert!(WatchConfig::glob_match_segment("**", "anything"));
1504 assert!(WatchConfig::glob_match_segment("a**b", "aXXXb"));
1505 }
1506
1507 #[test]
1508 fn test_glob_match_segment_star_followed_by_literal() {
1509 assert!(WatchConfig::glob_match_segment("*a", "aaa"));
1511 assert!(WatchConfig::glob_match_segment("*a", "XXXa"));
1512 assert!(!WatchConfig::glob_match_segment("*a", "XXXb"));
1513 }
1514
1515 #[test]
1516 fn test_glob_matches_leading_slash() {
1517 assert!(WatchConfig::glob_matches("**/*.rs", "/src/main.rs"));
1519 }
1520
1521 #[test]
1522 fn test_glob_match_parts_double_star_only() {
1523 let pattern = vec!["**"];
1525 let path = vec!["a", "b", "c"];
1526 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1527
1528 let empty: Vec<&str> = vec![];
1529 assert!(WatchConfig::glob_match_parts(&pattern, &empty));
1530 }
1531
1532 #[test]
1533 fn test_watch_config_watch_dirs_default() {
1534 let config = WatchConfig::default();
1535 assert!(!config.watch_dirs.is_empty());
1536 assert!(config.watch_dirs.contains(&PathBuf::from(".")));
1537 }
1538
1539 #[test]
1541 fn test_file_change_kind_debug() {
1542 let kinds = [
1543 (FileChangeKind::Created, "Created"),
1544 (FileChangeKind::Modified, "Modified"),
1545 (FileChangeKind::Deleted, "Deleted"),
1546 (FileChangeKind::Renamed, "Renamed"),
1547 (FileChangeKind::Other, "Other"),
1548 ];
1549 for (kind, expected) in kinds {
1550 let debug_str = format!("{:?}", kind);
1551 assert!(debug_str.contains(expected));
1552 }
1553 }
1554
1555 #[test]
1557 fn test_file_change_kind_from_create_any() {
1558 let kind = FileChangeKind::from(EventKind::Create(notify::event::CreateKind::Any));
1559 assert_eq!(kind, FileChangeKind::Created);
1560 }
1561
1562 #[test]
1563 fn test_file_change_kind_from_create_folder() {
1564 let kind = FileChangeKind::from(EventKind::Create(notify::event::CreateKind::Folder));
1565 assert_eq!(kind, FileChangeKind::Created);
1566 }
1567
1568 #[test]
1569 fn test_file_change_kind_from_modify_any() {
1570 let kind = FileChangeKind::from(EventKind::Modify(notify::event::ModifyKind::Any));
1571 assert_eq!(kind, FileChangeKind::Modified);
1572 }
1573
1574 #[test]
1575 fn test_file_change_kind_from_modify_name() {
1576 let kind = FileChangeKind::from(EventKind::Modify(notify::event::ModifyKind::Name(
1577 notify::event::RenameMode::Any,
1578 )));
1579 assert_eq!(kind, FileChangeKind::Modified);
1580 }
1581
1582 #[test]
1583 fn test_file_change_kind_from_modify_metadata() {
1584 let kind = FileChangeKind::from(EventKind::Modify(
1585 notify::event::ModifyKind::Metadata(notify::event::MetadataKind::Any),
1586 ));
1587 assert_eq!(kind, FileChangeKind::Modified);
1588 }
1589
1590 #[test]
1591 fn test_file_change_kind_from_remove_any() {
1592 let kind = FileChangeKind::from(EventKind::Remove(notify::event::RemoveKind::Any));
1593 assert_eq!(kind, FileChangeKind::Deleted);
1594 }
1595
1596 #[test]
1597 fn test_file_change_kind_from_remove_folder() {
1598 let kind = FileChangeKind::from(EventKind::Remove(notify::event::RemoveKind::Folder));
1599 assert_eq!(kind, FileChangeKind::Deleted);
1600 }
1601
1602 #[test]
1603 fn test_file_change_kind_from_access_close() {
1604 let kind = FileChangeKind::from(EventKind::Access(notify::event::AccessKind::Close(
1605 notify::event::AccessMode::Any,
1606 )));
1607 assert_eq!(kind, FileChangeKind::Other);
1608 }
1609
1610 #[test]
1612 fn test_watch_builder_all_options() {
1613 let config = WatchBuilder::new()
1614 .rust_files()
1615 .toml_files()
1616 .test_files()
1617 .src_dir()
1618 .ignore_target()
1619 .debounce(100)
1620 .build();
1621
1622 assert!(config.patterns.contains(&"**/*.rs".to_string()));
1623 assert!(config.patterns.contains(&"**/*.toml".to_string()));
1624 assert!(config.patterns.contains(&"**/tests/**/*.rs".to_string()));
1625 assert!(config.patterns.contains(&"**/*_test.rs".to_string()));
1626 assert!(config.patterns.contains(&"**/test_*.rs".to_string()));
1627 assert!(config.watch_dirs.contains(&PathBuf::from("src")));
1628 assert!(config.ignore_patterns.contains(&"**/target/**".to_string()));
1629 assert_eq!(config.debounce_ms, 100);
1630 }
1631
1632 #[test]
1634 fn test_matches_pattern_various_paths() {
1635 let config = WatchConfig::default();
1636
1637 assert!(config.matches_pattern(Path::new("/home/user/project/src/main.rs")));
1639
1640 let _ = config.matches_pattern(Path::new("C:\\Users\\test\\main.rs"));
1643 }
1644
1645 #[test]
1647 fn test_glob_matches_empty_strings() {
1648 assert!(!WatchConfig::glob_matches("", "src"));
1654
1655 assert!(!WatchConfig::glob_matches("", ""));
1658 }
1659
1660 #[test]
1662 fn test_file_change_timestamp() {
1663 let before = Instant::now();
1664 let change = FileChange {
1665 path: PathBuf::from("test.rs"),
1666 kind: FileChangeKind::Modified,
1667 timestamp: Instant::now(),
1668 };
1669 let after = Instant::now();
1670
1671 assert!(change.timestamp >= before);
1672 assert!(change.timestamp <= after);
1673 }
1674
1675 #[test]
1677 fn test_file_watcher_empty_watch_dirs() {
1678 let config = WatchConfig {
1679 patterns: vec!["**/*.rs".to_string()],
1680 ignore_patterns: vec![],
1681 debounce_ms: 300,
1682 clear_screen: true,
1683 run_on_start: true,
1684 watch_dirs: vec![],
1685 };
1686 let mut watcher = FileWatcher::new(config).unwrap();
1687 assert!(watcher.start().is_ok());
1689 assert!(watcher.is_running());
1690 watcher.stop();
1691 }
1692
1693 #[test]
1695 fn test_watch_config_all_custom_values() {
1696 let config = WatchConfig {
1697 patterns: vec!["custom".to_string()],
1698 ignore_patterns: vec!["ignore".to_string()],
1699 debounce_ms: 1000,
1700 clear_screen: false,
1701 run_on_start: false,
1702 watch_dirs: vec![PathBuf::from("/tmp")],
1703 };
1704
1705 assert_eq!(config.patterns, vec!["custom".to_string()]);
1706 assert_eq!(config.ignore_patterns, vec!["ignore".to_string()]);
1707 assert_eq!(config.debounce_ms, 1000);
1708 assert!(!config.clear_screen);
1709 assert!(!config.run_on_start);
1710 assert_eq!(config.watch_dirs, vec![PathBuf::from("/tmp")]);
1711 }
1712
1713 #[test]
1715 fn test_watch_stats_all_fields() {
1716 let mut stats = WatchStats {
1717 trigger_count: 10,
1718 change_count: 25,
1719 total_runtime: Duration::from_secs(60),
1720 last_trigger: Some(Instant::now()),
1721 };
1722
1723 assert_eq!(stats.trigger_count, 10);
1724 assert_eq!(stats.change_count, 25);
1725 assert_eq!(stats.total_runtime.as_secs(), 60);
1726 assert!(stats.last_trigger.is_some());
1727
1728 stats.record_trigger(5);
1730 assert_eq!(stats.trigger_count, 11);
1731 assert_eq!(stats.change_count, 30);
1732 }
1733
1734 #[test]
1736 fn test_glob_match_parts_first_segment_fail() {
1737 let pattern = vec!["foo", "bar"];
1738 let path = vec!["baz", "bar"];
1739 assert!(!WatchConfig::glob_match_parts(&pattern, &path));
1740 }
1741
1742 #[test]
1744 fn test_file_change_path_field() {
1745 let change = FileChange {
1746 path: PathBuf::from("/home/user/test.rs"),
1747 kind: FileChangeKind::Created,
1748 timestamp: Instant::now(),
1749 };
1750 assert_eq!(change.path, PathBuf::from("/home/user/test.rs"));
1751 }
1752
1753 #[test]
1755 fn test_fn_watch_handler_empty_changes() {
1756 use std::sync::atomic::{AtomicBool, Ordering};
1757 use std::sync::Arc;
1758
1759 let called = Arc::new(AtomicBool::new(false));
1760 let called_clone = Arc::clone(&called);
1761
1762 let handler = FnWatchHandler::new(move |changes| {
1763 called_clone.store(true, Ordering::SeqCst);
1764 assert!(changes.is_empty());
1765 Ok(())
1766 });
1767
1768 let empty_changes: Vec<FileChange> = vec![];
1769 handler.on_change(&empty_changes).unwrap();
1770 assert!(called.load(Ordering::SeqCst));
1771 }
1772
1773 #[test]
1775 fn test_watch_handler_trait_defaults() {
1776 struct MinimalHandler;
1777 impl WatchHandler for MinimalHandler {
1778 fn on_change(&self, _: &[FileChange]) -> ProbarResult<()> {
1779 Ok(())
1780 }
1781 }
1782
1783 let handler = MinimalHandler;
1784
1785 assert!(handler.on_start().is_ok());
1787 assert!(handler.on_stop().is_ok());
1788 assert!(handler.on_change(&[]).is_ok());
1789 }
1790
1791 #[test]
1793 fn test_matches_pattern_ignore_vs_pattern_priority() {
1794 let config = WatchConfig {
1795 patterns: vec!["**/*.rs".to_string()],
1796 ignore_patterns: vec!["**/*.rs".to_string()], debounce_ms: 300,
1798 clear_screen: true,
1799 run_on_start: true,
1800 watch_dirs: vec![],
1801 };
1802 assert!(!config.matches_pattern(Path::new("src/main.rs")));
1804 }
1805
1806 #[test]
1808 fn test_glob_match_segment_only_questions() {
1809 assert!(WatchConfig::glob_match_segment("???", "abc"));
1810 assert!(!WatchConfig::glob_match_segment("???", "ab"));
1811 assert!(!WatchConfig::glob_match_segment("???", "abcd"));
1812 }
1813
1814 #[test]
1816 fn test_glob_match_segment_only_stars() {
1817 assert!(WatchConfig::glob_match_segment("*", ""));
1818 assert!(WatchConfig::glob_match_segment("*", "anything"));
1819 assert!(WatchConfig::glob_match_segment("***", "test"));
1820 }
1821
1822 #[test]
1824 fn test_glob_match_parts_exact_match() {
1825 let pattern = vec!["src", "lib", "mod.rs"];
1826 let path = vec!["src", "lib", "mod.rs"];
1827 assert!(WatchConfig::glob_match_parts(&pattern, &path));
1828 }
1829
1830 #[test]
1832 fn test_file_watcher_config_reference() {
1833 let original_debounce = 500;
1834 let config = WatchConfig::new().with_debounce(original_debounce);
1835 let watcher = FileWatcher::new(config).unwrap();
1836 let config_ref = watcher.config();
1837 assert_eq!(config_ref.debounce_ms, original_debounce);
1838 }
1839
1840 #[test]
1842 fn test_watch_builder_default_impl() {
1843 let builder1 = WatchBuilder::default();
1844 let builder2 = WatchBuilder::new();
1845 assert_eq!(builder1.build().debounce_ms, builder2.build().debounce_ms);
1847 }
1848 }
1849}