use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant, SystemTime}; pub struct FileWatcher { watched: HashMap>, poll_interval: Duration, last_poll: Instant, } impl FileWatcher { pub fn new(poll_interval: Duration) -> Self { Self { watched: HashMap::new(), poll_interval, last_poll: Instant::now() - poll_interval, // allow immediate first poll } } pub fn watch(&mut self, path: PathBuf) { // Store None initially — first poll will record the mtime without reporting change self.watched.insert(path, None); } pub fn unwatch(&mut self, path: &Path) { self.watched.remove(path); } pub fn poll_changes(&mut self) -> Vec { let now = Instant::now(); if now.duration_since(self.last_poll) < self.poll_interval { return Vec::new(); } self.last_poll = now; let mut changed = Vec::new(); for (path, last_mtime) in &mut self.watched { let current = std::fs::metadata(path) .ok() .and_then(|m| m.modified().ok()); if let Some(prev) = last_mtime { // We have a previous mtime — compare if current != Some(*prev) { changed.push(path.clone()); } } // else: first poll, just record mtime, don't report *last_mtime = current; } changed } pub fn watched_count(&self) -> usize { self.watched.len() } } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn test_watch_and_poll_no_changes() { let mut watcher = FileWatcher::new(Duration::from_millis(0)); let dir = std::env::temp_dir().join("voltex_watcher_test_1"); let _ = fs::create_dir_all(&dir); let path = dir.join("test.txt"); fs::write(&path, "hello").unwrap(); watcher.watch(path.clone()); // First poll — should not report as changed (just registered) let changes = watcher.poll_changes(); assert!(changes.is_empty()); // Second poll without modification — still no changes let changes = watcher.poll_changes(); assert!(changes.is_empty()); let _ = fs::remove_dir_all(&dir); } #[test] fn test_detect_file_change() { let dir = std::env::temp_dir().join("voltex_watcher_test_2"); let _ = fs::create_dir_all(&dir); let path = dir.join("test2.txt"); fs::write(&path, "v1").unwrap(); let mut watcher = FileWatcher::new(Duration::from_millis(0)); watcher.watch(path.clone()); let _ = watcher.poll_changes(); // register initial mtime // Modify file std::thread::sleep(Duration::from_millis(50)); fs::write(&path, "v2 with more data").unwrap(); let changes = watcher.poll_changes(); assert!(changes.contains(&path)); let _ = fs::remove_dir_all(&dir); } #[test] fn test_unwatch() { let mut watcher = FileWatcher::new(Duration::from_millis(0)); let path = PathBuf::from("/nonexistent/test.txt"); watcher.watch(path.clone()); assert_eq!(watcher.watched_count(), 1); watcher.unwatch(&path); assert_eq!(watcher.watched_count(), 0); assert!(watcher.poll_changes().is_empty()); } }