use super::*;
use expect_test::expect;
use serde_json::Value;
use crate::common::render_text_edits;
fn render_diagnostics_notification(notif: &Value) -> String {
let diags = notif["params"]["diagnostics"].as_array();
let Some(diags) = diags else {
return "<no diagnostics field>".to_owned();
};
if diags.is_empty() {
return "<empty>".to_owned();
}
let mut rows: Vec<String> = diags
.iter()
.map(|d| {
let r = &d["range"];
let sev = d["severity"].as_u64().unwrap_or(0);
let code = d["code"].as_str().unwrap_or("?");
let msg = d["message"].as_str().unwrap_or("");
format!(
"{}:{}-{}:{} [{sev}] {code}: {msg}",
r["start"]["line"].as_u64().unwrap_or(0),
r["start"]["character"].as_u64().unwrap_or(0),
r["end"]["line"].as_u64().unwrap_or(0),
r["end"]["character"].as_u64().unwrap_or(0),
)
})
.collect();
rows.sort();
rows.join("\n")
}
#[tokio::test]
async fn did_close_clears_diagnostics() {
let mut server = TestServer::new().await;
let uri = server.uri("close_test.php");
let open_notif = server.open("close_test.php", "<?php function() {}\n").await;
assert!(
!open_notif["params"]["diagnostics"]
.as_array()
.unwrap_or(&vec![])
.is_empty(),
"expected parse errors before close: {open_notif:?}"
);
server.close("close_test.php").await;
let close_notif = server.client().wait_for_diagnostics(&uri).await;
assert!(
close_notif["params"]["diagnostics"]
.as_array()
.unwrap()
.is_empty(),
"expected empty diagnostics after close: {close_notif:?}"
);
}
#[tokio::test]
async fn did_close_unopened_does_not_crash() {
let mut server = TestServer::new().await;
let uri = server.uri("never_opened.php");
server.close("never_opened.php").await;
let notif = server.client().wait_for_diagnostics(&uri).await;
assert!(
notif["params"]["diagnostics"]
.as_array()
.unwrap()
.is_empty(),
"expected empty diagnostics for never-opened file: {notif:?}"
);
}
#[tokio::test]
async fn did_save_republishes_empty_diagnostics_for_clean_file() {
let mut server = TestServer::new().await;
server.open("save_clean.php", "<?php\n").await;
let save_notif = server.save("save_clean.php").await;
assert!(
save_notif["params"]["diagnostics"]
.as_array()
.unwrap()
.is_empty(),
"expected no diagnostics after save of clean file: {save_notif:?}"
);
}
#[tokio::test]
async fn did_save_republishes_diagnostics_for_duplicate_functions() {
let mut server = TestServer::new().await;
let open_notif = server
.open(
"save_dup.php",
"<?php\nfunction doWork() {}\nfunction doWork() {}\n",
)
.await;
assert!(
!open_notif["params"]["diagnostics"]
.as_array()
.unwrap_or(&vec![])
.is_empty(),
"expected duplicate-declaration diagnostic on open: {open_notif:?}"
);
let save_notif = server.save("save_dup.php").await;
assert!(
save_notif["params"]["diagnostics"]
.as_array()
.unwrap()
.len()
>= 1,
"expected >=1 diagnostic after save with duplicate functions: {save_notif:?}"
);
}
#[tokio::test]
async fn did_save_republishes_semantic_diagnostics() {
let mut server = TestServer::new().await;
let open_notif = server
.open(
"save_semantic.php",
"<?php\nfunction _wrap(): void {\n nonexistent_fn();\n}\n",
)
.await;
assert!(
!open_notif["params"]["diagnostics"]
.as_array()
.unwrap_or(&vec![])
.is_empty(),
"expected semantic diagnostic on open: {open_notif:?}"
);
let save_notif = server.save("save_semantic.php").await;
assert!(
!save_notif["params"]["diagnostics"]
.as_array()
.unwrap()
.is_empty(),
"did_save must republish semantic diagnostics, got empty list: {save_notif:?}"
);
}
#[tokio::test]
async fn will_save_keeps_document_state_unchanged() {
let mut server = TestServer::new().await;
let open_notif = server
.open(
"ws_state.php",
"<?php\nfunction _wrap(): void {\n nonexistent_fn();\n}\n",
)
.await;
expect!["2:4-2:20 [1] UndefinedFunction: Function nonexistent_fn() is not defined"]
.assert_eq(&render_diagnostics_notification(&open_notif));
for reason in [1u32, 2, 3] {
server.will_save("ws_state.php", reason).await;
}
let save_notif = server.save("ws_state.php").await;
expect!["2:4-2:20 [1] UndefinedFunction: Function nonexistent_fn() is not defined"]
.assert_eq(&render_diagnostics_notification(&save_notif));
}
#[tokio::test]
async fn will_save_does_not_publish_diagnostics() {
let mut server = TestServer::new().await;
server
.open("ws_nodiag.php", "<?php\nfunction foo() {}\n")
.await;
for reason in [1u32, 2, 3] {
server.will_save("ws_nodiag.php", reason).await;
}
let hover = server.hover("ws_nodiag.php", 1, 10).await;
assert!(hover["error"].is_null(), "hover errored: {hover:?}");
let uris = server
.client()
.drain_publish_diagnostics_uris(tokio::time::Duration::from_millis(100))
.await;
expect!["[]"].assert_eq(&format!("{uris:?}"));
}
#[tokio::test]
async fn will_save_for_unopened_file_does_not_crash() {
let mut server = TestServer::new().await;
server.will_save("ws_never_opened.php", 1).await;
server.will_save("ws_never_opened.php", 2).await;
server.will_save("ws_never_opened.php", 3).await;
let open_notif = server
.open(
"ws_after.php",
"<?php\nfunction _wrap(): void {\n nonexistent_fn();\n}\n",
)
.await;
expect!["2:4-2:20 [1] UndefinedFunction: Function nonexistent_fn() is not defined"]
.assert_eq(&render_diagnostics_notification(&open_notif));
}
#[tokio::test]
async fn will_save_after_did_close_does_not_crash() {
let mut server = TestServer::new().await;
server
.open("ws_closed.php", "<?php\nfunction foo() {}\n")
.await;
server.close("ws_closed.php").await;
let _ = server
.client()
.drain_publish_diagnostics_uris(tokio::time::Duration::from_millis(50))
.await;
server.will_save("ws_closed.php", 1).await;
let open_notif = server.open("ws_after_close.php", "<?php\n").await;
expect!["<empty>"].assert_eq(&render_diagnostics_notification(&open_notif));
}
#[tokio::test]
async fn will_save_does_not_disturb_pending_did_change() {
let mut server = TestServer::new().await;
server.open("ws_change.php", "<?php\n").await;
server
.change(
"ws_change.php",
2,
"<?php\nfunction _wrap(): void {\n nonexistent_fn();\n}\n",
)
.await;
server.will_save("ws_change.php", 1).await;
let save_notif = server.save("ws_change.php").await;
expect!["2:4-2:20 [1] UndefinedFunction: Function nonexistent_fn() is not defined"]
.assert_eq(&render_diagnostics_notification(&save_notif));
}
#[tokio::test]
async fn will_save_wait_until_returns_null_or_empty_for_formatted_file() {
let mut server = TestServer::new().await;
server.open("wswu_clean.php", "<?php\n").await;
let resp = server.will_save_wait_until("wswu_clean.php").await;
assert!(resp["error"].is_null(), "unexpected error: {resp:?}");
expect![r#"(no formatter available)"#].assert_eq(&render_text_edits(&resp));
}
#[tokio::test]
async fn will_save_wait_until_on_already_formatted_code() {
let mut server = TestServer::new().await;
server
.open(
"wswu_formatted.php",
"<?php\n\nfunction greet(): void\n{\n}\n",
)
.await;
let resp = server.will_save_wait_until("wswu_formatted.php").await;
assert!(resp["error"].is_null(), "unexpected error: {resp:?}");
let result = &resp["result"];
assert!(
result.is_null() || result.as_array().map(|a| a.is_empty()).unwrap_or(false),
"expected null or empty edits for already-formatted file: {resp:?}"
);
}
#[tokio::test]
async fn will_save_wait_until_returns_edits_or_null_for_unformatted_file() {
let mut server = TestServer::new().await;
server
.open("wswu_ugly.php", "<?php\nfunction ugly( $x ){return $x;}\n")
.await;
let resp = server.will_save_wait_until("wswu_ugly.php").await;
assert!(resp["error"].is_null(), "unexpected error: {resp:?}");
let result = &resp["result"];
if let Some(edits) = result.as_array() {
for edit in edits {
assert!(
edit["range"]["start"].is_object() && edit["range"]["end"].is_object(),
"edit missing range: {edit:?}"
);
assert!(
edit["newText"].is_string(),
"edit missing newText: {edit:?}"
);
}
} else {
assert!(result.is_null(), "expected null or array, got: {result:?}");
}
}
#[tokio::test]
async fn will_save_wait_until_on_unopened_file_returns_null() {
let mut server = TestServer::new().await;
let resp = server.will_save_wait_until("wswu_never_opened.php").await;
assert!(resp["error"].is_null(), "unexpected error: {resp:?}");
expect!["(no formatter available)"].assert_eq(&render_text_edits(&resp));
}
#[tokio::test]
async fn will_save_wait_until_on_empty_file() {
let mut server = TestServer::new().await;
server.open("wswu_empty.php", "").await;
let resp = server.will_save_wait_until("wswu_empty.php").await;
assert!(resp["error"].is_null(), "unexpected error: {resp:?}");
expect!["(no formatter available)"].assert_eq(&render_text_edits(&resp));
}
#[tokio::test]
async fn will_save_wait_until_without_php_tag() {
let mut server = TestServer::new().await;
server.open("wswu_no_tag.php", "function test( ){}\n").await;
let resp = server.will_save_wait_until("wswu_no_tag.php").await;
assert!(resp["error"].is_null(), "unexpected error: {resp:?}");
let result = &resp["result"];
assert!(
result.is_null() || result.as_array().is_some(),
"expected null or TextEdit array, got: {result:?}"
);
}
#[tokio::test]
async fn did_change_updates_document() {
let mut server = TestServer::new().await;
server.open("change.php", "<?php\n").await;
server
.change("change.php", 2, "<?php\nfunction updated() {}\n")
.await;
let resp = server.hover("change.php", 1, 10).await;
assert!(
resp["error"].is_null(),
"hover after change should not error"
);
}
#[tokio::test]
async fn document_link_returns_array() {
let mut server = TestServer::new().await;
server
.open("dlink.php", "<?php\nrequire_once 'vendor/autoload.php';\n")
.await;
let resp = server.document_link("dlink.php").await;
assert!(resp["error"].is_null(), "documentLink error: {:?}", resp);
let links = resp["result"]
.as_array()
.expect("documentLink must return an array");
assert!(
!links.is_empty(),
"expected at least one link for require_once path"
);
}