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