Skip to content

Commit 290d685

Browse files
committed
[ty] Implement mock language server for testing
1 parent f3a2740 commit 290d685

10 files changed

Lines changed: 817 additions & 41 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ tracing = { workspace = true }
3737
tracing-subscriber = { workspace = true, features = ["chrono"] }
3838

3939
[dev-dependencies]
40+
insta = { workspace = true, features = ["json"] }
4041

4142
[target.'cfg(target_vendor = "apple")'.dependencies]
4243
libc = { workspace = true }

crates/ty_server/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ mod server;
1313
mod session;
1414
mod system;
1515

16+
#[cfg(test)]
17+
pub mod test;
18+
1619
pub(crate) const SERVER_NAME: &str = "ty";
1720
pub(crate) const DIAGNOSTIC_NAME: &str = "ty";
1821

@@ -30,7 +33,7 @@ pub fn run_server() -> anyhow::Result<()> {
3033

3134
let (connection, io_threads) = Connection::stdio();
3235

33-
let server_result = Server::new(worker_threads, connection)
36+
let server_result = Server::new(worker_threads, connection, None)
3437
.context("Failed to start server")?
3538
.run();
3639

crates/ty_server/src/server.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ use lsp_types::{
1111
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
1212
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
1313
};
14+
use ruff_db::system::System;
1415
use std::num::NonZeroUsize;
15-
use std::panic::PanicHookInfo;
16+
use std::panic::{PanicHookInfo, RefUnwindSafe};
1617
use std::sync::Arc;
1718

1819
mod api;
@@ -35,7 +36,11 @@ pub(crate) struct Server {
3536
}
3637

3738
impl Server {
38-
pub(crate) fn new(worker_threads: NonZeroUsize, connection: Connection) -> crate::Result<Self> {
39+
pub(crate) fn new(
40+
worker_threads: NonZeroUsize,
41+
connection: Connection,
42+
fallback_system: Option<Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>>,
43+
) -> crate::Result<Self> {
3944
let (id, init_value) = connection.initialize_start()?;
4045
let init_params: InitializeParams = serde_json::from_value(init_value)?;
4146

@@ -102,7 +107,11 @@ impl Server {
102107
.collect()
103108
})
104109
.or_else(|| {
105-
let current_dir = std::env::current_dir().ok()?;
110+
let current_dir = if let Some(ref system) = fallback_system {
111+
system.current_directory().as_std_path().to_path_buf()
112+
} else {
113+
std::env::current_dir().ok()?
114+
};
106115
tracing::warn!(
107116
"No workspace(s) were provided during initialization. \
108117
Using the current working directory as a default workspace: {}",
@@ -143,6 +152,7 @@ impl Server {
143152
position_encoding,
144153
global_options,
145154
workspaces,
155+
fallback_system,
146156
)?,
147157
client_capabilities,
148158
})
@@ -286,3 +296,43 @@ impl Drop for ServerPanicHookHandler {
286296
}
287297
}
288298
}
299+
300+
#[cfg(test)]
301+
mod tests {
302+
use ruff_db::system::{InMemorySystem, SystemPathBuf};
303+
304+
use crate::session::ClientOptions;
305+
use crate::test::TestServerBuilder;
306+
307+
#[test]
308+
fn initialization_sequence() {
309+
let system = InMemorySystem::default();
310+
let test_server = TestServerBuilder::new()
311+
.with_memory_system(system)
312+
.build()
313+
.unwrap()
314+
.wait_until_workspaces_are_initialized()
315+
.unwrap();
316+
317+
let initialization_result = test_server.initialization_result().unwrap();
318+
319+
insta::assert_json_snapshot!("initialization_capabilities", initialization_result);
320+
}
321+
322+
#[test]
323+
fn initialization_with_workspace() {
324+
let workspace_root = SystemPathBuf::from("/foo");
325+
let system = InMemorySystem::new(workspace_root.clone());
326+
let test_server = TestServerBuilder::new()
327+
.with_memory_system(system)
328+
.with_workspace(&workspace_root, ClientOptions::default())
329+
.build()
330+
.unwrap()
331+
.wait_until_workspaces_are_initialized()
332+
.unwrap();
333+
334+
let initialization_result = test_server.initialization_result().unwrap();
335+
336+
insta::assert_json_snapshot!("initialization_with_workspace", initialization_result);
337+
}
338+
}

crates/ty_server/src/session.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
33
use std::collections::{BTreeMap, VecDeque};
44
use std::ops::{Deref, DerefMut};
5+
use std::panic::RefUnwindSafe;
56
use std::sync::Arc;
67

78
use anyhow::{Context, anyhow};
89
use index::DocumentQueryError;
910
use lsp_server::Message;
11+
use lsp_types::notification::{Exit, Notification};
12+
use lsp_types::request::{Request, Shutdown};
1013
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url};
1114
use options::GlobalOptions;
1215
use ruff_db::Db;
@@ -36,6 +39,9 @@ mod settings;
3639

3740
/// The global state for the LSP
3841
pub(crate) struct Session {
42+
/// A fallback system to use with the [`LSPSystem`].
43+
fallback_system: Option<Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>>,
44+
3945
/// Used to retrieve information about open documents and settings.
4046
///
4147
/// This will be [`None`] when a mutable reference is held to the index via [`index_mut`]
@@ -79,6 +85,7 @@ impl Session {
7985
position_encoding: PositionEncoding,
8086
global_options: GlobalOptions,
8187
workspace_folders: Vec<(Url, ClientOptions)>,
88+
fallback_system: Option<Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>>,
8289
) -> crate::Result<Self> {
8390
let index = Arc::new(Index::new(global_options.into_settings()));
8491

@@ -88,6 +95,7 @@ impl Session {
8895
}
8996

9097
Ok(Self {
98+
fallback_system,
9199
position_encoding,
92100
workspaces,
93101
deferred_messages: VecDeque::new(),
@@ -137,6 +145,9 @@ impl Session {
137145
} else {
138146
match &message {
139147
Message::Request(request) => {
148+
if request.method == Shutdown::METHOD {
149+
return Some(message);
150+
}
140151
tracing::debug!(
141152
"Deferring `{}` request until all workspaces are initialized",
142153
request.method
@@ -147,6 +158,9 @@ impl Session {
147158
return Some(message);
148159
}
149160
Message::Notification(notification) => {
161+
if notification.method == Exit::METHOD {
162+
return Some(message);
163+
}
150164
tracing::debug!(
151165
"Deferring `{}` notification until all workspaces are initialized",
152166
notification.method
@@ -171,9 +185,12 @@ impl Session {
171185
/// If the path is a virtual path, it will return the first project database in the session.
172186
pub(crate) fn project_db(&self, path: &AnySystemPath) -> &ProjectDatabase {
173187
match path {
174-
AnySystemPath::System(system_path) => self
175-
.project_db_for_path(system_path)
176-
.unwrap_or_else(|| self.default_project.get(self.index.as_ref())),
188+
AnySystemPath::System(system_path) => {
189+
self.project_db_for_path(system_path).unwrap_or_else(|| {
190+
self.default_project
191+
.get(self.index.as_ref(), self.fallback_system.as_ref())
192+
})
193+
}
177194
AnySystemPath::SystemVirtual(_virtual_path) => {
178195
// TODO: Currently, ty only supports single workspace but we need to figure out
179196
// which project should this virtual path belong to when there are multiple
@@ -196,7 +213,10 @@ impl Session {
196213
.range_mut(..=system_path.to_path_buf())
197214
.next_back()
198215
.map(|(_, db)| db)
199-
.unwrap_or_else(|| self.default_project.get_mut(self.index.as_ref())),
216+
.unwrap_or_else(|| {
217+
self.default_project
218+
.get_mut(self.index.as_ref(), self.fallback_system.as_ref())
219+
}),
200220
AnySystemPath::SystemVirtual(_virtual_path) => {
201221
// TODO: Currently, ty only supports single workspace but we need to figure out
202222
// which project should this virtual path belong to when there are multiple
@@ -268,7 +288,10 @@ impl Session {
268288
// For now, create one project database per workspace.
269289
// In the future, index the workspace directories to find all projects
270290
// and create a project database for each.
271-
let system = LSPSystem::new(self.index.as_ref().unwrap().clone());
291+
let system = LSPSystem::new(
292+
self.index.as_ref().unwrap().clone(),
293+
self.fallback_system.clone(),
294+
);
272295

273296
let project = ProjectMetadata::discover(&root, &system)
274297
.context("Failed to discover project configuration")
@@ -663,11 +686,15 @@ impl DefaultProject {
663686
DefaultProject(std::sync::OnceLock::new())
664687
}
665688

666-
pub(crate) fn get(&self, index: Option<&Arc<Index>>) -> &ProjectDatabase {
689+
pub(crate) fn get(
690+
&self,
691+
index: Option<&Arc<Index>>,
692+
fallback_system: Option<&Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>>,
693+
) -> &ProjectDatabase {
667694
self.0.get_or_init(|| {
668695
tracing::info!("Initialize default project");
669696

670-
let system = LSPSystem::new(index.unwrap().clone());
697+
let system = LSPSystem::new(index.unwrap().clone(), fallback_system.cloned());
671698
let metadata = ProjectMetadata::from_options(
672699
Options::default(),
673700
system.current_directory().to_path_buf(),
@@ -678,8 +705,12 @@ impl DefaultProject {
678705
})
679706
}
680707

681-
pub(crate) fn get_mut(&mut self, index: Option<&Arc<Index>>) -> &mut ProjectDatabase {
682-
let _ = self.get(index);
708+
pub(crate) fn get_mut(
709+
&mut self,
710+
index: Option<&Arc<Index>>,
711+
fallback_system: Option<&Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>>,
712+
) -> &mut ProjectDatabase {
713+
let _ = self.get(index, fallback_system);
683714

684715
// SAFETY: The `OnceLock` is guaranteed to be initialized at this point because
685716
// we called `get` above, which initializes it if it wasn't already.

crates/ty_server/src/session/options.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct WorkspaceOptions {
4848

4949
/// This is a direct representation of the settings schema sent by the client.
5050
#[derive(Clone, Debug, Deserialize, Default)]
51-
#[cfg_attr(test, derive(PartialEq, Eq))]
51+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
5252
#[serde(rename_all = "camelCase")]
5353
pub(crate) struct ClientOptions {
5454
/// Settings under the `python.*` namespace in VS Code that are useful for the ty language
@@ -62,7 +62,7 @@ pub(crate) struct ClientOptions {
6262

6363
/// Diagnostic mode for the language server.
6464
#[derive(Clone, Copy, Debug, Default, Deserialize)]
65-
#[cfg_attr(test, derive(PartialEq, Eq))]
65+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
6666
#[serde(rename_all = "camelCase")]
6767
pub(crate) enum DiagnosticMode {
6868
/// Check only currently open files.
@@ -139,21 +139,21 @@ impl ClientOptions {
139139
// all settings and not just the ones in "python.*".
140140

141141
#[derive(Clone, Debug, Deserialize, Default)]
142-
#[cfg_attr(test, derive(PartialEq, Eq))]
142+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
143143
#[serde(rename_all = "camelCase")]
144144
struct Python {
145145
ty: Option<Ty>,
146146
}
147147

148148
#[derive(Clone, Debug, Deserialize, Default)]
149-
#[cfg_attr(test, derive(PartialEq, Eq))]
149+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
150150
#[serde(rename_all = "camelCase")]
151151
struct PythonExtension {
152152
active_environment: Option<ActiveEnvironment>,
153153
}
154154

155155
#[derive(Clone, Debug, Deserialize)]
156-
#[cfg_attr(test, derive(PartialEq, Eq))]
156+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
157157
#[serde(rename_all = "camelCase")]
158158
pub(crate) struct ActiveEnvironment {
159159
pub(crate) executable: PythonExecutable,
@@ -162,7 +162,7 @@ pub(crate) struct ActiveEnvironment {
162162
}
163163

164164
#[derive(Clone, Debug, Deserialize)]
165-
#[cfg_attr(test, derive(PartialEq, Eq))]
165+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
166166
#[serde(rename_all = "camelCase")]
167167
pub(crate) struct EnvironmentVersion {
168168
pub(crate) major: i64,
@@ -174,7 +174,7 @@ pub(crate) struct EnvironmentVersion {
174174
}
175175

176176
#[derive(Clone, Debug, Deserialize)]
177-
#[cfg_attr(test, derive(PartialEq, Eq))]
177+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
178178
#[serde(rename_all = "camelCase")]
179179
pub(crate) struct PythonEnvironment {
180180
pub(crate) folder_uri: Url,
@@ -186,7 +186,7 @@ pub(crate) struct PythonEnvironment {
186186
}
187187

188188
#[derive(Clone, Debug, Deserialize)]
189-
#[cfg_attr(test, derive(PartialEq, Eq))]
189+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
190190
#[serde(rename_all = "camelCase")]
191191
pub(crate) struct PythonExecutable {
192192
#[allow(dead_code)]
@@ -195,7 +195,7 @@ pub(crate) struct PythonExecutable {
195195
}
196196

197197
#[derive(Clone, Debug, Deserialize, Default)]
198-
#[cfg_attr(test, derive(PartialEq, Eq))]
198+
#[cfg_attr(test, derive(serde::Serialize, PartialEq, Eq))]
199199
#[serde(rename_all = "camelCase")]
200200
struct Ty {
201201
disable_language_services: Option<bool>,

0 commit comments

Comments
 (0)