#![allow(clippy::collapsible_if)]
#![allow(dead_code, unused_imports)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use lsp_server::{Connection, Message, Notification, Request, RequestId, Response};
use lsp_types::notification::{
DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument,
DidSaveTextDocument, Exit, Initialized, Notification as LspNotification, PublishDiagnostics,
};
use lsp_types::request::{CodeActionRequest, ExecuteCommand, Request as LspRequest, Shutdown};
use lsp_types::{
CodeActionContext, CodeActionOrCommand, CodeActionParams, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult,
NumberOrString, PublishDiagnosticsParams, Range, TextDocumentContentChangeEvent,
TextDocumentIdentifier, TextDocumentItem, Uri, VersionedTextDocumentIdentifier,
WorkspaceFolder,
};
use serde_json::json;
use tempfile::TempDir;
#[allow(dead_code)]
const DIAGNOSTIC_TIMEOUT: Duration = Duration::from_secs(5);
const INITIAL_REQUEST_ID: i32 = 1;
fn path_to_uri(path: &Path) -> Uri {
let url = url::Url::from_file_path(path).expect("failed to create file URL");
url.as_str().parse::<Uri>().expect("failed to parse URI")
}
pub struct TestServer {
client_conn: Connection,
server_thread: Option<thread::JoinHandle<anyhow::Result<()>>>,
#[allow(dead_code)]
init_result: InitializeResult,
next_request_id: i32,
temp_dir: TempDir,
diagnostics_cache: HashMap<String, PublishDiagnosticsParams>,
diagnostic_count: usize,
}
impl TestServer {
pub fn start() -> Self {
let (client_conn, server_conn) = Connection::memory();
let server_thread = thread::spawn(move || diffguard_lsp::server::run_server(server_conn));
let temp_dir = TempDir::new().expect("failed to create temp workspace dir");
let workspace_uri = path_to_uri(temp_dir.path());
#[allow(deprecated)]
let init_params = InitializeParams {
root_uri: Some(workspace_uri.clone()),
workspace_folders: Some(vec![WorkspaceFolder {
uri: workspace_uri,
name: "test".to_string(),
}]),
..InitializeParams::default()
};
let init_request = Request::new(
RequestId::from(INITIAL_REQUEST_ID),
"initialize".to_string(),
serde_json::to_value(init_params).expect("failed to serialize initialize params"),
);
client_conn
.sender
.send(Message::Request(init_request))
.expect("failed to send initialize request");
let response = client_conn
.receiver
.recv()
.expect("failed to receive initialize response");
let init_response = match response {
Message::Response(resp) => resp.result.expect("initialize response has no result"),
_ => panic!("expected response message, got {:?}", response),
};
let init_result: InitializeResult =
serde_json::from_value(init_response).expect("failed to parse InitializeResult");
let initialized_notification =
Notification::new(Initialized::METHOD.to_string(), json!({}));
client_conn
.sender
.send(Message::Notification(initialized_notification))
.expect("failed to send initialized notification");
drain_messages(&client_conn);
TestServer {
client_conn,
server_thread: Some(server_thread),
init_result,
next_request_id: INITIAL_REQUEST_ID + 1,
temp_dir,
diagnostics_cache: HashMap::new(),
diagnostic_count: 0,
}
}
#[allow(dead_code)]
pub fn start_with_workspace(workspace_root: &Path) -> Self {
let (client_conn, server_conn) = Connection::memory();
let server_thread = thread::spawn(move || diffguard_lsp::server::run_server(server_conn));
let workspace_uri = path_to_uri(workspace_root);
#[allow(deprecated)]
let init_params = InitializeParams {
root_uri: Some(workspace_uri.clone()),
workspace_folders: Some(vec![WorkspaceFolder {
uri: workspace_uri,
name: "test".to_string(),
}]),
..InitializeParams::default()
};
let init_request = Request::new(
RequestId::from(INITIAL_REQUEST_ID),
"initialize".to_string(),
serde_json::to_value(init_params).expect("failed to serialize initialize params"),
);
client_conn
.sender
.send(Message::Request(init_request))
.expect("failed to send initialize request");
let init_response = loop {
let message = client_conn
.receiver
.recv()
.expect("failed to receive message");
match message {
Message::Response(resp) if resp.id == RequestId::from(INITIAL_REQUEST_ID) => {
break resp.result.expect("initialize response has no result");
}
Message::Notification(_) => {
continue;
}
_ => panic!("expected initialize response, got {:?}", message),
}
};
let init_result: InitializeResult =
serde_json::from_value(init_response).expect("failed to parse InitializeResult");
let initialized_notification =
Notification::new(Initialized::METHOD.to_string(), json!({}));
client_conn
.sender
.send(Message::Notification(initialized_notification))
.expect("failed to send initialized notification");
drain_messages(&client_conn);
TestServer {
client_conn,
server_thread: Some(server_thread),
init_result,
next_request_id: INITIAL_REQUEST_ID + 1,
temp_dir: TempDir::new().expect("failed to create temp dir for server"),
diagnostics_cache: HashMap::new(),
diagnostic_count: 0,
}
}
#[allow(dead_code)]
pub fn capabilities(&self) -> &InitializeResult {
&self.init_result
}
#[allow(dead_code)]
pub fn workspace_path(&self) -> &Path {
self.temp_dir.path()
}
pub fn create_file(&self, relative_path: &str, content: &str) -> Uri {
let full_path = self.temp_dir.path().join(relative_path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent).expect("failed to create parent directories");
}
std::fs::write(&full_path, content).expect("failed to write test file");
path_to_uri(&full_path)
}
pub fn send_did_open(&self, uri: &Uri, language_id: &str, version: i32, text: &str) {
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: language_id.to_string(),
version,
text: text.to_string(),
},
};
self.send_notification(
DidOpenTextDocument::METHOD,
serde_json::to_value(params).unwrap(),
);
}
pub fn send_did_change(&self, uri: &Uri, version: i32, text: &str) {
let params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version,
},
content_changes: vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: text.to_string(),
}],
};
self.send_notification(
DidChangeTextDocument::METHOD,
serde_json::to_value(params).unwrap(),
);
}
pub fn send_did_close(&self, uri: &Uri) {
let params = DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
};
self.send_notification(
DidCloseTextDocument::METHOD,
serde_json::to_value(params).unwrap(),
);
}
pub fn send_did_save(&self, uri: &Uri) {
let params = DidSaveTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
text: None,
};
self.send_notification(
DidSaveTextDocument::METHOD,
serde_json::to_value(params).unwrap(),
);
}
#[allow(dead_code)]
pub fn send_did_change_configuration(&self, settings: serde_json::Value) {
let params = DidChangeConfigurationParams { settings };
self.send_notification(
DidChangeConfiguration::METHOD,
serde_json::to_value(params).unwrap(),
);
}
#[allow(dead_code)]
pub fn send_code_action_request(
&mut self,
uri: &Uri,
range: Range,
diagnostics: &[lsp_types::Diagnostic],
) -> Vec<CodeActionOrCommand> {
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: diagnostics.to_vec(),
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let response = self.send_request(
CodeActionRequest::METHOD,
serde_json::to_value(params).unwrap(),
);
match response.result {
Some(value) => serde_json::from_value(value).unwrap_or_default(),
None => Vec::new(),
}
}
#[allow(dead_code)]
pub fn send_execute_command(
&mut self,
command: &str,
args: Vec<serde_json::Value>,
) -> Response {
let params = ExecuteCommandParams {
command: command.to_string(),
arguments: args,
work_done_progress_params: Default::default(),
};
self.send_request(
ExecuteCommand::METHOD,
serde_json::to_value(params).unwrap(),
)
}
pub fn collect_diagnostics(&mut self, timeout: Duration) -> Vec<PublishDiagnosticsParams> {
let deadline = Instant::now() + timeout;
let mut collected = Vec::new();
while Instant::now() < deadline {
let remaining = deadline.saturating_duration_since(Instant::now());
match self.client_conn.receiver.recv_timeout(remaining) {
Ok(message) => {
if let Some(params) = extract_diagnostics(&message) {
self.diagnostics_cache
.insert(params.uri.to_string(), params.clone());
self.diagnostic_count += 1;
collected.push(params);
}
}
Err(_) => break,
}
}
collected
}
pub fn collect_diagnostics_for_uri(
&mut self,
uri: &Uri,
timeout: Duration,
) -> Vec<lsp_types::Diagnostic> {
let uri_str = uri.to_string();
let all = self.collect_diagnostics(timeout);
for params in &all {
if params.uri.to_string() == uri_str {
return params.diagnostics.clone();
}
}
if let Some(cached) = self.diagnostics_cache.get(&uri_str) {
return cached.diagnostics.clone();
}
Vec::new()
}
#[allow(dead_code)]
pub fn drain_pending_messages(&self) {
drain_messages(&self.client_conn);
}
#[allow(dead_code)]
pub fn shutdown(mut self) {
self.do_shutdown();
}
fn do_shutdown(&mut self) {
let shutdown_id = self.next_request_id();
let shutdown_request = Request::new(
RequestId::from(shutdown_id),
Shutdown::METHOD.to_string(),
json!(null),
);
let _ = self
.client_conn
.sender
.send(Message::Request(shutdown_request));
let exit_notification = Notification::new(Exit::METHOD.to_string(), json!(null));
let _ = self
.client_conn
.sender
.send(Message::Notification(exit_notification));
if let Some(handle) = self.server_thread.take() {
let _ = handle.join();
}
}
fn next_request_id(&mut self) -> i32 {
let id = self.next_request_id;
self.next_request_id += 1;
id
}
#[allow(dead_code)]
fn send_request(&mut self, method: &str, params: serde_json::Value) -> Response {
let id = self.next_request_id();
let request = Request::new(RequestId::from(id), method.to_string(), params);
self.client_conn
.sender
.send(Message::Request(request))
.expect("failed to send request");
let target_id = RequestId::from(id);
loop {
let message = self
.client_conn
.receiver
.recv_timeout(DIAGNOSTIC_TIMEOUT)
.expect("timed out waiting for response");
match message {
Message::Response(response) if response.id == target_id => {
return response;
}
Message::Notification(_) => {
}
_ => {
}
}
}
}
fn send_notification(&self, method: &str, params: serde_json::Value) {
let notification = Notification::new(method.to_string(), params);
self.client_conn
.sender
.send(Message::Notification(notification))
.expect("failed to send notification");
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.do_shutdown();
}
}
#[allow(dead_code)]
pub fn create_test_config(dir: &Path, config_content: &str) -> PathBuf {
let config_path = dir.join("diffguard.toml");
std::fs::write(&config_path, config_content).expect("failed to write test config");
config_path
}
#[allow(dead_code)]
pub fn create_test_file(dir: &Path, name: &str, content: &str) -> Uri {
let file_path = dir.join(name);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).expect("failed to create parent dirs");
}
std::fs::write(&file_path, content).expect("failed to write test file");
path_to_uri(&file_path)
}
#[allow(dead_code)]
pub fn make_test_diff(path: &str, added_lines: &[&str]) -> String {
let mut diff = format!(
"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n",
path = path
);
for (i, line) in added_lines.iter().enumerate() {
let line_number = i + 1;
diff.push_str(&format!("@@ -0,0 +{},1 @@\n", line_number));
diff.push('+');
diff.push_str(line);
diff.push('\n');
}
diff
}
fn drain_messages(conn: &Connection) {
while conn.receiver.try_recv().is_ok() {}
}
fn extract_diagnostics(message: &Message) -> Option<PublishDiagnosticsParams> {
if let Message::Notification(notification) = message {
if notification.method == PublishDiagnostics::METHOD {
let params: PublishDiagnosticsParams =
serde_json::from_value(notification.params.clone()).ok()?;
return Some(params);
}
}
None
}
#[allow(dead_code)]
pub fn assert_diagnostic_with_code(diagnostics: &[lsp_types::Diagnostic], rule_code: &str) {
assert!(
diagnostics.iter().any(|d| {
d.code.as_ref().is_some_and(|c| match c {
NumberOrString::String(s) => s == rule_code,
NumberOrString::Number(_) => false,
})
}),
"Expected diagnostic with code '{}', but found codes: {:?}",
rule_code,
diagnostics
.iter()
.filter_map(|d| d.code.as_ref())
.collect::<Vec<_>>(),
);
}
#[allow(dead_code)]
pub fn assert_no_diagnostic_with_code(diagnostics: &[lsp_types::Diagnostic], rule_code: &str) {
assert!(
!diagnostics.iter().any(|d| {
d.code.as_ref().is_some_and(|c| match c {
NumberOrString::String(s) => s == rule_code,
NumberOrString::Number(_) => false,
})
}),
"Did NOT expect diagnostic with code '{}', but found one. Diagnostics: {:?}",
rule_code,
diagnostics,
);
}
#[allow(dead_code)]
pub fn diagnostic_lines(diagnostics: &[lsp_types::Diagnostic]) -> Vec<u32> {
let mut lines: Vec<u32> = diagnostics.iter().map(|d| d.range.start.line).collect();
lines.sort();
lines.dedup();
lines
}