Skip to main content

jugar_probar/
watch.rs

1//! Watch Mode with Hot Reload (Feature 6)
2//!
3//! Automatic test re-execution on file changes.
4//!
5//! ## EXTREME TDD: Tests written FIRST per spec
6//!
7//! ## Toyota Way Application
8//!
9//! - **Jidoka**: Immediate feedback on test failures
10//! - **Kaizen**: Continuous improvement through rapid iteration
11//! - **Muda**: Only re-run affected tests (smart filtering)
12
13use 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/// Configuration for watch mode
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct WatchConfig {
24    /// Patterns to watch (glob patterns)
25    pub patterns: Vec<String>,
26    /// Patterns to ignore
27    pub ignore_patterns: Vec<String>,
28    /// Debounce duration in milliseconds
29    pub debounce_ms: u64,
30    /// Whether to clear screen before re-run
31    pub clear_screen: bool,
32    /// Whether to run on initial start
33    pub run_on_start: bool,
34    /// Directories to watch
35    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    /// Create a new watch config
57    #[must_use]
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Add a pattern to watch
63    #[must_use]
64    pub fn with_pattern(mut self, pattern: &str) -> Self {
65        self.patterns.push(pattern.to_string());
66        self
67    }
68
69    /// Add a pattern to ignore
70    #[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    /// Set debounce duration
77    #[must_use]
78    pub const fn with_debounce(mut self, ms: u64) -> Self {
79        self.debounce_ms = ms;
80        self
81    }
82
83    /// Set clear screen behavior
84    #[must_use]
85    pub const fn with_clear_screen(mut self, clear: bool) -> Self {
86        self.clear_screen = clear;
87        self
88    }
89
90    /// Add a directory to watch
91    #[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    /// Check if a path matches watch patterns
98    #[must_use]
99    pub fn matches_pattern(&self, path: &Path) -> bool {
100        let path_str = path.to_string_lossy();
101
102        // Check ignore patterns first
103        for pattern in &self.ignore_patterns {
104            if Self::glob_matches(pattern, &path_str) {
105                return false;
106            }
107        }
108
109        // Check watch patterns
110        for pattern in &self.patterns {
111            if Self::glob_matches(pattern, &path_str) {
112                return true;
113            }
114        }
115
116        false
117    }
118
119    /// Simple glob matching (supports **, *, ?)
120    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            // ** matches zero or more path segments
136            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        // Match current segment
156        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        // Handle * and ? in segment matching
165        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                    // * matches any sequence of characters
172                    if pattern_chars.peek().is_none() {
173                        return true;
174                    }
175                    // Try matching remaining pattern at each position
176                    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                    // ? matches any single character
187                    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/// A file change event
204#[derive(Debug, Clone)]
205pub struct FileChange {
206    /// The changed file path
207    pub path: PathBuf,
208    /// Type of change
209    pub kind: FileChangeKind,
210    /// Timestamp of the change
211    pub timestamp: Instant,
212}
213
214/// Kind of file change
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216pub enum FileChangeKind {
217    /// File was created
218    Created,
219    /// File was modified
220    Modified,
221    /// File was deleted
222    Deleted,
223    /// File was renamed
224    Renamed,
225    /// Unknown change type
226    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
241/// Watch mode handler trait
242pub trait WatchHandler: Send + Sync {
243    /// Called when files change
244    fn on_change(&self, changes: &[FileChange]) -> ProbarResult<()>;
245
246    /// Called when watch starts
247    fn on_start(&self) -> ProbarResult<()> {
248        Ok(())
249    }
250
251    /// Called when watch stops
252    fn on_stop(&self) -> ProbarResult<()> {
253        Ok(())
254    }
255}
256
257/// Simple closure-based watch handler
258pub 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    /// Create a new function-based handler
279    #[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
294/// File system watcher for watch mode
295pub 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    /// Create a new file watcher
305    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    /// Start watching
316    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                // Ignore send errors (receiver may have dropped)
327                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        // Watch configured directories
339        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    /// Stop watching
356    pub fn stop(&mut self) {
357        self.watcher = None;
358        self.receiver = None;
359    }
360
361    /// Check for changes (non-blocking)
362    pub fn check_changes(&mut self) -> Option<Vec<FileChange>> {
363        let receiver = self.receiver.as_ref()?;
364        let now = Instant::now();
365
366        // Collect all pending events
367        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        // Check if we should trigger (debounce)
382        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            // Deduplicate by path
396            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    /// Get the configuration
413    #[must_use]
414    pub fn config(&self) -> &WatchConfig {
415        &self.config
416    }
417
418    /// Check if watcher is running
419    #[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/// Watch session state
436#[derive(Debug, Clone, Default)]
437pub struct WatchStats {
438    /// Number of times tests were triggered
439    pub trigger_count: u64,
440    /// Number of file changes detected
441    pub change_count: u64,
442    /// Total run time
443    pub total_runtime: Duration,
444    /// Time of last trigger
445    pub last_trigger: Option<Instant>,
446}
447
448impl WatchStats {
449    /// Create new stats
450    #[must_use]
451    pub fn new() -> Self {
452        Self::default()
453    }
454
455    /// Record a trigger
456    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/// Builder for creating watch mode configurations
464#[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    /// Create a new builder
486    #[must_use]
487    pub fn new() -> Self {
488        Self::default()
489    }
490
491    /// Watch Rust files
492    #[must_use]
493    pub fn rust_files(mut self) -> Self {
494        self.config.patterns.push("**/*.rs".to_string());
495        self
496    }
497
498    /// Watch TOML files
499    #[must_use]
500    pub fn toml_files(mut self) -> Self {
501        self.config.patterns.push("**/*.toml".to_string());
502        self
503    }
504
505    /// Watch test files only
506    #[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    /// Watch source directory
515    #[must_use]
516    pub fn src_dir(mut self) -> Self {
517        self.config.watch_dirs.push(PathBuf::from("src"));
518        self
519    }
520
521    /// Ignore target directory
522    #[must_use]
523    pub fn ignore_target(mut self) -> Self {
524        self.config.ignore_patterns.push("**/target/**".to_string());
525        self
526    }
527
528    /// Set debounce duration
529    #[must_use]
530    pub const fn debounce(mut self, ms: u64) -> Self {
531        self.config.debounce_ms = ms;
532        self
533    }
534
535    /// Build the configuration
536    #[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            // Start again
745            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            // No changes should be detected immediately
758            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        // FileChangeKind additional coverage
964        #[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            // Test the Renamed variant exists and can be used
973            let kind = FileChangeKind::Renamed;
974            assert_eq!(kind, FileChangeKind::Renamed);
975        }
976
977        #[test]
978        fn test_file_change_kind_hash() {
979            // Test Hash trait for FileChangeKind
980            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        // WatchConfig serialization tests
998        #[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        // Glob matching edge cases
1039        #[test]
1040        fn test_glob_matches_empty_pattern_parts() {
1041            // Test with path having multiple slashes creating empty parts
1042            assert!(WatchConfig::glob_matches("**/*.rs", "//src//main.rs"));
1043        }
1044
1045        #[test]
1046        fn test_glob_match_segment_star_at_end() {
1047            // Star at end of pattern matches any suffix
1048            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            // Star in middle of pattern
1056            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            // Multiple stars in pattern
1064            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            // Question mark at end
1071            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            // Question mark in middle
1079            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            // Double star at end matches any remaining path
1112            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            // Double star can match zero path segments
1120            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            // Double star followed by pattern that doesn't match anywhere
1128            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            // Test with empty patterns - should not match anything
1142            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            // File type that doesn't match any pattern
1156            let config = WatchConfig::default();
1157            assert!(!config.matches_pattern(Path::new("src/main.xyz")));
1158        }
1159
1160        // WatchStats additional coverage
1161        #[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        // WatchBuilder additional coverage
1188        #[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        // FileChange additional tests
1207        #[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        // FnWatchHandler additional tests
1227        #[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        // FileWatcher with non-existent directory
1273        #[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            // Should not error because we skip non-existent directories
1279            let result = watcher.start();
1280            assert!(result.is_ok());
1281            watcher.stop();
1282        }
1283
1284        // Test watch config with multiple patterns
1285        #[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        // More glob edge cases
1299        #[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            // Star pattern that can't match
1316            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 WatchConfig::new explicitly
1331        #[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 glob_match_parts when pattern doesn't match path segment
1339        #[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 glob_match_parts with single non-matching segment
1347        #[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 pattern non-empty but path is empty (coverage for line 149-151)
1355        #[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 non-** pattern with empty path
1363        #[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 double star matching multiple segments then failing
1371        #[test]
1372        fn test_glob_match_parts_double_star_exhaustive_search() {
1373            // ** tries all positions but none match
1374            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 segment with star that needs to try multiple positions
1380        #[test]
1381        fn test_glob_match_segment_star_backtrack() {
1382            // Pattern "a*b*c" against "aXXbYYc"
1383            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 question mark when segment is shorter than pattern needs
1389        #[test]
1390        fn test_glob_match_segment_question_exhausts_segment() {
1391            assert!(!WatchConfig::glob_match_segment("a??", "ab"));
1392        }
1393
1394        // Test literal char mismatch at specific position
1395        #[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 pattern ends but segment has more chars
1402        #[test]
1403        fn test_glob_match_segment_pattern_shorter() {
1404            assert!(!WatchConfig::glob_match_segment("ab", "abc"));
1405        }
1406
1407        // Test FileWatcher internal state - pending changes
1408        #[test]
1409        fn test_file_watcher_pending_changes_init() {
1410            let config = WatchConfig::new();
1411            let watcher = FileWatcher::new(config).unwrap();
1412            // Initial state has no pending changes
1413            assert!(!watcher.is_running());
1414            assert_eq!(watcher.config().debounce_ms, 300);
1415        }
1416
1417        // Test FileWatcher debug with running state
1418        #[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")); // is_running is true
1425            watcher.stop();
1426        }
1427
1428        // Test WatchStats with total_runtime modification
1429        #[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 ignore pattern matching more thoroughly
1437        #[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            // Should be ignored even though it matches *.rs
1448            assert!(!config.matches_pattern(Path::new("test/main.rs")));
1449            // Should match since not in ignored path
1450            assert!(config.matches_pattern(Path::new("src/main.rs")));
1451        }
1452
1453        // Test glob_matches with exact file name (no directory)
1454        #[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 the ** matching exactly at different depths
1461        #[test]
1462        fn test_double_star_various_depths() {
1463            // ** at start matches 0 segments
1464            let pattern = vec!["**", "src", "main.rs"];
1465            let path = vec!["src", "main.rs"];
1466            assert!(WatchConfig::glob_match_parts(&pattern, &path));
1467
1468            // ** at start matches 1 segment
1469            let path2 = vec!["project", "src", "main.rs"];
1470            assert!(WatchConfig::glob_match_parts(&pattern, &path2));
1471
1472            // ** at start matches many segments
1473            let path3 = vec!["a", "b", "c", "src", "main.rs"];
1474            assert!(WatchConfig::glob_match_parts(&pattern, &path3));
1475        }
1476
1477        // WatchConfig run_on_start field test
1478        #[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        // Additional glob edge cases for remaining coverage
1485        #[test]
1486        fn test_glob_match_segment_star_with_remaining_pattern() {
1487            // * in middle followed by more pattern
1488            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            // Star can match zero characters
1497            assert!(WatchConfig::glob_match_segment("a*b", "ab"));
1498        }
1499
1500        #[test]
1501        fn test_glob_match_segment_consecutive_stars() {
1502            // Multiple consecutive stars (edge case)
1503            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            // Star followed by literal that appears multiple times
1510            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            // Path with leading slash
1518            assert!(WatchConfig::glob_matches("**/*.rs", "/src/main.rs"));
1519        }
1520
1521        #[test]
1522        fn test_glob_match_parts_double_star_only() {
1523            // Just ** matches everything
1524            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 FileChange kind variants debug
1540        #[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 EventKind conversion comprehensively
1556        #[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 WatchBuilder with all methods chained
1611        #[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 matches_pattern with various path formats
1633        #[test]
1634        fn test_matches_pattern_various_paths() {
1635            let config = WatchConfig::default();
1636
1637            // Absolute-like paths (Unix style)
1638            assert!(config.matches_pattern(Path::new("/home/user/project/src/main.rs")));
1639
1640            // Windows-like paths (if running on Windows this would match differently)
1641            // For now just test that it handles them gracefully
1642            let _ = config.matches_pattern(Path::new("C:\\Users\\test\\main.rs"));
1643        }
1644
1645        // Test empty string pattern matching
1646        #[test]
1647        fn test_glob_matches_empty_strings() {
1648            // Empty pattern splits to [""], path splits to [] after filtering empty strings
1649            // glob_match_parts([""], []) -> first_pattern = "", path_parts is empty
1650            // Since "" != "**" and path_parts is empty, returns false
1651
1652            // Empty pattern against non-empty path should not match
1653            assert!(!WatchConfig::glob_matches("", "src"));
1654
1655            // Empty pattern against empty path
1656            // This won't match because pattern_parts [""] is not empty but path_parts [] is
1657            assert!(!WatchConfig::glob_matches("", ""));
1658        }
1659
1660        // Test FileChange timestamp field
1661        #[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 FileWatcher with empty watch_dirs
1676        #[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            // Should succeed with no directories to watch
1688            assert!(watcher.start().is_ok());
1689            assert!(watcher.is_running());
1690            watcher.stop();
1691        }
1692
1693        // Test WatchConfig fields with custom values
1694        #[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 WatchStats fields
1714        #[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            // Test record_trigger updates
1729            stats.record_trigger(5);
1730            assert_eq!(stats.trigger_count, 11);
1731            assert_eq!(stats.change_count, 30);
1732        }
1733
1734        // Test glob_match_parts with segment that doesn't match
1735        #[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 FileChange path field
1743        #[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 FnWatchHandler with empty changes
1754        #[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 WatchHandler trait default implementations explicitly
1774        #[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            // These use the default implementations
1786            assert!(handler.on_start().is_ok());
1787            assert!(handler.on_stop().is_ok());
1788            assert!(handler.on_change(&[]).is_ok());
1789        }
1790
1791        // Test matches_pattern when pattern matches but ignore also matches
1792        #[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()], // Same pattern in ignore
1797                debounce_ms: 300,
1798                clear_screen: true,
1799                run_on_start: true,
1800                watch_dirs: vec![],
1801            };
1802            // Ignore patterns are checked first, so this should be ignored
1803            assert!(!config.matches_pattern(Path::new("src/main.rs")));
1804        }
1805
1806        // Test glob_match_segment with only question marks
1807        #[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 glob_match_segment with only stars
1815        #[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 glob_match_parts matching exactly
1823        #[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 config accessor returns reference
1831        #[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 WatchBuilder default impl
1841        #[test]
1842        fn test_watch_builder_default_impl() {
1843            let builder1 = WatchBuilder::default();
1844            let builder2 = WatchBuilder::new();
1845            // Both should produce configs with same defaults
1846            assert_eq!(builder1.build().debounce_ms, builder2.build().debounce_ms);
1847        }
1848    }
1849}