#![allow(missing_docs)]
use serde_json::{Value, json};
use std::io::{Read, Write};
use std::process::{Command, Stdio};
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_noyalib-lsp")
}
fn frame(payload: &Value) -> Vec<u8> {
let body = serde_json::to_string(payload).unwrap();
let mut out = format!("Content-Length: {}\r\n\r\n", body.len()).into_bytes();
out.extend_from_slice(body.as_bytes());
out
}
fn round_trip(messages: &[Value]) -> Vec<Value> {
let mut child = Command::new(bin())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn noyalib-lsp");
let mut stdin = child.stdin.take().expect("stdin");
for m in messages {
stdin.write_all(&frame(m)).unwrap();
}
drop(stdin);
let mut stdout = child.stdout.take().expect("stdout");
let mut all = Vec::new();
let _ = stdout.read_to_end(&mut all);
let text = String::from_utf8_lossy(&all);
let mut out = Vec::new();
let mut rest = text.as_ref();
while let Some(idx) = rest.find("Content-Length:") {
let after_header = &rest[idx..];
let Some(header_end) = after_header.find("\r\n\r\n") else {
break;
};
let header = &after_header[..header_end];
let length: usize = header
.lines()
.find_map(|l| {
l.strip_prefix("Content-Length:")
.map(str::trim)
.and_then(|n| n.parse().ok())
})
.unwrap_or(0);
let body_start = idx + header_end + 4;
if body_start + length > rest.len() {
break;
}
let body = &rest[body_start..body_start + length];
if let Ok(v) = serde_json::from_str(body) {
out.push(v);
}
rest = &rest[body_start + length..];
}
let _ = child.wait();
out
}
#[test]
fn initialize_returns_capabilities() {
let resps = round_trip(&[json!({
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {}
})]);
assert_eq!(resps.len(), 1);
let r = &resps[0];
assert_eq!(
r["result"]["serverInfo"]["name"].as_str(),
Some("noyalib-lsp")
);
assert_eq!(
r["result"]["capabilities"]["documentFormattingProvider"].as_bool(),
Some(true),
);
}
#[test]
fn did_open_publishes_diagnostics_notification() {
let resps = round_trip(&[
json!({"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}),
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/x.yaml",
"languageId": "yaml",
"version": 1,
"text": "a: 1\n"
}
}
}),
]);
assert!(resps.len() >= 2);
let note = resps
.iter()
.find(|r| r["method"].as_str() == Some("textDocument/publishDiagnostics"))
.expect("expected publishDiagnostics notification");
assert_eq!(note["params"]["uri"].as_str(), Some("file:///tmp/x.yaml"));
}
#[test]
fn formatting_round_trip_returns_text_edits_array() {
let resps = round_trip(&[
json!({"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}),
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {"textDocument": {
"uri": "file:///tmp/y.yaml",
"languageId": "yaml",
"version": 1,
"text": "name: noyalib\n"
}}
}),
json!({
"jsonrpc": "2.0",
"method": "textDocument/formatting",
"id": 2,
"params": {
"textDocument": {"uri": "file:///tmp/y.yaml"},
"options": {"tabSize": 2, "insertSpaces": true}
}
}),
]);
let fmt_reply = resps
.iter()
.find(|r| r["id"].as_i64() == Some(2))
.expect("formatting reply");
assert!(fmt_reply["result"].is_array());
}
#[test]
fn hover_round_trip_returns_markdown_or_null() {
let resps = round_trip(&[
json!({"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}),
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {"textDocument": {
"uri": "file:///tmp/z.yaml",
"languageId": "yaml",
"version": 1,
"text": "k: v\n"
}}
}),
json!({
"jsonrpc": "2.0",
"method": "textDocument/hover",
"id": 3,
"params": {
"textDocument": {"uri": "file:///tmp/z.yaml"},
"position": {"line": 0, "character": 0}
}
}),
]);
let hover_reply = resps
.iter()
.find(|r| r["id"].as_i64() == Some(3))
.expect("hover reply");
let result = &hover_reply["result"];
assert!(result.is_null() || result["contents"]["kind"].as_str() == Some("markdown"));
}