use crate::error::Result;
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
pub(crate) struct FileWatcher {
_watcher: RecommendedWatcher,
receiver: mpsc::UnboundedReceiver<notify::Result<Event>>,
file_path: PathBuf,
}
impl FileWatcher {
pub(crate) fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let file_path = path.as_ref().to_path_buf();
let (tx, rx) = mpsc::unbounded_channel();
let watcher = RecommendedWatcher::new(
move |res| {
let _ = tx.send(res);
},
Config::default(),
)?;
Ok(Self {
_watcher: watcher,
receiver: rx,
file_path,
})
}
pub(crate) fn start_watching(&mut self) -> Result<()> {
let watch_path = self.file_path.parent().unwrap_or(&self.file_path);
self._watcher
.watch(watch_path, RecursiveMode::NonRecursive)?;
Ok(())
}
pub(crate) async fn next_event(&mut self) -> Option<notify::Result<Event>> {
self.receiver.recv().await
}
#[cfg(test)]
pub fn file_path(&self) -> &Path {
&self.file_path
}
}
pub(crate) fn is_event_relevant_to_file(event: &Event, target_file_name: &str) -> bool {
event.paths.iter().any(|path| {
path.file_name()
.map(|name| name.to_string_lossy() == target_file_name)
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
use notify::{Event, EventKind};
use std::path::PathBuf;
#[test]
fn test_file_watcher_creation() {
let file_path = PathBuf::from("/tmp/test.log");
let watcher = FileWatcher::new(&file_path);
assert!(watcher.is_ok());
let watcher = watcher.unwrap();
assert_eq!(watcher.file_path(), file_path.as_path());
}
#[test]
fn test_file_watcher_with_relative_path() {
let file_path = PathBuf::from("test.log");
let watcher = FileWatcher::new(&file_path);
assert!(watcher.is_ok());
let watcher = watcher.unwrap();
assert_eq!(watcher.file_path(), file_path.as_path());
}
#[test]
fn test_file_watcher_with_nested_path() {
let file_path = PathBuf::from("/var/log/app/test.log");
let watcher = FileWatcher::new(&file_path);
assert!(watcher.is_ok());
let watcher = watcher.unwrap();
assert_eq!(watcher.file_path(), file_path.as_path());
}
#[test]
fn test_is_event_relevant_to_file_exact_match() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![PathBuf::from("/tmp/test.log")],
attrs: Default::default(),
};
assert!(is_event_relevant_to_file(&event, "test.log"));
assert!(!is_event_relevant_to_file(&event, "other.log"));
}
#[test]
fn test_is_event_relevant_to_file_multiple_paths() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![
PathBuf::from("/tmp/other.log"),
PathBuf::from("/tmp/test.log"),
PathBuf::from("/tmp/another.log"),
],
attrs: Default::default(),
};
assert!(is_event_relevant_to_file(&event, "test.log"));
assert!(is_event_relevant_to_file(&event, "other.log"));
assert!(is_event_relevant_to_file(&event, "another.log"));
assert!(!is_event_relevant_to_file(&event, "missing.log"));
}
#[test]
fn test_is_event_relevant_to_file_different_directories() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![
PathBuf::from("/var/log/test.log"),
PathBuf::from("/tmp/test.log"),
],
attrs: Default::default(),
};
assert!(is_event_relevant_to_file(&event, "test.log"));
}
#[test]
fn test_is_event_relevant_to_file_no_file_name() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![PathBuf::from("/")], attrs: Default::default(),
};
assert!(!is_event_relevant_to_file(&event, "test.log"));
}
#[test]
fn test_is_event_relevant_to_file_empty_paths() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![],
attrs: Default::default(),
};
assert!(!is_event_relevant_to_file(&event, "test.log"));
}
#[test]
fn test_is_event_relevant_to_file_case_sensitivity() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![PathBuf::from("/tmp/Test.Log")],
attrs: Default::default(),
};
assert!(!is_event_relevant_to_file(&event, "test.log"));
assert!(is_event_relevant_to_file(&event, "Test.Log"));
}
#[test]
fn test_is_event_relevant_to_file_special_characters() {
let event = Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![PathBuf::from("/tmp/app-test_file.log")],
attrs: Default::default(),
};
assert!(is_event_relevant_to_file(&event, "app-test_file.log"));
assert!(!is_event_relevant_to_file(&event, "app-test-file.log"));
}
#[tokio::test]
async fn test_file_watcher_start_watching_existing_file() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let mut watcher = FileWatcher::new(&file_path).unwrap();
let result = watcher.start_watching();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_watcher_start_watching_nonexistent_file() {
let file_path = PathBuf::from("fixtures/nonexistent.log");
let mut watcher = FileWatcher::new(&file_path).unwrap();
let result = watcher.start_watching();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_watcher_start_watching_file_without_parent() {
let file_path = PathBuf::from("test.log");
let mut watcher = FileWatcher::new(&file_path).unwrap();
let result = watcher.start_watching();
match result {
Ok(_) => {} Err(crate::error::Error::Watcher(notify_error)) => {
match notify_error.kind {
notify::ErrorKind::Generic(_)
| notify::ErrorKind::Io(_)
| notify::ErrorKind::PathNotFound => {}
_ => panic!("Unexpected notify error: {:?}", notify_error),
}
}
Err(e) => panic!("Unexpected error type: {:?}", e),
}
}
}