[ty] Move server tests as integration tests (#19522)

## Summary

Reference:
https://github.com/astral-sh/ruff/pull/19391#discussion_r2222780892
This commit is contained in:
Dhruv Manilawala 2025-07-24 21:40:17 +05:30 committed by GitHub
parent 1d2181623c
commit f9091ea8bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 172 additions and 128 deletions

1
Cargo.lock generated
View file

@ -4344,6 +4344,7 @@ dependencies = [
"ty_ide",
"ty_project",
"ty_python_semantic",
"ty_server",
"ty_vendored",
]

View file

@ -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 }

View file

@ -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";

View file

@ -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]

View file

@ -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<T> = std::result::Result<T, api::Error>;
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<dyn System + 'static + Send + Sync + RefUnwindSafe>,
@ -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::<PublishDiagnostics>()?;
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(())
}
}

View file

@ -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;

View file

@ -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<Python>,
@ -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<Ty>,
}
#[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<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 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<bool>,

View file

@ -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(())
}

View file

@ -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<Connection>,
/// 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<R>(&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<SystemPath>) -> 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<P, C, I>(mut self, files: I) -> Result<Self>
where

View file

@ -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::<PublishDiagnostics>()?;
insta::assert_debug_snapshot!(diagnostics);
Ok(())
}

View file

@ -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(())
}

View file

@ -1,5 +1,5 @@
---
source: crates/ty_server/src/server.rs
source: crates/ty_server/tests/e2e/initialize.rs
expression: initialization_result
---
{

View file

@ -1,5 +1,5 @@
---
source: crates/ty_server/src/server.rs
source: crates/ty_server/tests/e2e/initialize.rs
expression: initialization_result
---
{

View file

@ -1,5 +1,5 @@
---
source: crates/ty_server/src/server.rs
source: crates/ty_server/tests/e2e/publish_diagnostics.rs
expression: diagnostics
---
PublishDiagnosticsParams {

View file

@ -1,5 +1,5 @@
---
source: crates/ty_server/src/server.rs
source: crates/ty_server/tests/e2e/pull_diagnostics.rs
expression: diagnostics
---
Report(