use file_id::FileId; use mock_instant::Instant; use notify::event::{ AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind, RenameMode, }; use notify::{ErrorKind, EventKind}; use std::collections::{HashMap, VecDeque}; use std::path::PathBuf; use std::time::Duration; use crate::{schema, TestCache}; use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub(crate) struct Error { /// The error kind is parsed by `into_notify_error` pub kind: String, /// The error paths #[serde(default)] pub paths: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub(crate) struct Event { /// The timestamp the event occurred #[serde(default)] pub time: u64, /// The event kind is parsed by `into_notify_event` pub kind: String, /// The event paths #[serde(default)] pub paths: Vec, /// The event flags #[serde(default)] pub flags: Vec, /// The event tracker pub tracker: Option, /// The event info pub info: Option, /// The file id for the file associated with the event /// /// Only used for the rename event. pub file_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub(crate) struct Queue { pub events: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub(crate) struct State { /// Timeout for the debouncer /// /// Only used for the initial state. pub timeout: Option, /// The event queues for each file #[serde(default)] pub queues: HashMap, /// Cached file ids #[serde(default)] pub cache: HashMap, /// A map of file ids, used instead of accessing the file system #[serde(default)] pub file_system: HashMap, /// Current rename event pub rename_event: Option, /// Current rescan event pub rescan_event: Option, /// Debounced events /// /// Only used for the expected state. #[serde(default)] pub events: HashMap>, /// Errors #[serde(default)] pub errors: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub(crate) struct TestCase { /// Initial state pub state: State, /// Events that are added during the test #[serde(default)] pub events: Vec, /// Errors that are added during the test #[serde(default)] pub errors: Vec, /// Expected state after the test pub expected: State, } impl Error { pub fn into_notify_error(self) -> notify::Error { let kind = match &*self.kind { "path-not-found" => ErrorKind::PathNotFound, "watch-not-found" => ErrorKind::WatchNotFound, "max-files-watch" => ErrorKind::MaxFilesWatch, _ => panic!("unknown error type `{}`", self.kind), }; let mut error = notify::Error::new(kind); for p in self.paths { error = error.add_path(PathBuf::from(p)); } error } } impl Event { #[rustfmt::skip] pub fn into_debounced_event(self, time: Instant, path: Option<&str>) -> DebouncedEvent { let kind = match &*self.kind { "any" => EventKind::Any, "other" => EventKind::Other, "access-any" => EventKind::Access(AccessKind::Any), "access-read" => EventKind::Access(AccessKind::Read), "access-open-any" => EventKind::Access(AccessKind::Open(AccessMode::Any)), "access-open-execute" => EventKind::Access(AccessKind::Open(AccessMode::Execute)), "access-open-read" => EventKind::Access(AccessKind::Open(AccessMode::Read)), "access-open-write" => EventKind::Access(AccessKind::Open(AccessMode::Write)), "access-open-other" => EventKind::Access(AccessKind::Open(AccessMode::Other)), "access-close-any" => EventKind::Access(AccessKind::Close(AccessMode::Any)), "access-close-execute" => EventKind::Access(AccessKind::Close(AccessMode::Execute)), "access-close-read" => EventKind::Access(AccessKind::Close(AccessMode::Read)), "access-close-write" => EventKind::Access(AccessKind::Close(AccessMode::Write)), "access-close-other" => EventKind::Access(AccessKind::Close(AccessMode::Other)), "access-other" => EventKind::Access(AccessKind::Other), "create-any" => EventKind::Create(CreateKind::Any), "create-file" => EventKind::Create(CreateKind::File), "create-folder" => EventKind::Create(CreateKind::Folder), "create-other" => EventKind::Create(CreateKind::Other), "modify-any" => EventKind::Modify(ModifyKind::Any), "modify-other" => EventKind::Modify(ModifyKind::Other), "modify-data-any" => EventKind::Modify(ModifyKind::Data(DataChange::Any)), "modify-data-size" => EventKind::Modify(ModifyKind::Data(DataChange::Size)), "modify-data-content" => EventKind::Modify(ModifyKind::Data(DataChange::Content)), "modify-data-other" => EventKind::Modify(ModifyKind::Data(DataChange::Other)), "modify-metadata-any" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)), "modify-metadata-access-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)), "modify-metadata-write-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)), "modify-metadata-permissions" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)), "modify-metadata-ownership" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)), "modify-metadata-extended" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)), "modify-metadata-other" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other)), "rename-any" => EventKind::Modify(ModifyKind::Name(RenameMode::Any)), "rename-from" => EventKind::Modify(ModifyKind::Name(RenameMode::From)), "rename-to" => EventKind::Modify(ModifyKind::Name(RenameMode::To)), "rename-both" => EventKind::Modify(ModifyKind::Name(RenameMode::Both)), "rename-other" => EventKind::Modify(ModifyKind::Name(RenameMode::Other)), "remove-any" => EventKind::Remove(RemoveKind::Any), "remove-file" => EventKind::Remove(RemoveKind::File), "remove-folder" => EventKind::Remove(RemoveKind::Folder), "remove-other" => EventKind::Remove(RemoveKind::Other), _ => panic!("unknown event type `{}`", self.kind), }; let mut event = notify::Event::new(kind); for p in self.paths { event = event.add_path(if p == "*" { PathBuf::from(path.expect("cannot replace `*`")) } else { PathBuf::from(p) }); if let Some(tracker) = self.tracker { event = event.set_tracker(tracker); } if let Some(info) = &self.info { event = event.set_info(info.as_str()); } } for f in self.flags { let flag = match &*f { "rescan" => Flag::Rescan, _ => panic!("unknown event flag `{f}`"), }; event = event.set_flag(flag); } DebouncedEvent { event, time: time + Duration::from_millis(self.time) } } } impl State { pub(crate) fn into_debounce_data_inner(self, time: Instant) -> DebounceDataInner { let queues = self .queues .into_iter() .map(|(path, queue)| { let queue = Queue { events: queue .events .into_iter() .map(|event| event.into_debounced_event(time, Some(&path))) .collect::>(), }; (path.into(), queue) }) .collect(); let cache = self .cache .into_iter() .map(|(path, id)| { let path = PathBuf::from(path); let id = FileId::new_inode(id, id); (path, id) }) .collect::>(); let file_system = self .file_system .into_iter() .map(|(path, id)| { let path = PathBuf::from(path); let id = FileId::new_inode(id, id); (path, id) }) .collect::>(); let cache = TestCache::new(cache, file_system); let rename_event = self.rename_event.map(|e| { let file_id = e.file_id.map(|id| FileId::new_inode(id, id)); let event = e.into_debounced_event(time, None); (event, file_id) }); let rescan_event = self .rescan_event .map(|e| e.into_debounced_event(time, None)); DebounceDataInner { queues, roots: Vec::new(), cache, rename_event, rescan_event, errors: Vec::new(), timeout: Duration::from_millis(self.timeout.unwrap_or(50)), } } }