#![allow(dead_code)]
use php_lsp::backend::Backend;
use serde_json::{Value, json};
use std::collections::VecDeque;
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream, ReadHalf, WriteHalf};
use tokio::time::Duration;
use tower_lsp_server::{LspService, Server};
pub(super) fn frame(msg: &Value) -> Vec<u8> {
let body = serde_json::to_string(msg).unwrap();
format!("Content-Length: {}\r\n\r\n{}", body.len(), body).into_bytes()
}
pub(super) async fn read_msg(reader: &mut (impl AsyncReadExt + Unpin)) -> Value {
let mut header_buf = Vec::new();
loop {
let b = reader.read_u8().await.expect("read byte");
header_buf.push(b);
if header_buf.ends_with(b"\r\n\r\n") {
break;
}
}
let header_str = std::str::from_utf8(&header_buf).unwrap();
let content_length: usize = header_str
.lines()
.find(|l| l.to_lowercase().starts_with("content-length:"))
.and_then(|l| l.split(':').nth(1))
.and_then(|v| v.trim().parse().ok())
.expect("Content-Length header");
let mut body = vec![0u8; content_length];
reader.read_exact(&mut body).await.expect("read body");
serde_json::from_slice(&body).expect("parse JSON")
}
async fn recv_or_buffered(
pending: &mut VecDeque<Value>,
read: &mut ReadHalf<DuplexStream>,
write: &mut WriteHalf<DuplexStream>,
budget: &mut usize,
) -> Value {
loop {
let msg = if *budget > 0 {
*budget -= 1;
pending
.pop_front()
.expect("budget bounded by pending.len()")
} else {
read_msg(read).await
};
if msg.get("method").is_some()
&& let Some(id) = msg.get("id")
{
let response = json!({
"jsonrpc": "2.0",
"id": id,
"result": null,
});
write.write_all(&frame(&response)).await.unwrap();
continue;
}
return msg;
}
}
pub struct TestClient {
pub(crate) write: WriteHalf<DuplexStream>,
pub(crate) read: ReadHalf<DuplexStream>,
pub(crate) next_id: u64,
pending: VecDeque<Value>,
}
impl TestClient {
pub async fn request(&mut self, method: &str, params: Value) -> Value {
let id = self.next_id;
self.next_id += 1;
let msg = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
self.write.write_all(&frame(&msg)).await.unwrap();
let method_owned = method.to_owned();
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("id") == Some(&json!(id)) {
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for response to {method_owned}"))
}
pub async fn request_no_params(&mut self, method: &str) -> Value {
let id = self.next_id;
self.next_id += 1;
let msg = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
});
self.write.write_all(&frame(&msg)).await.unwrap();
let method_owned = method.to_owned();
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("id") == Some(&json!(id)) {
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for response to {method_owned}"))
}
pub async fn request_then_cancel(&mut self, method: &str, params: Value) -> Value {
let id = self.next_id;
self.next_id += 1;
let msg = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
self.write.write_all(&frame(&msg)).await.unwrap();
let cancel = json!({
"jsonrpc": "2.0",
"method": "$/cancelRequest",
"params": { "id": id },
});
self.write.write_all(&frame(&cancel)).await.unwrap();
let method_owned = method.to_owned();
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("id") == Some(&json!(id)) {
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for response to {method_owned}"))
}
pub async fn notify(&mut self, method: &str, params: Value) {
let msg = json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
});
self.write.write_all(&frame(&msg)).await.unwrap();
}
pub async fn request_capturing_notifications(
&mut self,
method: &str,
params: Value,
capture_method: &str,
) -> (Value, Vec<Value>) {
let id = self.next_id;
self.next_id += 1;
let msg = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
self.write.write_all(&frame(&msg)).await.unwrap();
let method_owned = method.to_owned();
let mut captured = Vec::new();
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
let resp = tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!(capture_method)) {
captured.push(msg);
continue;
}
if msg.get("id") == Some(&json!(id)) {
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for response to {method_owned}"));
(resp, captured)
}
pub async fn read_notification(&mut self, method: &str) -> Value {
let mut budget = 0;
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!(method)) {
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for {method} notification"))
}
pub async fn wait_for_diagnostics(&mut self, uri: &str) -> Value {
let uri_val = json!(uri);
let mut budget = 0;
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!("textDocument/publishDiagnostics"))
&& msg["params"]["uri"] == uri_val
{
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for publishDiagnostics for {uri}"))
}
pub async fn wait_for_diagnostics_multi(
&mut self,
uris: &[&str],
) -> std::collections::HashMap<String, Value> {
let mut remaining: std::collections::HashSet<String> =
uris.iter().map(|s| s.to_string()).collect();
let mut collected: std::collections::HashMap<String, Value> =
std::collections::HashMap::new();
let expected = remaining.clone();
let mut budget = 0;
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(10), async {
while !remaining.is_empty() {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
let wanted_uri = (msg.get("method")
== Some(&json!("textDocument/publishDiagnostics")))
.then(|| msg["params"]["uri"].as_str())
.flatten()
.filter(|uri| remaining.contains(*uri))
.map(|uri| uri.to_string());
match wanted_uri {
Some(uri) => {
remaining.remove(&uri);
collected.insert(uri, msg);
}
None => pending.push_back(msg),
}
}
})
.await
.unwrap_or_else(|_| {
panic!("timed out; expected publishDiagnostics for {expected:?}, got {collected:?}")
});
collected
}
pub async fn drain_publish_diagnostics_uris(&mut self, duration: Duration) -> Vec<String> {
let mut uris = Vec::new();
let mut budget = 0;
let TestClient {
pending,
read,
write,
..
} = self;
let _ = tokio::time::timeout(duration, async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!("textDocument/publishDiagnostics")) {
if let Some(uri) = msg["params"]["uri"].as_str() {
uris.push(uri.to_string());
}
} else {
pending.push_back(msg);
}
}
})
.await;
uris
}
pub async fn expect_server_request(&mut self, method: &str) -> (Value, Value) {
tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = read_msg(&mut self.read).await;
if msg.get("method") == Some(&json!(method)) && msg.get("id").is_some() {
let id = msg["id"].clone();
let params = msg.get("params").cloned().unwrap_or(json!(null));
return (id, params);
}
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for server request {method}"))
}
pub async fn reply_to_server_request(&mut self, id: Value, result: Value) {
let response = json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
});
self.write.write_all(&frame(&response)).await.unwrap();
}
pub async fn collect_until_index_ready(&mut self) -> Vec<Value> {
let mut notifications: Vec<Value> = Vec::new();
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!("$/php-lsp/indexReady")) {
return;
}
notifications.push(msg.clone());
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for $/php-lsp/indexReady"));
notifications
}
pub async fn wait_for_index_ready_secs(&mut self, secs: u64) {
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(secs), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!("$/php-lsp/indexReady")) {
return;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for $/php-lsp/indexReady after {secs}s"))
}
pub async fn wait_for_index_ready(&mut self) {
let mut budget = self.pending.len();
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(10), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!("$/php-lsp/indexReady")) {
return;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for $/php-lsp/indexReady"))
}
pub async fn wait_for_log_message_starting_with(&mut self, prefix: &str) -> Value {
let mut budget = 0;
let TestClient {
pending,
read,
write,
..
} = self;
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let msg = recv_or_buffered(pending, read, write, &mut budget).await;
if msg.get("method") == Some(&json!("window/logMessage"))
&& msg["params"]["message"]
.as_str()
.unwrap_or("")
.starts_with(prefix)
{
return msg;
}
pending.push_back(msg);
}
})
.await
.unwrap_or_else(|_| panic!("timed out waiting for log message starting with {prefix:?}"))
}
}
pub(crate) fn spawn_server() -> TestClient {
let (client_stream, server_stream) = tokio::io::duplex(1 << 20);
let (server_read, server_write) = tokio::io::split(server_stream);
let (client_read, client_write) = tokio::io::split(client_stream);
let (service, socket) = LspService::build(Backend::new)
.custom_method("$/php-lsp/debugStats", Backend::debug_stats)
.finish();
tokio::spawn(Server::new(server_read, server_write, socket).serve(service));
TestClient {
write: client_write,
read: client_read,
next_id: 1,
pending: VecDeque::new(),
}
}