diff --git a/.zed/tasks.json b/.zed/tasks.json index 0f630b0..8a59df2 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -16,11 +16,21 @@ "command": "cargo nextest run -p $ZED_CUSTOM_RUST_PACKAGE $ZED_STEM::$ZED_SYMBOL", "tags": ["rust-mod-test"], }, + { + "label": "cargo watch nextest $ZED_STEM (package: $ZED_CUSTOM_RUST_PACKAGE)", + "command": "cargo watch -q -c -w $ZED_FILE -x 'nextest run -p $ZED_CUSTOM_RUST_PACKAGE $ZED_STEM::$ZED_SYMBOL'", + "tags": ["rust-mod-test"], + }, { "label": "cargo nextest tests (crate root)", "command": "cargo nextest run -E 'test(/^tests/)'", "tags": ["rust-mod-test"], }, + { + "label": "cargo watch nextest tests (crate root)", + "command": "cargo watch -q -c -w $ZED_FILE", + "tags": ["rust-mod-test"], + }, { "label": "cargo nextest run --workspace", "command": "cargo nextest run --workspace", diff --git a/lsp/src/backend.rs b/lsp/src/backend.rs index 5e29c07..ea970c1 100644 --- a/lsp/src/backend.rs +++ b/lsp/src/backend.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use anyhow::Result; @@ -59,7 +59,7 @@ impl Backend { Ok(()) } - fn unwatch_path(&self, path: &PathBuf) -> Result<()> { + fn unwatch_path(&self, path: &Path) -> Result<()> { let info_msg = format!("Unwatching path: {}", path.display()); #[allow(clippy::expect_used)] diff --git a/lsp/src/watching/path_store.rs b/lsp/src/watching/path_store.rs index a175c0b..07d56d9 100644 --- a/lsp/src/watching/path_store.rs +++ b/lsp/src/watching/path_store.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::{path::PathBuf, pin::Pin, sync::Arc}; use anyhow::Result; @@ -69,7 +70,7 @@ impl PathStore { self.watched_set.watch(file_path) } - pub fn unwatch(&mut self, file_path: &PathBuf) -> anyhow::Result<()> { + pub fn unwatch(&mut self, file_path: &Path) -> anyhow::Result<()> { self.watched_set.unwatch(file_path) } } diff --git a/lsp/src/watching/path_watcher.rs b/lsp/src/watching/path_watcher.rs index 33e659c..ec3501e 100644 --- a/lsp/src/watching/path_watcher.rs +++ b/lsp/src/watching/path_watcher.rs @@ -20,6 +20,7 @@ pub struct PathWatcher { event_handler: Option>, // DebugIgnore because Fn traits can't implement Debug } +#[cfg_attr(test, mockall::automock)] impl PathWatcher { pub fn new(event_handler: EventHandler) -> Result { let (tx, rx) = channel(1); @@ -68,7 +69,7 @@ impl PathWatcher { }); } - pub fn watch>(&self, path: P) -> Result<()> { + pub fn watch(&self, path: &Path) -> Result<()> { self.watcher .lock() .map_err(|_| anyhow!("Path watcher mutex is poisoned"))? @@ -77,7 +78,7 @@ impl PathWatcher { Ok(()) } - pub fn unwatch>(&self, path: P) -> Result<()> { + pub fn unwatch(&self, path: &Path) -> Result<()> { self.watcher .lock() .map_err(|_| anyhow!("Path watcher mutex is poisoned"))? @@ -86,3 +87,36 @@ impl PathWatcher { Ok(()) } } + +mod tests { + + /* + Tests TODO + - events handling + - test create file does not trigger event handler + - create a store with the MockGithubClient passed + - start watcher + - add a new path to watch (assert_fs::TempDir), maybe with an already existing file + - create a new file in that dir + - ensure event was not triggered (MockGithubClient) + - test delete file does not trigger event handler + - create a store with the MockGithubClient passed + - start watcher + - add a new path to watch (assert_fs::TempDir), with an already existing file + - delete the file + - ensure event was not triggered (MockGithubClient) + - test modify file data triggers event handler + - create a store with the MockGithubClient passed + - start watcher + - add a new path to watch (assert_fs::TempDir), with an already existing file + - modify the file data + - ensure event was triggered (MockGithubClient) + - test modify file data outside of watched paths does not trigger event handler + - create a store with the MockGithubClient passed + - start watcher + - add a new path to watch (assert_fs::TempDir) + - create another assert_fs::TempDir with an existing file + - modify that file data + - ensure event was not triggered (MockGithubClient) + */ +} diff --git a/lsp/src/watching/watched_set.rs b/lsp/src/watching/watched_set.rs index 5d607ad..d463c24 100644 --- a/lsp/src/watching/watched_set.rs +++ b/lsp/src/watching/watched_set.rs @@ -1,8 +1,15 @@ -use std::{collections::HashSet, path::PathBuf, sync::Mutex}; +use std::{ + collections::HashSet, + path::{Path, PathBuf}, + sync::Mutex, +}; use anyhow::{Result, bail}; +use mockall_double::double; -use crate::watching::{EventHandler, PathWatcher}; +use crate::watching::EventHandler; +#[double] +use crate::watching::PathWatcher; #[derive(Debug)] pub struct WatchedSet { @@ -44,7 +51,7 @@ impl WatchedSet { Ok(()) } - pub fn unwatch(&mut self, path: &PathBuf) -> Result<()> { + pub fn unwatch(&mut self, path: &Path) -> Result<()> { #[allow(clippy::expect_used)] let _lock = self.mx.lock().expect("Watched set mutex is poisoned"); @@ -64,34 +71,163 @@ impl WatchedSet { #[cfg(test)] mod tests { - /* - Tests TODO - - - events handling - - test create file does not trigger event handler - - create a store with the MockGithubClient passed - - start watcher - - add a new path to watch (assert_fs::TempDir), maybe with an already existing file - - create a new file in that dir - - ensure event was not triggered (MockGithubClient) - - test delete file does not trigger event handler - - create a store with the MockGithubClient passed - - start watcher - - add a new path to watch (assert_fs::TempDir), with an already existing file - - delete the file - - ensure event was not triggered (MockGithubClient) - - test modify file data triggers event handler - - create a store with the MockGithubClient passed - - start watcher - - add a new path to watch (assert_fs::TempDir), with an already existing file - - modify the file data - - ensure event was triggered (MockGithubClient) - - test modify file data outside of watched paths does not trigger event handler - - create a store with the MockGithubClient passed - - start watcher - - add a new path to watch (assert_fs::TempDir) - - create another assert_fs::TempDir with an existing file - - modify that file data - - ensure event was not triggered (MockGithubClient) - */ + #![allow(clippy::unwrap_used, clippy::expect_used)] + + use std::{path::PathBuf, sync::OnceLock}; + + use anyhow::Result; + use notify::{Event, EventKind}; + use tokio::runtime::Runtime; + + use crate::watching::{EventHandler, MockPathWatcher, WatchedSet}; + + fn new_watched_set() -> Result { + WatchedSet::new(Box::new(|_ev: Event| Box::pin(async {}))) + } + + #[test] + fn test_new_successful() -> Result<()> { + static EVENT_HANDLER_CALLED: OnceLock = OnceLock::new(); + + let event_handler: EventHandler = Box::new(|_| { + Box::pin(async { + EVENT_HANDLER_CALLED + .set(true) + .expect("Flag was already set"); + }) + }); + + let ctx = MockPathWatcher::new_context(); + ctx.expect().return_once(|event_handler| { + let rt = Runtime::new()?; + rt.block_on(async { + event_handler(Event::new(EventKind::Any)).await; + }); + Ok(MockPathWatcher::default()) + }); + + let set = WatchedSet::new(event_handler)?; + assert!(set.paths.is_empty()); + assert!(EVENT_HANDLER_CALLED.get().expect("Flag was not set")); // testing that WatchedSet passes the event handler to PathWatcher + + Ok(()) + } + + #[test] + fn test_start_watcher_successful() -> Result<()> { + let ctx = MockPathWatcher::new_context(); + ctx.expect().return_once(|_| { + let mut mock_path_watcher = MockPathWatcher::default(); + mock_path_watcher.expect_start().return_once(|| ()); + Ok(mock_path_watcher) + }); + + let mut set = new_watched_set()?; + set.start_watcher(); + + Ok(()) + } + + #[test] + fn test_watch_successful() -> Result<()> { + let path = PathBuf::from("/hello/there"); + let path_clone = path.clone(); + + let ctx = MockPathWatcher::new_context(); + ctx.expect().return_once(|_| { + let mut mock_path_watcher = MockPathWatcher::default(); + mock_path_watcher.expect_watch().return_once(move |path| { + assert_eq!(path, path_clone); + Ok(()) + }); + + Ok(mock_path_watcher) + }); + + let mut set = new_watched_set()?; + set.watch(path.clone())?; + assert!(set.paths.contains(&path)); + + Ok(()) + } + + #[test] + fn test_watch_failure_if_already_watched() -> Result<()> { + let path = PathBuf::from("/hello/there"); + let path_clone = path.clone(); + + let ctx = MockPathWatcher::new_context(); + ctx.expect().return_once(|_| { + let mut mock_path_watcher = MockPathWatcher::default(); + mock_path_watcher.expect_watch().return_once(move |path| { + assert_eq!(path, path_clone); + Ok(()) + }); + + Ok(mock_path_watcher) + }); + + let mut set = new_watched_set()?; + set.watch(path.clone())?; + assert_eq!( + set.watch(path.clone()).unwrap_err().to_string(), + "Path is already being watched: /hello/there" + ); + + Ok(()) + } + + #[test] + fn test_unwatch_successful() -> Result<()> { + let path = PathBuf::from("/hello/there"); + let path_clone_to_watch = path.clone(); + let path_clone_to_unwatch = path.clone(); + + let ctx = MockPathWatcher::new_context(); + ctx.expect().return_once(|_| { + let mut mock_path_watcher = MockPathWatcher::default(); + mock_path_watcher.expect_watch().return_once(move |path| { + assert_eq!(path, path_clone_to_watch); + Ok(()) + }); + mock_path_watcher.expect_unwatch().return_once(move |path| { + assert_eq!(path, path_clone_to_unwatch); + Ok(()) + }); + + Ok(mock_path_watcher) + }); + + let mut set = new_watched_set()?; + set.watch(path.clone())?; + set.unwatch(&path)?; + assert!(!set.paths.contains(&path)); + + Ok(()) + } + + #[test] + fn test_unwatch_failure_if_not_watched() -> Result<()> { + let path = PathBuf::from("/hello/there"); + let path_clone_to_unwatch = path.clone(); + + let ctx = MockPathWatcher::new_context(); + ctx.expect().return_once(|_| { + let mut mock_path_watcher = MockPathWatcher::default(); + mock_path_watcher.expect_unwatch().return_once(move |path| { + assert_eq!(path, path_clone_to_unwatch); + Ok(()) + }); + + Ok(mock_path_watcher) + }); + + let mut set = new_watched_set()?; + assert_eq!( + set.unwatch(&path).unwrap_err().to_string(), + "Path is not being watched, failed to unwatch: /hello/there" + ); + + Ok(()) + } }