use super::*;
use expect_test::expect;
const FEATURE_CAP_PAIRS: &[(&str, &str)] = &[
("completion", "completionProvider"),
("hover", "hoverProvider"),
("definition", "definitionProvider"),
("declaration", "declarationProvider"),
("references", "referencesProvider"),
("documentSymbols", "documentSymbolProvider"),
("workspaceSymbols", "workspaceSymbolProvider"),
("rename", "renameProvider"),
("signatureHelp", "signatureHelpProvider"),
("inlayHints", "inlayHintProvider"),
("semanticTokens", "semanticTokensProvider"),
("selectionRange", "selectionRangeProvider"),
("callHierarchy", "callHierarchyProvider"),
("documentHighlight", "documentHighlightProvider"),
("implementation", "implementationProvider"),
("codeAction", "codeActionProvider"),
("typeDefinition", "typeDefinitionProvider"),
("codeLens", "codeLensProvider"),
("formatting", "documentFormattingProvider"),
("rangeFormatting", "documentRangeFormattingProvider"),
("onTypeFormatting", "documentOnTypeFormattingProvider"),
("documentLink", "documentLinkProvider"),
("linkedEditingRange", "linkedEditingRangeProvider"),
("inlineValues", "inlineValueProvider"),
];
const UNCONDITIONAL_CAPS: &[&str] = &[
"textDocumentSync",
"foldingRangeProvider",
"executeCommandProvider",
"diagnosticProvider",
"workspace",
"monikerProvider",
];
#[tokio::test]
async fn all_features_disabled_removes_all_toggleable_capabilities() {
let mut all_off = serde_json::json!({ "diagnostics": { "enabled": true }, "features": {} });
for (flag, _) in FEATURE_CAP_PAIRS {
all_off["features"][flag] = serde_json::json!(false);
}
let (_, resp) = TestServer::new_with_options(all_off).await;
let caps = &resp["result"]["capabilities"];
for (flag, cap_field) in FEATURE_CAP_PAIRS {
assert!(
caps[cap_field].is_null(),
"expected {cap_field} to be absent when feature '{flag}' is disabled, got: {}",
caps[cap_field]
);
}
for cap_field in UNCONDITIONAL_CAPS {
assert!(
!caps[cap_field].is_null(),
"expected {cap_field} to remain present (unconditional), got null"
);
}
}
#[tokio::test]
async fn all_features_enabled_by_default() {
let (_, resp) = TestServer::new_with_options(serde_json::json!({
"diagnostics": { "enabled": true }
}))
.await;
let caps = &resp["result"]["capabilities"];
for (flag, cap_field) in FEATURE_CAP_PAIRS {
assert!(
!caps[cap_field].is_null(),
"expected {cap_field} to be present by default (feature '{flag}' not mentioned), got null"
);
}
}
#[tokio::test]
async fn initialize_returns_server_capabilities() {
let (mut server, init_resp) = TestServer::new_with_options(serde_json::json!({})).await;
assert!(
!init_resp["result"]["capabilities"]["hoverProvider"].is_null(),
"expected hoverProvider to be advertised: {init_resp:?}"
);
server
.open("cap.php", "<?php\nfunction f(): void {}\n")
.await;
let resp = server.hover("cap.php", 1, 10).await;
expect![[r#"
```php
function f(): void
```"#]]
.assert_eq(&render_hover(&resp));
}
#[tokio::test]
async fn backslash_is_a_completion_trigger_character() {
let (_, init_resp) = TestServer::new_with_options(serde_json::json!({})).await;
let triggers = init_resp["result"]["capabilities"]["completionProvider"]["triggerCharacters"]
.as_array()
.expect("completionProvider should advertise triggerCharacters")
.iter()
.map(|v| v.as_str().unwrap_or_default())
.collect::<Vec<_>>();
assert!(
triggers.contains(&"\\"),
"expected '\\\\' among trigger characters, got {triggers:?}"
);
}
#[tokio::test]
async fn shutdown_responds_correctly() {
let mut server = TestServer::new().await;
let resp = server.shutdown().await;
assert!(
resp["error"].is_null(),
"shutdown should not error: {:?}",
resp
);
assert!(resp["result"].is_null(), "shutdown result should be null");
}
#[tokio::test]
async fn many_files_hover_each_returns_own_signature() {
let mut server = TestServer::new().await;
for i in 0..10 {
let src = format!("<?php\nfunction fn_{i}(int $x): int {{ return $x; }}\n");
server.open(&format!("c{i}.php"), &src).await;
}
for i in 0..10 {
let resp = server.hover(&format!("c{i}.php"), 1, 10).await;
let contents = resp["result"]["contents"].to_string();
assert!(
contents.contains(&format!("fn_{i}")),
"file c{i}.php hover must mention fn_{i}, got: {contents}"
);
}
}
#[tokio::test]
async fn sustained_hover_volley_all_succeed() {
let mut server = TestServer::new().await;
server
.open(
"pipe.php",
"<?php\nfunction pipeHover(int $x): int { return $x; }\n",
)
.await;
for _ in 0..20 {
let resp = server.hover("pipe.php", 1, 10).await;
assert!(resp["error"].is_null(), "hover errored in volley: {resp:?}");
let out = render_hover(&resp);
assert!(
out.contains("pipeHover"),
"hover content must stay correct across volley, got: {out}"
);
}
}
#[tokio::test]
async fn didchange_followed_by_request_sees_new_state_every_iteration() {
let mut server = TestServer::new().await;
server.open("iter.php", "<?php\n").await;
for v in 2..=8 {
let src = format!("<?php\nfunction iter_{v}(): int {{ return {v}; }}\niter_{v}();\n");
server.change("iter.php", v, &src).await;
let resp = server.hover("iter.php", 1, 10).await;
let contents = resp["result"]["contents"].to_string();
assert!(
contents.contains(&format!("iter_{v}")),
"iteration {v}: hover must see latest name, got: {contents}"
);
let resp = server.references("iter.php", 1, 10, false).await;
let refs = resp["result"].as_array().cloned().unwrap_or_default();
assert_eq!(
refs.len(),
1,
"iteration {v}: expected 1 ref, got {}: {refs:?}",
refs.len()
);
}
}
#[tokio::test]
async fn request_after_close_and_reopen_returns_fresh_data() {
let mut server = TestServer::new().await;
server
.open("ro.php", "<?php\nfunction first(): void {}\n")
.await;
let uri = server.uri("ro.php");
server
.client()
.notify(
"textDocument/didClose",
serde_json::json!({ "textDocument": { "uri": uri } }),
)
.await;
server
.open("ro.php", "<?php\nfunction second(): void {}\n")
.await;
let resp = server.hover("ro.php", 1, 10).await;
expect![[r#"
```php
function second(): void
```"#]]
.assert_eq(&render_hover(&resp));
}
#[tokio::test]
async fn cancel_request_returns_request_cancelled_and_server_stays_alive() {
let mut server = TestServer::new().await;
for i in 0..30 {
let src =
format!("<?php\nclass Worker{i} {{\n public function doWork(): void {{}}\n}}\n");
server.open(&format!("w{i}.php"), &src).await;
}
server
.open(
"cancel_main.php",
"<?php\n$w = new Worker0();\n$w->doWork();\n",
)
.await;
let uri = server.uri("cancel_main.php");
let mut cancelled = false;
for _ in 0..10 {
let resp = server
.request_then_cancel(
"textDocument/references",
serde_json::json!({
"textDocument": { "uri": uri },
"position": { "line": 2, "character": 5 },
"context": { "includeDeclaration": true },
}),
)
.await;
match resp.get("error") {
None => continue, Some(err) => {
assert_eq!(
err["code"],
serde_json::json!(-32800),
"expected RequestCancelled (-32800), got: {resp}"
);
cancelled = true;
break;
}
}
}
assert!(
cancelled,
"no attempt out of 10 produced a RequestCancelled response"
);
let hover = server.hover("cancel_main.php", 2, 5).await;
assert!(
hover.get("error").is_none(),
"hover after cancellation must succeed, got: {hover}"
);
}
#[tokio::test]
async fn references_and_hover_concurrent_complete_without_deadlock() {
let (mut srv_a, mut srv_b) = tokio::join!(TestServer::new(), TestServer::new());
srv_a
.open(
"conc_a.php",
"<?php\nfunction greet(string $name): string { return $name; }\ngreet('world');\n",
)
.await;
srv_b
.open(
"conc_b.php",
"<?php\nfunction greet(string $name): string { return $name; }\ngreet('world');\n",
)
.await;
let (refs_resp, hover_resp) = tokio::join!(
srv_a.references("conc_a.php", 1, 10, false),
srv_b.hover("conc_b.php", 1, 10)
);
expect!["conc_a.php:2:0-2:5"].assert_eq(&render_locations(&refs_resp, &srv_a.uri("")));
expect![[r#"
```php
function greet(string $name): string
```"#]]
.assert_eq(&render_hover(&hover_resp));
}