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;
6use tracing::warn;
7
8pub struct FileWatcher {
9 watcher: RecommendedWatcher,
10 receiver: Receiver<Result<notify::Event, notify::Error>>,
11 debounce_duration: Duration,
12}
13
14impl FileWatcher {
15 pub fn new() -> Result<Self, notify::Error> {
17 Self::with_config(&WatchConfig::default())
18 }
19
20 pub fn with_config(config: &WatchConfig) -> Result<Self, notify::Error> {
22 let (tx, rx) = channel();
23
24 let watcher = RecommendedWatcher::new(
25 move |res| {
26 let _ = tx.send(res);
27 },
28 Config::default().with_poll_interval(Duration::from_millis(config.poll_interval_ms)),
29 )?;
30
31 Ok(Self {
32 watcher,
33 receiver: rx,
34 debounce_duration: Duration::from_millis(config.debounce_ms),
35 })
36 }
37
38 pub fn watch(&mut self, path: &Path) -> Result<(), notify::Error> {
39 self.watcher.watch(path, RecursiveMode::Recursive)
40 }
41
42 pub fn wait_for_change(&self) -> bool {
43 let mut has_change = false;
45
46 loop {
47 match self.receiver.recv_timeout(if has_change {
48 self.debounce_duration
49 } else {
50 Duration::from_secs(60 * 60) }) {
52 Ok(Ok(event)) => {
53 if matches!(
55 event.kind,
56 EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
57 ) {
58 has_change = true;
59 }
60 }
61 Ok(Err(e)) => {
62 warn!(error = %e, "File watch error");
63 }
64 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
65 if has_change {
67 return true;
68 }
69 }
71 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
72 return false;
73 }
74 }
75 }
76 }
77}
78
79impl Default for FileWatcher {
83 fn default() -> Self {
89 Self::new().expect("Failed to create file watcher")
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::fs;
97 use tempfile::TempDir;
98
99 #[test]
100 fn test_file_watcher_creation() {
101 let watcher = FileWatcher::new();
102 assert!(watcher.is_ok());
103 }
104
105 #[test]
106 fn test_file_watcher_with_config() {
107 let config = WatchConfig {
108 debounce_ms: 500,
109 poll_interval_ms: 1000,
110 };
111 let watcher = FileWatcher::with_config(&config);
112 assert!(watcher.is_ok());
113 let watcher = watcher.unwrap();
114 assert_eq!(watcher.debounce_duration, Duration::from_millis(500));
115 }
116
117 #[test]
118 fn test_file_watcher_with_custom_debounce() {
119 let config = WatchConfig {
120 debounce_ms: 100,
121 poll_interval_ms: 200,
122 };
123 let watcher = FileWatcher::with_config(&config).unwrap();
124 assert_eq!(watcher.debounce_duration, Duration::from_millis(100));
125 }
126
127 #[test]
128 fn test_watch_directory() {
129 let temp_dir = TempDir::new().unwrap();
130 let mut watcher = FileWatcher::new().unwrap();
131 let result = watcher.watch(temp_dir.path());
132 assert!(result.is_ok());
133 }
134
135 #[test]
136 fn test_watch_nonexistent_directory() {
137 let mut watcher = FileWatcher::new().unwrap();
138 let result = watcher.watch(Path::new("/nonexistent/path/12345"));
139 assert!(result.is_err());
140 }
141
142 #[test]
143 fn test_default_trait() {
144 let _watcher = FileWatcher::default();
146 }
147
148 #[test]
149 fn test_watch_file_change() {
150 use std::thread;
151 use std::time::Duration;
152
153 let temp_dir = TempDir::new().unwrap();
154 let test_file = temp_dir.path().join("test.md");
155 fs::write(&test_file, "initial content").unwrap();
156
157 let mut watcher = FileWatcher::new().unwrap();
158 watcher.watch(temp_dir.path()).unwrap();
159
160 let test_file_clone = test_file.clone();
162 let handle = thread::spawn(move || {
163 thread::sleep(Duration::from_millis(100));
164 fs::write(&test_file_clone, "modified content").unwrap();
165 });
166
167 let (tx, _rx) = channel();
170 let watcher_receiver = watcher.receiver;
171 thread::spawn(move || {
172 let result = watcher_receiver.recv_timeout(Duration::from_secs(2));
173 let _ = tx.send(result.is_ok());
174 });
175
176 handle.join().unwrap();
177
178 thread::sleep(Duration::from_millis(500));
180 }
181
182 #[test]
183 fn test_wait_for_change_with_create_event() {
184 use std::thread;
185 use std::time::Duration;
186
187 let temp_dir = TempDir::new().unwrap();
188 let mut watcher = FileWatcher::new().unwrap();
189 watcher.watch(temp_dir.path()).unwrap();
190
191 let test_file = temp_dir.path().join("new_file.txt");
193 thread::spawn(move || {
194 thread::sleep(Duration::from_millis(100));
195 fs::write(&test_file, "new content").unwrap();
196 });
197
198 let result = watcher.wait_for_change();
200 assert!(result);
201 }
202
203 #[test]
204 fn test_wait_for_change_with_remove_event() {
205 use std::thread;
206 use std::time::Duration;
207
208 let temp_dir = TempDir::new().unwrap();
209 let test_file = temp_dir.path().join("to_remove.txt");
210 fs::write(&test_file, "content").unwrap();
211
212 let mut watcher = FileWatcher::new().unwrap();
213 watcher.watch(temp_dir.path()).unwrap();
214
215 let test_file_clone = test_file.clone();
217 thread::spawn(move || {
218 thread::sleep(Duration::from_millis(100));
219 fs::remove_file(&test_file_clone).unwrap();
220 });
221
222 let result = watcher.wait_for_change();
224 assert!(result);
225 }
226
227 #[test]
228 fn test_wait_for_change_disconnected() {
229 use std::thread;
230 use std::time::Duration;
231
232 let (tx, rx) = channel::<Result<notify::Event, notify::Error>>();
234
235 let watcher_handle = thread::spawn(move || {
238 drop(tx);
241 });
242
243 thread::sleep(Duration::from_millis(50));
245
246 let result = rx.recv_timeout(Duration::from_millis(100));
248 assert!(result.is_err());
249
250 watcher_handle.join().unwrap();
251 }
252
253 #[test]
254 fn test_debounce_duration() {
255 let watcher = FileWatcher::new().unwrap();
256 assert_eq!(watcher.debounce_duration, Duration::from_millis(300));
257 }
258
259 #[test]
260 fn test_wait_for_change_with_modify_event() {
261 use std::thread;
262 use std::time::Duration;
263
264 let temp_dir = TempDir::new().unwrap();
265 let test_file = temp_dir.path().join("existing.txt");
266 fs::write(&test_file, "initial").unwrap();
267
268 let mut watcher = FileWatcher::new().unwrap();
269 watcher.watch(temp_dir.path()).unwrap();
270
271 let test_file_clone = test_file.clone();
273 thread::spawn(move || {
274 thread::sleep(Duration::from_millis(100));
275 fs::write(&test_file_clone, "modified").unwrap();
276 });
277
278 let result = watcher.wait_for_change();
280 assert!(result);
281 }
282
283 #[test]
284 fn test_receiver_fields() {
285 let watcher = FileWatcher::new().unwrap();
287 assert_eq!(watcher.debounce_duration, Duration::from_millis(300));
288 }
290}