Skip to content

Commit afd1f82

Browse files
committed
Assert no pending messages before we drop the test server
1 parent 2b63c0d commit afd1f82

File tree

1 file changed

+89
-26
lines changed

1 file changed

+89
-26
lines changed

crates/ty_server/src/test.rs

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Testing server for the ty language server.
22
//!
3-
//! This module provides mock server infrastructure for testing LSP functionality
4-
//! without requiring actual file system operations or network connections.
3+
//! This module provides mock server infrastructure for testing LSP functionality without requiring
4+
//! actual file system operations.
55
//!
66
//! The design is inspired by the Starlark LSP test server but adapted for ty server architecture.
77
@@ -14,6 +14,7 @@ use std::thread::JoinHandle;
1414
use std::time::Duration;
1515

1616
use anyhow::Result;
17+
use crossbeam::channel::RecvTimeoutError;
1718
use lsp_server::{Connection, Message, RequestId, Response, ResponseError};
1819
use lsp_types::notification::{
1920
DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument, Exit,
@@ -53,10 +54,16 @@ pub(crate) enum TestServerError {
5354

5455
#[error("Test client received an unrecognized request from the server: {0:?}")]
5556
UnrecognizedRequest(lsp_server::Request),
57+
58+
#[error(transparent)]
59+
RecvTimeoutError(#[from] RecvTimeoutError),
5660
}
5761

5862
/// A test server for the ty language server that provides helpers for sending requests,
5963
/// correlating responses, and handling notifications.
64+
///
65+
/// The [`Drop`] implementation ensures that the server is shut down gracefully using the described
66+
/// protocol in the LSP specification.
6067
pub(crate) struct TestServer {
6168
/// The thread that's actually running the server
6269
server_thread: Option<JoinHandle<()>>,
@@ -94,28 +101,34 @@ pub(crate) struct TestServer {
94101

95102
impl Drop for TestServer {
96103
fn drop(&mut self) {
104+
self.drain_messages();
105+
97106
// Follow the LSP protocol to shutdown the server gracefully
98-
match self.send_request::<Shutdown>(()) {
107+
let shutdown_error = match self.send_request::<Shutdown>(()) {
99108
Ok(shutdown_id) => match self.get_response::<()>(shutdown_id) {
100109
Ok(()) => {
101110
if let Err(err) = self.send_notification::<Exit>(()) {
102-
panic!("Failed to send exit notification: {err:?}");
111+
Some(format!("Failed to send exit notification: {err:?}"))
112+
} else {
113+
None
103114
}
104115
}
105-
Err(err) => {
106-
panic!("Failed to get shutdown response: {err:?}");
107-
}
116+
Err(err) => Some(format!("Failed to get shutdown response: {err:?}")),
108117
},
109-
Err(err) => {
110-
panic!("Failed to send shutdown request: {err:?}");
111-
}
112-
}
118+
Err(err) => Some(format!("Failed to send shutdown request: {err:?}")),
119+
};
113120

114121
if let Some(server_thread) = self.server_thread.take() {
115122
if let Err(err) = server_thread.join() {
116123
panic!("Test server thread did not join when dropped: {err:?}");
117124
}
118125
}
126+
127+
if let Some(error) = shutdown_error {
128+
panic!("Test server did not shut down gracefully: {error}");
129+
}
130+
131+
self.assert_no_pending_messages();
119132
}
120133
}
121134

@@ -177,7 +190,7 @@ impl TestServer {
177190
responses: HashMap::new(),
178191
notifications: VecDeque::new(),
179192
requests: VecDeque::new(),
180-
recv_timeout: Duration::from_secs(1),
193+
recv_timeout: Duration::from_secs(2),
181194
initialize_response: None,
182195
workspace_configurations,
183196
registered_capabilities: Vec::new(),
@@ -194,7 +207,8 @@ impl TestServer {
194207
let init_params = InitializeParams {
195208
capabilities,
196209
workspace_folders: Some(workspace_folders),
197-
// TODO: This should be configurable by the test server builder
210+
// TODO: This should be configurable by the test server builder. This might not be
211+
// required after client settings are implemented in the server.
198212
initialization_options: Some(serde_json::Value::Object(serde_json::Map::new())),
199213
..Default::default()
200214
};
@@ -218,6 +232,43 @@ impl TestServer {
218232
Ok(self)
219233
}
220234

235+
/// Drain all messages from the server.
236+
fn drain_messages(&mut self) {
237+
while let Err(TestServerError::RecvTimeoutError(_)) = self.receive() {
238+
// Keep receiving until we get no more messages
239+
}
240+
}
241+
242+
/// Validate that there are no pending messages from the server.
243+
///
244+
/// This should be called before the test server is dropped to ensure that all server messages
245+
/// have been properly consumed by the test. If there are any pending messages, this will panic
246+
/// with detailed information about what was left unconsumed.
247+
fn assert_no_pending_messages(&self) {
248+
let mut errors = Vec::new();
249+
250+
if !self.responses.is_empty() {
251+
errors.push(format!("Unclaimed responses: {:#?}", self.responses));
252+
}
253+
254+
if !self.notifications.is_empty() {
255+
errors.push(format!(
256+
"Unclaimed notifications: {:#?}",
257+
self.notifications
258+
));
259+
}
260+
261+
if !self.requests.is_empty() {
262+
errors.push(format!("Unclaimed requests: {:#?}", self.requests));
263+
}
264+
265+
assert!(
266+
errors.is_empty(),
267+
"Test server has pending messages that were not consumed by the test:\n{}",
268+
errors.join("\n")
269+
);
270+
}
271+
221272
/// Generate a new request ID
222273
fn next_request_id(&mut self) -> RequestId {
223274
self.request_counter += 1;
@@ -261,31 +312,35 @@ impl TestServer {
261312

262313
/// Get a server response for the given request ID.
263314
///
264-
/// The request should have already been sent using [`send_request`].
315+
/// This should only be called if a request was already sent to the server via [`send_request`]
316+
/// which returns the request ID that should be used here.
317+
///
318+
/// This method will remove the response from the internal data structure, so it can only be
319+
/// called once per request ID.
265320
///
266321
/// [`send_request`]: TestServer::send_request
267322
pub(crate) fn get_response<T: DeserializeOwned>(&mut self, id: RequestId) -> Result<T> {
268323
loop {
269324
self.receive()?;
270325

271-
if let Some(response) = self.responses.get(&id) {
326+
if let Some(response) = self.responses.remove(&id) {
272327
match response {
273328
Response {
274329
error: None,
275330
result: Some(result),
276331
..
277332
} => {
278-
return Ok(serde_json::from_value::<T>(result.clone())?);
333+
return Ok(serde_json::from_value::<T>(result)?);
279334
}
280335
Response {
281336
error: Some(err),
282337
result: None,
283338
..
284339
} => {
285-
return Err(TestServerError::ResponseError(err.clone()).into());
340+
return Err(TestServerError::ResponseError(err).into());
286341
}
287342
response => {
288-
return Err(TestServerError::InvalidResponse(id, response.clone()).into());
343+
return Err(TestServerError::InvalidResponse(id, response).into());
289344
}
290345
}
291346
}
@@ -295,8 +350,12 @@ impl TestServer {
295350
/// Get a notification of the specified type from the server and return its parameters.
296351
///
297352
/// The caller should ensure that the server is expected to send this notification type. It
298-
/// will keep polling the server for notifications up to 10 times before giving up. It can
299-
/// return an error if the notification is not received within `recv_timeout` duration.
353+
/// will keep polling the server for this notification up to 10 times before giving up after
354+
/// which it will return an error. It will also return an error if the notification is not
355+
/// received within `recv_timeout` duration.
356+
///
357+
/// This method will remove the notification from the internal data structure, so it should
358+
/// only be called if the notification is expected to be sent by the server.
300359
pub(crate) fn get_notification<N: Notification>(&mut self) -> Result<N::Params> {
301360
for _ in 0..10 {
302361
self.receive()?;
@@ -326,8 +385,12 @@ impl TestServer {
326385
/// parameters.
327386
///
328387
/// The caller should ensure that the server is expected to send this request type. It will
329-
/// keep polling the server for requests up to 10 times before giving up. It can return an
330-
/// error if the request is not received within `recv_timeout` duration.
388+
/// keep polling the server for this request up to 10 times before giving up after which it
389+
/// will return an error. It can also return an error if the request is not received within
390+
/// `recv_timeout` duration.
391+
///
392+
/// This method will remove the request from the internal data structure, so it should only be
393+
/// called if the request is expected to be sent by the server.
331394
pub(crate) fn get_request<R: Request>(&mut self) -> Result<(RequestId, R::Params)> {
332395
for _ in 0..10 {
333396
self.receive()?;
@@ -363,7 +426,8 @@ impl TestServer {
363426
/// - Requests are stored in `requests`
364427
/// - Responses are stored in `responses`
365428
/// - Notifications are stored in `notifications`
366-
fn receive(&mut self) -> Result<()> {
429+
#[allow(clippy::result_large_err)]
430+
fn receive(&mut self) -> Result<(), TestServerError> {
367431
let message = self
368432
.client_connection
369433
.receiver
@@ -378,8 +442,7 @@ impl TestServer {
378442
return Err(TestServerError::DuplicateResponse(
379443
response.id,
380444
existing.get().clone(),
381-
)
382-
.into());
445+
));
383446
}
384447
Entry::Vacant(entry) => {
385448
entry.insert(response);
@@ -417,7 +480,7 @@ impl TestServer {
417480
// TODO: Handle `python` section once it's implemented in the server
418481
// As per the spec:
419482
//
420-
// > If the client cant provide a configuration setting for a given scope
483+
// > If the client can't provide a configuration setting for a given scope
421484
// > then null needs to be present in the returned array.
422485
serde_json::Value::Null
423486
}

0 commit comments

Comments
 (0)