#![cfg(feature = "watch-mode")]
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher};
use std::path::PathBuf;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::{Duration, Instant};
pub struct FileWatcher {
_watcher: RecommendedWatcher,
rx: Receiver<notify::Result<Event>>,
last_change: Option<Instant>,
debounce_duration: Duration,
}
impl FileWatcher {
pub fn new(paths: Vec<PathBuf>, debounce_ms: u64) -> notify::Result<Self> {
let (tx, rx): (
Sender<notify::Result<Event>>,
Receiver<notify::Result<Event>>,
) = channel();
let mut watcher = notify::recommended_watcher(move |res| {
let _ = tx.send(res);
})?;
for path in paths {
watcher.watch(&path, RecursiveMode::Recursive)?;
}
Ok(Self {
_watcher: watcher,
rx,
last_change: None,
debounce_duration: Duration::from_millis(debounce_ms),
})
}
pub fn check_changes(&mut self) -> Option<Vec<PathBuf>> {
let mut changed_files = Vec::new();
while let Ok(event) = self.rx.try_recv() {
if let Ok(event) = event {
if matches!(
event.kind,
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
) {
changed_files.extend(event.paths);
}
}
}
if changed_files.is_empty() {
return None;
}
let now = Instant::now();
if let Some(last) = self.last_change {
if now.duration_since(last) < self.debounce_duration {
return None; }
}
self.last_change = Some(now);
Some(changed_files)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::thread;
use tempfile::tempdir;
#[test]
fn test_watcher_detects_file_creation() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let mut watcher = FileWatcher::new(vec![watch_path.clone()], 100)
.expect("operation should succeed in test");
let test_file = watch_path.join("test.txt");
fs::write(&test_file, "hello").expect("operation should succeed in test");
thread::sleep(Duration::from_millis(200));
let changes = watcher.check_changes();
assert!(changes.is_some(), "Should detect file creation");
}
#[test]
fn test_watcher_debounces_rapid_changes() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let mut watcher = FileWatcher::new(vec![watch_path.clone()], 3000)
.expect("operation should succeed in test");
let test_file = watch_path.join("test.txt");
fs::write(&test_file, "1").expect("operation should succeed in test");
thread::sleep(Duration::from_millis(50));
fs::write(&test_file, "2").expect("operation should succeed in test");
thread::sleep(Duration::from_millis(50));
fs::write(&test_file, "3").expect("operation should succeed in test");
let mut first = None;
for _ in 0..20 {
thread::sleep(Duration::from_millis(100));
first = watcher.check_changes();
if first.is_some() {
break;
}
}
assert!(first.is_some(), "First check should detect changes");
fs::write(&test_file, "4").expect("operation should succeed in test");
thread::sleep(Duration::from_millis(200)); let second = watcher.check_changes();
assert!(second.is_none(), "Should debounce rapid changes");
}
#[test]
fn test_watcher_new_creates_instance() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let result = FileWatcher::new(vec![watch_path], 500);
assert!(result.is_ok(), "Should create watcher successfully");
}
#[test]
fn test_watcher_check_changes_no_events() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let mut watcher =
FileWatcher::new(vec![watch_path], 100).expect("operation should succeed in test");
let changes = watcher.check_changes();
assert!(changes.is_none(), "Should return None when no changes");
}
#[test]
fn test_watcher_detects_file_modification() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let test_file = watch_path.join("existing.txt");
fs::write(&test_file, "initial").expect("operation should succeed in test");
let mut watcher =
FileWatcher::new(vec![watch_path], 100).expect("operation should succeed in test");
thread::sleep(Duration::from_millis(100));
fs::write(&test_file, "modified").expect("operation should succeed in test");
thread::sleep(Duration::from_millis(200));
let changes = watcher.check_changes();
assert!(changes.is_some(), "Should detect file modification");
}
#[test]
fn test_watcher_detects_file_deletion() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let test_file = watch_path.join("to_delete.txt");
fs::write(&test_file, "content").expect("operation should succeed in test");
let mut watcher =
FileWatcher::new(vec![watch_path], 100).expect("operation should succeed in test");
thread::sleep(Duration::from_millis(100));
fs::remove_file(&test_file).expect("operation should succeed in test");
thread::sleep(Duration::from_millis(200));
let changes = watcher.check_changes();
assert!(changes.is_some(), "Should detect file deletion");
}
#[test]
fn test_watcher_with_zero_debounce() {
let temp_dir = tempdir().expect("operation should succeed in test");
let watch_path = temp_dir.path().to_path_buf();
let result = FileWatcher::new(vec![watch_path], 0);
assert!(result.is_ok(), "Should create watcher with zero debounce");
}
}