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;
1414use std:: time:: Duration ;
1515
1616use anyhow:: Result ;
17+ use crossbeam:: channel:: RecvTimeoutError ;
1718use lsp_server:: { Connection , Message , RequestId , Response , ResponseError } ;
1819use 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.
6067pub ( 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
95102impl 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 can’ t 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