use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use lsp_types::LspErrorCodes;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use tokio::sync::{Mutex, mpsc, oneshot};
use tokio::task::JoinHandle;
use tokio::time::{Duration, timeout};
use tracing::{debug, error, trace, warn};
use crate::config::LspServerConfig;
use crate::error::{Error, Result};
use crate::lsp::transport::{LspTransport, LspTransportReader};
use crate::lsp::types::{
InboundMessage, JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
LspNotification, RequestId,
};
const JSONRPC_VERSION: &str = "2.0";
const SERVER_CANCELLED_CODE: i32 = -32802;
const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
const READER_CHANNEL_CAPACITY: usize = 100;
const READER_TASK_ABORT_GRACE: Duration = Duration::from_secs(1);
pub const CONTENT_MODIFIED_RETRY_METHODS: &[&str] = &[
"textDocument/signatureHelp",
"textDocument/inlayHint",
"textDocument/completion",
"textDocument/prepareCallHierarchy",
"callHierarchy/incomingCalls",
"callHierarchy/outgoingCalls",
"textDocument/diagnostic",
"textDocument/hover",
"textDocument/definition",
"textDocument/references",
"textDocument/implementation",
"textDocument/typeDefinition",
"textDocument/documentSymbol",
"workspace/symbol",
];
const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
const CODE_ACTION_RESOLVE_TIMEOUT_CAP: Duration = Duration::from_secs(10);
type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
fn spawn_reader_task(
mut reader: LspTransportReader,
) -> (JoinHandle<()>, mpsc::Receiver<Result<InboundMessage>>) {
let (tx, rx) = mpsc::channel(READER_CHANNEL_CAPACITY);
let handle = tokio::spawn(async move {
loop {
let message = reader.receive().await;
let is_err = message.is_err();
if tx.send(message).await.is_err() || is_err {
break;
}
}
});
(handle, rx)
}
#[derive(Debug)]
pub struct LspClient {
config: LspServerConfig,
state: Arc<Mutex<super::ServerState>>,
request_counter: Arc<AtomicI64>,
command_tx: mpsc::Sender<ClientCommand>,
pending_requests: Arc<Mutex<PendingRequests>>,
receiver_task: Option<JoinHandle<Result<()>>>,
}
impl Clone for LspClient {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
state: Arc::clone(&self.state),
request_counter: Arc::clone(&self.request_counter),
command_tx: self.command_tx.clone(),
pending_requests: Arc::clone(&self.pending_requests),
receiver_task: None,
}
}
}
enum ClientCommand {
SendRequest { request: JsonRpcRequest },
SendNotification {
method: String,
params: Option<Value>,
},
Shutdown,
}
impl LspClient {
#[must_use]
pub fn new(config: LspServerConfig) -> Self {
let (command_tx, _command_rx) = mpsc::channel(1);
Self {
config,
state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
request_counter: Arc::new(AtomicI64::new(1)),
command_tx,
pending_requests: Arc::new(Mutex::new(HashMap::new())),
receiver_task: None,
}
}
#[cfg(test)]
pub(crate) fn from_transport(
config: LspServerConfig,
transport: (LspTransport, LspTransportReader),
) -> Self {
let state = Arc::new(Mutex::new(super::ServerState::Initializing));
let request_counter = Arc::new(AtomicI64::new(1));
let pending_requests = Arc::new(Mutex::new(HashMap::new()));
let (command_tx, command_rx) = mpsc::channel(100);
let receiver_task = tokio::spawn(Self::message_loop(
transport,
command_rx,
Arc::clone(&pending_requests),
None,
None,
));
Self {
config,
state,
request_counter,
command_tx,
pending_requests,
receiver_task: Some(receiver_task),
}
}
pub(crate) fn from_transport_with_notifications(
config: LspServerConfig,
transport: (LspTransport, LspTransportReader),
notification_tx: mpsc::Sender<LspNotification>,
lifecycle_tx: mpsc::Sender<LspNotification>,
) -> Self {
let state = Arc::new(Mutex::new(super::ServerState::Initializing));
let request_counter = Arc::new(AtomicI64::new(1));
let pending_requests = Arc::new(Mutex::new(HashMap::new()));
let (command_tx, command_rx) = mpsc::channel(100);
let receiver_task = tokio::spawn(Self::message_loop(
transport,
command_rx,
Arc::clone(&pending_requests),
Some(notification_tx),
Some(lifecycle_tx),
));
Self {
config,
state,
request_counter,
command_tx,
pending_requests,
receiver_task: Some(receiver_task),
}
}
#[must_use]
pub fn language_id(&self) -> &str {
&self.config.language_id
}
pub async fn state(&self) -> super::ServerState {
*self.state.lock().await
}
#[must_use]
pub fn request_timeout(&self) -> Duration {
Duration::from_secs(
self.config
.request_timeout_seconds
.clamp(1, crate::config::MAX_TIMEOUT_SECONDS),
)
}
#[must_use]
pub fn completion_timeout(&self) -> Duration {
self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
}
#[must_use]
pub fn code_action_resolve_timeout(&self) -> Duration {
self.request_timeout().min(CODE_ACTION_RESOLVE_TIMEOUT_CAP)
}
async fn register_and_send_request(
&self,
request: JsonRpcRequest,
response_tx: oneshot::Sender<Result<Value>>,
) -> Result<()> {
let id = request.id.clone();
self.pending_requests
.lock()
.await
.insert(id.clone(), response_tx);
if self
.command_tx
.send(ClientCommand::SendRequest { request })
.await
.is_err()
{
self.pending_requests.lock().await.remove(&id);
return Err(Error::ServerTerminated);
}
Ok(())
}
pub async fn request<P, R>(
&self,
method: &str,
params: P,
timeout_duration: Duration,
) -> Result<R>
where
P: Serialize,
R: DeserializeOwned,
{
let params_value = Self::omit_null_params(serde_json::to_value(params)?);
let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
if attempt > 0 {
debug!(
"Retrying {} (attempt {}/{}), backoff={}ms",
method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
);
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
delay_ms *= 2;
}
let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
let (response_tx, response_rx) = oneshot::channel();
let request = JsonRpcRequest {
jsonrpc: JSONRPC_VERSION.to_string(),
id: id.clone(),
method: method.to_string(),
params: params_value.clone(),
};
debug!("Sending request: {} (id={:?})", method, id);
self.register_and_send_request(request, response_tx).await?;
let outcome = match timeout(timeout_duration, response_rx).await {
Ok(received) => received.map_err(|_| Error::ServerTerminated)?,
Err(_elapsed) => {
self.pending_requests.lock().await.remove(&id);
return Err(Error::Timeout(timeout_duration.as_secs()));
}
};
match outcome {
Ok(result_value) => {
return serde_json::from_value(result_value).map_err(|e| {
Error::LspProtocolError(format!("Failed to deserialize response: {e}"))
});
}
Err(Error::LspServerError {
code,
message,
data,
}) if (code == SERVER_CANCELLED_CODE
|| (LspErrorCodes::from(code) == LspErrorCodes::ContentModified
&& CONTENT_MODIFIED_RETRY_METHODS.contains(&method)))
&& Self::should_retrigger(data.as_ref()) =>
{
if attempt == SERVER_CANCELLED_MAX_RETRIES {
error!(
"LSP error response: {} (code {}) on '{}' (id={:?}), retries exhausted",
Self::truncate_error_message_for_log(&message),
code,
method,
id
);
return Err(Error::LspServerError {
code,
message,
data,
});
}
warn!(
"LSP error response: {} (code {}) on '{}' (id={:?}), will retry",
Self::truncate_error_message_for_log(&message),
code,
method,
id
);
}
Err(Error::LspServerError {
code,
message,
data,
}) => {
error!(
"LSP error response: {} (code {}) on '{}' (id={:?})",
Self::truncate_error_message_for_log(&message),
code,
method,
id
);
return Err(Error::LspServerError {
code,
message,
data,
});
}
Err(e) => return Err(e),
}
}
Err(Error::ServerTerminated)
}
pub async fn request_typed<R>(
&self,
params: R::Params,
timeout_duration: Duration,
) -> Result<R::Result>
where
R: lsp_types::Request,
{
self.request(R::METHOD.as_str(), params, timeout_duration)
.await
}
fn should_retrigger(data: Option<&Value>) -> bool {
data.is_none_or(|v| {
v.get("retriggerRequest")
.and_then(Value::as_bool)
.unwrap_or(true)
})
}
pub(crate) async fn fail_pending_requests(&self) {
Self::drain_and_fail_pending(&self.pending_requests).await;
}
async fn drain_and_fail_pending(pending: &Arc<Mutex<PendingRequests>>) {
for (_, sender) in pending.lock().await.drain() {
let _ = sender.send(Err(Error::ServerTerminated));
}
}
pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
where
P: Serialize,
{
let params_value = Self::omit_null_params(serde_json::to_value(params)?);
debug!("Sending notification: {}", method);
self.command_tx
.send(ClientCommand::SendNotification {
method: method.to_string(),
params: params_value,
})
.await
.map_err(|_| Error::ServerTerminated)?;
Ok(())
}
pub async fn notify_typed<N>(&self, params: N::Params) -> Result<()>
where
N: lsp_types::Notification,
{
self.notify(N::METHOD.as_str(), params).await
}
pub async fn shutdown(mut self) -> Result<()> {
debug!("Shutting down LSP client");
let _ = self.command_tx.send(ClientCommand::Shutdown).await;
if let Some(task) = self.receiver_task.take() {
task.await
.map_err(|e| Error::Transport(format!("Receiver task failed: {e}")))??;
}
*self.state.lock().await = super::ServerState::Shutdown;
Ok(())
}
async fn message_loop(
transport: (LspTransport, LspTransportReader),
mut command_rx: mpsc::Receiver<ClientCommand>,
pending_requests: Arc<Mutex<PendingRequests>>,
notification_tx: Option<mpsc::Sender<LspNotification>>,
lifecycle_tx: Option<mpsc::Sender<LspNotification>>,
) -> Result<()> {
debug!("Message loop started");
let (mut transport, reader) = transport;
let (reader_handle, mut msg_rx) = spawn_reader_task(reader);
let result = {
let _abort_reader_on_drop = crate::AbortOnDrop(&reader_handle);
Self::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
notification_tx.as_ref(),
lifecycle_tx.as_ref(),
)
.await
};
let _ = timeout(READER_TASK_ABORT_GRACE, reader_handle).await;
drop(command_rx);
Self::drain_and_fail_pending(&pending_requests).await;
if let Err(ref e) = result {
error!("Message loop exiting with error: {}", e);
} else {
debug!("Message loop exiting normally");
}
result
}
fn omit_null_params(params: Value) -> Option<Value> {
if params.is_null() { None } else { Some(params) }
}
fn truncate_error_message_for_log(message: &str) -> String {
crate::util::truncate_str(message, crate::util::MAX_LOG_STRING_BYTES)
}
fn notification_lane<'a>(
notification: &LspNotification,
notification_tx: Option<&'a mpsc::Sender<LspNotification>>,
lifecycle_tx: Option<&'a mpsc::Sender<LspNotification>>,
) -> Option<(&'static str, &'a mpsc::Sender<LspNotification>)> {
match notification {
LspNotification::PublishDiagnostics(_)
| LspNotification::LogMessage(_)
| LspNotification::ShowMessage(_) => notification_tx.map(|tx| ("notification", tx)),
LspNotification::Progress(params) => {
crate::lsp::types::ProgressKind::from_value(¶ms.value)
.and(lifecycle_tx)
.map(|tx| ("lifecycle", tx))
}
LspNotification::Other { method, .. } if method.as_ref() == "$/progress" => None,
LspNotification::Other { .. } => lifecycle_tx.map(|tx| ("lifecycle", tx)),
}
}
#[allow(clippy::too_many_lines)]
async fn message_loop_inner(
transport: &mut LspTransport,
msg_rx: &mut mpsc::Receiver<Result<InboundMessage>>,
command_rx: &mut mpsc::Receiver<ClientCommand>,
pending_requests: &Arc<Mutex<PendingRequests>>,
notification_tx: Option<&mpsc::Sender<LspNotification>>,
lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
) -> Result<()> {
loop {
tokio::select! {
Some(command) = command_rx.recv() => {
match command {
ClientCommand::SendRequest { request } => {
let value = serde_json::to_value(&request)?;
transport.send(&value).await?;
}
ClientCommand::SendNotification { method, params } => {
let notification = serde_json::to_value(JsonRpcNotification {
jsonrpc: JSONRPC_VERSION.to_string(),
method,
params,
})?;
transport.send(¬ification).await?;
}
ClientCommand::Shutdown => {
debug!("Client shutdown requested");
while let Ok(message) = msg_rx.try_recv() {
match message {
Ok(m) => {
Self::handle_inbound_message(
transport,
m,
pending_requests,
notification_tx,
lifecycle_tx,
)
.await?;
}
Err(e) => {
error!("Transport receive error while draining on shutdown: {}", e);
break;
}
}
}
break;
}
}
}
message = msg_rx.recv() => {
let message = match message {
Some(Ok(m)) => m,
Some(Err(e)) => {
error!("Transport receive error: {}", e);
return Err(e);
}
None => {
error!("Transport receive error: reader task ended unexpectedly");
return Err(Error::ServerTerminated);
}
};
Self::handle_inbound_message(
transport,
message,
pending_requests,
notification_tx,
lifecycle_tx,
)
.await?;
}
}
}
Ok(())
}
async fn handle_inbound_message(
transport: &mut LspTransport,
message: InboundMessage,
pending_requests: &Arc<Mutex<PendingRequests>>,
notification_tx: Option<&mpsc::Sender<LspNotification>>,
lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
) -> Result<()> {
match message {
InboundMessage::Response(response) => {
trace!("Received response: id={:?}", response.id);
let sender = pending_requests.lock().await.remove(&response.id);
if let Some(sender) = sender {
if let Some(error) = response.error {
trace!(
"LSP error response: {} (code {})",
Self::truncate_error_message_for_log(&error.message),
error.code
);
let caller_message = crate::util::truncate_str(
&error.message,
MAX_ERROR_MESSAGE_CALLER_BYTES,
);
let _ = sender.send(Err(Error::LspServerError {
code: error.code,
message: caller_message,
data: error.data,
}));
} else if let Some(result) = response.result {
let _ = sender.send(Ok(result));
} else {
trace!("Response with null result: {:?}", response.id);
let _ = sender.send(Ok(Value::Null));
}
} else {
warn!(
"Received response for unknown request ID: {:?}",
response.id
);
}
}
InboundMessage::Request(request) => {
debug!(
"Received server request: {} (id={:?})",
request.method, request.id
);
let response = Self::server_request_response(request);
let value = serde_json::to_value(&response)?;
transport.send(&value).await?;
}
InboundMessage::Notification(notification) => {
debug!("Received notification: {}", notification.method);
let typed = LspNotification::parse(¬ification.method, notification.params);
let destination = Self::notification_lane(&typed, notification_tx, lifecycle_tx);
if let Some((lane, tx)) = destination {
if let LspNotification::PublishDiagnostics(ref params) = typed {
debug!(
"Forwarding diagnostics for {}: {} items",
params.uri.as_ref(),
params.diagnostics.len()
);
} else {
trace!("Forwarding notification: {:?}", typed);
}
if tx.try_send(typed).is_err() {
warn!(
"Dropping notification: lane={lane}, method={} \
(channel full or closed)",
notification.method
);
}
}
}
}
Ok(())
}
fn server_request_response(request: JsonRpcRequest) -> JsonRpcResponse {
match Self::server_request_result(&request.method, request.params.as_ref()) {
Ok(result) => JsonRpcResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id: request.id,
result: Some(result),
error: None,
},
Err(error) => JsonRpcResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
id: request.id,
result: None,
error: Some(error),
},
}
}
fn server_request_result(
method: &str,
params: Option<&Value>,
) -> std::result::Result<Value, JsonRpcError> {
match method {
"client/registerCapability"
| "client/unregisterCapability"
| "workspace/workspaceFolders"
| "workspace/diagnostic/refresh"
| "workspace/semanticTokens/refresh"
| "workspace/inlayHint/refresh"
| "workspace/codeLens/refresh"
| "window/showMessageRequest"
| "window/workDoneProgress/create" => Ok(Value::Null),
"workspace/configuration" => Ok(Self::workspace_configuration_result(params)),
"workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
_ => Err(JsonRpcError {
code: -32601,
message: format!("Unhandled server request: {method}"),
data: None,
}),
}
}
fn workspace_configuration_result(params: Option<&Value>) -> Value {
let item_count = params
.and_then(|value| value.get("items"))
.and_then(Value::as_array)
.map_or(0, Vec::len);
Value::Array(vec![Value::Null; item_count])
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_request_id_generation() {
let counter = AtomicI64::new(1);
let id1 = counter.fetch_add(1, Ordering::SeqCst);
let id2 = counter.fetch_add(1, Ordering::SeqCst);
let id3 = counter.fetch_add(1, Ordering::SeqCst);
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert_eq!(id3, 3);
}
#[test]
fn test_client_creation() {
let config = LspServerConfig::rust_analyzer();
let client = LspClient::new(config);
assert_eq!(client.language_id(), "rust");
}
#[test]
fn test_client_clone() {
let config = LspServerConfig::rust_analyzer();
let client = LspClient::new(config);
#[allow(clippy::redundant_clone)]
let cloned = client.clone();
assert_eq!(cloned.language_id(), "rust");
assert!(
cloned.receiver_task.is_none(),
"Cloned client should not own receiver task"
);
}
#[test]
fn test_request_timeout_and_completion_timeout_at_default() {
let config = LspServerConfig::rust_analyzer();
let client = LspClient::new(config);
assert_eq!(client.request_timeout(), Duration::from_secs(30));
assert_eq!(client.completion_timeout(), Duration::from_secs(10));
}
#[test]
fn test_completion_timeout_clamps_to_ten_seconds() {
for secs in [1, 2, 3, 30, 300] {
let mut config = LspServerConfig::rust_analyzer();
config.request_timeout_seconds = secs;
let client = LspClient::new(config);
assert_eq!(
client.completion_timeout(),
Duration::from_secs(secs.min(10)),
"request_timeout_seconds={secs}"
);
assert!(client.completion_timeout() <= client.request_timeout());
}
}
#[test]
fn test_code_action_resolve_timeout_clamps_to_ten_seconds() {
for secs in [1, 2, 3, 30, 300] {
let mut config = LspServerConfig::rust_analyzer();
config.request_timeout_seconds = secs;
let client = LspClient::new(config);
assert_eq!(
client.code_action_resolve_timeout(),
Duration::from_secs(secs.min(10)),
"request_timeout_seconds={secs}"
);
assert!(client.code_action_resolve_timeout() <= client.request_timeout());
}
}
#[test]
fn test_request_timeout_clamps_zero_to_one_second() {
let mut config = LspServerConfig::rust_analyzer();
config.request_timeout_seconds = 0;
let client = LspClient::new(config);
assert_eq!(client.request_timeout(), Duration::from_secs(1));
assert_eq!(client.completion_timeout(), Duration::from_secs(1));
}
#[test]
fn test_request_timeout_clamps_above_max_to_max() {
let mut config = LspServerConfig::rust_analyzer();
config.request_timeout_seconds = u64::MAX;
let client = LspClient::new(config);
assert_eq!(
client.request_timeout(),
Duration::from_secs(crate::config::MAX_TIMEOUT_SECONDS)
);
}
#[test]
fn test_request_timeout_independent_per_server() {
let mut config_a = LspServerConfig::rust_analyzer();
config_a.request_timeout_seconds = 5;
let mut config_b = LspServerConfig::pyright();
config_b.request_timeout_seconds = 15;
let client_a = LspClient::new(config_a);
let client_b = LspClient::new(config_b);
assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
}
#[test]
fn test_register_capability_request_is_acknowledged() {
let request = JsonRpcRequest {
jsonrpc: JSONRPC_VERSION.to_string(),
id: RequestId::String("ts1".to_string()),
method: "client/registerCapability".to_string(),
params: Some(serde_json::json!({ "registrations": [] })),
};
let response = LspClient::server_request_response(request);
assert_eq!(response.id, RequestId::String("ts1".to_string()));
assert_eq!(response.result, Some(Value::Null));
assert!(response.error.is_none());
}
#[test]
fn test_workspace_configuration_request_returns_null_per_item() {
let result = LspClient::workspace_configuration_result(Some(&serde_json::json!({
"items": [{ "section": "typescript" }, { "section": "editor" }]
})));
assert_eq!(result, serde_json::json!([null, null]));
}
#[test]
fn test_work_done_progress_create_request_is_acknowledged() {
let request = JsonRpcRequest {
jsonrpc: JSONRPC_VERSION.to_string(),
id: RequestId::String("wdp1".to_string()),
method: "window/workDoneProgress/create".to_string(),
params: Some(serde_json::json!({ "token": "indexing" })),
};
let response = LspClient::server_request_response(request);
assert_eq!(response.result, Some(Value::Null));
assert!(response.error.is_none());
}
#[test]
fn test_report_progress_frame_reaches_neither_lane() {
let (notification_tx, _notification_rx) = mpsc::channel(8);
let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
let report = LspNotification::Progress(lsp_types::ProgressParams {
token: lsp_types::ProgressToken::Int(1),
value: serde_json::json!({ "kind": "report", "percentage": 50 }),
});
let destination =
LspClient::notification_lane(&report, Some(¬ification_tx), Some(&lifecycle_tx));
assert!(
destination.is_none(),
"a report-kind frame must be dropped before reaching either lane"
);
}
#[test]
fn test_malformed_progress_other_reaches_neither_lane() {
let (notification_tx, _notification_rx) = mpsc::channel(8);
let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
let malformed = LspNotification::Other {
method: std::borrow::Cow::Borrowed("$/progress"),
params: None,
};
let destination =
LspClient::notification_lane(&malformed, Some(¬ification_tx), Some(&lifecycle_tx));
assert!(
destination.is_none(),
"a malformed $/progress frame must be dropped, not routed to the lifecycle lane"
);
}
#[test]
fn test_begin_and_other_notifications_reach_lifecycle_lane() {
let (notification_tx, _notification_rx) = mpsc::channel(8);
let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
let begin = LspNotification::Progress(lsp_types::ProgressParams {
token: lsp_types::ProgressToken::Int(1),
value: serde_json::json!({ "kind": "begin", "title": "Indexing" }),
});
assert_eq!(
LspClient::notification_lane(&begin, Some(¬ification_tx), Some(&lifecycle_tx))
.map(|(lane, _)| lane),
Some("lifecycle")
);
let server_status = LspNotification::Other {
method: std::borrow::Cow::Borrowed("experimental/serverStatus"),
params: Some(serde_json::json!({ "quiescent": false })),
};
assert_eq!(
LspClient::notification_lane(
&server_status,
Some(¬ification_tx),
Some(&lifecycle_tx)
)
.map(|(lane, _)| lane),
Some("lifecycle")
);
}
#[test]
fn test_unknown_server_request_returns_method_not_found() {
let request = JsonRpcRequest {
jsonrpc: JSONRPC_VERSION.to_string(),
id: RequestId::String("unknown-1".to_string()),
method: "custom/request".to_string(),
params: None,
};
let response = LspClient::server_request_response(request);
assert!(response.result.is_none());
match response.error {
Some(error) => {
assert_eq!(error.code, -32601);
assert_eq!(error.message, "Unhandled server request: custom/request");
}
None => panic!("unknown request should return error"),
}
}
#[tokio::test]
async fn test_null_response_handling() {
use crate::lsp::types::{JsonRpcResponse, RequestId};
let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(RequestId::Number(1), response_tx);
let null_response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: RequestId::Number(1),
result: None,
error: None,
};
let sender = pending_requests.lock().await.remove(&null_response.id);
if let Some(sender) = sender {
let _ = sender.send(Ok(Value::Null));
}
let timeout_result =
tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
assert!(timeout_result.is_ok(), "Should not timeout");
let channel_result = timeout_result.unwrap();
assert!(
channel_result.is_ok(),
"Channel should not be closed: {:?}",
channel_result.err()
);
let response = channel_result.unwrap();
assert!(
response.is_ok(),
"Should receive Ok(Value::Null), not Err: {:?}",
response.err()
);
let value = response.unwrap();
assert_eq!(value, Value::Null, "Should receive Value::Null");
}
#[tokio::test]
async fn test_error_response_handling() {
use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(RequestId::Number(1), response_tx);
let error_response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: RequestId::Number(1),
result: None,
error: Some(JsonRpcError {
code: -32601,
message: "Method not found".to_string(),
data: None,
}),
};
let sender = pending_requests.lock().await.remove(&error_response.id);
if let Some(sender) = sender
&& let Some(error) = error_response.error
{
let _ = sender.send(Err(Error::LspServerError {
code: error.code,
message: error.message,
data: error.data,
}));
}
let result = response_rx.await.unwrap();
assert!(result.is_err(), "Should receive error");
if let Err(Error::LspServerError { code, message, .. }) = result {
assert_eq!(code, -32601);
assert_eq!(message, "Method not found");
} else {
panic!("Expected LspServerError");
}
}
#[tokio::test]
async fn test_unknown_request_id() {
use crate::lsp::types::{JsonRpcResponse, RequestId};
let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
let response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: RequestId::Number(999),
result: Some(Value::Null),
error: None,
};
let sender = pending_requests.lock().await.remove(&response.id);
assert!(sender.is_none(), "Should not find sender for unknown ID");
}
#[test]
fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
let truncated = LspClient::truncate_error_message_for_log(&message);
assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
}
#[test]
fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
let exact = "x".repeat(200);
assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
assert_eq!(LspClient::truncate_error_message_for_log(""), "");
}
#[test]
fn test_truncate_error_message_for_log_truncates_just_above_limit() {
let message = "x".repeat(201);
assert_eq!(
LspClient::truncate_error_message_for_log(&message),
format!("{}... (truncated)", "x".repeat(200))
);
}
#[test]
fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
let truncated = LspClient::truncate_error_message_for_log(&message);
assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
}
#[tokio::test]
async fn test_concurrent_request_ids() {
let counter = Arc::new(AtomicI64::new(1));
let counter1 = Arc::clone(&counter);
let counter2 = Arc::clone(&counter);
let counter3 = Arc::clone(&counter);
let handles = vec![
tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
];
let mut ids = Vec::new();
for handle in handles {
ids.push(handle.await.unwrap());
}
ids.sort_unstable();
assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
}
#[test]
fn test_jsonrpc_version_constant() {
assert_eq!(JSONRPC_VERSION, "2.0");
}
#[cfg(unix)]
#[tokio::test]
async fn test_request_timeout_removes_pending_entry() {
let mut child = tokio::process::Command::new("sleep")
.arg("2")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let transport = LspTransport::new(stdin, stdout);
let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
let result: Result<Value> = client
.request(
"textDocument/hover",
serde_json::json!({}),
Duration::from_millis(50),
)
.await;
assert!(matches!(result, Err(Error::Timeout(_))), "got {result:?}");
assert!(
client.pending_requests.lock().await.is_empty(),
"timed-out request must not remain in pending_requests"
);
}
#[tokio::test]
async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
let (command_tx, _command_rx) = mpsc::channel(1);
let client = LspClient {
config: LspServerConfig::rust_analyzer(),
state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
request_counter: Arc::new(AtomicI64::new(1)),
command_tx,
pending_requests: Arc::clone(&pending_requests),
receiver_task: None,
};
let (tx1, rx1) = oneshot::channel::<Result<Value>>();
let (tx2, rx2) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(RequestId::Number(1), tx1);
pending_requests
.lock()
.await
.insert(RequestId::Number(2), tx2);
client.fail_pending_requests().await;
assert!(pending_requests.lock().await.is_empty());
assert!(matches!(rx1.await.unwrap(), Err(Error::ServerTerminated)));
assert!(matches!(rx2.await.unwrap(), Err(Error::ServerTerminated)));
}
#[test]
fn test_should_retrigger_defaults_to_true_when_data_absent() {
assert!(LspClient::should_retrigger(None));
}
#[test]
fn test_should_retrigger_false_when_flag_false() {
assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
"retriggerRequest": false
}))));
}
#[test]
fn test_should_retrigger_true_when_flag_true() {
assert!(LspClient::should_retrigger(Some(&serde_json::json!({
"retriggerRequest": true
}))));
}
mod void_params_wire {
use tokio::io::BufReader;
use super::*;
use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
#[tokio::test]
async fn test_request_with_null_params_omits_params_key() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>("shutdown", Value::Null, Duration::from_secs(5))
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
assert_eq!(request["method"], "shutdown");
assert!(
request.get("params").is_none(),
"null params must be omitted, got: {request}"
);
write_response(&mut server.read_half_stdin, &request["id"], Value::Null).await;
request_task.await.unwrap().unwrap();
}
#[tokio::test]
async fn test_notify_with_null_params_omits_params_key() {
let (client, mut server) = fake_lsp_client();
client.notify("exit", Value::Null).await.unwrap();
let mut reader = BufReader::new(&mut server.write_stdout);
let notification = read_framed_message(&mut reader).await;
assert_eq!(notification["method"], "exit");
assert!(
notification.get("params").is_none(),
"null params must be omitted, got: {notification}"
);
}
#[tokio::test]
async fn test_notify_with_empty_object_params_keeps_params_key() {
let (client, mut server) = fake_lsp_client();
client
.notify("initialized", lsp_types::InitializedParams {})
.await
.unwrap();
let mut reader = BufReader::new(&mut server.write_stdout);
let notification = read_framed_message(&mut reader).await;
assert_eq!(notification["method"], "initialized");
assert_eq!(
notification["params"],
serde_json::json!({}),
"non-null params must still be sent"
);
}
}
mod retry_behavior {
use tokio::io::{AsyncWriteExt, BufReader, DuplexStream};
use super::*;
use crate::test_lsp::{
CapturedLogs, fake_lsp_client, read_framed_message, write_error_response,
write_response as write_success_response,
};
async fn write_retryable_error_response(
stdin: &mut DuplexStream,
id: &Value,
code: i32,
message: &str,
retrigger: bool,
) {
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": code,
"message": message,
"data": { "retriggerRequest": retrigger },
},
});
let content = serde_json::to_string(&response).unwrap();
let header = format!("Content-Length: {}\r\n\r\n", content.len());
stdin.write_all(header.as_bytes()).await.unwrap();
stdin.write_all(content.as_bytes()).await.unwrap();
stdin.flush().await.unwrap();
}
#[tokio::test]
async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
use tracing_subscriber::layer::SubscriberExt as _;
let (client, mut server) = fake_lsp_client();
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
SERVER_CANCELLED_CODE,
"server cancelled the request",
true,
)
.await;
}
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError {
code,
message,
data,
}) => {
assert_eq!(code, SERVER_CANCELLED_CODE);
assert_eq!(message, "server cancelled the request");
assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
}
other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
}
drop(guard);
let logs = captured.entries();
assert_eq!(
logs.iter()
.filter(|(level, _)| *level == tracing::Level::ERROR)
.count(),
1,
"exactly the final exhausted attempt must log at ERROR, got: {logs:?}"
);
assert!(
logs.iter()
.any(|(level, msg)| *level == tracing::Level::ERROR
&& msg.contains("LSP error response")
&& msg.contains("retries exhausted")),
"expected an ERROR log sharing the 'LSP error response' prefix and naming \
retry exhaustion, got: {logs:?}"
);
assert_eq!(
logs.iter()
.filter(
|(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
)
.count(),
usize::try_from(SERVER_CANCELLED_MAX_RETRIES).unwrap(),
"every attempt before the last must log a WARN 'will retry' line, got: {logs:?}"
);
}
#[tokio::test]
async fn test_retrigger_false_returns_immediately_without_retry() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
SERVER_CANCELLED_CODE,
"server cancelled the request",
false,
)
.await;
let result = tokio::time::timeout(Duration::from_millis(200), request_task)
.await
.unwrap()
.unwrap();
match result {
Err(Error::LspServerError { code, .. }) => {
assert_eq!(code, SERVER_CANCELLED_CODE);
}
other => panic!("expected immediate ServerCancelled error, got {other:?}"),
}
let second_request =
tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
.await;
assert!(
second_request.is_err(),
"no retry should have been sent after retriggerRequest: false"
);
}
#[tokio::test]
async fn test_retry_succeeds_after_one_server_cancelled_response() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
write_retryable_error_response(
&mut server.read_half_stdin,
&first["id"].clone(),
SERVER_CANCELLED_CODE,
"server cancelled the request",
true,
)
.await;
let second = read_framed_message(&mut reader).await;
assert_ne!(
first["id"], second["id"],
"retry must use a fresh request id"
);
let expected_result = serde_json::json!({ "contents": "resolved on retry" });
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
expected_result.clone(),
)
.await;
let result = request_task.await.unwrap();
assert_eq!(result.unwrap(), expected_result);
}
#[tokio::test]
async fn test_retry_exhaustion_returns_original_content_modified_error() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
}
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError {
code,
message,
data,
}) => {
assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
assert_eq!(message, "content modified");
assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
}
other => panic!("expected exhausted ContentModified error, got {other:?}"),
}
}
#[tokio::test]
async fn test_retrigger_false_returns_immediately_without_retry_for_content_modified() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
false,
)
.await;
let result = tokio::time::timeout(Duration::from_millis(200), request_task)
.await
.unwrap()
.unwrap();
match result {
Err(Error::LspServerError { code, .. }) => {
assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
}
other => panic!("expected immediate ContentModified error, got {other:?}"),
}
let second_request =
tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
.await;
assert!(
second_request.is_err(),
"no retry should have been sent after retriggerRequest: false"
);
}
#[tokio::test]
async fn test_retry_succeeds_after_one_content_modified_response() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
write_retryable_error_response(
&mut server.read_half_stdin,
&first["id"].clone(),
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
let second = read_framed_message(&mut reader).await;
assert_ne!(
first["id"], second["id"],
"retry must use a fresh request id"
);
let expected_result = serde_json::json!({ "contents": "resolved on retry" });
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
expected_result.clone(),
)
.await;
let result = request_task.await.unwrap();
assert_eq!(result.unwrap(), expected_result);
}
#[tokio::test]
async fn test_content_modified_on_non_allowlisted_method_does_not_retry() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/rename",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
let result = tokio::time::timeout(Duration::from_millis(200), request_task)
.await
.unwrap()
.unwrap();
match result {
Err(Error::LspServerError { code, .. }) => {
assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
}
other => panic!("expected immediate ContentModified error, got {other:?}"),
}
let second_request =
tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
.await;
assert!(
second_request.is_err(),
"no retry should have been sent for a non-allowlisted method"
);
}
#[tokio::test]
async fn test_oversized_error_message_truncated_for_caller() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
.await;
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError { code, message, .. }) => {
assert_eq!(code, -32603);
assert!(
message.len() < oversized_message.len(),
"caller-facing message must be truncated, got {} bytes",
message.len()
);
assert!(message.ends_with("... (truncated)"));
}
other => panic!("expected truncated LspServerError, got {other:?}"),
}
}
#[tokio::test]
async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
let message = "x".repeat(crate::util::MAX_LOG_STRING_BYTES + 50);
write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError {
message: returned, ..
}) => {
assert_eq!(
returned, message,
"message under the caller cap must not be truncated"
);
}
other => panic!("expected untruncated LspServerError, got {other:?}"),
}
}
#[tokio::test]
async fn test_retried_error_that_recovers_does_not_log_error_level() {
use tracing_subscriber::layer::SubscriberExt as _;
let (client, mut server) = fake_lsp_client();
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
write_retryable_error_response(
&mut server.read_half_stdin,
&first["id"].clone(),
SERVER_CANCELLED_CODE,
"server cancelled the request",
true,
)
.await;
let second = read_framed_message(&mut reader).await;
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
serde_json::json!({ "contents": "resolved on retry" }),
)
.await;
let result = request_task.await.unwrap();
assert!(result.is_ok(), "expected retry to recover, got {result:?}");
drop(guard);
let logs = captured.entries();
assert!(
!logs
.iter()
.any(|(level, _)| *level == tracing::Level::ERROR),
"a retried-and-recovered error must not log at ERROR, got: {logs:?}"
);
assert!(
logs.iter().any(
|(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
),
"expected a WARN 'will retry' log line, got: {logs:?}"
);
}
#[tokio::test]
async fn test_non_retryable_error_logs_error_level() {
use tracing_subscriber::layer::SubscriberExt as _;
let (client, mut server) = fake_lsp_client();
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/rename",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
let result = request_task.await.unwrap();
assert!(result.is_err(), "expected a non-retryable error");
drop(guard);
let logs = captured.entries();
assert!(
logs.iter()
.any(|(level, msg)| *level == tracing::Level::ERROR
&& msg.contains("LSP error response")
&& msg.contains("content modified")),
"a non-retryable error must still surface an ERROR log sharing the \
'LSP error response' prefix, got: {logs:?}"
);
}
}
mod reader_task_regression {
use tokio::io::BufReader;
use super::*;
use crate::test_lsp::{
fake_lsp_client, inert_transport, read_framed_message, write_response,
};
#[tokio::test]
async fn test_message_loop_inner_treats_reader_channel_close_as_server_terminated() {
let (mut transport, _reader) = inert_transport();
let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests: Arc<Mutex<PendingRequests>> =
Arc::new(Mutex::new(HashMap::new()));
drop(msg_tx);
let result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
None,
)
.await;
assert!(
matches!(result, Err(Error::ServerTerminated)),
"got {result:?}"
);
}
#[tokio::test]
async fn test_message_loop_inner_reader_gone_then_drain_fails_pending_request() {
let (mut transport, _reader) = inert_transport();
let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests: Arc<Mutex<PendingRequests>> =
Arc::new(Mutex::new(HashMap::new()));
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(RequestId::Number(1), response_tx);
drop(msg_tx);
let result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
None,
)
.await;
assert!(
matches!(result, Err(Error::ServerTerminated)),
"got {result:?}"
);
LspClient::drain_and_fail_pending(&pending_requests).await;
let received = response_rx.await.unwrap();
assert!(
matches!(received, Err(Error::ServerTerminated)),
"got {received:?}"
);
assert!(pending_requests.lock().await.is_empty());
}
#[tokio::test]
async fn test_shutdown_drains_buffered_responses_instead_of_dropping_them() {
let (mut transport, _reader) = inert_transport();
let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests: Arc<Mutex<PendingRequests>> =
Arc::new(Mutex::new(HashMap::new()));
let id = RequestId::Number(1);
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(id.clone(), response_tx);
msg_tx
.send(Ok(InboundMessage::Response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: id.clone(),
result: Some(serde_json::json!({ "ok": true })),
error: None,
})))
.await
.unwrap();
command_tx.send(ClientCommand::Shutdown).await.unwrap();
drop(command_tx);
let result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
None,
)
.await;
assert!(result.is_ok(), "got {result:?}");
let received = response_rx.await.unwrap();
assert_eq!(received.unwrap(), serde_json::json!({ "ok": true }));
assert!(
pending_requests.lock().await.is_empty(),
"the drained response must resolve its pending request entry"
);
}
#[tokio::test]
async fn test_message_loop_fails_pending_requests_on_transport_error_exit() {
let (transport, reader) = inert_transport();
let (_command_tx, command_rx) = mpsc::channel::<ClientCommand>(1);
let pending_requests: Arc<Mutex<PendingRequests>> =
Arc::new(Mutex::new(HashMap::new()));
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(RequestId::Number(1), response_tx);
let result = LspClient::message_loop(
(transport, reader),
command_rx,
Arc::clone(&pending_requests),
None,
None,
)
.await;
assert!(result.is_err(), "got {result:?}");
let received = response_rx.await.unwrap();
assert!(
matches!(received, Err(Error::ServerTerminated)),
"got {received:?}"
);
assert!(pending_requests.lock().await.is_empty());
}
#[tokio::test]
async fn test_message_loop_shutdown_resolves_answered_and_fails_unanswered_pending() {
let (mut transport, _reader) = inert_transport();
let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests: Arc<Mutex<PendingRequests>> =
Arc::new(Mutex::new(HashMap::new()));
let answered_id = RequestId::Number(1);
let (answered_tx, answered_rx) = oneshot::channel::<Result<Value>>();
let unanswered_id = RequestId::Number(2);
let (unanswered_tx, unanswered_rx) = oneshot::channel::<Result<Value>>();
pending_requests
.lock()
.await
.insert(answered_id.clone(), answered_tx);
pending_requests
.lock()
.await
.insert(unanswered_id.clone(), unanswered_tx);
msg_tx
.send(Ok(InboundMessage::Response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: answered_id,
result: Some(serde_json::json!({ "ok": true })),
error: None,
})))
.await
.unwrap();
command_tx.send(ClientCommand::Shutdown).await.unwrap();
drop(command_tx);
let inner_result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
None,
)
.await;
assert!(inner_result.is_ok(), "got {inner_result:?}");
LspClient::drain_and_fail_pending(&pending_requests).await;
let answered = answered_rx.await.unwrap();
assert_eq!(answered.unwrap(), serde_json::json!({ "ok": true }));
let unanswered = unanswered_rx.await.unwrap();
assert!(
matches!(unanswered, Err(Error::ServerTerminated)),
"got {unanswered:?}"
);
assert!(pending_requests.lock().await.is_empty());
}
#[tokio::test]
async fn test_message_loop_end_to_end_resolves_answered_then_fails_unanswered_on_shutdown()
{
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let answered_client = client.clone();
let answered_task = tokio::spawn(async move {
answered_client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_response(
&mut server.read_half_stdin,
&id,
serde_json::json!({ "ok": true }),
)
.await;
let answered = answered_task.await.unwrap().unwrap();
assert_eq!(answered, serde_json::json!({ "ok": true }));
let unanswered_client = client.clone();
let unanswered_task = tokio::spawn(async move {
unanswered_client
.request::<_, Value>(
"textDocument/definition",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let _unanswered_request = read_framed_message(&mut reader).await;
client.shutdown().await.unwrap();
let unanswered = tokio::time::timeout(Duration::from_millis(200), unanswered_task)
.await
.unwrap_or_else(|_| {
panic!(
"unanswered request must fail immediately on shutdown, not hang until its own timeout"
)
})
.unwrap();
assert!(
matches!(unanswered, Err(Error::ServerTerminated)),
"got {unanswered:?}"
);
}
#[tokio::test]
async fn test_dense_concurrent_requests_all_resolve_to_matching_responses() {
const REQUEST_COUNT: usize = 20;
let (client, mut server) = fake_lsp_client();
let mut request_tasks = Vec::with_capacity(REQUEST_COUNT);
for i in 0..REQUEST_COUNT {
let client = client.clone();
request_tasks.push(tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({ "n": i }),
Duration::from_secs(30),
)
.await
}));
}
let server_task = tokio::spawn(async move {
let mut reader = BufReader::new(&mut server.write_stdout);
let mut requests = Vec::with_capacity(REQUEST_COUNT);
for _ in 0..REQUEST_COUNT {
requests.push(read_framed_message(&mut reader).await);
}
for request in requests.into_iter().rev() {
let id = request["id"].clone();
let n = request["params"]["n"].clone();
write_response(
&mut server.read_half_stdin,
&id,
serde_json::json!({ "echo": n }),
)
.await;
}
});
server_task.await.unwrap();
for (i, task) in request_tasks.into_iter().enumerate() {
let value = task
.await
.unwrap()
.unwrap_or_else(|e| panic!("request {i} failed: {e:?}"));
assert_eq!(
value["echo"],
serde_json::json!(i),
"response for request {i} carried the wrong payload -- id/response mismatch"
);
}
}
#[tokio::test]
async fn test_request_after_shutdown_fails_fast_instead_of_hanging() {
let (client, _server) = fake_lsp_client();
let post_shutdown_client = client.clone();
client.shutdown().await.unwrap();
let result = tokio::time::timeout(
Duration::from_millis(200),
post_shutdown_client.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
),
)
.await
.unwrap_or_else(|_| {
panic!(
"request after shutdown must fail immediately, not hang until its own timeout"
)
});
assert!(
matches!(result, Err(Error::ServerTerminated)),
"got {result:?}"
);
}
}
}