mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 22:54:42 +00:00
[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:
parent
1d2181623c
commit
f9091ea8bb
15 changed files with 172 additions and 128 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4344,6 +4344,7 @@ dependencies = [
|
||||||
"ty_ide",
|
"ty_ide",
|
||||||
"ty_project",
|
"ty_project",
|
||||||
"ty_python_semantic",
|
"ty_python_semantic",
|
||||||
|
"ty_server",
|
||||||
"ty_vendored",
|
"ty_vendored",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -38,11 +38,16 @@ tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["chrono"] }
|
tracing-subscriber = { workspace = true, features = ["chrono"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
ty_server = { workspace = true, features = ["testing"] }
|
||||||
|
|
||||||
dunce = { workspace = true }
|
dunce = { workspace = true }
|
||||||
insta = { workspace = true, features = ["filters", "json"] }
|
insta = { workspace = true, features = ["filters", "json"] }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
testing = []
|
||||||
|
|
||||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ use anyhow::Context;
|
||||||
use lsp_server::Connection;
|
use lsp_server::Connection;
|
||||||
use ruff_db::system::{OsSystem, SystemPathBuf};
|
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 use document::{NotebookDocument, PositionEncoding, TextDocument};
|
||||||
pub(crate) use session::{DocumentQuery, Session};
|
pub(crate) use session::{DocumentQuery, Session};
|
||||||
|
|
||||||
|
@ -14,9 +16,6 @@ mod server;
|
||||||
mod session;
|
mod session;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod test;
|
|
||||||
|
|
||||||
pub(crate) const SERVER_NAME: &str = "ty";
|
pub(crate) const SERVER_NAME: &str = "ty";
|
||||||
pub(crate) const DIAGNOSTIC_NAME: &str = "ty";
|
pub(crate) const DIAGNOSTIC_NAME: &str = "ty";
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use tracing_subscriber::fmt::time::ChronoLocal;
|
||||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
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
|
let log_file = log_file
|
||||||
.map(|path| {
|
.map(|path| {
|
||||||
// this expands `logFile` so that tildes and environment variables
|
// 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`.
|
/// The default log level is `info`.
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub(crate) enum LogLevel {
|
pub enum LogLevel {
|
||||||
Error,
|
Error,
|
||||||
Warn,
|
Warn,
|
||||||
#[default]
|
#[default]
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub(crate) use api::publish_settings_diagnostics;
|
||||||
pub(crate) use main_loop::{Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender};
|
pub(crate) use main_loop::{Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender};
|
||||||
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
|
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
|
||||||
|
|
||||||
pub(crate) struct Server {
|
pub struct Server {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
client_capabilities: ClientCapabilities,
|
client_capabilities: ClientCapabilities,
|
||||||
worker_threads: NonZeroUsize,
|
worker_threads: NonZeroUsize,
|
||||||
|
@ -36,7 +36,7 @@ pub(crate) struct Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub(crate) fn new(
|
pub fn new(
|
||||||
worker_threads: NonZeroUsize,
|
worker_threads: NonZeroUsize,
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
native_system: Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>,
|
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(
|
let client = Client::new(
|
||||||
self.main_loop_sender.clone(),
|
self.main_loop_sender.clone(),
|
||||||
self.connection.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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata};
|
||||||
|
|
||||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||||
pub(crate) use self::index::DocumentQuery;
|
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;
|
pub(crate) use self::settings::ClientSettings;
|
||||||
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
|
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||||
use crate::server::publish_settings_diagnostics;
|
use crate::server::publish_settings_diagnostics;
|
||||||
|
|
|
@ -49,9 +49,10 @@ struct WorkspaceOptions {
|
||||||
|
|
||||||
/// This is a direct representation of the settings schema sent by the client.
|
/// This is a direct representation of the settings schema sent by the client.
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[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")]
|
#[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
|
/// Settings under the `python.*` namespace in VS Code that are useful for the ty language
|
||||||
/// server.
|
/// server.
|
||||||
python: Option<Python>,
|
python: Option<Python>,
|
||||||
|
@ -63,7 +64,8 @@ pub(crate) struct ClientOptions {
|
||||||
|
|
||||||
/// Diagnostic mode for the language server.
|
/// Diagnostic mode for the language server.
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) enum DiagnosticMode {
|
pub(crate) enum DiagnosticMode {
|
||||||
/// Check only currently open files.
|
/// Check only currently open files.
|
||||||
|
@ -147,21 +149,24 @@ impl ClientOptions {
|
||||||
// all settings and not just the ones in "python.*".
|
// all settings and not just the ones in "python.*".
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct Python {
|
struct Python {
|
||||||
ty: Option<Ty>,
|
ty: Option<Ty>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct PythonExtension {
|
struct PythonExtension {
|
||||||
active_environment: Option<ActiveEnvironment>,
|
active_environment: Option<ActiveEnvironment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct ActiveEnvironment {
|
pub(crate) struct ActiveEnvironment {
|
||||||
pub(crate) executable: PythonExecutable,
|
pub(crate) executable: PythonExecutable,
|
||||||
|
@ -170,7 +175,8 @@ pub(crate) struct ActiveEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct EnvironmentVersion {
|
pub(crate) struct EnvironmentVersion {
|
||||||
pub(crate) major: i64,
|
pub(crate) major: i64,
|
||||||
|
@ -182,7 +188,8 @@ pub(crate) struct EnvironmentVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct PythonEnvironment {
|
pub(crate) struct PythonEnvironment {
|
||||||
pub(crate) folder_uri: Url,
|
pub(crate) folder_uri: Url,
|
||||||
|
@ -194,7 +201,8 @@ pub(crate) struct PythonEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct PythonExecutable {
|
pub(crate) struct PythonExecutable {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -203,7 +211,8 @@ pub(crate) struct PythonExecutable {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct Ty {
|
struct Ty {
|
||||||
disable_language_services: Option<bool>,
|
disable_language_services: Option<bool>,
|
||||||
|
|
33
crates/ty_server/tests/e2e/initialize.rs
Normal file
33
crates/ty_server/tests/e2e/initialize.rs
Normal 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(())
|
||||||
|
}
|
|
@ -1,9 +1,35 @@
|
||||||
//! Testing server for the ty language server.
|
//! Testing server for the ty language server.
|
||||||
//!
|
//!
|
||||||
//! This module provides mock server infrastructure for testing LSP functionality using
|
//! This module provides mock server infrastructure for testing LSP functionality using a
|
||||||
//! temporary directories on the real filesystem.
|
//! temporary directory on the real filesystem.
|
||||||
//!
|
//!
|
||||||
//! The design is inspired by the Starlark LSP test server but adapted for ty server architecture.
|
//! 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::hash_map::Entry;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
@ -39,9 +65,7 @@ use rustc_hash::FxHashMap;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::logging::{LogLevel, init_logging};
|
use ty_server::{ClientOptions, LogLevel, Server, init_logging};
|
||||||
use crate::server::Server;
|
|
||||||
use crate::session::ClientOptions;
|
|
||||||
|
|
||||||
/// Number of times to retry receiving a message before giving up
|
/// Number of times to retry receiving a message before giving up
|
||||||
const RETRY_COUNT: usize = 5;
|
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,
|
/// A test server for the ty language server that provides helpers for sending requests,
|
||||||
/// correlating responses, and handling notifications.
|
/// 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 {
|
pub(crate) struct TestServer {
|
||||||
/// The thread that's actually running the server.
|
/// The thread that's actually running the server.
|
||||||
///
|
///
|
||||||
|
@ -103,10 +123,10 @@ pub(crate) struct TestServer {
|
||||||
/// the connection to be cleaned up properly.
|
/// the connection to be cleaned up properly.
|
||||||
client_connection: Option<Connection>,
|
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.
|
/// This directory is automatically cleaned up when the [`TestServer`] is dropped.
|
||||||
test_dir: TestContext,
|
test_context: TestContext,
|
||||||
|
|
||||||
/// Incrementing counter to automatically generate request IDs
|
/// Incrementing counter to automatically generate request IDs
|
||||||
request_counter: i32,
|
request_counter: i32,
|
||||||
|
@ -175,7 +195,7 @@ impl TestServer {
|
||||||
Self {
|
Self {
|
||||||
server_thread: Some(server_thread),
|
server_thread: Some(server_thread),
|
||||||
client_connection: Some(client_connection),
|
client_connection: Some(client_connection),
|
||||||
test_dir,
|
test_context: test_dir,
|
||||||
request_counter: 0,
|
request_counter: 0,
|
||||||
responses: FxHashMap::default(),
|
responses: FxHashMap::default(),
|
||||||
notifications: VecDeque::new(),
|
notifications: VecDeque::new(),
|
||||||
|
@ -297,9 +317,9 @@ impl TestServer {
|
||||||
|
|
||||||
/// Send a request to the server and return the request ID.
|
/// 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
|
pub(crate) fn send_request<R>(&mut self, params: R::Params) -> RequestId
|
||||||
where
|
where
|
||||||
R: Request,
|
R: Request,
|
||||||
|
@ -505,7 +525,7 @@ impl TestServer {
|
||||||
/// Use the [`get_request`] method to wait for the server to send this request.
|
/// Use the [`get_request`] method to wait for the server to send this request.
|
||||||
///
|
///
|
||||||
/// [`get_request`]: TestServer::get_request
|
/// [`get_request`]: TestServer::get_request
|
||||||
pub(crate) fn handle_workspace_configuration_request(
|
fn handle_workspace_configuration_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
request_id: RequestId,
|
request_id: RequestId,
|
||||||
params: &ConfigurationParams,
|
params: &ConfigurationParams,
|
||||||
|
@ -548,7 +568,7 @@ impl TestServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_uri(&self, path: impl AsRef<SystemPath>) -> Url {
|
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")
|
.expect("Path must be a valid URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -628,7 +648,7 @@ impl TestServer {
|
||||||
impl fmt::Debug for TestServer {
|
impl fmt::Debug for TestServer {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("TestServer")
|
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("request_counter", &self.request_counter)
|
||||||
.field("responses", &self.responses)
|
.field("responses", &self.responses)
|
||||||
.field("notifications", &self.notifications)
|
.field("notifications", &self.notifications)
|
||||||
|
@ -750,6 +770,7 @@ impl TestServerBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable or disable pull diagnostics capability
|
/// Enable or disable pull diagnostics capability
|
||||||
|
#[must_use]
|
||||||
pub(crate) fn enable_pull_diagnostics(mut self, enabled: bool) -> Self {
|
pub(crate) fn enable_pull_diagnostics(mut self, enabled: bool) -> Self {
|
||||||
self.client_capabilities
|
self.client_capabilities
|
||||||
.text_document
|
.text_document
|
||||||
|
@ -763,6 +784,7 @@ impl TestServerBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable or disable file watching capability
|
/// Enable or disable file watching capability
|
||||||
|
#[must_use]
|
||||||
#[expect(dead_code)]
|
#[expect(dead_code)]
|
||||||
pub(crate) fn enable_did_change_watched_files(mut self, enabled: bool) -> Self {
|
pub(crate) fn enable_did_change_watched_files(mut self, enabled: bool) -> Self {
|
||||||
self.client_capabilities
|
self.client_capabilities
|
||||||
|
@ -777,6 +799,7 @@ impl TestServerBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set custom client capabilities (overrides any previously set capabilities)
|
/// Set custom client capabilities (overrides any previously set capabilities)
|
||||||
|
#[must_use]
|
||||||
#[expect(dead_code)]
|
#[expect(dead_code)]
|
||||||
pub(crate) fn with_client_capabilities(mut self, capabilities: ClientCapabilities) -> Self {
|
pub(crate) fn with_client_capabilities(mut self, capabilities: ClientCapabilities) -> Self {
|
||||||
self.client_capabilities = capabilities;
|
self.client_capabilities = capabilities;
|
||||||
|
@ -798,7 +821,7 @@ impl TestServerBuilder {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write multiple files to the temporary directory
|
/// Write multiple files to the test directory
|
||||||
#[expect(dead_code)]
|
#[expect(dead_code)]
|
||||||
pub(crate) fn with_files<P, C, I>(mut self, files: I) -> Result<Self>
|
pub(crate) fn with_files<P, C, I>(mut self, files: I) -> Result<Self>
|
||||||
where
|
where
|
30
crates/ty_server/tests/e2e/publish_diagnostics.rs
Normal file
30
crates/ty_server/tests/e2e/publish_diagnostics.rs
Normal 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(())
|
||||||
|
}
|
29
crates/ty_server/tests/e2e/pull_diagnostics.rs
Normal file
29
crates/ty_server/tests/e2e/pull_diagnostics.rs
Normal 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(())
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: crates/ty_server/src/server.rs
|
source: crates/ty_server/tests/e2e/initialize.rs
|
||||||
expression: initialization_result
|
expression: initialization_result
|
||||||
---
|
---
|
||||||
{
|
{
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: crates/ty_server/src/server.rs
|
source: crates/ty_server/tests/e2e/initialize.rs
|
||||||
expression: initialization_result
|
expression: initialization_result
|
||||||
---
|
---
|
||||||
{
|
{
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: crates/ty_server/src/server.rs
|
source: crates/ty_server/tests/e2e/publish_diagnostics.rs
|
||||||
expression: diagnostics
|
expression: diagnostics
|
||||||
---
|
---
|
||||||
PublishDiagnosticsParams {
|
PublishDiagnosticsParams {
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: crates/ty_server/src/server.rs
|
source: crates/ty_server/tests/e2e/pull_diagnostics.rs
|
||||||
expression: diagnostics
|
expression: diagnostics
|
||||||
---
|
---
|
||||||
Report(
|
Report(
|
Loading…
Add table
Add a link
Reference in a new issue