From 5dfde85a1da2b03fdef53935a341ca94ffe9fcda Mon Sep 17 00:00:00 2001 From: Sysix <3897725+Sysix@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:27:33 +0000 Subject: [PATCH] test(lsp): add tests for diagnostics on `didOpen|didChange|didSave` (#16466) > This PR adds comprehensive test coverage for diagnostic functionality in the Language Server Protocol (LSP) implementation, specifically testing diagnostics on didOpen, didChange, and didSave events. The changes include both unit tests for the worker module and integration tests for the backend > along with a bug fix in the did_save handler. This is not really a bug fix, just a little refactoring to pass the tests. In general, I do not expect that `params.text (didSave)` has changed from `didChange` content --- crates/oxc_language_server/src/backend.rs | 3 +- crates/oxc_language_server/src/tests.rs | 122 +++++++++++++++++++++- crates/oxc_language_server/src/worker.rs | 119 +++++++++++++++++++++ 3 files changed, 241 insertions(+), 3 deletions(-) diff --git a/crates/oxc_language_server/src/backend.rs b/crates/oxc_language_server/src/backend.rs index 83ab80f564e1d..d9aa42cc7a9b8 100644 --- a/crates/oxc_language_server/src/backend.rs +++ b/crates/oxc_language_server/src/backend.rs @@ -506,7 +506,8 @@ impl LanguageServer for Backend { // saving the file means we can read again from the file system self.file_system.write().await.remove(uri); - if let Some(diagnostics) = worker.run_diagnostic_on_save(uri, None).await { + if let Some(diagnostics) = worker.run_diagnostic_on_save(uri, params.text.as_deref()).await + { self.client.publish_diagnostics(uri.clone(), diagnostics, None).await; } } diff --git a/crates/oxc_language_server/src/tests.rs b/crates/oxc_language_server/src/tests.rs index 3cf519a89d265..e432a6a3b7ea9 100644 --- a/crates/oxc_language_server/src/tests.rs +++ b/crates/oxc_language_server/src/tests.rs @@ -148,6 +148,33 @@ impl Tool for FakeTool { vec![] } + + fn run_diagnostic(&self, uri: &Uri, content: Option<&str>) -> Option> { + if uri.as_str().ends_with("diagnostics.config") { + return Some(vec![Diagnostic { + message: format!( + "Fake diagnostic for content: {}", + content.unwrap_or("") + ), + ..Default::default() + }]); + } + None + } + + fn run_diagnostic_on_change( + &self, + uri: &Uri, + content: Option<&str>, + ) -> Option> { + // For this fake tool, we use the same logic as run_diagnostic + self.run_diagnostic(uri, content) + } + + fn run_diagnostic_on_save(&self, uri: &Uri, content: Option<&str>) -> Option> { + // For this fake tool, we use the same logic as run_diagnostic + self.run_diagnostic(uri, content) + } } // A test server that can send requests and receive responses. @@ -454,8 +481,8 @@ mod test_suite { use tower_lsp_server::{ jsonrpc::{Id, Response}, lsp_types::{ - ApplyWorkspaceEditResponse, InitializeResult, ServerInfo, WorkspaceEdit, - WorkspaceFolder, + ApplyWorkspaceEditResponse, InitializeResult, PublishDiagnosticsParams, ServerInfo, + WorkspaceEdit, WorkspaceFolder, }, }; @@ -989,4 +1016,95 @@ mod test_suite { server.shutdown(4).await; } + + #[tokio::test] + async fn test_diagnostic_on_open() { + let mut server = TestServer::new_initialized( + |client| Backend::new(client, server_info(), vec![Box::new(FakeToolBuilder)]), + initialize_request(false, false, false), + ) + .await; + + let file = format!("{WORKSPACE}/diagnostics.config"); + let content = "some text"; + server.send_request(did_open(&file, content)).await; + + let diagnostic_response = server.recv_notification().await; + assert_eq!(diagnostic_response.method(), "textDocument/publishDiagnostics"); + let params: PublishDiagnosticsParams = + serde_json::from_value(diagnostic_response.params().unwrap().clone()).unwrap(); + assert_eq!(params.uri, file.parse().unwrap()); + assert_eq!(params.diagnostics.len(), 1); + assert_eq!( + params.diagnostics[0].message, + format!("Fake diagnostic for content: {content}") + ); + + server.shutdown(4).await; + } + + #[tokio::test] + async fn test_diagnostic_on_change() { + let mut server = TestServer::new_initialized( + |client| Backend::new(client, server_info(), vec![Box::new(FakeToolBuilder)]), + initialize_request(false, false, false), + ) + .await; + + let file = format!("{WORKSPACE}/diagnostics.config"); + let content = "new text"; + server.send_request(did_open(&file, "old text")).await; + let diagnostic_response = server.recv_notification().await; + assert_eq!(diagnostic_response.method(), "textDocument/publishDiagnostics"); + + server.send_request(did_change(&file, content)).await; + + let diagnostic_response = server.recv_notification().await; + assert_eq!(diagnostic_response.method(), "textDocument/publishDiagnostics"); + let params: PublishDiagnosticsParams = + serde_json::from_value(diagnostic_response.params().unwrap().clone()).unwrap(); + assert_eq!(params.uri, file.parse().unwrap()); + assert_eq!(params.diagnostics.len(), 1); + assert_eq!( + params.diagnostics[0].message, + format!("Fake diagnostic for content: {content}") + ); + + server.shutdown(4).await; + } + + #[tokio::test] + async fn test_diagnostic_on_save() { + let mut server = TestServer::new_initialized( + |client| Backend::new(client, server_info(), vec![Box::new(FakeToolBuilder)]), + initialize_request(false, false, false), + ) + .await; + + let file = format!("{WORKSPACE}/diagnostics.config"); + let content = "new text"; + server.send_request(did_open(&file, "old text")).await; + let diagnostic_response = server.recv_notification().await; + assert_eq!(diagnostic_response.method(), "textDocument/publishDiagnostics"); + + server.send_request(did_change(&file, content)).await; + + let diagnostic_response = server.recv_notification().await; + assert_eq!(diagnostic_response.method(), "textDocument/publishDiagnostics"); + + server.send_request(did_save(&file, content)).await; + + let diagnostic_response = server.recv_notification().await; + assert_eq!(diagnostic_response.method(), "textDocument/publishDiagnostics"); + let params: PublishDiagnosticsParams = + serde_json::from_value(diagnostic_response.params().unwrap().clone()).unwrap(); + assert_eq!(params.uri, file.parse().unwrap()); + assert_eq!(params.diagnostics.len(), 1); + assert_eq!( + params.diagnostics[0].message, + format!("Fake diagnostic for content: {content}") + ); + + server.shutdown(4).await; + } } diff --git a/crates/oxc_language_server/src/worker.rs b/crates/oxc_language_server/src/worker.rs index a77ef7e4e8544..6c9346cc30264 100644 --- a/crates/oxc_language_server/src/worker.rs +++ b/crates/oxc_language_server/src/worker.rs @@ -563,4 +563,123 @@ mod tests { panic!("Expected CodeAction"); } } + + #[tokio::test] + async fn test_run_diagnostic() { + let worker = WorkspaceWorker::new(Uri::from_str("file:///root/").unwrap()); + let tools: Vec> = vec![Box::new(FakeToolBuilder)]; + worker.start_worker(serde_json::Value::Null, &tools).await; + + let diagnostics_no_content = worker + .run_diagnostic(&Uri::from_str("file:///root/diagnostics.config").unwrap(), None) + .await; + + assert!(diagnostics_no_content.is_some()); + assert_eq!(diagnostics_no_content.as_ref().unwrap().len(), 1); + assert_eq!( + diagnostics_no_content.unwrap()[0].message, + "Fake diagnostic for content: " + ); + + let diagnostics_with_content = worker + .run_diagnostic( + &Uri::from_str("file:///root/diagnostics.config").unwrap(), + Some("helloworld"), + ) + .await; + + assert!(diagnostics_with_content.is_some()); + assert_eq!(diagnostics_with_content.as_ref().unwrap().len(), 1); + assert_eq!( + diagnostics_with_content.unwrap()[0].message, + "Fake diagnostic for content: helloworld" + ); + + let no_diagnostics = + worker.run_diagnostic(&Uri::from_str("file:///root/unknown.file").unwrap(), None).await; + + assert!(no_diagnostics.is_none()); + } + + #[tokio::test] + async fn test_run_diagnostic_on_change() { + let worker = WorkspaceWorker::new(Uri::from_str("file:///root/").unwrap()); + let tools: Vec> = vec![Box::new(FakeToolBuilder)]; + worker.start_worker(serde_json::Value::Null, &tools).await; + + let diagnostics_no_content = worker + .run_diagnostic_on_change( + &Uri::from_str("file:///root/diagnostics.config").unwrap(), + None, + ) + .await; + + assert!(diagnostics_no_content.is_some()); + assert_eq!(diagnostics_no_content.as_ref().unwrap().len(), 1); + assert_eq!( + diagnostics_no_content.unwrap()[0].message, + "Fake diagnostic for content: " + ); + + let diagnostics_with_content = worker + .run_diagnostic_on_change( + &Uri::from_str("file:///root/diagnostics.config").unwrap(), + Some("helloworld"), + ) + .await; + + assert!(diagnostics_with_content.is_some()); + assert_eq!(diagnostics_with_content.as_ref().unwrap().len(), 1); + assert_eq!( + diagnostics_with_content.unwrap()[0].message, + "Fake diagnostic for content: helloworld" + ); + + let no_diagnostics = worker + .run_diagnostic_on_change(&Uri::from_str("file:///root/unknown.file").unwrap(), None) + .await; + + assert!(no_diagnostics.is_none()); + } + + #[tokio::test] + async fn test_run_diagnostic_on_save() { + let worker = WorkspaceWorker::new(Uri::from_str("file:///root/").unwrap()); + let tools: Vec> = vec![Box::new(FakeToolBuilder)]; + worker.start_worker(serde_json::Value::Null, &tools).await; + + let diagnostics_no_content = worker + .run_diagnostic_on_save( + &Uri::from_str("file:///root/diagnostics.config").unwrap(), + None, + ) + .await; + + assert!(diagnostics_no_content.is_some()); + assert_eq!(diagnostics_no_content.as_ref().unwrap().len(), 1); + assert_eq!( + diagnostics_no_content.unwrap()[0].message, + "Fake diagnostic for content: " + ); + + let diagnostics_with_content = worker + .run_diagnostic_on_save( + &Uri::from_str("file:///root/diagnostics.config").unwrap(), + Some("helloworld"), + ) + .await; + + assert!(diagnostics_with_content.is_some()); + assert_eq!(diagnostics_with_content.as_ref().unwrap().len(), 1); + assert_eq!( + diagnostics_with_content.unwrap()[0].message, + "Fake diagnostic for content: helloworld" + ); + + let no_diagnostics = worker + .run_diagnostic_on_save(&Uri::from_str("file:///root/unknown.file").unwrap(), None) + .await; + + assert!(no_diagnostics.is_none()); + } }