diff --git a/common/src/config.rs b/common/src/config.rs index 46b343c..608a131 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -139,7 +139,7 @@ mod tests { } #[tokio::test] - async fn test_from_file_successfully_reads_correct_config_structure() -> Result<()> { + async fn test_from_file_success() -> Result<()> { zed_settings_file().write_str( r#" { @@ -164,7 +164,7 @@ mod tests { } #[tokio::test] - async fn test_from_file_fails_when_settings_file_is_missing() { + async fn test_from_file_failure_when_settings_file_is_missing() { let config = Config::from_settings_file(); assert_eq!( @@ -283,7 +283,7 @@ mod tests { } #[tokio::test] - async fn test_from_user_input_successfully_reads_config() -> Result<()> { + async fn test_from_user_input_success() -> Result<()> { let input_lines = "\nabcdef1234567890\n"; // empty line followed by fake gist id let mut io = CursorInteractiveIO::new(input_lines); diff --git a/lsp/src/app_state.rs b/lsp/src/app_state.rs index 978a907..8fee73e 100644 --- a/lsp/src/app_state.rs +++ b/lsp/src/app_state.rs @@ -7,6 +7,9 @@ use tower_lsp::Client as LspClient; #[cfg(test)] use crate::mocks::MockLspClient as LspClient; +#[cfg(test)] +use crate::watching::MockPathStore as PathStore; +#[cfg(not(test))] use crate::watching::PathStore; #[derive(Debug)] diff --git a/lsp/src/backend.rs b/lsp/src/backend.rs index ea970c1..3c2a530 100644 --- a/lsp/src/backend.rs +++ b/lsp/src/backend.rs @@ -48,7 +48,7 @@ impl Backend { #[allow(clippy::expect_used)] self.app_state .get() - .expect("App state must already be initialized") + .expect("App state must be initialized") .lock() .expect("Watched paths store mutex is poisoned") .watched_paths @@ -65,7 +65,7 @@ impl Backend { #[allow(clippy::expect_used)] self.app_state .get() - .expect("App state must be already initialized") + .expect("App state must be initialized") .lock() .expect("Watched paths store mutex is poisoned") .watched_paths @@ -104,12 +104,12 @@ impl LanguageServer for Backend { #[allow(clippy::expect_used)] self.app_state .set(Mutex::new(app_state)) - .expect("AppState should not yet be initialized"); + .expect("AppState was already initialized"); #[allow(clippy::expect_used)] self.app_state .get() - .expect("App state should have been already initialized") + .expect("App state should be initialized") .lock() .expect("Watched paths store mutex is poisoned") .watched_paths @@ -195,3 +195,227 @@ impl LanguageServer for Backend { } } } + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use std::path::Path; + use std::path::PathBuf; + + use anyhow::Result; + use anyhow::anyhow; + use mockall::{Sequence, predicate}; + use tower_lsp::{LanguageServer, lsp_types::InitializeParams}; + use zed_extension_api::serde_json::{Value, json}; + + use crate::{backend::Backend, mocks::MockLspClient, watching::MockPathStore}; + + async fn init_lsp_backend(initialization_options: Option) -> Result { + let mut mock_lsp_client = MockLspClient::default(); + mock_lsp_client + .expect_clone() + .returning(MockLspClient::default); + + let backend = Backend::new(mock_lsp_client); + let initialize_params = InitializeParams { + initialization_options, + ..Default::default() + }; + backend.initialize(initialize_params).await?; + + Ok(backend) + } + + async fn init_lsp_backend_default() -> Result { + init_lsp_backend(Some(json!({ + "github_token": "gho_my-shiny-token", + "gist_id": "deadbeefdeadbeefdeadbeefdeadbeef" + }))) + .await + } + + #[tokio::test] + async fn test_initialize_success() -> Result<()> { + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut mock_path_store = MockPathStore::default(); + mock_path_store.expect_start_watcher().returning(|| ()); + Ok(mock_path_store) + }); + + init_lsp_backend_default().await?; + + Ok(()) + } + + #[tokio::test] + async fn test_initialize_failure_missing_initialization_options() -> Result<()> { + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut mock_path_store = MockPathStore::default(); + mock_path_store.expect_start_watcher().returning(|| ()); + Ok(mock_path_store) + }); + + assert!(init_lsp_backend(None).await.is_err()); + + Ok(()) + } + + #[tokio::test] + async fn test_initialize_failure_invalid_initialization_options() -> Result<()> { + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut mock_path_store = MockPathStore::default(); + mock_path_store.expect_start_watcher().returning(|| ()); + Ok(mock_path_store) + }); + + let test_cases = [ + json!({ + "hello": "world" + }), + json!({ + "gist_id": "1234" + }), + json!({ + "github_token": "tok" + }), + json!({ + "gist_id": "1234", + "random_key": "value" + }), + json!({ + "github_token": "5678", + "random_prop": "val" + }), + ]; + + for test in test_cases { + assert!(init_lsp_backend(Some(test)).await.is_err()); + } + + Ok(()) + } + + #[tokio::test] + async fn test_watch_path_success() -> Result<()> { + let path = "/path/to/watch"; + + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut seq = Sequence::new(); + let mut mock_path_store = MockPathStore::default(); + mock_path_store + .expect_start_watcher() + .in_sequence(&mut seq) + .returning(|| ()); + mock_path_store + .expect_watch() + .in_sequence(&mut seq) + .with(predicate::eq(PathBuf::from(path.to_string()))) + .returning(|_| Ok(())); + Ok(mock_path_store) + }); + + let backend = init_lsp_backend_default().await?; + + backend.watch_path(PathBuf::from(path))?; + + Ok(()) + } + + #[tokio::test] + async fn test_watch_path_failure_path_store_watch_failed() -> Result<()> { + let path = "/path/to/watch"; + + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut seq = Sequence::new(); + let mut mock_path_store = MockPathStore::default(); + mock_path_store + .expect_start_watcher() + .in_sequence(&mut seq) + .returning(|| ()); + mock_path_store + .expect_watch() + .in_sequence(&mut seq) + .with(predicate::eq(PathBuf::from(path.to_string()))) + .returning(|_| Err(anyhow!("Failed to watch path"))); + Ok(mock_path_store) + }); + + let backend = init_lsp_backend_default().await?; + + assert_eq!( + backend + .watch_path(PathBuf::from(path)) + .unwrap_err() + .to_string(), + "Failed to watch path" + ); + + Ok(()) + } + + #[tokio::test] + async fn test_unwatch_path_success() -> Result<()> { + let path = "/path/to/watch"; + + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut seq = Sequence::new(); + let mut mock_path_store = MockPathStore::default(); + mock_path_store + .expect_start_watcher() + .in_sequence(&mut seq) + .returning(|| ()); + mock_path_store + .expect_unwatch() + .in_sequence(&mut seq) + .with(predicate::eq(PathBuf::from(path.to_string()))) + .returning(|_| Ok(())); + Ok(mock_path_store) + }); + + let backend = init_lsp_backend_default().await?; + + backend.unwatch_path(Path::new(path))?; + + Ok(()) + } + + #[tokio::test] + async fn test_unwatch_path_failure_path_store_watch_failed() -> Result<()> { + let path = "/path/to/watch"; + + let ctx = MockPathStore::new_context(); + ctx.expect().returning(|_, _| { + let mut seq = Sequence::new(); + let mut mock_path_store = MockPathStore::default(); + mock_path_store + .expect_start_watcher() + .in_sequence(&mut seq) + .returning(|| ()); + mock_path_store + .expect_unwatch() + .in_sequence(&mut seq) + .with(predicate::eq(PathBuf::from(path.to_string()))) + .returning(|_| Err(anyhow!("Failed to unwatch path"))); + Ok(mock_path_store) + }); + + let backend = init_lsp_backend_default().await?; + + assert_eq!( + backend + .unwatch_path(Path::new(path)) + .unwrap_err() + .to_string(), + "Failed to unwatch path" + ); + + Ok(()) + } +} diff --git a/lsp/src/mocks.rs b/lsp/src/mocks.rs index b218da5..a65eca2 100644 --- a/lsp/src/mocks.rs +++ b/lsp/src/mocks.rs @@ -8,15 +8,15 @@ mock! { pub fn show_message(&self, msg_type: MessageType, message: String) -> impl Future + Send + Sync; } - impl fmt::Debug for LspClient { - fn fmt<'a>(&self, f: &mut std::fmt::Formatter<'a>) -> std::fmt::Result { - f.debug_struct("LspClient").finish() - } - } - impl Clone for LspClient { fn clone(&self) -> Self { Self::default() } } } + +impl fmt::Debug for MockLspClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LspClient").finish() + } +} diff --git a/lsp/src/watching/path_store.rs b/lsp/src/watching/path_store.rs index 9299037..004d5d2 100644 --- a/lsp/src/watching/path_store.rs +++ b/lsp/src/watching/path_store.rs @@ -22,6 +22,7 @@ pub struct PathStore { watched_set: WatchedSet, } +#[cfg_attr(test, mockall::automock)] impl PathStore { pub fn new(sync_client: Arc, lsp_client: Arc) -> Result { let event_handler = Box::new(move |event| { @@ -112,7 +113,7 @@ mod tests { }; #[test] - fn test_successful_creation() { + fn test_creation_success() { let ctx = MockWatchedSet::new_context(); ctx.expect().returning(|_| Ok(MockWatchedSet::default())); @@ -126,7 +127,7 @@ mod tests { } #[test] - fn test_unsuccessful_creation_when_watched_set_creation_failed() { + fn test_creation_failure_when_watched_set_creation_failed() { let ctx = MockWatchedSet::new_context(); ctx.expect() .returning(|_| Err(anyhow!("Failed to create watched set"))); @@ -184,7 +185,7 @@ mod tests { } #[test] - fn test_successful_watch_path() -> Result<()> { + fn test_watch_path_success() -> Result<()> { let dir = TempDir::new()?; dir.child("foobar").touch()?; let path = dir.path().to_path_buf(); @@ -203,7 +204,7 @@ mod tests { } #[test] - fn test_unsuccessful_watch_path() -> Result<()> { + fn test_watch_path_failure() -> Result<()> { let dir = TempDir::new()?; dir.child("foobar").touch()?; let path = dir.path().to_path_buf(); @@ -226,7 +227,7 @@ mod tests { } #[test] - fn test_successful_unwatch_path() -> Result<()> { + fn test_unwatch_path_success() -> Result<()> { let dir = TempDir::new()?; dir.child("foobar").touch()?; let path = dir.path().to_path_buf(); @@ -245,7 +246,7 @@ mod tests { } #[test] - fn test_unsuccessful_unwatch_path() -> Result<()> { + fn test_unwatch_path_failure() -> Result<()> { let dir = TempDir::new()?; dir.child("foobar").touch()?; let path = dir.path().to_path_buf(); @@ -303,7 +304,7 @@ mod tests { } #[test] - fn test_modify_event_handling_without_modified_path() -> Result<()> { + fn test_modify_event_without_modified_path_notification() -> Result<()> { let ctx = MockWatchedSet::new_context(); ctx.expect().returning(move |event_handler: EventHandler| { let event = Event::new(EventKind::Modify(ModifyKind::Data(DataChange::Any))); @@ -335,7 +336,7 @@ mod tests { } #[test] - fn test_modify_event_handling_with_file_read_error() -> Result<()> { + fn test_modify_event_with_file_read_error_notification() -> Result<()> { let ctx = MockWatchedSet::new_context(); ctx.expect().returning(move |event_handler: EventHandler| { let mut event = Event::new(EventKind::Modify(ModifyKind::Data(DataChange::Any))); diff --git a/lsp/src/watching/path_watcher.rs b/lsp/src/watching/path_watcher.rs index f330fff..58b7004 100644 --- a/lsp/src/watching/path_watcher.rs +++ b/lsp/src/watching/path_watcher.rs @@ -74,7 +74,6 @@ impl PathWatcher { } pub fn watch(&self, path: &Path) -> Result<()> { - // println!("Watcher is running: {}", self.watcher.lock().unwrap()) self.watcher .lock() .map_err(|_| anyhow!("Path watcher mutex is poisoned"))? @@ -196,7 +195,7 @@ mod tests { } #[tokio::test] - async fn test_unwatch_successful() -> Result<()> { + async fn test_unwatch_success() -> Result<()> { init_event_handler!(event_handler); let mut path_watcher = PathWatcher::new(event_handler)?; diff --git a/lsp/src/watching/watched_set.rs b/lsp/src/watching/watched_set.rs index d0bd4cc..b8bc52c 100644 --- a/lsp/src/watching/watched_set.rs +++ b/lsp/src/watching/watched_set.rs @@ -89,7 +89,7 @@ mod tests { } #[test] - fn test_new_successful() -> Result<()> { + fn test_new_success() -> Result<()> { static EVENT_HANDLER_CALLED: AtomicBool = AtomicBool::new(false); let event_handler: EventHandler = Box::new(|_| { @@ -115,7 +115,7 @@ mod tests { } #[test] - fn test_start_watcher_successful() -> Result<()> { + fn test_start_watcher_success() -> Result<()> { let ctx = MockPathWatcher::new_context(); ctx.expect().return_once(|_| { let mut mock_path_watcher = MockPathWatcher::default(); @@ -130,7 +130,7 @@ mod tests { } #[test] - fn test_watch_successful() -> Result<()> { + fn test_watch_success() -> Result<()> { let path = PathBuf::from("/hello/there"); let path_clone = path.clone(); @@ -179,7 +179,7 @@ mod tests { } #[test] - fn test_unwatch_successful() -> Result<()> { + fn test_unwatch_success() -> Result<()> { let path = PathBuf::from("/hello/there"); let path_clone_to_watch = path.clone(); let path_clone_to_unwatch = path.clone(); diff --git a/lsp/src/watching/zed_config.rs b/lsp/src/watching/zed_config.rs index f6f4d5a..dfd70cd 100644 --- a/lsp/src/watching/zed_config.rs +++ b/lsp/src/watching/zed_config.rs @@ -4,7 +4,10 @@ use std::{ }; use anyhow::Result; +#[cfg(not(test))] use paths as zed_paths; +#[cfg(test)] +use test_support::zed_paths; use tower_lsp::lsp_types::Url; #[derive(Debug, PartialEq, Eq, Hash)] @@ -66,7 +69,69 @@ impl AsRef for ZedConfigFilePath { } } +#[derive(Debug, PartialEq, Eq)] pub enum ZedConfigPathError { NotZedConfigFile, WrongFileUriFormat, } + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use test_support::zed_paths; + use tower_lsp::lsp_types::Url; + + use crate::watching::{ZedConfigFilePath, ZedConfigPathError}; + + #[test] + fn test_from_file_uri_success() { + let file_uri = Url::parse(&format!( + "file:///{}/tasks.json", + zed_paths::config_dir().display() + )) + .unwrap(); + let config_path = ZedConfigFilePath::from_file_uri(&file_uri).unwrap(); + + assert_eq!( + config_path.path.to_string_lossy(), + zed_paths::config_dir().join("tasks.json").to_string_lossy() + ); + } + + #[test] + fn test_from_file_uri_failure_wrong_format() { + let file_uri = Url::parse("lol:///home/user/.config/zed/settings.json").unwrap(); + + assert_eq!( + ZedConfigFilePath::from_file_uri(&file_uri).unwrap_err(), + ZedConfigPathError::WrongFileUriFormat + ); + } + + #[test] + fn test_from_file_uri_failure_not_zed_config_file() { + let file_uri = Url::parse(&format!( + "file:///{}/settings.kek", + zed_paths::config_dir().display() + )) + .unwrap(); + + assert_eq!( + ZedConfigFilePath::from_file_uri(&file_uri), + Err(ZedConfigPathError::NotZedConfigFile) + ); + } + + #[test] + fn test_to_watched_path_buf_success() { + let file_uri = + Url::parse(&format!("file:///{}", zed_paths::settings_file().display())).unwrap(); + let config_path = ZedConfigFilePath::from_file_uri(&file_uri).unwrap(); + + assert_eq!( + config_path.to_watched_path_buf().display().to_string(), + zed_paths::settings_file().display().to_string() + ); + } +}