Skip to main content

cc_audit/
watch.rs

1use crate::config::WatchConfig;
2use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
3use std::path::Path;
4use std::sync::mpsc::{Receiver, channel};
5use std::time::Duration;
6
7pub struct FileWatcher {
8    watcher: RecommendedWatcher,
9    receiver: Receiver<Result<notify::Event, notify::Error>>,
10    debounce_duration: Duration,
11}
12
13impl FileWatcher {
14    /// Creates a new FileWatcher with default configuration.
15    pub fn new() -> Result<Self, notify::Error> {
16        Self::with_config(&WatchConfig::default())
17    }
18
19    /// Creates a new FileWatcher with custom configuration.
20    pub fn with_config(config: &WatchConfig) -> Result<Self, notify::Error> {
21        let (tx, rx) = channel();
22
23        let watcher = RecommendedWatcher::new(
24            move |res| {
25                let _ = tx.send(res);
26            },
27            Config::default().with_poll_interval(Duration::from_millis(config.poll_interval_ms)),
28        )?;
29
30        Ok(Self {
31            watcher,
32            receiver: rx,
33            debounce_duration: Duration::from_millis(config.debounce_ms),
34        })
35    }
36
37    pub fn watch(&mut self, path: &Path) -> Result<(), notify::Error> {
38        self.watcher.watch(path, RecursiveMode::Recursive)
39    }
40
41    pub fn wait_for_change(&self) -> bool {
42        // Simple debounce: collect events for debounce_duration
43        let mut has_change = false;
44
45        loop {
46            match self.receiver.recv_timeout(if has_change {
47                self.debounce_duration
48            } else {
49                Duration::from_secs(60 * 60) // 1 hour timeout when waiting for first event
50            }) {
51                Ok(Ok(event)) => {
52                    // Only react to meaningful changes
53                    if matches!(
54                        event.kind,
55                        EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
56                    ) {
57                        has_change = true;
58                    }
59                }
60                Ok(Err(e)) => {
61                    eprintln!("Watch error: {}", e);
62                }
63                Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
64                    // Debounce period complete or timeout
65                    if has_change {
66                        return true;
67                    }
68                    // Continue waiting if no change yet
69                }
70                Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
71                    return false;
72                }
73            }
74        }
75    }
76}
77
78impl Default for FileWatcher {
79    fn default() -> Self {
80        Self::new().expect("Failed to create file watcher")
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use std::fs;
88    use tempfile::TempDir;
89
90    #[test]
91    fn test_file_watcher_creation() {
92        let watcher = FileWatcher::new();
93        assert!(watcher.is_ok());
94    }
95
96    #[test]
97    fn test_file_watcher_with_config() {
98        let config = WatchConfig {
99            debounce_ms: 500,
100            poll_interval_ms: 1000,
101        };
102        let watcher = FileWatcher::with_config(&config);
103        assert!(watcher.is_ok());
104        let watcher = watcher.unwrap();
105        assert_eq!(watcher.debounce_duration, Duration::from_millis(500));
106    }
107
108    #[test]
109    fn test_file_watcher_with_custom_debounce() {
110        let config = WatchConfig {
111            debounce_ms: 100,
112            poll_interval_ms: 200,
113        };
114        let watcher = FileWatcher::with_config(&config).unwrap();
115        assert_eq!(watcher.debounce_duration, Duration::from_millis(100));
116    }
117
118    #[test]
119    fn test_watch_directory() {
120        let temp_dir = TempDir::new().unwrap();
121        let mut watcher = FileWatcher::new().unwrap();
122        let result = watcher.watch(temp_dir.path());
123        assert!(result.is_ok());
124    }
125
126    #[test]
127    fn test_watch_nonexistent_directory() {
128        let mut watcher = FileWatcher::new().unwrap();
129        let result = watcher.watch(Path::new("/nonexistent/path/12345"));
130        assert!(result.is_err());
131    }
132
133    #[test]
134    fn test_default_trait() {
135        // This will panic if it fails, which is expected behavior
136        let _watcher = FileWatcher::default();
137    }
138
139    #[test]
140    fn test_watch_file_change() {
141        use std::thread;
142        use std::time::Duration;
143
144        let temp_dir = TempDir::new().unwrap();
145        let test_file = temp_dir.path().join("test.md");
146        fs::write(&test_file, "initial content").unwrap();
147
148        let mut watcher = FileWatcher::new().unwrap();
149        watcher.watch(temp_dir.path()).unwrap();
150
151        // Spawn a thread to modify the file after a short delay
152        let test_file_clone = test_file.clone();
153        let handle = thread::spawn(move || {
154            thread::sleep(Duration::from_millis(100));
155            fs::write(&test_file_clone, "modified content").unwrap();
156        });
157
158        // Wait for change (with timeout via recv_timeout)
159        // We use a separate thread to avoid blocking forever
160        let (tx, _rx) = channel();
161        let watcher_receiver = watcher.receiver;
162        thread::spawn(move || {
163            let result = watcher_receiver.recv_timeout(Duration::from_secs(2));
164            let _ = tx.send(result.is_ok());
165        });
166
167        handle.join().unwrap();
168
169        // Give the watcher some time to process
170        thread::sleep(Duration::from_millis(500));
171    }
172
173    #[test]
174    fn test_wait_for_change_with_create_event() {
175        use std::thread;
176        use std::time::Duration;
177
178        let temp_dir = TempDir::new().unwrap();
179        let mut watcher = FileWatcher::new().unwrap();
180        watcher.watch(temp_dir.path()).unwrap();
181
182        // Spawn a thread to create a new file
183        let test_file = temp_dir.path().join("new_file.txt");
184        thread::spawn(move || {
185            thread::sleep(Duration::from_millis(100));
186            fs::write(&test_file, "new content").unwrap();
187        });
188
189        // wait_for_change should return true on file creation
190        let result = watcher.wait_for_change();
191        assert!(result);
192    }
193
194    #[test]
195    fn test_wait_for_change_with_remove_event() {
196        use std::thread;
197        use std::time::Duration;
198
199        let temp_dir = TempDir::new().unwrap();
200        let test_file = temp_dir.path().join("to_remove.txt");
201        fs::write(&test_file, "content").unwrap();
202
203        let mut watcher = FileWatcher::new().unwrap();
204        watcher.watch(temp_dir.path()).unwrap();
205
206        // Spawn a thread to remove the file
207        let test_file_clone = test_file.clone();
208        thread::spawn(move || {
209            thread::sleep(Duration::from_millis(100));
210            fs::remove_file(&test_file_clone).unwrap();
211        });
212
213        // wait_for_change should return true on file removal
214        let result = watcher.wait_for_change();
215        assert!(result);
216    }
217
218    #[test]
219    fn test_wait_for_change_disconnected() {
220        use std::thread;
221        use std::time::Duration;
222
223        // Create a watcher but manually drop the sender to simulate disconnection
224        let (tx, rx) = channel::<Result<notify::Event, notify::Error>>();
225
226        // Create a minimal watcher struct with the receiver
227        // We need to simulate the disconnection scenario
228        let watcher_handle = thread::spawn(move || {
229            // This tests the Disconnected branch
230            // Drop the sender to disconnect
231            drop(tx);
232        });
233
234        // Small delay to ensure sender is dropped
235        thread::sleep(Duration::from_millis(50));
236
237        // Now try to receive - should get disconnected error
238        let result = rx.recv_timeout(Duration::from_millis(100));
239        assert!(result.is_err());
240
241        watcher_handle.join().unwrap();
242    }
243
244    #[test]
245    fn test_debounce_duration() {
246        let watcher = FileWatcher::new().unwrap();
247        assert_eq!(watcher.debounce_duration, Duration::from_millis(300));
248    }
249
250    #[test]
251    fn test_wait_for_change_with_modify_event() {
252        use std::thread;
253        use std::time::Duration;
254
255        let temp_dir = TempDir::new().unwrap();
256        let test_file = temp_dir.path().join("existing.txt");
257        fs::write(&test_file, "initial").unwrap();
258
259        let mut watcher = FileWatcher::new().unwrap();
260        watcher.watch(temp_dir.path()).unwrap();
261
262        // Spawn a thread to modify the file
263        let test_file_clone = test_file.clone();
264        thread::spawn(move || {
265            thread::sleep(Duration::from_millis(100));
266            fs::write(&test_file_clone, "modified").unwrap();
267        });
268
269        // wait_for_change should return true on file modification
270        let result = watcher.wait_for_change();
271        assert!(result);
272    }
273
274    #[test]
275    fn test_receiver_fields() {
276        // Test that FileWatcher fields are properly initialized
277        let watcher = FileWatcher::new().unwrap();
278        assert_eq!(watcher.debounce_duration, Duration::from_millis(300));
279        // receiver is private but we can test via wait behavior
280    }
281}