diff --git a/Cargo.lock b/Cargo.lock index aca0235ddf2cdd..0cb75042f9f09b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4344,6 +4344,7 @@ dependencies = [ "ty_ide", "ty_project", "ty_python_semantic", + "ty_server", "ty_vendored", ] diff --git a/crates/ty_server/Cargo.toml b/crates/ty_server/Cargo.toml index f0efbfe1c50b29..0602f78eba5494 100644 --- a/crates/ty_server/Cargo.toml +++ b/crates/ty_server/Cargo.toml @@ -38,11 +38,16 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["chrono"] } [dev-dependencies] +ty_server = { workspace = true, features = ["testing"] } + dunce = { workspace = true } insta = { workspace = true, features = ["filters", "json"] } regex = { workspace = true } tempfile = { workspace = true } +[features] +testing = [] + [target.'cfg(target_vendor = "apple")'.dependencies] libc = { workspace = true } diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index 4496957e0d0e12..8c3d4626aedee0 100644 --- a/crates/ty_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -4,7 +4,9 @@ use anyhow::Context; use lsp_server::Connection; use ruff_db::system::{OsSystem, SystemPathBuf}; -use crate::server::Server; +pub use crate::logging::{LogLevel, init_logging}; +pub use crate::server::Server; +pub use crate::session::ClientOptions; pub use document::{NotebookDocument, PositionEncoding, TextDocument}; pub(crate) use session::{DocumentQuery, Session}; @@ -14,9 +16,6 @@ mod server; mod session; mod system; -#[cfg(test)] -pub mod test; - pub(crate) const SERVER_NAME: &str = "ty"; pub(crate) const DIAGNOSTIC_NAME: &str = "ty"; diff --git a/crates/ty_server/src/logging.rs b/crates/ty_server/src/logging.rs index ce92a803ab19ff..a571087340def7 100644 --- a/crates/ty_server/src/logging.rs +++ b/crates/ty_server/src/logging.rs @@ -14,7 +14,7 @@ use tracing_subscriber::fmt::time::ChronoLocal; use tracing_subscriber::fmt::writer::BoxMakeWriter; use tracing_subscriber::layer::SubscriberExt; -pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&SystemPath>) { +pub fn init_logging(log_level: LogLevel, log_file: Option<&SystemPath>) { let log_file = log_file .map(|path| { // this expands `logFile` so that tildes and environment variables @@ -66,7 +66,7 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&SystemPath>) { /// The default log level is `info`. #[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] -pub(crate) enum LogLevel { +pub enum LogLevel { Error, Warn, #[default] diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 02b3ea58815a1f..29a34d8a23d893 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -26,7 +26,7 @@ pub(crate) use api::publish_settings_diagnostics; pub(crate) use main_loop::{Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender}; pub(crate) type Result = std::result::Result; -pub(crate) struct Server { +pub struct Server { connection: Connection, client_capabilities: ClientCapabilities, worker_threads: NonZeroUsize, @@ -36,7 +36,7 @@ pub(crate) struct Server { } impl Server { - pub(crate) fn new( + pub fn new( worker_threads: NonZeroUsize, connection: Connection, native_system: Arc, @@ -161,7 +161,7 @@ impl Server { }) } - pub(crate) fn run(mut self) -> crate::Result<()> { + pub fn run(mut self) -> crate::Result<()> { let client = Client::new( self.main_loop_sender.clone(), self.connection.sender.clone(), @@ -302,89 +302,3 @@ impl Drop for ServerPanicHookHandler { } } } - -#[cfg(test)] -mod tests { - use anyhow::Result; - use lsp_types::notification::PublishDiagnostics; - use ruff_db::system::SystemPath; - - use crate::session::ClientOptions; - use crate::test::TestServerBuilder; - - #[test] - fn initialization() -> Result<()> { - let server = TestServerBuilder::new()? - .build()? - .wait_until_workspaces_are_initialized()?; - - let initialization_result = server.initialization_result().unwrap(); - - insta::assert_json_snapshot!("initialization", initialization_result); - - Ok(()) - } - - #[test] - fn initialization_with_workspace() -> Result<()> { - let workspace_root = SystemPath::new("foo"); - let server = TestServerBuilder::new()? - .with_workspace(workspace_root, ClientOptions::default())? - .build()? - .wait_until_workspaces_are_initialized()?; - - let initialization_result = server.initialization_result().unwrap(); - - insta::assert_json_snapshot!("initialization_with_workspace", initialization_result); - - Ok(()) - } - - #[test] - fn publish_diagnostics_on_did_open() -> Result<()> { - let workspace_root = SystemPath::new("src"); - let foo = SystemPath::new("src/foo.py"); - let foo_content = "\ -def foo() -> str: - return 42 -"; - - let mut server = TestServerBuilder::new()? - .with_workspace(workspace_root, ClientOptions::default())? - .with_file(foo, foo_content)? - .enable_pull_diagnostics(false) - .build()? - .wait_until_workspaces_are_initialized()?; - - server.open_text_document(foo, &foo_content, 1); - let diagnostics = server.await_notification::()?; - - insta::assert_debug_snapshot!(diagnostics); - - Ok(()) - } - - #[test] - fn pull_diagnostics_on_did_open() -> Result<()> { - let workspace_root = SystemPath::new("src"); - let foo = SystemPath::new("src/foo.py"); - let foo_content = "\ -def foo() -> str: - return 42 -"; - - let mut server = TestServerBuilder::new()? - .with_workspace(workspace_root, ClientOptions::default())? - .with_file(foo, foo_content)? - .enable_pull_diagnostics(true) - .build()? - .wait_until_workspaces_are_initialized()?; - - server.open_text_document(foo, &foo_content, 1); - let diagnostics = server.document_diagnostic_request(foo)?; - - insta::assert_debug_snapshot!(diagnostics); - - Ok(()) - } -} diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index a78d219a9e4660..40801ab37a1b9b 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -21,7 +21,8 @@ use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata}; pub(crate) use self::capabilities::ResolvedClientCapabilities; pub(crate) use self::index::DocumentQuery; -pub(crate) use self::options::{AllOptions, ClientOptions, DiagnosticMode}; +pub use self::options::ClientOptions; +pub(crate) use self::options::{AllOptions, DiagnosticMode}; pub(crate) use self::settings::ClientSettings; use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; use crate::server::publish_settings_diagnostics; diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs index 934b87afa1c88d..f9eb42af4c8469 100644 --- a/crates/ty_server/src/session/options.rs +++ b/crates/ty_server/src/session/options.rs @@ -49,9 +49,10 @@ struct WorkspaceOptions { /// This is a direct representation of the settings schema sent by the client. #[derive(Clone, Debug, Deserialize, Default)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] -pub(crate) struct ClientOptions { +pub struct ClientOptions { /// Settings under the `python.*` namespace in VS Code that are useful for the ty language /// server. python: Option, @@ -63,7 +64,8 @@ pub(crate) struct ClientOptions { /// Diagnostic mode for the language server. #[derive(Clone, Copy, Debug, Default, Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] pub(crate) enum DiagnosticMode { /// Check only currently open files. @@ -147,21 +149,24 @@ impl ClientOptions { // all settings and not just the ones in "python.*". #[derive(Clone, Debug, Deserialize, Default)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] struct Python { ty: Option, } #[derive(Clone, Debug, Deserialize, Default)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] struct PythonExtension { active_environment: Option, } #[derive(Clone, Debug, Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] pub(crate) struct ActiveEnvironment { pub(crate) executable: PythonExecutable, @@ -170,7 +175,8 @@ pub(crate) struct ActiveEnvironment { } #[derive(Clone, Debug, Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] pub(crate) struct EnvironmentVersion { pub(crate) major: i64, @@ -182,7 +188,8 @@ pub(crate) struct EnvironmentVersion { } #[derive(Clone, Debug, Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] pub(crate) struct PythonEnvironment { pub(crate) folder_uri: Url, @@ -194,7 +201,8 @@ pub(crate) struct PythonEnvironment { } #[derive(Clone, Debug, Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] pub(crate) struct PythonExecutable { #[allow(dead_code)] @@ -203,7 +211,8 @@ pub(crate) struct PythonExecutable { } #[derive(Clone, Debug, Deserialize, Default)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(feature = "testing", derive(serde::Serialize))] #[serde(rename_all = "camelCase")] struct Ty { disable_language_services: Option, diff --git a/crates/ty_server/tests/e2e/initialize.rs b/crates/ty_server/tests/e2e/initialize.rs new file mode 100644 index 00000000000000..f9a695aba093b2 --- /dev/null +++ b/crates/ty_server/tests/e2e/initialize.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use ruff_db::system::SystemPath; +use ty_server::ClientOptions; + +use crate::TestServerBuilder; + +#[test] +fn empty_workspace_folders() -> Result<()> { + let server = TestServerBuilder::new()? + .build()? + .wait_until_workspaces_are_initialized()?; + + let initialization_result = server.initialization_result().unwrap(); + + insta::assert_json_snapshot!("initialization", initialization_result); + + Ok(()) +} + +#[test] +fn single_workspace_folder() -> Result<()> { + let workspace_root = SystemPath::new("foo"); + let server = TestServerBuilder::new()? + .with_workspace(workspace_root, ClientOptions::default())? + .build()? + .wait_until_workspaces_are_initialized()?; + + let initialization_result = server.initialization_result().unwrap(); + + insta::assert_json_snapshot!("initialization_with_workspace", initialization_result); + + Ok(()) +} diff --git a/crates/ty_server/src/test.rs b/crates/ty_server/tests/e2e/main.rs similarity index 93% rename from crates/ty_server/src/test.rs rename to crates/ty_server/tests/e2e/main.rs index 022a838cb65d35..6c8cf3606ae0a9 100644 --- a/crates/ty_server/src/test.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -1,9 +1,35 @@ //! Testing server for the ty language server. //! -//! This module provides mock server infrastructure for testing LSP functionality using -//! temporary directories on the real filesystem. +//! This module provides mock server infrastructure for testing LSP functionality using a +//! temporary directory on the real filesystem. //! //! The design is inspired by the Starlark LSP test server but adapted for ty server architecture. +//! +//! To get started, use the [`TestServerBuilder`] to configure the server with workspace folders, +//! enable or disable specific client capabilities, and add test files. Then, use the [`build`] +//! method to create the [`TestServer`]. This will start the server and perform the initialization +//! handshake. It might be useful to call [`wait_until_workspaces_are_initialized`] to ensure that +//! the server side initialization is complete before sending any requests. +//! +//! Once the setup is done, you can use the server to [`send_request`] and [`send_notification`] to +//! send messages to the server and [`await_response`], [`await_request`], and +//! [`await_notification`] to wait for responses, requests, and notifications from the server. +//! +//! The [`Drop`] implementation of the [`TestServer`] ensures that the server is shut down +//! gracefully using the LSP protocol. It also asserts that all messages sent by the server +//! have been handled by the test client before the server is dropped. +//! +//! [`build`]: TestServerBuilder::build +//! [`wait_until_workspaces_are_initialized`]: TestServer::wait_until_workspaces_are_initialized +//! [`send_request`]: TestServer::send_request +//! [`send_notification`]: TestServer::send_notification +//! [`await_response`]: TestServer::await_response +//! [`await_request`]: TestServer::await_request +//! [`await_notification`]: TestServer::await_notification + +mod initialize; +mod publish_diagnostics; +mod pull_diagnostics; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; @@ -39,9 +65,7 @@ use rustc_hash::FxHashMap; use serde::de::DeserializeOwned; use tempfile::TempDir; -use crate::logging::{LogLevel, init_logging}; -use crate::server::Server; -use crate::session::ClientOptions; +use ty_server::{ClientOptions, LogLevel, Server, init_logging}; /// Number of times to retry receiving a message before giving up const RETRY_COUNT: usize = 5; @@ -86,10 +110,6 @@ impl TestServerError { /// A test server for the ty language server that provides helpers for sending requests, /// correlating responses, and handling notifications. -/// -/// The [`Drop`] implementation ensures that the server is shut down gracefully using the described -/// protocol in the LSP specification. It also ensures that all messages sent by the server have -/// been handled by the test client before the server is dropped. pub(crate) struct TestServer { /// The thread that's actually running the server. /// @@ -103,10 +123,10 @@ pub(crate) struct TestServer { /// the connection to be cleaned up properly. client_connection: Option, - /// Test directory that holds all test files. + /// Test context that provides the project root directory that holds all test files. /// /// This directory is automatically cleaned up when the [`TestServer`] is dropped. - test_dir: TestContext, + test_context: TestContext, /// Incrementing counter to automatically generate request IDs request_counter: i32, @@ -175,7 +195,7 @@ impl TestServer { Self { server_thread: Some(server_thread), client_connection: Some(client_connection), - test_dir, + test_context: test_dir, request_counter: 0, responses: FxHashMap::default(), notifications: VecDeque::new(), @@ -297,9 +317,9 @@ impl TestServer { /// Send a request to the server and return the request ID. /// - /// The caller can use this ID to later retrieve the response using [`get_response`]. + /// The caller can use this ID to later retrieve the response using [`await_response`]. /// - /// [`get_response`]: TestServer::get_response + /// [`await_response`]: TestServer::await_response pub(crate) fn send_request(&mut self, params: R::Params) -> RequestId where R: Request, @@ -505,7 +525,7 @@ impl TestServer { /// Use the [`get_request`] method to wait for the server to send this request. /// /// [`get_request`]: TestServer::get_request - pub(crate) fn handle_workspace_configuration_request( + fn handle_workspace_configuration_request( &mut self, request_id: RequestId, params: &ConfigurationParams, @@ -548,7 +568,7 @@ impl TestServer { } fn file_uri(&self, path: impl AsRef) -> Url { - Url::from_file_path(self.test_dir.root().join(path.as_ref()).as_std_path()) + Url::from_file_path(self.test_context.root().join(path.as_ref()).as_std_path()) .expect("Path must be a valid URL") } @@ -628,7 +648,7 @@ impl TestServer { impl fmt::Debug for TestServer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TestServer") - .field("temp_dir", &self.test_dir.root()) + .field("temp_dir", &self.test_context.root()) .field("request_counter", &self.request_counter) .field("responses", &self.responses) .field("notifications", &self.notifications) @@ -750,6 +770,7 @@ impl TestServerBuilder { } /// Enable or disable pull diagnostics capability + #[must_use] pub(crate) fn enable_pull_diagnostics(mut self, enabled: bool) -> Self { self.client_capabilities .text_document @@ -763,6 +784,7 @@ impl TestServerBuilder { } /// Enable or disable file watching capability + #[must_use] #[expect(dead_code)] pub(crate) fn enable_did_change_watched_files(mut self, enabled: bool) -> Self { self.client_capabilities @@ -777,6 +799,7 @@ impl TestServerBuilder { } /// Set custom client capabilities (overrides any previously set capabilities) + #[must_use] #[expect(dead_code)] pub(crate) fn with_client_capabilities(mut self, capabilities: ClientCapabilities) -> Self { self.client_capabilities = capabilities; @@ -798,7 +821,7 @@ impl TestServerBuilder { Ok(self) } - /// Write multiple files to the temporary directory + /// Write multiple files to the test directory #[expect(dead_code)] pub(crate) fn with_files(mut self, files: I) -> Result where diff --git a/crates/ty_server/tests/e2e/publish_diagnostics.rs b/crates/ty_server/tests/e2e/publish_diagnostics.rs new file mode 100644 index 00000000000000..ebb90c98c5c159 --- /dev/null +++ b/crates/ty_server/tests/e2e/publish_diagnostics.rs @@ -0,0 +1,30 @@ +use anyhow::Result; +use lsp_types::notification::PublishDiagnostics; +use ruff_db::system::SystemPath; +use ty_server::ClientOptions; + +use crate::TestServerBuilder; + +#[test] +fn on_did_open() -> Result<()> { + let workspace_root = SystemPath::new("src"); + let foo = SystemPath::new("src/foo.py"); + let foo_content = "\ +def foo() -> str: + return 42 +"; + + let mut server = TestServerBuilder::new()? + .with_workspace(workspace_root, ClientOptions::default())? + .with_file(foo, foo_content)? + .enable_pull_diagnostics(false) + .build()? + .wait_until_workspaces_are_initialized()?; + + server.open_text_document(foo, &foo_content, 1); + let diagnostics = server.await_notification::()?; + + insta::assert_debug_snapshot!(diagnostics); + + Ok(()) +} diff --git a/crates/ty_server/tests/e2e/pull_diagnostics.rs b/crates/ty_server/tests/e2e/pull_diagnostics.rs new file mode 100644 index 00000000000000..cfc803038f0b7b --- /dev/null +++ b/crates/ty_server/tests/e2e/pull_diagnostics.rs @@ -0,0 +1,29 @@ +use anyhow::Result; +use ruff_db::system::SystemPath; +use ty_server::ClientOptions; + +use crate::TestServerBuilder; + +#[test] +fn on_did_open() -> Result<()> { + let workspace_root = SystemPath::new("src"); + let foo = SystemPath::new("src/foo.py"); + let foo_content = "\ +def foo() -> str: + return 42 +"; + + let mut server = TestServerBuilder::new()? + .with_workspace(workspace_root, ClientOptions::default())? + .with_file(foo, foo_content)? + .enable_pull_diagnostics(true) + .build()? + .wait_until_workspaces_are_initialized()?; + + server.open_text_document(foo, &foo_content, 1); + let diagnostics = server.document_diagnostic_request(foo)?; + + insta::assert_debug_snapshot!(diagnostics); + + Ok(()) +} diff --git a/crates/ty_server/src/snapshots/ty_server__server__tests__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap similarity index 96% rename from crates/ty_server/src/snapshots/ty_server__server__tests__initialization.snap rename to crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index 9c76c557427fec..8dcfd7346927d2 100644 --- a/crates/ty_server/src/snapshots/ty_server__server__tests__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -1,5 +1,5 @@ --- -source: crates/ty_server/src/server.rs +source: crates/ty_server/tests/e2e/initialize.rs expression: initialization_result --- { diff --git a/crates/ty_server/src/snapshots/ty_server__server__tests__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap similarity index 96% rename from crates/ty_server/src/snapshots/ty_server__server__tests__initialization_with_workspace.snap rename to crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index 9c76c557427fec..8dcfd7346927d2 100644 --- a/crates/ty_server/src/snapshots/ty_server__server__tests__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -1,5 +1,5 @@ --- -source: crates/ty_server/src/server.rs +source: crates/ty_server/tests/e2e/initialize.rs expression: initialization_result --- { diff --git a/crates/ty_server/src/snapshots/ty_server__server__tests__publish_diagnostics_on_did_open.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open.snap similarity index 98% rename from crates/ty_server/src/snapshots/ty_server__server__tests__publish_diagnostics_on_did_open.snap rename to crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open.snap index 5ed7b5a20be536..be48dde9dca045 100644 --- a/crates/ty_server/src/snapshots/ty_server__server__tests__publish_diagnostics_on_did_open.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__on_did_open.snap @@ -1,5 +1,5 @@ --- -source: crates/ty_server/src/server.rs +source: crates/ty_server/tests/e2e/publish_diagnostics.rs expression: diagnostics --- PublishDiagnosticsParams { diff --git a/crates/ty_server/src/snapshots/ty_server__server__tests__pull_diagnostics_on_did_open.snap b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__on_did_open.snap similarity index 98% rename from crates/ty_server/src/snapshots/ty_server__server__tests__pull_diagnostics_on_did_open.snap rename to crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__on_did_open.snap index 27f94d7b4d396b..89c5daba5dd326 100644 --- a/crates/ty_server/src/snapshots/ty_server__server__tests__pull_diagnostics_on_did_open.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__on_did_open.snap @@ -1,5 +1,5 @@ --- -source: crates/ty_server/src/server.rs +source: crates/ty_server/tests/e2e/pull_diagnostics.rs expression: diagnostics --- Report(