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,50 @@ impl TestServer {
218232 Ok ( self )
219233 }
220234
235+ /// Drain all messages from the server.
236+ fn drain_messages ( & mut self ) {
237+ loop {
238+ match self . receive ( ) {
239+ Ok ( ( ) ) => { }
240+ Err ( TestServerError :: RecvTimeoutError ( _) ) => {
241+ // Only break if we have no more messages to process.
242+ break ;
243+ }
244+ Err ( _) => { }
245+ }
246+ }
247+ }
248+
249+ /// Validate that there are no pending messages from the server.
250+ ///
251+ /// This should be called before the test server is dropped to ensure that all server messages
252+ /// have been properly consumed by the test. If there are any pending messages, this will panic
253+ /// with detailed information about what was left unconsumed.
254+ fn assert_no_pending_messages ( & self ) {
255+ let mut errors = Vec :: new ( ) ;
256+
257+ if !self . responses . is_empty ( ) {
258+ errors. push ( format ! ( "Unclaimed responses: {:#?}" , self . responses) ) ;
259+ }
260+
261+ if !self . notifications . is_empty ( ) {
262+ errors. push ( format ! (
263+ "Unclaimed notifications: {:#?}" ,
264+ self . notifications
265+ ) ) ;
266+ }
267+
268+ if !self . requests . is_empty ( ) {
269+ errors. push ( format ! ( "Unclaimed requests: {:#?}" , self . requests) ) ;
270+ }
271+
272+ assert ! (
273+ errors. is_empty( ) ,
274+ "Test server has pending messages that were not consumed by the test:\n {}" ,
275+ errors. join( "\n " )
276+ ) ;
277+ }
278+
221279 /// Generate a new request ID
222280 fn next_request_id ( & mut self ) -> RequestId {
223281 self . request_counter += 1 ;
@@ -261,31 +319,35 @@ impl TestServer {
261319
262320 /// Get a server response for the given request ID.
263321 ///
264- /// The request should have already been sent using [`send_request`].
322+ /// This should only be called if a request was already sent to the server via [`send_request`]
323+ /// which returns the request ID that should be used here.
324+ ///
325+ /// This method will remove the response from the internal data structure, so it can only be
326+ /// called once per request ID.
265327 ///
266328 /// [`send_request`]: TestServer::send_request
267329 pub ( crate ) fn get_response < T : DeserializeOwned > ( & mut self , id : RequestId ) -> Result < T > {
268330 loop {
269331 self . receive ( ) ?;
270332
271- if let Some ( response) = self . responses . get ( & id) {
333+ if let Some ( response) = self . responses . remove ( & id) {
272334 match response {
273335 Response {
274336 error : None ,
275337 result : Some ( result) ,
276338 ..
277339 } => {
278- return Ok ( serde_json:: from_value :: < T > ( result. clone ( ) ) ?) ;
340+ return Ok ( serde_json:: from_value :: < T > ( result) ?) ;
279341 }
280342 Response {
281343 error : Some ( err) ,
282344 result : None ,
283345 ..
284346 } => {
285- return Err ( TestServerError :: ResponseError ( err. clone ( ) ) . into ( ) ) ;
347+ return Err ( TestServerError :: ResponseError ( err) . into ( ) ) ;
286348 }
287349 response => {
288- return Err ( TestServerError :: InvalidResponse ( id, response. clone ( ) ) . into ( ) ) ;
350+ return Err ( TestServerError :: InvalidResponse ( id, response) . into ( ) ) ;
289351 }
290352 }
291353 }
@@ -295,8 +357,12 @@ impl TestServer {
295357 /// Get a notification of the specified type from the server and return its parameters.
296358 ///
297359 /// 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.
360+ /// will keep polling the server for this notification up to 10 times before giving up after
361+ /// which it will return an error. It will also return an error if the notification is not
362+ /// received within `recv_timeout` duration.
363+ ///
364+ /// This method will remove the notification from the internal data structure, so it should
365+ /// only be called if the notification is expected to be sent by the server.
300366 pub ( crate ) fn get_notification < N : Notification > ( & mut self ) -> Result < N :: Params > {
301367 for _ in 0 ..10 {
302368 self . receive ( ) ?;
@@ -326,8 +392,12 @@ impl TestServer {
326392 /// parameters.
327393 ///
328394 /// 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.
395+ /// keep polling the server for this request up to 10 times before giving up after which it
396+ /// will return an error. It can also return an error if the request is not received within
397+ /// `recv_timeout` duration.
398+ ///
399+ /// This method will remove the request from the internal data structure, so it should only be
400+ /// called if the request is expected to be sent by the server.
331401 pub ( crate ) fn get_request < R : Request > ( & mut self ) -> Result < ( RequestId , R :: Params ) > {
332402 for _ in 0 ..10 {
333403 self . receive ( ) ?;
@@ -363,7 +433,8 @@ impl TestServer {
363433 /// - Requests are stored in `requests`
364434 /// - Responses are stored in `responses`
365435 /// - Notifications are stored in `notifications`
366- fn receive ( & mut self ) -> Result < ( ) > {
436+ #[ allow( clippy:: result_large_err) ]
437+ fn receive ( & mut self ) -> Result < ( ) , TestServerError > {
367438 let message = self
368439 . client_connection
369440 . receiver
@@ -378,8 +449,7 @@ impl TestServer {
378449 return Err ( TestServerError :: DuplicateResponse (
379450 response. id ,
380451 existing. get ( ) . clone ( ) ,
381- )
382- . into ( ) ) ;
452+ ) ) ;
383453 }
384454 Entry :: Vacant ( entry) => {
385455 entry. insert ( response) ;
@@ -417,7 +487,7 @@ impl TestServer {
417487 // TODO: Handle `python` section once it's implemented in the server
418488 // As per the spec:
419489 //
420- // > If the client can’ t provide a configuration setting for a given scope
490+ // > If the client can' t provide a configuration setting for a given scope
421491 // > then null needs to be present in the returned array.
422492 serde_json:: Value :: Null
423493 }
0 commit comments