mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 04:29:47 +00:00
[ty] Add panic-by-default await methods to TestServer (#21451)
This commit is contained in:
parent
8529d79a70
commit
008e9d06e1
10 changed files with 365 additions and 270 deletions
|
|
@ -34,6 +34,7 @@ salsa = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
shellexpand = { workspace = true }
|
||||
smallvec = { workspace=true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["chrono"] }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ fn execute_command(
|
|||
server: &mut TestServer,
|
||||
command: String,
|
||||
arguments: Vec<serde_json::Value>,
|
||||
) -> anyhow::Result<Option<serde_json::Value>> {
|
||||
) -> Option<serde_json::Value> {
|
||||
let params = ExecuteCommandParams {
|
||||
command,
|
||||
arguments,
|
||||
|
|
@ -32,10 +32,10 @@ return 42
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_pull_diagnostics(false)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let response = execute_command(&mut server, "ty.printDebugInformation".to_string(), vec![])?;
|
||||
let response = execute_command(&mut server, "ty.printDebugInformation".to_string(), vec![]);
|
||||
let response = response.expect("expect server response");
|
||||
|
||||
let response = response
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use crate::TestServerBuilder;
|
|||
#[test]
|
||||
fn empty_workspace_folders() -> Result<()> {
|
||||
let server = TestServerBuilder::new()?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let initialization_result = server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -24,8 +24,8 @@ fn single_workspace_folder() -> Result<()> {
|
|||
let workspace_root = SystemPath::new("foo");
|
||||
let server = TestServerBuilder::new()?
|
||||
.with_workspace(workspace_root, None)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let initialization_result = server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -47,12 +47,12 @@ fn workspace_diagnostic_registration_without_configuration() -> Result<()> {
|
|||
.with_workspace(workspace_root, None)?
|
||||
.enable_workspace_configuration(false)
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?;
|
||||
.build();
|
||||
|
||||
// No need to wait for workspaces to initialize as the client does not support workspace
|
||||
// configuration.
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -90,12 +90,12 @@ fn open_files_diagnostic_registration_without_configuration() -> Result<()> {
|
|||
.with_workspace(workspace_root, None)?
|
||||
.enable_workspace_configuration(false)
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?;
|
||||
.build();
|
||||
|
||||
// No need to wait for workspaces to initialize as the client does not support workspace
|
||||
// configuration.
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -131,10 +131,10 @@ fn workspace_diagnostic_registration_via_initialization() -> Result<()> {
|
|||
)
|
||||
.with_workspace(workspace_root, None)?
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -170,10 +170,10 @@ fn open_files_diagnostic_registration_via_initialization() -> Result<()> {
|
|||
)
|
||||
.with_workspace(workspace_root, None)?
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -209,10 +209,10 @@ fn workspace_diagnostic_registration() -> Result<()> {
|
|||
Some(ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace)),
|
||||
)?
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -248,10 +248,10 @@ fn open_files_diagnostic_registration() -> Result<()> {
|
|||
Some(ClientOptions::default().with_diagnostic_mode(DiagnosticMode::OpenFilesOnly)),
|
||||
)?
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -291,11 +291,11 @@ def foo() -> str:
|
|||
.with_workspace(workspace_root, None)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.with_file(foo, foo_content)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let hover = server.hover_request(foo, Position::new(0, 5))?;
|
||||
let hover = server.hover_request(foo, Position::new(0, 5));
|
||||
|
||||
assert!(
|
||||
hover.is_none(),
|
||||
|
|
@ -323,11 +323,11 @@ def foo() -> str:
|
|||
)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.with_file(foo, foo_content)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let hover = server.hover_request(foo, Position::new(0, 5))?;
|
||||
let hover = server.hover_request(foo, Position::new(0, 5));
|
||||
|
||||
assert!(
|
||||
hover.is_none(),
|
||||
|
|
@ -364,18 +364,18 @@ def bar() -> str:
|
|||
.enable_pull_diagnostics(true)
|
||||
.with_file(foo, foo_content)?
|
||||
.with_file(bar, bar_content)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let hover_foo = server.hover_request(foo, Position::new(0, 5))?;
|
||||
let hover_foo = server.hover_request(foo, Position::new(0, 5));
|
||||
assert!(
|
||||
hover_foo.is_none(),
|
||||
"Expected no hover information for workspace A, got: {hover_foo:?}"
|
||||
);
|
||||
|
||||
server.open_text_document(bar, &bar_content, 1);
|
||||
let hover_bar = server.hover_request(bar, Position::new(0, 5))?;
|
||||
let hover_bar = server.hover_request(bar, Position::new(0, 5));
|
||||
assert!(
|
||||
hover_bar.is_some(),
|
||||
"Expected hover information for workspace B, got: {hover_bar:?}"
|
||||
|
|
@ -394,10 +394,10 @@ fn unknown_initialization_options() -> Result<()> {
|
|||
.with_initialization_options(
|
||||
ClientOptions::default().with_unknown([("bar".to_string(), Value::Null)].into()),
|
||||
)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let show_message_params = server.await_notification::<ShowMessage>()?;
|
||||
let show_message_params = server.await_notification::<ShowMessage>();
|
||||
|
||||
insta::assert_json_snapshot!(show_message_params, @r#"
|
||||
{
|
||||
|
|
@ -419,10 +419,10 @@ fn unknown_options_in_workspace_configuration() -> Result<()> {
|
|||
workspace_root,
|
||||
Some(ClientOptions::default().with_unknown([("bar".to_string(), Value::Null)].into())),
|
||||
)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let show_message_params = server.await_notification::<ShowMessage>()?;
|
||||
let show_message_params = server.await_notification::<ShowMessage>();
|
||||
|
||||
insta::assert_json_snapshot!(show_message_params, @r#"
|
||||
{
|
||||
|
|
@ -443,10 +443,10 @@ fn register_rename_capability_when_enabled() -> Result<()> {
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_initialization_options(ClientOptions::default().with_experimental_rename(true))
|
||||
.enable_rename_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let [registration] = params.registrations.as_slice() else {
|
||||
panic!(
|
||||
"Expected a single registration, got: {:#?}",
|
||||
|
|
@ -477,8 +477,8 @@ fn rename_available_without_dynamic_registration() -> Result<()> {
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_initialization_options(ClientOptions::default().with_experimental_rename(true))
|
||||
.enable_rename_dynamic_registration(false)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let initialization_result = server.initialization_result().unwrap();
|
||||
insta::assert_json_snapshot!(initialization_result.capabilities.rename_provider, @r#"
|
||||
|
|
@ -500,8 +500,8 @@ fn not_register_rename_capability_when_disabled() -> Result<()> {
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_initialization_options(ClientOptions::default().with_experimental_rename(false))
|
||||
.enable_rename_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
// The `Drop` implementation will make sure that the client did not receive any registration
|
||||
// request.
|
||||
|
|
@ -525,10 +525,10 @@ fn register_multiple_capabilities() -> Result<()> {
|
|||
)
|
||||
.enable_rename_dynamic_registration(true)
|
||||
.enable_diagnostic_dynamic_registration(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let (_, params) = server.await_request::<RegisterCapability>()?;
|
||||
let (_, params) = server.await_request::<RegisterCapability>();
|
||||
let registrations = params.registrations;
|
||||
|
||||
assert_eq!(registrations.len(), 2);
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ y = foo(1)
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_inlay_hints(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let _ = server.await_notification::<PublishDiagnostics>()?;
|
||||
let _ = server.await_notification::<PublishDiagnostics>();
|
||||
|
||||
let hints = server
|
||||
.inlay_hints_request(foo, Range::new(Position::new(0, 0), Position::new(6, 0)))?
|
||||
.inlay_hints_request(foo, Range::new(Position::new(0, 0), Position::new(6, 0)))
|
||||
.unwrap();
|
||||
|
||||
insta::assert_json_snapshot!(hints, @r#"
|
||||
|
|
@ -87,14 +87,14 @@ fn variable_inlay_hints_disabled() -> Result<()> {
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_inlay_hints(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let _ = server.await_notification::<PublishDiagnostics>()?;
|
||||
let _ = server.await_notification::<PublishDiagnostics>();
|
||||
|
||||
let hints = server
|
||||
.inlay_hints_request(foo, Range::new(Position::new(0, 0), Position::new(0, 5)))?
|
||||
.inlay_hints_request(foo, Range::new(Position::new(0, 0), Position::new(0, 5)))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ mod notebook;
|
|||
mod publish_diagnostics;
|
||||
mod pull_diagnostics;
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
|
@ -88,32 +87,69 @@ fn setup_tracing() {
|
|||
});
|
||||
}
|
||||
|
||||
/// Errors that can occur during testing
|
||||
/// Errors when receiving a notification or request from the server.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum TestServerError {
|
||||
/// The response came back, but was an error response, not a successful one.
|
||||
#[error("Response error: {0:?}")]
|
||||
ResponseError(ResponseError),
|
||||
pub(crate) enum ServerMessageError {
|
||||
#[error("waiting for message timed out")]
|
||||
Timeout,
|
||||
|
||||
#[error("Invalid response message for request {0}: {1:?}")]
|
||||
InvalidResponse(RequestId, Box<Response>),
|
||||
#[error("server disconnected")]
|
||||
ServerDisconnected,
|
||||
|
||||
#[error("Got a duplicate response for request ID {0}: {1:?}")]
|
||||
DuplicateResponse(RequestId, Box<Response>),
|
||||
|
||||
#[error("Failed to receive message from server: {0}")]
|
||||
RecvTimeoutError(RecvTimeoutError),
|
||||
#[error("Failed to deserialize message body: {0}")]
|
||||
DeserializationError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl TestServerError {
|
||||
fn is_disconnected(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestServerError::RecvTimeoutError(RecvTimeoutError::Disconnected)
|
||||
)
|
||||
impl From<ReceiveError> for ServerMessageError {
|
||||
fn from(value: ReceiveError) -> Self {
|
||||
match value {
|
||||
ReceiveError::Timeout => Self::Timeout,
|
||||
ReceiveError::ServerDisconnected => Self::ServerDisconnected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors when receiving a response from the server.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum AwaitResponseError {
|
||||
/// The response came back, but was an error response, not a successful one.
|
||||
#[error("request failed because the server replied with an error: {0:?}")]
|
||||
RequestFailed(ResponseError),
|
||||
|
||||
#[error("malformed response message with both result and error: {0:#?}")]
|
||||
MalformedResponse(Box<Response>),
|
||||
|
||||
#[error("received multiple responses for the same request ID: {0:#?}")]
|
||||
MultipleResponses(Box<[Response]>),
|
||||
|
||||
#[error("waiting for response timed out")]
|
||||
Timeout,
|
||||
|
||||
#[error("server disconnected")]
|
||||
ServerDisconnected,
|
||||
|
||||
#[error("failed to deserialize response result: {0}")]
|
||||
DeserializationError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<ReceiveError> for AwaitResponseError {
|
||||
fn from(err: ReceiveError) -> Self {
|
||||
match err {
|
||||
ReceiveError::Timeout => Self::Timeout,
|
||||
ReceiveError::ServerDisconnected => Self::ServerDisconnected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum ReceiveError {
|
||||
#[error("waiting for message timed out")]
|
||||
Timeout,
|
||||
|
||||
#[error("server disconnected")]
|
||||
ServerDisconnected,
|
||||
}
|
||||
|
||||
/// A test server for the ty language server that provides helpers for sending requests,
|
||||
/// correlating responses, and handling notifications.
|
||||
pub(crate) struct TestServer {
|
||||
|
|
@ -137,8 +173,12 @@ pub(crate) struct TestServer {
|
|||
/// Incrementing counter to automatically generate request IDs
|
||||
request_counter: i32,
|
||||
|
||||
/// A mapping of request IDs to responses received from the server
|
||||
responses: FxHashMap<RequestId, Response>,
|
||||
/// A mapping of request IDs to responses received from the server.
|
||||
///
|
||||
/// Valid responses contain exactly one response but may contain multiple responses
|
||||
/// when the server sends multiple responses for a single request.
|
||||
/// The responses are guaranteed to never be empty.
|
||||
responses: FxHashMap<RequestId, smallvec::SmallVec<[Response; 1]>>,
|
||||
|
||||
/// An ordered queue of all the notifications received from the server
|
||||
notifications: VecDeque<lsp_server::Notification>,
|
||||
|
|
@ -164,7 +204,7 @@ impl TestServer {
|
|||
test_context: TestContext,
|
||||
capabilities: ClientCapabilities,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
) -> Result<Self> {
|
||||
) -> Self {
|
||||
setup_tracing();
|
||||
|
||||
tracing::debug!("Starting test client with capabilities {:#?}", capabilities);
|
||||
|
|
@ -227,7 +267,7 @@ impl TestServer {
|
|||
workspace_folders: Vec<WorkspaceFolder>,
|
||||
capabilities: ClientCapabilities,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
) -> Result<Self> {
|
||||
) -> Self {
|
||||
let init_params = InitializeParams {
|
||||
capabilities,
|
||||
workspace_folders: Some(workspace_folders),
|
||||
|
|
@ -240,10 +280,10 @@ impl TestServer {
|
|||
};
|
||||
|
||||
let init_request_id = self.send_request::<Initialize>(init_params);
|
||||
self.initialize_response = Some(self.await_response::<Initialize>(&init_request_id)?);
|
||||
self.initialize_response = Some(self.await_response::<Initialize>(&init_request_id));
|
||||
self.send_notification::<Initialized>(InitializedParams {});
|
||||
|
||||
Ok(self)
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait until the server has initialized all workspaces.
|
||||
|
|
@ -252,28 +292,18 @@ impl TestServer {
|
|||
/// server, and handles the request.
|
||||
///
|
||||
/// This should only be called if the server is expected to send this request.
|
||||
pub(crate) fn wait_until_workspaces_are_initialized(mut self) -> Result<Self> {
|
||||
let (request_id, params) = self.await_request::<WorkspaceConfiguration>()?;
|
||||
self.handle_workspace_configuration_request(request_id, ¶ms)?;
|
||||
Ok(self)
|
||||
#[track_caller]
|
||||
pub(crate) fn wait_until_workspaces_are_initialized(mut self) -> Self {
|
||||
let (request_id, params) = self.await_request::<WorkspaceConfiguration>();
|
||||
self.handle_workspace_configuration_request(request_id, ¶ms);
|
||||
self
|
||||
}
|
||||
|
||||
/// Drain all messages from the server.
|
||||
fn drain_messages(&mut self) {
|
||||
loop {
|
||||
// Don't wait too long to drain the messages, as this is called in the `Drop`
|
||||
// implementation which happens everytime the test ends.
|
||||
match self.receive(Some(Duration::from_millis(10))) {
|
||||
Ok(()) => {}
|
||||
Err(TestServerError::RecvTimeoutError(_)) => {
|
||||
// Only break if we have no more messages to process.
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error while draining messages: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't wait too long to drain the messages, as this is called in the `Drop`
|
||||
// implementation which happens everytime the test ends.
|
||||
while let Ok(()) = self.receive(Some(Duration::from_millis(10))) {}
|
||||
}
|
||||
|
||||
/// Validate that there are no pending messages from the server.
|
||||
|
|
@ -372,13 +402,48 @@ impl TestServer {
|
|||
/// This method will remove the response from the internal data structure, so it can only be
|
||||
/// called once per request ID.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the server didn't send a response, the response failed with an error code, failed to deserialize,
|
||||
/// or the server responded twice. Use [`Self::try_await_response`] if you want a non-panicking version.
|
||||
///
|
||||
/// [`send_request`]: TestServer::send_request
|
||||
pub(crate) fn await_response<R>(&mut self, id: &RequestId) -> Result<R::Result>
|
||||
#[track_caller]
|
||||
pub(crate) fn await_response<R>(&mut self, id: &RequestId) -> R::Result
|
||||
where
|
||||
R: Request,
|
||||
{
|
||||
self.try_await_response::<R>(id, None)
|
||||
.unwrap_or_else(|err| panic!("Failed to receive response for request {id}: {err}"))
|
||||
}
|
||||
|
||||
/// Wait for a server response corresponding to the given request ID.
|
||||
///
|
||||
/// This should only be called if a request was already sent to the server via [`send_request`]
|
||||
/// which returns the request ID that should be used here.
|
||||
///
|
||||
/// This method will remove the response from the internal data structure, so it can only be
|
||||
/// called once per request ID.
|
||||
///
|
||||
/// [`send_request`]: TestServer::send_request
|
||||
pub(crate) fn try_await_response<R>(
|
||||
&mut self,
|
||||
id: &RequestId,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<R::Result, AwaitResponseError>
|
||||
where
|
||||
R: Request,
|
||||
{
|
||||
loop {
|
||||
if let Some(response) = self.responses.remove(id) {
|
||||
if let Some(mut responses) = self.responses.remove(id) {
|
||||
if responses.len() > 1 {
|
||||
return Err(AwaitResponseError::MultipleResponses(
|
||||
responses.into_boxed_slice(),
|
||||
));
|
||||
}
|
||||
|
||||
let response = responses.pop().unwrap();
|
||||
|
||||
match response {
|
||||
Response {
|
||||
error: None,
|
||||
|
|
@ -392,19 +457,15 @@ impl TestServer {
|
|||
result: None,
|
||||
..
|
||||
} => {
|
||||
return Err(TestServerError::ResponseError(err).into());
|
||||
return Err(AwaitResponseError::RequestFailed(err));
|
||||
}
|
||||
response => {
|
||||
return Err(TestServerError::InvalidResponse(
|
||||
id.clone(),
|
||||
Box::new(response),
|
||||
)
|
||||
.into());
|
||||
return Err(AwaitResponseError::MalformedResponse(Box::new(response)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.receive_or_panic()?;
|
||||
self.receive(timeout)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -417,7 +478,31 @@ impl TestServer {
|
|||
///
|
||||
/// This method will remove the notification from the internal data structure, so it should
|
||||
/// only be called if the notification is expected to be sent by the server.
|
||||
pub(crate) fn await_notification<N: Notification>(&mut self) -> Result<N::Params> {
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the server doesn't send the notification within the default timeout or
|
||||
/// the notification failed to deserialize. Use [`Self::try_await_notification`] for
|
||||
/// a panic-free alternative.
|
||||
#[track_caller]
|
||||
pub(crate) fn await_notification<N: Notification>(&mut self) -> N::Params {
|
||||
self.try_await_notification::<N>(None)
|
||||
.unwrap_or_else(|err| panic!("Failed to receive notification `{}`: {err}", N::METHOD))
|
||||
}
|
||||
|
||||
/// Wait for a notification of the specified type from the server and return its parameters.
|
||||
///
|
||||
/// The caller should ensure that the server is expected to send this notification type. It
|
||||
/// will keep polling the server for this notification up to 10 times before giving up after
|
||||
/// which it will return an error. It will also return an error if the notification is not
|
||||
/// received within `recv_timeout` duration.
|
||||
///
|
||||
/// This method will remove the notification from the internal data structure, so it should
|
||||
/// only be called if the notification is expected to be sent by the server.
|
||||
pub(crate) fn try_await_notification<N: Notification>(
|
||||
&mut self,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<N::Params, ServerMessageError> {
|
||||
for retry_count in 0..RETRY_COUNT {
|
||||
if retry_count > 0 {
|
||||
tracing::info!("Retrying to receive `{}` notification", N::METHOD);
|
||||
|
|
@ -428,29 +513,30 @@ impl TestServer {
|
|||
.position(|notification| N::METHOD == notification.method)
|
||||
.and_then(|index| self.notifications.remove(index));
|
||||
if let Some(notification) = notification {
|
||||
return Ok(serde_json::from_value(notification.params)?);
|
||||
let params = serde_json::from_value(notification.params)?;
|
||||
return Ok(params);
|
||||
}
|
||||
self.receive_or_panic()?;
|
||||
|
||||
self.receive(timeout)?;
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to receive `{}` notification after {RETRY_COUNT} retries",
|
||||
N::METHOD
|
||||
))
|
||||
|
||||
Err(ServerMessageError::Timeout)
|
||||
}
|
||||
|
||||
/// Collects `N` publish diagnostic notifications into a map, indexed by the document url.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are multiple publish diagnostics notifications for the same document.
|
||||
#[track_caller]
|
||||
pub(crate) fn collect_publish_diagnostic_notifications(
|
||||
&mut self,
|
||||
count: usize,
|
||||
) -> Result<BTreeMap<lsp_types::Url, Vec<lsp_types::Diagnostic>>> {
|
||||
) -> BTreeMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> {
|
||||
let mut results = BTreeMap::default();
|
||||
|
||||
for _ in 0..count {
|
||||
let notification =
|
||||
self.await_notification::<lsp_types::notification::PublishDiagnostics>()?;
|
||||
self.await_notification::<lsp_types::notification::PublishDiagnostics>();
|
||||
|
||||
if let Some(existing) =
|
||||
results.insert(notification.uri.clone(), notification.diagnostics)
|
||||
|
|
@ -462,7 +548,7 @@ impl TestServer {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
results
|
||||
}
|
||||
|
||||
/// Wait for a request of the specified type from the server and return the request ID and
|
||||
|
|
@ -475,7 +561,31 @@ impl TestServer {
|
|||
///
|
||||
/// This method will remove the request from the internal data structure, so it should only be
|
||||
/// called if the request is expected to be sent by the server.
|
||||
pub(crate) fn await_request<R: Request>(&mut self) -> Result<(RequestId, R::Params)> {
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If receiving the request fails.
|
||||
#[track_caller]
|
||||
pub(crate) fn await_request<R: Request>(&mut self) -> (RequestId, R::Params) {
|
||||
self.try_await_request::<R>(None)
|
||||
.unwrap_or_else(|err| panic!("Failed to receive server request `{}`: {err}", R::METHOD))
|
||||
}
|
||||
|
||||
/// Wait for a request of the specified type from the server and return the request ID and
|
||||
/// parameters.
|
||||
///
|
||||
/// The caller should ensure that the server is expected to send this request type. It will
|
||||
/// keep polling the server for this request up to 10 times before giving up after which it
|
||||
/// will return an error. It can also return an error if the request is not received within
|
||||
/// `recv_timeout` duration.
|
||||
///
|
||||
/// This method will remove the request from the internal data structure, so it should only be
|
||||
/// called if the request is expected to be sent by the server.
|
||||
#[track_caller]
|
||||
pub(crate) fn try_await_request<R: Request>(
|
||||
&mut self,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<(RequestId, R::Params), ServerMessageError> {
|
||||
for retry_count in 0..RETRY_COUNT {
|
||||
if retry_count > 0 {
|
||||
tracing::info!("Retrying to receive `{}` request", R::METHOD);
|
||||
|
|
@ -489,12 +599,10 @@ impl TestServer {
|
|||
let params = serde_json::from_value(request.params)?;
|
||||
return Ok((request.id, params));
|
||||
}
|
||||
self.receive_or_panic()?;
|
||||
|
||||
self.receive(timeout)?;
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to receive `{}` request after {RETRY_COUNT} retries",
|
||||
R::METHOD
|
||||
))
|
||||
Err(ServerMessageError::Timeout)
|
||||
}
|
||||
|
||||
/// Receive a message from the server.
|
||||
|
|
@ -503,45 +611,33 @@ impl TestServer {
|
|||
/// within that time, it will return an error.
|
||||
///
|
||||
/// If `timeout` is `None`, it will use a default timeout of 10 second.
|
||||
fn receive(&mut self, timeout: Option<Duration>) -> Result<(), TestServerError> {
|
||||
fn receive(&mut self, timeout: Option<Duration>) -> Result<(), ReceiveError> {
|
||||
static DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
let receiver = self.client_connection.as_ref().unwrap().receiver.clone();
|
||||
let message = receiver
|
||||
.recv_timeout(timeout.unwrap_or(DEFAULT_TIMEOUT))
|
||||
.map_err(TestServerError::RecvTimeoutError)?;
|
||||
.map_err(|err| match err {
|
||||
RecvTimeoutError::Disconnected => ReceiveError::ServerDisconnected,
|
||||
RecvTimeoutError::Timeout => ReceiveError::Timeout,
|
||||
})?;
|
||||
|
||||
self.handle_message(message)?;
|
||||
self.handle_message(message);
|
||||
|
||||
for message in receiver.try_iter() {
|
||||
self.handle_message(message)?;
|
||||
self.handle_message(message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is a convenience method that's same as [`receive`], but panics if the server got
|
||||
/// disconnected. It will pass other errors as is.
|
||||
///
|
||||
/// [`receive`]: TestServer::receive
|
||||
fn receive_or_panic(&mut self) -> Result<(), TestServerError> {
|
||||
if let Err(err) = self.receive(None) {
|
||||
if err.is_disconnected() {
|
||||
self.panic_on_server_disconnect();
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle the incoming message from the server.
|
||||
///
|
||||
/// This method will store the message as follows:
|
||||
/// - Requests are stored in `self.requests`
|
||||
/// - Responses are stored in `self.responses` with the request ID as the key
|
||||
/// - Notifications are stored in `self.notifications`
|
||||
fn handle_message(&mut self, message: Message) -> Result<(), TestServerError> {
|
||||
fn handle_message(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
tracing::debug!("Received server request `{}`", &request.method);
|
||||
|
|
@ -549,24 +645,16 @@ impl TestServer {
|
|||
}
|
||||
Message::Response(response) => {
|
||||
tracing::debug!("Received server response for request {}", &response.id);
|
||||
match self.responses.entry(response.id.clone()) {
|
||||
Entry::Occupied(existing) => {
|
||||
return Err(TestServerError::DuplicateResponse(
|
||||
response.id,
|
||||
Box::new(existing.get().clone()),
|
||||
));
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(response);
|
||||
}
|
||||
}
|
||||
self.responses
|
||||
.entry(response.id.clone())
|
||||
.or_default()
|
||||
.push(response);
|
||||
}
|
||||
Message::Notification(notification) => {
|
||||
tracing::debug!("Received notification `{}`", ¬ification.method);
|
||||
self.notifications.push_back(notification);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
|
@ -599,11 +687,12 @@ impl TestServer {
|
|||
/// Use the [`get_request`] method to wait for the server to send this request.
|
||||
///
|
||||
/// [`get_request`]: TestServer::get_request
|
||||
#[track_caller]
|
||||
fn handle_workspace_configuration_request(
|
||||
&mut self,
|
||||
request_id: RequestId,
|
||||
params: &ConfigurationParams,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for item in ¶ms.items {
|
||||
|
|
@ -618,7 +707,12 @@ impl TestServer {
|
|||
// > If the client can't provide a configuration setting for a given scope
|
||||
// > then null needs to be present in the returned array.
|
||||
match item.section.as_deref() {
|
||||
Some("ty") => serde_json::to_value(options)?,
|
||||
Some("ty") => match serde_json::to_value(options) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
panic!("Failed to deserialize workspace configuration options: {err}",)
|
||||
}
|
||||
},
|
||||
Some(section) => {
|
||||
tracing::debug!("Unrecognized section `{section}` for {scope_uri}");
|
||||
serde_json::Value::Null
|
||||
|
|
@ -639,8 +733,6 @@ impl TestServer {
|
|||
|
||||
let response = Response::new_ok(request_id, results);
|
||||
self.send(Message::Response(response));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the initialization result
|
||||
|
|
@ -711,7 +803,7 @@ impl TestServer {
|
|||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
previous_result_id: Option<String>,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
) -> DocumentDiagnosticReportResult {
|
||||
let params = DocumentDiagnosticParams {
|
||||
text_document: TextDocumentIdentifier {
|
||||
uri: self.file_uri(path),
|
||||
|
|
@ -730,7 +822,7 @@ impl TestServer {
|
|||
&mut self,
|
||||
work_done_token: Option<lsp_types::NumberOrString>,
|
||||
previous_result_ids: Option<Vec<PreviousResultId>>,
|
||||
) -> Result<WorkspaceDiagnosticReportResult> {
|
||||
) -> WorkspaceDiagnosticReportResult {
|
||||
let params = WorkspaceDiagnosticParams {
|
||||
identifier: Some("ty".to_string()),
|
||||
previous_result_ids: previous_result_ids.unwrap_or_default(),
|
||||
|
|
@ -747,7 +839,7 @@ impl TestServer {
|
|||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
position: Position,
|
||||
) -> Result<Option<Hover>> {
|
||||
) -> Option<Hover> {
|
||||
let params = HoverParams {
|
||||
text_document_position_params: TextDocumentPositionParams {
|
||||
text_document: TextDocumentIdentifier {
|
||||
|
|
@ -766,7 +858,7 @@ impl TestServer {
|
|||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
range: Range,
|
||||
) -> Result<Option<Vec<InlayHint>>> {
|
||||
) -> Option<Vec<InlayHint>> {
|
||||
let params = InlayHintParams {
|
||||
text_document: TextDocumentIdentifier {
|
||||
uri: self.file_uri(path),
|
||||
|
|
@ -803,7 +895,7 @@ impl Drop for TestServer {
|
|||
// it dropped the client connection.
|
||||
let shutdown_error = if self.server_thread.is_some() && !self.shutdown_requested {
|
||||
let shutdown_id = self.send_request::<Shutdown>(());
|
||||
match self.await_response::<Shutdown>(&shutdown_id) {
|
||||
match self.try_await_response::<Shutdown>(&shutdown_id, None) {
|
||||
Ok(()) => {
|
||||
self.send_notification::<Exit>(());
|
||||
|
||||
|
|
@ -834,10 +926,7 @@ impl Drop for TestServer {
|
|||
);
|
||||
}
|
||||
Ok(message) => {
|
||||
// Ignore any errors: A duplicate pending message
|
||||
// won't matter that much because `assert_no_pending_messages` will
|
||||
// panic anyway.
|
||||
let _ = self.handle_message(message);
|
||||
self.handle_message(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1048,7 +1137,7 @@ impl TestServerBuilder {
|
|||
}
|
||||
|
||||
/// Build the test server
|
||||
pub(crate) fn build(self) -> Result<TestServer> {
|
||||
pub(crate) fn build(self) -> TestServer {
|
||||
TestServer::new(
|
||||
self.workspaces,
|
||||
self.test_context,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ use crate::{TestServer, TestServerBuilder};
|
|||
#[test]
|
||||
fn publish_diagnostics_open() -> anyhow::Result<()> {
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -42,11 +42,11 @@ type Style = Literal["italic", "bold", "underline"]"#,
|
|||
builder.open(&mut server);
|
||||
|
||||
let cell1_diagnostics =
|
||||
server.await_notification::<lsp_types::notification::PublishDiagnostics>()?;
|
||||
server.await_notification::<lsp_types::notification::PublishDiagnostics>();
|
||||
let cell2_diagnostics =
|
||||
server.await_notification::<lsp_types::notification::PublishDiagnostics>()?;
|
||||
server.await_notification::<lsp_types::notification::PublishDiagnostics>();
|
||||
let cell3_diagnostics =
|
||||
server.await_notification::<lsp_types::notification::PublishDiagnostics>()?;
|
||||
server.await_notification::<lsp_types::notification::PublishDiagnostics>();
|
||||
|
||||
assert_json_snapshot!([cell1_diagnostics, cell2_diagnostics, cell3_diagnostics]);
|
||||
|
||||
|
|
@ -56,8 +56,8 @@ type Style = Literal["italic", "bold", "underline"]"#,
|
|||
#[test]
|
||||
fn diagnostic_end_of_file() -> anyhow::Result<()> {
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ IOError"#,
|
|||
|
||||
let notebook_url = builder.open(&mut server);
|
||||
|
||||
server.collect_publish_diagnostic_notifications(3)?;
|
||||
server.collect_publish_diagnostic_notifications(3);
|
||||
|
||||
server.send_notification::<lsp_types::notification::DidChangeNotebookDocument>(
|
||||
lsp_types::DidChangeNotebookDocumentParams {
|
||||
|
|
@ -122,7 +122,7 @@ IOError"#,
|
|||
},
|
||||
);
|
||||
|
||||
let diagnostics = server.collect_publish_diagnostic_notifications(3)?;
|
||||
let diagnostics = server.collect_publish_diagnostic_notifications(3);
|
||||
assert_json_snapshot!(diagnostics);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -131,8 +131,8 @@ IOError"#,
|
|||
#[test]
|
||||
fn semantic_tokens() -> anyhow::Result<()> {
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -164,13 +164,13 @@ type Style = Literal["italic", "bold", "underline"]"#,
|
|||
|
||||
builder.open(&mut server);
|
||||
|
||||
let cell1_tokens = semantic_tokens_full_for_cell(&mut server, &first_cell)?;
|
||||
let cell2_tokens = semantic_tokens_full_for_cell(&mut server, &second_cell)?;
|
||||
let cell3_tokens = semantic_tokens_full_for_cell(&mut server, &third_cell)?;
|
||||
let cell1_tokens = semantic_tokens_full_for_cell(&mut server, &first_cell);
|
||||
let cell2_tokens = semantic_tokens_full_for_cell(&mut server, &second_cell);
|
||||
let cell3_tokens = semantic_tokens_full_for_cell(&mut server, &third_cell);
|
||||
|
||||
assert_json_snapshot!([cell1_tokens, cell2_tokens, cell3_tokens]);
|
||||
|
||||
server.collect_publish_diagnostic_notifications(3)?;
|
||||
server.collect_publish_diagnostic_notifications(3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -178,8 +178,8 @@ type Style = Literal["italic", "bold", "underline"]"#,
|
|||
#[test]
|
||||
fn swap_cells() -> anyhow::Result<()> {
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ fn swap_cells() -> anyhow::Result<()> {
|
|||
|
||||
let notebook = builder.open(&mut server);
|
||||
|
||||
let diagnostics = server.collect_publish_diagnostic_notifications(3)?;
|
||||
let diagnostics = server.collect_publish_diagnostic_notifications(3);
|
||||
assert_json_snapshot!(diagnostics, @r###"
|
||||
{
|
||||
"vscode-notebook-cell://src/test.ipynb#0": [
|
||||
|
|
@ -265,7 +265,7 @@ fn swap_cells() -> anyhow::Result<()> {
|
|||
},
|
||||
);
|
||||
|
||||
let diagnostics = server.collect_publish_diagnostic_notifications(3)?;
|
||||
let diagnostics = server.collect_publish_diagnostic_notifications(3);
|
||||
|
||||
assert_json_snapshot!(diagnostics, @r###"
|
||||
{
|
||||
|
|
@ -285,8 +285,8 @@ fn auto_import() -> anyhow::Result<()> {
|
|||
SystemPath::new("src"),
|
||||
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
||||
)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -305,9 +305,9 @@ b: Litera
|
|||
|
||||
builder.open(&mut server);
|
||||
|
||||
server.collect_publish_diagnostic_notifications(2)?;
|
||||
server.collect_publish_diagnostic_notifications(2);
|
||||
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9))?;
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
|
||||
|
|
@ -321,8 +321,8 @@ fn auto_import_same_cell() -> anyhow::Result<()> {
|
|||
SystemPath::new("src"),
|
||||
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
||||
)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -336,9 +336,9 @@ b: Litera
|
|||
|
||||
builder.open(&mut server);
|
||||
|
||||
server.collect_publish_diagnostic_notifications(1)?;
|
||||
server.collect_publish_diagnostic_notifications(1);
|
||||
|
||||
let completions = literal_completions(&mut server, &first_cell, Position::new(1, 9))?;
|
||||
let completions = literal_completions(&mut server, &first_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
|
||||
|
|
@ -352,8 +352,8 @@ fn auto_import_from_future() -> anyhow::Result<()> {
|
|||
SystemPath::new("src"),
|
||||
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
||||
)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -369,9 +369,9 @@ b: Litera
|
|||
|
||||
builder.open(&mut server);
|
||||
|
||||
server.collect_publish_diagnostic_notifications(2)?;
|
||||
server.collect_publish_diagnostic_notifications(2);
|
||||
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9))?;
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
|
||||
|
|
@ -385,8 +385,8 @@ fn auto_import_docstring() -> anyhow::Result<()> {
|
|||
SystemPath::new("src"),
|
||||
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
||||
)?
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.initialization_result().unwrap();
|
||||
|
||||
|
|
@ -405,9 +405,9 @@ b: Litera
|
|||
|
||||
builder.open(&mut server);
|
||||
|
||||
server.collect_publish_diagnostic_notifications(2)?;
|
||||
server.collect_publish_diagnostic_notifications(2);
|
||||
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9))?;
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ b: Litera
|
|||
fn semantic_tokens_full_for_cell(
|
||||
server: &mut TestServer,
|
||||
cell_uri: &lsp_types::Url,
|
||||
) -> crate::Result<Option<lsp_types::SemanticTokensResult>> {
|
||||
) -> Option<lsp_types::SemanticTokensResult> {
|
||||
let cell1_tokens_req_id = server.send_request::<lsp_types::request::SemanticTokensFullRequest>(
|
||||
lsp_types::SemanticTokensParams {
|
||||
work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
|
||||
|
|
@ -502,7 +502,7 @@ fn literal_completions(
|
|||
server: &mut TestServer,
|
||||
cell: &lsp_types::Url,
|
||||
position: Position,
|
||||
) -> crate::Result<Vec<lsp_types::CompletionItem>> {
|
||||
) -> Vec<lsp_types::CompletionItem> {
|
||||
let completions_id =
|
||||
server.send_request::<lsp_types::request::Completion>(lsp_types::CompletionParams {
|
||||
text_document_position: lsp_types::TextDocumentPositionParams {
|
||||
|
|
@ -520,14 +520,14 @@ fn literal_completions(
|
|||
// There are a ton of imports we don't care about in here...
|
||||
// The import bit is that an edit is always restricted to the current cell. That means,
|
||||
// we can't add `Literal` to the `from typing import TYPE_CHECKING` import in cell 1
|
||||
let completions = server.await_response::<lsp_types::request::Completion>(&completions_id)?;
|
||||
let completions = server.await_response::<lsp_types::request::Completion>(&completions_id);
|
||||
let mut items = match completions {
|
||||
Some(CompletionResponse::Array(array)) => array,
|
||||
Some(CompletionResponse::List(lsp_types::CompletionList { items, .. })) => items,
|
||||
None => return Ok(vec![]),
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
items.retain(|item| item.label.starts_with("Litera"));
|
||||
|
||||
Ok(items)
|
||||
items
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ def foo() -> str:
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_pull_diagnostics(false)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let diagnostics = server.await_notification::<PublishDiagnostics>()?;
|
||||
let diagnostics = server.await_notification::<PublishDiagnostics>();
|
||||
|
||||
insta::assert_debug_snapshot!(diagnostics);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use insta::{assert_compact_debug_snapshot, assert_debug_snapshot};
|
||||
use lsp_server::RequestId;
|
||||
|
|
@ -9,7 +11,7 @@ use lsp_types::{
|
|||
use ruff_db::system::SystemPath;
|
||||
use ty_server::{ClientOptions, DiagnosticMode, PartialWorkspaceProgress};
|
||||
|
||||
use crate::{TestServer, TestServerBuilder, TestServerError};
|
||||
use crate::{AwaitResponseError, TestServer, TestServerBuilder};
|
||||
|
||||
#[test]
|
||||
fn on_did_open() -> Result<()> {
|
||||
|
|
@ -26,11 +28,11 @@ def foo() -> str:
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let diagnostics = server.document_diagnostic_request(foo, None)?;
|
||||
let diagnostics = server.document_diagnostic_request(foo, None);
|
||||
|
||||
assert_debug_snapshot!(diagnostics);
|
||||
|
||||
|
|
@ -52,13 +54,13 @@ def foo() -> str:
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
|
||||
// First request with no previous result ID
|
||||
let first_response = server.document_diagnostic_request(foo, None)?;
|
||||
let first_response = server.document_diagnostic_request(foo, None);
|
||||
|
||||
// Extract result ID from first response
|
||||
let result_id = match &first_response {
|
||||
|
|
@ -74,7 +76,7 @@ def foo() -> str:
|
|||
};
|
||||
|
||||
// Second request with the previous result ID - should return Unchanged
|
||||
let second_response = server.document_diagnostic_request(foo, Some(result_id))?;
|
||||
let second_response = server.document_diagnostic_request(foo, Some(result_id));
|
||||
|
||||
// Verify it's an unchanged report
|
||||
match second_response {
|
||||
|
|
@ -108,13 +110,13 @@ def foo() -> str:
|
|||
.with_workspace(workspace_root, None)?
|
||||
.with_file(foo, foo_content_v1)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(foo, &foo_content_v1, 1);
|
||||
|
||||
// First request with no previous result ID
|
||||
let first_response = server.document_diagnostic_request(foo, None)?;
|
||||
let first_response = server.document_diagnostic_request(foo, None);
|
||||
|
||||
// Extract result ID from first response
|
||||
let result_id = match &first_response {
|
||||
|
|
@ -141,7 +143,7 @@ def foo() -> str:
|
|||
);
|
||||
|
||||
// Second request with the previous result ID - should return a new full report
|
||||
let second_response = server.document_diagnostic_request(foo, Some(result_id))?;
|
||||
let second_response = server.document_diagnostic_request(foo, Some(result_id));
|
||||
|
||||
// Verify it's a full report (not unchanged)
|
||||
match second_response {
|
||||
|
|
@ -228,16 +230,14 @@ def foo() -> str:
|
|||
.with_file(file_d, file_d_content_v1)?
|
||||
.with_file(file_e, file_e_content_v1)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(file_a, &file_a_content, 1);
|
||||
|
||||
// First request with no previous result IDs
|
||||
let mut first_response = server.workspace_diagnostic_request(
|
||||
Some(NumberOrString::String("progress-1".to_string())),
|
||||
None,
|
||||
)?;
|
||||
let mut first_response = server
|
||||
.workspace_diagnostic_request(Some(NumberOrString::String("progress-1".to_string())), None);
|
||||
sort_workspace_diagnostic_response(&mut first_response);
|
||||
|
||||
assert_debug_snapshot!("workspace_diagnostic_initial_state", first_response);
|
||||
|
|
@ -309,7 +309,7 @@ def foo() -> str:
|
|||
let mut second_response = server.workspace_diagnostic_request(
|
||||
Some(NumberOrString::String("progress-2".to_string())),
|
||||
Some(previous_result_ids),
|
||||
)?;
|
||||
);
|
||||
sort_workspace_diagnostic_response(&mut second_response);
|
||||
|
||||
// Consume all progress notifications sent during the second workspace diagnostics
|
||||
|
|
@ -339,10 +339,10 @@ def foo() -> str:
|
|||
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace),
|
||||
)
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let first_response = server.workspace_diagnostic_request(None, None).unwrap();
|
||||
let first_response = server.workspace_diagnostic_request(None, None);
|
||||
|
||||
// Extract result IDs from the first response
|
||||
let mut previous_result_ids = extract_result_ids_from_response(&first_response);
|
||||
|
|
@ -366,7 +366,7 @@ def foo() -> str:
|
|||
// The server needs to match the previous result IDs by the path, not the URL.
|
||||
assert_workspace_diagnostics_suspends_for_long_polling(&mut server, &workspace_request_id);
|
||||
|
||||
let second_response = shutdown_and_await_workspace_diagnostic(server, &workspace_request_id)?;
|
||||
let second_response = shutdown_and_await_workspace_diagnostic(server, &workspace_request_id);
|
||||
|
||||
assert_compact_debug_snapshot!(second_response, @"Report(WorkspaceDiagnosticReport { items: [] })");
|
||||
|
||||
|
|
@ -382,7 +382,7 @@ fn filter_result_id() -> insta::internals::SettingsBindDropGuard {
|
|||
|
||||
fn consume_all_progress_notifications(server: &mut TestServer) -> Result<()> {
|
||||
// Always consume Begin
|
||||
let begin_params = server.await_notification::<lsp_types::notification::Progress>()?;
|
||||
let begin_params = server.await_notification::<lsp_types::notification::Progress>();
|
||||
|
||||
// The params are already the ProgressParams type
|
||||
let lsp_types::ProgressParamsValue::WorkDone(lsp_types::WorkDoneProgress::Begin(_)) =
|
||||
|
|
@ -394,7 +394,7 @@ fn consume_all_progress_notifications(server: &mut TestServer) -> Result<()> {
|
|||
// Consume Report notifications - there may be multiple based on number of files
|
||||
// Keep consuming until we hit the End notification
|
||||
loop {
|
||||
let params = server.await_notification::<lsp_types::notification::Progress>()?;
|
||||
let params = server.await_notification::<lsp_types::notification::Progress>();
|
||||
|
||||
if let lsp_types::ProgressParamsValue::WorkDone(lsp_types::WorkDoneProgress::End(_)) =
|
||||
params.value
|
||||
|
|
@ -442,8 +442,8 @@ def foo() -> str:
|
|||
|
||||
let mut server = builder
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
let partial_token = lsp_types::ProgressToken::String("streaming-diagnostics".to_string());
|
||||
let request_id = server.send_request::<WorkspaceDiagnosticRequest>(WorkspaceDiagnosticParams {
|
||||
|
|
@ -461,7 +461,7 @@ def foo() -> str:
|
|||
|
||||
// First, read the response of the workspace diagnostic request.
|
||||
// Note: This response comes after the progress notifications but it simplifies the test to read it first.
|
||||
let final_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id)?;
|
||||
let final_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id);
|
||||
|
||||
// Process the final report.
|
||||
// This should always be a partial report. However, the type definition in the LSP specification
|
||||
|
|
@ -478,7 +478,9 @@ def foo() -> str:
|
|||
received_results += response_items.len();
|
||||
|
||||
// Collect any partial results sent via progress notifications
|
||||
while let Ok(params) = server.await_notification::<PartialWorkspaceProgress>() {
|
||||
while let Ok(params) =
|
||||
server.try_await_notification::<PartialWorkspaceProgress>(Some(Duration::from_secs(1)))
|
||||
{
|
||||
if params.token == partial_token {
|
||||
let streamed_items = match params.value {
|
||||
// Ideally we'd assert that only the first response is a full report
|
||||
|
|
@ -531,15 +533,15 @@ fn workspace_diagnostic_streaming_with_caching() -> Result<()> {
|
|||
|
||||
let mut server = builder
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
server.open_text_document(SystemPath::new("src/error_0.py"), &error_content, 1);
|
||||
server.open_text_document(SystemPath::new("src/error_1.py"), &error_content, 1);
|
||||
server.open_text_document(SystemPath::new("src/error_2.py"), &error_content, 1);
|
||||
|
||||
// First request to get result IDs (non-streaming for simplicity)
|
||||
let first_response = server.workspace_diagnostic_request(None, None)?;
|
||||
let first_response = server.workspace_diagnostic_request(None, None);
|
||||
|
||||
let result_ids = extract_result_ids_from_response(&first_response);
|
||||
|
||||
|
|
@ -590,7 +592,7 @@ fn workspace_diagnostic_streaming_with_caching() -> Result<()> {
|
|||
},
|
||||
});
|
||||
|
||||
let final_response2 = server.await_response::<WorkspaceDiagnosticRequest>(&request2_id)?;
|
||||
let final_response2 = server.await_response::<WorkspaceDiagnosticRequest>(&request2_id);
|
||||
|
||||
let mut all_items = Vec::new();
|
||||
|
||||
|
|
@ -605,7 +607,9 @@ fn workspace_diagnostic_streaming_with_caching() -> Result<()> {
|
|||
all_items.extend(items);
|
||||
|
||||
// Collect any partial results sent via progress notifications
|
||||
while let Ok(params) = server.await_notification::<PartialWorkspaceProgress>() {
|
||||
while let Ok(params) =
|
||||
server.try_await_notification::<PartialWorkspaceProgress>(Some(Duration::from_secs(1)))
|
||||
{
|
||||
if params.token == partial_token {
|
||||
let streamed_items = match params.value {
|
||||
// Ideally we'd assert that only the first response is a full report
|
||||
|
|
@ -679,7 +683,7 @@ def hello() -> str:
|
|||
assert_workspace_diagnostics_suspends_for_long_polling(&mut server, &request_id);
|
||||
|
||||
// The workspace diagnostic request should now respond with an empty report
|
||||
let workspace_response = shutdown_and_await_workspace_diagnostic(server, &request_id)?;
|
||||
let workspace_response = shutdown_and_await_workspace_diagnostic(server, &request_id);
|
||||
|
||||
// Verify we got an empty report (default response during shutdown)
|
||||
assert_debug_snapshot!(
|
||||
|
|
@ -733,7 +737,7 @@ def hello() -> str:
|
|||
);
|
||||
|
||||
// The workspace diagnostic request should now complete with the new diagnostic
|
||||
let workspace_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id)?;
|
||||
let workspace_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id);
|
||||
|
||||
// Verify we got a report with one file containing the new diagnostic
|
||||
assert_debug_snapshot!(
|
||||
|
|
@ -775,7 +779,7 @@ def hello() -> str:
|
|||
server.cancel(&request_id);
|
||||
|
||||
// The workspace diagnostic request should now respond with a cancellation response (Err).
|
||||
let result = server.await_response::<WorkspaceDiagnosticRequest>(&request_id);
|
||||
let result = server.try_await_response::<WorkspaceDiagnosticRequest>(&request_id, None);
|
||||
assert_debug_snapshot!(
|
||||
"workspace_diagnostic_long_polling_cancellation_result",
|
||||
result
|
||||
|
|
@ -833,7 +837,7 @@ def hello() -> str:
|
|||
);
|
||||
|
||||
// First request should complete with diagnostics
|
||||
let first_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id_1)?;
|
||||
let first_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id_1);
|
||||
|
||||
// Extract result IDs from the first response for the second request
|
||||
let previous_result_ids = extract_result_ids_from_response(&first_response);
|
||||
|
|
@ -866,7 +870,7 @@ def hello() -> str:
|
|||
);
|
||||
|
||||
// Second request should complete with the fix (no diagnostics)
|
||||
let second_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id_2)?;
|
||||
let second_response = server.await_response::<WorkspaceDiagnosticRequest>(&request_id_2);
|
||||
|
||||
// Snapshot both responses to verify the full cycle
|
||||
assert_debug_snapshot!(
|
||||
|
|
@ -887,15 +891,15 @@ fn create_workspace_server_with_file(
|
|||
file_path: &SystemPath,
|
||||
file_content: &str,
|
||||
) -> Result<TestServer> {
|
||||
TestServerBuilder::new()?
|
||||
Ok(TestServerBuilder::new()?
|
||||
.with_workspace(workspace_root, None)?
|
||||
.with_file(file_path, file_content)?
|
||||
.with_initialization_options(
|
||||
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace),
|
||||
)
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized())
|
||||
}
|
||||
|
||||
/// Sends a workspace diagnostic request to the server.
|
||||
|
|
@ -917,7 +921,7 @@ fn send_workspace_diagnostic_request(server: &mut TestServer) -> lsp_server::Req
|
|||
fn shutdown_and_await_workspace_diagnostic(
|
||||
mut server: TestServer,
|
||||
request_id: &RequestId,
|
||||
) -> Result<WorkspaceDiagnosticReportResult> {
|
||||
) -> WorkspaceDiagnosticReportResult {
|
||||
// Send shutdown request - this should cause the suspended workspace diagnostic request to respond
|
||||
let shutdown_id = server.send_request::<lsp_types::request::Shutdown>(());
|
||||
|
||||
|
|
@ -925,7 +929,7 @@ fn shutdown_and_await_workspace_diagnostic(
|
|||
let workspace_response = server.await_response::<WorkspaceDiagnosticRequest>(request_id);
|
||||
|
||||
// Complete shutdown sequence
|
||||
server.await_response::<lsp_types::request::Shutdown>(&shutdown_id)?;
|
||||
server.await_response::<lsp_types::request::Shutdown>(&shutdown_id);
|
||||
server.send_notification::<lsp_types::notification::Exit>(());
|
||||
|
||||
workspace_response
|
||||
|
|
@ -936,19 +940,19 @@ fn assert_workspace_diagnostics_suspends_for_long_polling(
|
|||
server: &mut TestServer,
|
||||
request_id: &lsp_server::RequestId,
|
||||
) {
|
||||
match server.await_response::<WorkspaceDiagnosticRequest>(request_id) {
|
||||
match server
|
||||
.try_await_response::<WorkspaceDiagnosticRequest>(request_id, Some(Duration::from_secs(2)))
|
||||
{
|
||||
Ok(_) => {
|
||||
panic!("Expected workspace diagnostic request to suspend for long-polling.");
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(test_error) = error.downcast_ref::<TestServerError>() {
|
||||
assert!(
|
||||
matches!(test_error, TestServerError::RecvTimeoutError(_)),
|
||||
"Response should time out because the request is suspended for long-polling"
|
||||
);
|
||||
} else {
|
||||
panic!("Unexpected error type: {error:?}");
|
||||
}
|
||||
Err(AwaitResponseError::Timeout) => {
|
||||
// Ok
|
||||
}
|
||||
Err(err) => {
|
||||
panic!(
|
||||
"Response should time out because the request is suspended for long-polling but errored with: {err}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ source: crates/ty_server/tests/e2e/pull_diagnostics.rs
|
|||
expression: result
|
||||
---
|
||||
Err(
|
||||
ResponseError(
|
||||
RequestFailed(
|
||||
ResponseError {
|
||||
code: -32800,
|
||||
message: "request was cancelled by client",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue