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)]
|
||||
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]
|
||||
fn completion_and_hover_use_template_context_facts() {
|
||||
@@ -783,6 +786,133 @@ mod tests {
|
||||
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]
|
||||
fn lsp_serves_compiler_diagnostics_and_completion() {
|
||||
// req: diagnostics/004 req: diagnostics/005
|
||||
@@ -824,6 +954,29 @@ mod tests {
|
||||
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 {
|
||||
let body = serde_json::to_string(&message).expect("lsp json");
|
||||
format!("Content-Length: {}\r\n\r\n{}", body.len(), body)
|
||||
|
||||
Reference in New Issue
Block a user