test(lsp): cover diagnostic publish lifecycle
Add protocol-level LSP tests for build-equivalent diagnostics across open/change/save/close and file-backed save without text. req: diagnostics/004 req: diagnostics/005
This commit is contained in:
+154
-1
@@ -682,7 +682,10 @@ fn diagnostic_lsp_severity(severity: DiagnosticSeverity) -> u8 {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{hemplate_completion_items, hemplate_hover, serve_lsp};
|
use super::{
|
||||||
|
diagnostics_for_uri_source, file_uri, hemplate_completion_items, hemplate_hover,
|
||||||
|
lsp_diagnostic, serve_lsp,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn completion_and_hover_use_template_context_facts() {
|
fn completion_and_hover_use_template_context_facts() {
|
||||||
@@ -783,6 +786,133 @@ mod tests {
|
|||||||
assert!(hover.to_string().contains("exercise.name: &'static str"));
|
assert!(hover.to_string().contains("exercise.name: &'static str"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lsp_publishes_and_clears_build_equivalent_diagnostics() {
|
||||||
|
// req: diagnostics/004 req: diagnostics/005
|
||||||
|
let uri = "file:///tmp/todo.heml";
|
||||||
|
let invalid = r#"<main data-hemx-root="todos"><template h-for="todo in &self.todos"><li data-hemx-slot="todo_row">{+ todo.title +}</li></template></main>"#;
|
||||||
|
let valid = r#"<main data-hemx-root="todos"><template h-for="todo in &self.todos" h-key="todo.id"><li data-hemx-slot="todo_row">{+ todo.title +}</li></template></main>"#;
|
||||||
|
let expected =
|
||||||
|
diagnostics_for_uri_source(uri, invalid.to_string()).expect("build diagnostics");
|
||||||
|
assert!(
|
||||||
|
!expected.is_empty(),
|
||||||
|
"fixture should exercise build diagnostics"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
diagnostics_for_uri_source(uri, valid.to_string())
|
||||||
|
.expect("valid diagnostics")
|
||||||
|
.is_empty(),
|
||||||
|
"valid fixture should clear diagnostics"
|
||||||
|
);
|
||||||
|
|
||||||
|
let input = [
|
||||||
|
lsp_message(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})),
|
||||||
|
lsp_message(serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didOpen",
|
||||||
|
"params": {"textDocument": {"uri": uri, "languageId": "heml", "version": 1, "text": invalid}}
|
||||||
|
})),
|
||||||
|
lsp_message(serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didChange",
|
||||||
|
"params": {"textDocument": {"uri": uri, "version": 2}, "contentChanges": [{"text": valid}]}
|
||||||
|
})),
|
||||||
|
lsp_message(serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didSave",
|
||||||
|
"params": {"textDocument": {"uri": uri}, "text": invalid}
|
||||||
|
})),
|
||||||
|
lsp_message(serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didClose",
|
||||||
|
"params": {"textDocument": {"uri": uri}}
|
||||||
|
})),
|
||||||
|
lsp_message(serde_json::json!({"jsonrpc": "2.0", "method": "exit"})),
|
||||||
|
]
|
||||||
|
.join("");
|
||||||
|
let mut reader = std::io::BufReader::new(input.as_bytes());
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
serve_lsp(&mut reader, &mut output).expect("serve LSP");
|
||||||
|
let messages = lsp_messages(&output);
|
||||||
|
let published = messages
|
||||||
|
.iter()
|
||||||
|
.filter(|message| message["method"] == "textDocument/publishDiagnostics")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(published.len(), 4, "open/change/save/close should publish");
|
||||||
|
assert_eq!(published[0]["params"]["uri"], uri);
|
||||||
|
assert_eq!(
|
||||||
|
published[0]["params"]["diagnostics"],
|
||||||
|
serde_json::to_value(expected.iter().map(lsp_diagnostic).collect::<Vec<_>>()).unwrap(),
|
||||||
|
"didOpen diagnostics should match build diagnostics"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
published[1]["params"]["diagnostics"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
0,
|
||||||
|
"didChange should clear fixed diagnostics"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
published[2]["params"]["diagnostics"], published[0]["params"]["diagnostics"],
|
||||||
|
"didSave text diagnostics should match didOpen/build diagnostics"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
published[3]["params"]["diagnostics"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
0,
|
||||||
|
"didClose should clear diagnostics"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn did_save_without_text_reads_file_when_document_is_not_open() {
|
||||||
|
// req: diagnostics/004 req: diagnostics/005
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"hemx-lsp-save-{}-{}.heml",
|
||||||
|
std::process::id(),
|
||||||
|
std::thread::current().name().unwrap_or("test")
|
||||||
|
));
|
||||||
|
let invalid = r#"<main data-hemx-root="todos"><template h-for="todo in &self.todos"><li data-hemx-slot="todo_row">{+ todo.title +}</li></template></main>"#;
|
||||||
|
std::fs::write(&path, invalid).expect("write temp heml fixture");
|
||||||
|
let uri = file_uri(&path);
|
||||||
|
let expected = hemx_build::diagnostics_for_heml_file(&path).expect("file diagnostics");
|
||||||
|
assert!(
|
||||||
|
!expected.is_empty(),
|
||||||
|
"file fixture should produce diagnostics"
|
||||||
|
);
|
||||||
|
|
||||||
|
let input = [
|
||||||
|
lsp_message(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})),
|
||||||
|
lsp_message(serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didSave",
|
||||||
|
"params": {"textDocument": {"uri": uri}}
|
||||||
|
})),
|
||||||
|
lsp_message(serde_json::json!({"jsonrpc": "2.0", "method": "exit"})),
|
||||||
|
]
|
||||||
|
.join("");
|
||||||
|
let mut reader = std::io::BufReader::new(input.as_bytes());
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
serve_lsp(&mut reader, &mut output).expect("serve LSP");
|
||||||
|
let messages = lsp_messages(&output);
|
||||||
|
let published = messages
|
||||||
|
.iter()
|
||||||
|
.find(|message| message["method"] == "textDocument/publishDiagnostics")
|
||||||
|
.expect("publish diagnostics");
|
||||||
|
assert_eq!(published["params"]["uri"], uri);
|
||||||
|
assert_eq!(
|
||||||
|
published["params"]["diagnostics"],
|
||||||
|
serde_json::to_value(expected.iter().map(lsp_diagnostic).collect::<Vec<_>>()).unwrap(),
|
||||||
|
"didSave without text/open document should match file diagnostics"
|
||||||
|
);
|
||||||
|
std::fs::remove_file(path).ok();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lsp_serves_compiler_diagnostics_and_completion() {
|
fn lsp_serves_compiler_diagnostics_and_completion() {
|
||||||
// req: diagnostics/004 req: diagnostics/005
|
// req: diagnostics/004 req: diagnostics/005
|
||||||
@@ -824,6 +954,29 @@ mod tests {
|
|||||||
assert!(output.contains("docs/hemplate-syntax.md"));
|
assert!(output.contains("docs/hemplate-syntax.md"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lsp_messages(output: &[u8]) -> Vec<serde_json::Value> {
|
||||||
|
let mut bytes = output;
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
while !bytes.is_empty() {
|
||||||
|
let header_end = bytes
|
||||||
|
.windows(4)
|
||||||
|
.position(|window| window == b"\r\n\r\n")
|
||||||
|
.expect("LSP header terminator");
|
||||||
|
let header = std::str::from_utf8(&bytes[..header_end]).expect("utf8 header");
|
||||||
|
let length = header
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("Content-Length: "))
|
||||||
|
.expect("content length")
|
||||||
|
.parse::<usize>()
|
||||||
|
.expect("numeric content length");
|
||||||
|
let body_start = header_end + 4;
|
||||||
|
let body_end = body_start + length;
|
||||||
|
messages.push(serde_json::from_slice(&bytes[body_start..body_end]).expect("LSP json"));
|
||||||
|
bytes = &bytes[body_end..];
|
||||||
|
}
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
|
||||||
fn lsp_message(message: serde_json::Value) -> String {
|
fn lsp_message(message: serde_json::Value) -> String {
|
||||||
let body = serde_json::to_string(&message).expect("lsp json");
|
let body = serde_json::to_string(&message).expect("lsp json");
|
||||||
format!("Content-Length: {}\r\n\r\n{}", body.len(), body)
|
format!("Content-Length: {}\r\n\r\n{}", body.len(), body)
|
||||||
|
|||||||
Reference in New Issue
Block a user