use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, DuplexStream};
use tokio::time::Duration;
use tokio_util::sync::CancellationToken;
#[cfg(unix)]
use crate::ServerId;
use crate::bridge::WorkspaceRoots;
#[cfg(unix)]
use crate::config::{LanguageId, ServerCommand, TimeoutSecs};
use crate::config::{LspServerConfig, PositionEncodings};
use crate::lsp::{LspClient, LspTransport, LspTransportReader, ServerInitConfig};
const MOCK_PIPE_CAPACITY: usize = 256 * 1024;
const READ_FRAME_TIMEOUT: Duration = Duration::from_secs(30);
pub struct FakeServer {
pub read_half_stdin: DuplexStream,
pub write_stdout: DuplexStream,
}
pub fn fake_lsp_client() -> (LspClient, FakeServer) {
fake_lsp_client_with_config(LspServerConfig::rust_analyzer())
}
pub fn fake_lsp_client_with_config(config: LspServerConfig) -> (LspClient, FakeServer) {
let (transport, fake_server) = fake_transport();
(LspClient::from_transport(config, transport), fake_server)
}
pub struct FakeLanes {
pub notification_rx: tokio::sync::mpsc::Receiver<crate::lsp::ServerMessage>,
pub publishes: crate::lsp::PublishReader,
pub lifecycle_rx: tokio::sync::mpsc::Receiver<crate::lsp::LspNotification>,
}
pub fn fake_lsp_client_with_lanes() -> (LspClient, FakeServer, FakeLanes) {
fake_lsp_client_with_redactions(crate::redaction::Redactions::default())
}
pub fn fake_lsp_client_with_redactions(
redactions: crate::redaction::Redactions,
) -> (LspClient, FakeServer, FakeLanes) {
let (transport, fake_server) = fake_transport();
let (notification_tx, notification_rx) = tokio::sync::mpsc::channel(32);
let (notification_sink, publishes) = crate::lsp::NotificationSink::new(notification_tx);
let (lifecycle_tx, lifecycle_rx) = tokio::sync::mpsc::channel(8);
let client = LspClient::from_transport_with_notifications(
LspServerConfig::rust_analyzer(),
transport,
notification_sink,
lifecycle_tx,
std::sync::Arc::new(redactions),
);
(
client,
fake_server,
FakeLanes {
notification_rx,
publishes,
lifecycle_rx,
},
)
}
fn fake_transport() -> ((LspTransport, LspTransportReader), FakeServer) {
let (client_stdin, write_stdout) = tokio::io::duplex(MOCK_PIPE_CAPACITY);
let (read_half_stdin, client_stdout) = tokio::io::duplex(MOCK_PIPE_CAPACITY);
(
LspTransport::new(client_stdin, client_stdout),
FakeServer {
read_half_stdin,
write_stdout,
},
)
}
#[must_use]
pub fn inert_transport() -> (LspTransport, LspTransportReader) {
let (stdin, _unused_read) = tokio::io::duplex(1);
let (_unused_write, stdout) = tokio::io::duplex(1);
LspTransport::new(stdin, stdout)
}
pub async fn read_framed_message(reader: &mut BufReader<&mut DuplexStream>) -> Value {
tokio::time::timeout(READ_FRAME_TIMEOUT, read_framed_message_inner(reader))
.await
.expect("timed out waiting for a complete framed JSON-RPC message")
}
pub async fn next_framed_message(reader: &mut BufReader<&mut DuplexStream>) -> Value {
read_framed_message_inner(reader).await
}
async fn read_framed_message_inner(reader: &mut BufReader<&mut DuplexStream>) -> Value {
let mut content_length = None;
let mut line = String::new();
loop {
line.clear();
let bytes_read = reader.read_line(&mut line).await.unwrap();
assert!(bytes_read != 0, "EOF before a complete frame was read");
if line == "\r\n" || line == "\n" {
break;
}
if let Some((key, value)) = line.trim_end().split_once(':')
&& key.trim().eq_ignore_ascii_case("content-length")
{
content_length = Some(value.trim().parse::<usize>().unwrap());
}
}
let mut buf = vec![0u8; content_length.unwrap()];
reader.read_exact(&mut buf).await.unwrap();
serde_json::from_slice(&buf).unwrap()
}
pub async fn write_response(writer: &mut DuplexStream, id: &Value, result: Value) {
write_framed(
writer,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
}),
)
.await;
}
pub async fn write_error_response(writer: &mut DuplexStream, id: &Value, code: i64, message: &str) {
write_framed(
writer,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": code, "message": message },
}),
)
.await;
}
pub async fn write_request(writer: &mut DuplexStream, id: &Value, method: &str, params: Value) {
write_framed(
writer,
&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
}),
)
.await;
}
pub async fn write_notification(writer: &mut DuplexStream, method: &str, params: Value) {
write_framed(
writer,
&serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
}),
)
.await;
}
async fn write_framed(writer: &mut DuplexStream, message: &Value) {
write_raw_frame(writer, &serde_json::to_string(message).unwrap()).await;
}
pub async fn write_raw_frame(writer: &mut DuplexStream, content: &str) {
let header = format!("Content-Length: {}\r\n\r\n", content.len());
writer.write_all(header.as_bytes()).await.unwrap();
writer.write_all(content.as_bytes()).await.unwrap();
writer.flush().await.unwrap();
}
#[derive(Clone, Default)]
pub struct CapturedLogs(std::sync::Arc<std::sync::Mutex<Vec<(tracing::Level, String)>>>);
impl CapturedLogs {
pub fn entries(&self) -> Vec<(tracing::Level, String)> {
self.0.lock().unwrap().clone()
}
pub fn messages(&self) -> Vec<String> {
self.0
.lock()
.unwrap()
.iter()
.map(|(_, message)| message.clone())
.collect()
}
}
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for CapturedLogs {
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
struct MessageVisitor(String);
impl tracing::field::Visit for MessageVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0 = format!("{value:?}");
}
}
}
let mut visitor = MessageVisitor(String::new());
event.record(&mut visitor);
self.0
.lock()
.unwrap()
.push((*event.metadata().level(), visitor.0));
}
}
pub fn init_config_for(server_config: LspServerConfig) -> ServerInitConfig {
ServerInitConfig::new(
server_config,
WorkspaceRoots::default(),
PositionEncodings::DEFAULT,
std::sync::Arc::default(),
)
}
#[cfg(unix)]
pub fn make_fifo(path: &std::path::Path) {
let status = std::process::Command::new("mkfifo")
.arg(path)
.status()
.unwrap();
assert!(status.success(), "mkfifo must succeed to set up this test");
}
pub fn test_extensions()
-> std::collections::HashMap<crate::config::FileExtension, crate::config::LanguageId> {
std::collections::HashMap::from([
(
crate::config::FileExtension::from_static("rs"),
crate::config::LanguageId::from_static("rust"),
),
(
crate::config::FileExtension::from_static("tsx"),
crate::config::LanguageId::from_static("typescriptreact"),
),
])
}
#[cfg(unix)]
pub fn sh_script_init_config(dir: &std::path::Path, script_body: &str) -> ServerInitConfig {
let script = dir.join("server.sh");
std::fs::write(&script, script_body).unwrap();
let mut server_config = LspServerConfig::rust_analyzer();
server_config.command = ServerCommand::from_static("sh").into();
server_config.args =
crate::config::ServerArgs::new([script.to_string_lossy().to_string()]).unwrap();
init_config_for(server_config)
}
#[cfg(unix)]
pub fn answer_initialize_script(
announce: Option<&std::path::Path>,
wait_for: Option<&std::path::Path>,
) -> String {
let announce =
announce.map_or_else(String::new, |path| format!("touch '{}'\n", path.display()));
let wait = wait_for.map_or_else(String::new, |path| {
format!("while [ ! -f '{}' ]; do sleep 0.05; done\n", path.display())
});
let answer = r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
sleep 60
"#;
format!("{announce}{wait}{}", with_read_preamble(answer))
}
#[cfg(unix)]
pub fn named_sh_init_config(
dir: &std::path::Path,
name: &str,
language: &str,
script_body: &str,
) -> ServerInitConfig {
let sub = dir.join(name);
std::fs::create_dir_all(&sub).unwrap();
let mut config = sh_script_init_config(&sub, script_body);
config.server_config_mut().name = Some(ServerId::new(name).unwrap());
config.server_config_mut().language_id = LanguageId::new(language).unwrap();
config.server_config_mut().timeout_seconds = TimeoutSecs::new(10).unwrap();
config
}
#[cfg(unix)]
pub const READ_REQUEST_SH: &str = r#"content_length=0
while IFS= read -r header; do
header=$(printf '%s' "$header" | tr -d '\r')
[ -z "$header" ] && break
case "$header" in
Content-Length:*) content_length=$(printf '%s' "$header" | sed 's/^Content-Length: *//') ;;
esac
done
[ "$content_length" -gt 0 ] 2>/dev/null && dd bs=1 count="$content_length" 2>/dev/null >/dev/null
"#;
#[cfg(unix)]
pub fn with_read_preamble(body: &str) -> String {
format!("{READ_REQUEST_SH}{body}")
}
#[derive(Debug)]
pub struct PumpFeed {
messages: tokio::sync::mpsc::Sender<crate::lsp::ServerMessage>,
publishes: crate::lsp::PublishWriter,
}
impl PumpFeed {
pub async fn send(
&self,
notification: crate::lsp::LspNotification,
) -> Result<(), tokio::sync::mpsc::error::SendError<crate::lsp::ServerMessage>> {
use crate::lsp::{LspNotification, ServerMessage};
match notification {
LspNotification::PublishDiagnostics(params) => {
self.publishes.publish(params);
Ok(())
}
LspNotification::LogMessage(params) => {
self.messages.send(ServerMessage::Log(params)).await
}
LspNotification::ShowMessage(params) => {
self.messages.send(ServerMessage::Show(params)).await
}
lifecycle @ (LspNotification::Progress(_) | LspNotification::Other { .. }) => {
panic!("not a notification-lane frame: {lifecycle:?}")
}
}
}
}
pub fn pump_feed() -> (PumpFeed, crate::lsp::NotificationInbox) {
let (messages, messages_rx) = tokio::sync::mpsc::channel(32);
let (publishes, publishes_rx) = crate::lsp::mailbox(crate::lsp::MailboxLimits::default());
(
PumpFeed {
messages,
publishes,
},
crate::lsp::NotificationInbox::new(messages_rx, publishes_rx),
)
}
pub fn spawn_test_pump(
subs: crate::mcp::SubscriptionRegistry,
workspace_roots: WorkspaceRoots,
) -> (PumpFeed, CancellationToken) {
let cache = std::sync::Arc::new(tokio::sync::Mutex::new(
crate::bridge::NotificationCache::new(),
));
spawn_test_pump_with_cache(subs, workspace_roots, cache)
}
pub fn spawn_test_pump_with_cache(
subs: crate::mcp::SubscriptionRegistry,
workspace_roots: WorkspaceRoots,
notification_cache: std::sync::Arc<tokio::sync::Mutex<crate::bridge::NotificationCache>>,
) -> (PumpFeed, CancellationToken) {
let (tx, rx) = pump_feed();
let (lifecycle_tx, lifecycle_rx) = tokio::sync::mpsc::channel(8);
let (_cache, cancel) = spawn_pump(
rx,
lifecycle_rx,
lifecycle_tx,
subs,
workspace_roots,
notification_cache,
None,
);
(tx, cancel)
}
pub fn spawn_test_pump_with_tsserver_pin(
pinned: std::path::PathBuf,
) -> (
tokio::sync::mpsc::Sender<crate::lsp::LspNotification>,
CancellationToken,
) {
let (feed, rx) = pump_feed();
let (lifecycle_tx, lifecycle_rx) = tokio::sync::mpsc::channel(8);
let (_cache, cancel) = spawn_pump(
rx,
lifecycle_rx,
feed,
crate::mcp::SubscriptionRegistry::default(),
WorkspaceRoots::default(),
std::sync::Arc::new(tokio::sync::Mutex::new(
crate::bridge::NotificationCache::new(),
)),
Some(pinned),
);
(lifecycle_tx, cancel)
}
pub fn spawn_test_pump_over_lanes(
lanes: FakeLanes,
) -> (
std::sync::Arc<tokio::sync::Mutex<crate::bridge::NotificationCache>>,
CancellationToken,
) {
spawn_pump(
crate::lsp::NotificationInbox::new(lanes.notification_rx, lanes.publishes),
lanes.lifecycle_rx,
(),
crate::mcp::SubscriptionRegistry::default(),
WorkspaceRoots::default(),
std::sync::Arc::new(tokio::sync::Mutex::new(
crate::bridge::NotificationCache::new(),
)),
None,
)
}
fn spawn_pump<K: Send + 'static>(
rx: impl Into<crate::lsp::NotificationInbox> + Send + 'static,
lifecycle_rx: tokio::sync::mpsc::Receiver<crate::lsp::LspNotification>,
keep_alive: K,
subs: crate::mcp::SubscriptionRegistry,
workspace_roots: WorkspaceRoots,
notification_cache: std::sync::Arc<tokio::sync::Mutex<crate::bridge::NotificationCache>>,
pinned_tsserver: Option<std::path::PathBuf>,
) -> (
std::sync::Arc<tokio::sync::Mutex<crate::bridge::NotificationCache>>,
CancellationToken,
) {
let cancel = CancellationToken::new();
let shared = crate::runtime::pump::PumpShared {
roles: crate::runtime::pump::DiagnosticsRoles::default(),
notification_cache: std::sync::Arc::clone(¬ification_cache),
subs,
workspace_roots,
};
let pump_cancel = cancel.clone();
tokio::spawn(async move {
let _keep_alive = keep_alive;
crate::runtime::pump::diagnostics_pump(
crate::config::ServerId::from_static("rust"),
rx,
lifecycle_rx,
pump_cancel,
tokio::sync::watch::channel(crate::bridge::DiagnosticsRole::Authoritative).1,
pinned_tsserver,
shared,
)
.await;
});
(notification_cache, cancel)
}
pub fn workspace_with_main_rs() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
let dir = tempfile::TempDir::new().unwrap();
let root = dunce::canonicalize(dir.path()).unwrap();
let file = root.join("main.rs");
std::fs::write(&file, "fn main() {}").unwrap();
(dir, root, file)
}
pub fn absolute_path(relative: &str) -> std::path::PathBuf {
let root = if cfg!(windows) { r"C:\" } else { "/" };
std::path::PathBuf::from(root).join(relative)
}
pub fn toml_path_literal(path: &std::path::Path) -> String {
toml::Value::String(path.to_string_lossy().into_owned()).to_string()
}
pub fn client_path(path: impl AsRef<std::path::Path>) -> crate::bridge::ClientPath {
crate::bridge::ClientPath::try_from(path.as_ref().to_path_buf()).unwrap()
}
pub fn absolute_uri(relative: &str) -> String {
crate::bridge::resources::make_uri(&absolute_path(relative)).unwrap()
}
pub fn diagnostics_uri(uri: &str) -> crate::bridge::resources::DiagnosticsResourceUri {
crate::bridge::resources::DiagnosticsResourceUri::for_test(uri)
}
#[cfg(unix)]
#[derive(Debug)]
pub struct GatedWiring {
gate: tokio::sync::Semaphore,
arrived: std::sync::atomic::AtomicUsize,
published: std::sync::atomic::AtomicUsize,
}
#[cfg(unix)]
impl GatedWiring {
pub fn closed() -> Self {
Self {
gate: tokio::sync::Semaphore::new(0),
arrived: std::sync::atomic::AtomicUsize::new(0),
published: std::sync::atomic::AtomicUsize::new(0),
}
}
pub fn release(&self) {
self.gate.add_permits(1);
}
pub fn arrived(&self) -> usize {
self.arrived.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn published(&self) -> usize {
self.published.load(std::sync::atomic::Ordering::SeqCst)
}
pub async fn wait_arrived(&self) {
while self.arrived.load(std::sync::atomic::Ordering::SeqCst) == 0 {
tokio::task::yield_now().await;
}
}
pub async fn wait_published(&self) {
while self.published() == 0 {
tokio::task::yield_now().await;
}
}
}
#[cfg(unix)]
impl crate::bridge::NotificationWiring for GatedWiring {
fn spawn_pump(
&self,
_id: crate::config::ServerId,
_receivers: crate::bridge::NotificationReceivers,
_role: crate::bridge::DiagnosticsRole,
) -> tokio::task::AbortHandle {
tokio::spawn(async {}).abort_handle()
}
fn publish_invalidated<'a>(
&'a self,
_cleared: &'a [crate::bridge::DiagnosticsKey],
) -> futures::future::BoxFuture<'a, ()> {
Box::pin(async move {
self.arrived
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if let Ok(permit) = self.gate.acquire().await {
permit.forget();
}
self.published
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
})
}
fn publish_changed<'a>(
&'a self,
_file: &'a lsp_types::Uri,
) -> futures::future::BoxFuture<'a, ()> {
Box::pin(async {})
}
}
#[cfg(all(test, unix))]
mod gated_wiring_tests {
use std::sync::Arc;
use super::GatedWiring;
use crate::bridge::NotificationWiring as _;
#[tokio::test]
async fn one_release_lets_exactly_one_invalidation_through() {
let wiring = Arc::new(GatedWiring::closed());
wiring.release();
for _ in 0..2 {
let wiring = Arc::clone(&wiring);
tokio::spawn(async move { wiring.publish_invalidated(&[]).await });
}
while wiring.arrived() < 2 {
tokio::task::yield_now().await;
}
for _ in 0..16 {
tokio::task::yield_now().await;
}
assert_eq!(wiring.published(), 1);
}
}