🧪 test: add LSP json fixtures and provider suite
This commit is contained in:
parent
53f4588c53
commit
bb0cbde79c
|
|
@ -36,3 +36,7 @@ endif()
|
|||
if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/test_module/CMakeLists.txt)
|
||||
add_subdirectory(test_module)
|
||||
endif()
|
||||
|
||||
if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/test_provider/CMakeLists.txt)
|
||||
add_subdirectory(test_provider)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "$/cancelRequest",
|
||||
"params": {
|
||||
"id": "nonexistent"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"id": 2,
|
||||
"method": "textDocument/completion",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///home/cobia/test_lsp/1.tsl"
|
||||
"uri": "{{MAIN_UNIT_URI}}"
|
||||
},
|
||||
"position": {
|
||||
"line": 2,
|
||||
"character": 5
|
||||
"line": 0,
|
||||
"character": 0
|
||||
},
|
||||
"context": {
|
||||
"triggerKind": 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "completionItem/resolve",
|
||||
"params": {
|
||||
"label": "Widget",
|
||||
"data": {
|
||||
"ctx": "new",
|
||||
"class": "Widget",
|
||||
"unit": "MainUnit",
|
||||
"uri": "{{MAIN_UNIT_URI}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "textDocument/definition",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "{{MAIN_UNIT_URI}}"
|
||||
},
|
||||
"position": {
|
||||
"line": 72,
|
||||
"character": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,23 +3,23 @@
|
|||
"method": "textDocument/didChange",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///home/cobia/test_lsp/1.tsl",
|
||||
"version": 4
|
||||
"uri": "{{MAIN_UNIT_URI}}",
|
||||
"version": 2
|
||||
},
|
||||
"contentChanges": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 2,
|
||||
"character": 4
|
||||
"line": 0,
|
||||
"character": 0
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"character": 4
|
||||
"line": 0,
|
||||
"character": 0
|
||||
}
|
||||
},
|
||||
"rangeLength": 0,
|
||||
"text": "e"
|
||||
"text": " "
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didClose",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "{{MAIN_UNIT_URI}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didOpen",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "{{MAIN_UNIT_URI}}",
|
||||
"languageId": "tsl",
|
||||
"version": 1,
|
||||
"text": {{MAIN_UNIT_TEXT}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
"method": "textDocument/didOpen",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "file:///home/cobia/test_lsp/1.tsl",
|
||||
"uri": "{{MAIN_UNIT_URI}}",
|
||||
"languageId": "tsl",
|
||||
"version": 1,
|
||||
"text": "function aaa();\nbegin\nend;"
|
||||
"text": {{MAIN_UNIT_TEXT}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "exit"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"processId": 61310,
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
"version": "1.102.0"
|
||||
},
|
||||
"locale": "en",
|
||||
"rootPath": "/home/cobia/test_lsp",
|
||||
"rootUri": "file:///home/cobia/test_lsp",
|
||||
"rootPath": "{{WORKSPACE_PATH}}",
|
||||
"rootUri": "{{WORKSPACE_URI}}",
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": true,
|
||||
|
|
@ -479,7 +479,7 @@
|
|||
"trace": "off",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "file:///home/cobia/test_lsp",
|
||||
"uri": "{{WORKSPACE_URI}}",
|
||||
"name": "test_lsp"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialized",
|
||||
"params": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 6,
|
||||
"method": "textDocument/references",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "{{RENAME_CASE_URI}}"
|
||||
},
|
||||
"position": {
|
||||
"line": 1,
|
||||
"character": 0
|
||||
},
|
||||
"context": {
|
||||
"includeDeclaration": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "textDocument/rename",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": "{{RENAME_CASE_URI}}"
|
||||
},
|
||||
"position": {
|
||||
"line": 1,
|
||||
"character": 0
|
||||
},
|
||||
"newName": "renamed"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
initialize.json
|
||||
initialized.json
|
||||
setTrace.json
|
||||
didOpen.json
|
||||
completion.json
|
||||
completion_resolve.json
|
||||
definition.json
|
||||
rename.json
|
||||
references.json
|
||||
didChange.json
|
||||
cancelRequest.json
|
||||
didClose.json
|
||||
shutdown.json
|
||||
exit.json
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "$/setTrace",
|
||||
"params": {
|
||||
"value": "verbose"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"method": "shutdown"
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def path_to_uri(path: Path) -> str:
|
||||
return path.resolve().as_uri()
|
||||
|
||||
|
||||
def load_sequence(sequence_path: Path) -> list[str]:
|
||||
lines = []
|
||||
for raw in sequence_path.read_text().splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def load_messages(request_dir: Path, replacements: dict[str, str], files: list[str]) -> list[dict]:
|
||||
messages = []
|
||||
for name in files:
|
||||
text = (request_dir / name).read_text()
|
||||
for key, value in replacements.items():
|
||||
text = text.replace(key, value)
|
||||
try:
|
||||
messages.append(json.loads(text))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"Invalid JSON in {name}: {exc}") from exc
|
||||
return messages
|
||||
|
||||
|
||||
def build_payload(message: dict) -> bytes:
|
||||
body = json.dumps(message, separators=(",", ":")).encode("utf-8")
|
||||
header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
|
||||
return header + body
|
||||
|
||||
|
||||
def read_response(stream, buffer: bytes, timeout_seconds: float) -> tuple[dict, bytes]:
|
||||
deadline = time.time() + timeout_seconds
|
||||
fd = stream.fileno()
|
||||
|
||||
while True:
|
||||
header_end = buffer.find(b"\r\n\r\n")
|
||||
if header_end != -1:
|
||||
header = buffer[:header_end].decode("ascii", errors="replace")
|
||||
length = None
|
||||
for line in header.splitlines():
|
||||
if line.lower().startswith("content-length:"):
|
||||
length = int(line.split(":", 1)[1].strip())
|
||||
break
|
||||
if length is None:
|
||||
raise RuntimeError("Missing Content-Length in response header")
|
||||
|
||||
body_start = header_end + 4
|
||||
if len(buffer) - body_start >= length:
|
||||
body = buffer[body_start:body_start + length]
|
||||
buffer = buffer[body_start + length:]
|
||||
try:
|
||||
return json.loads(body.decode("utf-8")), buffer
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"Failed to decode response JSON: {exc}") from exc
|
||||
|
||||
remaining = deadline - time.time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError("Timed out waiting for response from tsl-server")
|
||||
|
||||
ready, _, _ = select.select([fd], [], [], remaining)
|
||||
if not ready:
|
||||
continue
|
||||
|
||||
chunk = os.read(fd, 4096)
|
||||
if not chunk:
|
||||
raise RuntimeError("tsl-server closed stdout unexpectedly")
|
||||
buffer += chunk
|
||||
|
||||
|
||||
def index_by_id(responses: list[dict]) -> dict[str, dict]:
|
||||
by_id = {}
|
||||
for response in responses:
|
||||
if "id" in response:
|
||||
by_id[str(response["id"])] = response
|
||||
return by_id
|
||||
|
||||
|
||||
def assert_has_key(obj: dict, key: str, context: str) -> None:
|
||||
if key not in obj:
|
||||
raise RuntimeError(f"Missing '{key}' in {context}")
|
||||
|
||||
|
||||
def assert_error_code(response: dict, expected_code: int, context: str) -> None:
|
||||
assert_has_key(response, "error", context)
|
||||
error = response["error"]
|
||||
if error.get("code") != expected_code:
|
||||
raise RuntimeError(f"{context} error code {error.get('code')} != {expected_code}")
|
||||
|
||||
|
||||
def validate_responses(by_id: dict[str, dict]) -> None:
|
||||
for request_id in ("1", "2", "3", "4", "5", "6", "7"):
|
||||
if request_id not in by_id:
|
||||
raise RuntimeError(f"Missing response for request id {request_id}")
|
||||
|
||||
init = by_id["1"]
|
||||
assert_has_key(init, "result", "initialize response")
|
||||
capabilities = init["result"].get("capabilities", {})
|
||||
if "completionProvider" not in capabilities:
|
||||
raise RuntimeError("initialize response missing completionProvider")
|
||||
|
||||
completion = by_id["2"]
|
||||
assert_has_key(completion, "result", "completion response")
|
||||
result = completion["result"]
|
||||
items = result.get("items") if isinstance(result, dict) else result
|
||||
if not isinstance(items, list):
|
||||
raise RuntimeError("completion response items missing or not a list")
|
||||
if not any(item.get("label") == "function" for item in items if isinstance(item, dict)):
|
||||
raise RuntimeError("completion response missing keyword completions")
|
||||
|
||||
resolved = by_id["3"]
|
||||
if "result" in resolved:
|
||||
if not isinstance(resolved["result"], dict) or resolved["result"].get("label") != "Widget":
|
||||
raise RuntimeError("completion resolve should return the resolved item")
|
||||
else:
|
||||
assert_error_code(resolved, -32603, "completion resolve response")
|
||||
|
||||
assert_error_code(by_id["4"], -32601, "definition response")
|
||||
assert_error_code(by_id["5"], -32601, "rename response")
|
||||
assert_error_code(by_id["6"], -32601, "references response")
|
||||
|
||||
shutdown = by_id["7"]
|
||||
if "error" in shutdown:
|
||||
raise RuntimeError("shutdown response should not include error")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run LSP JSON tests against tsl-server.")
|
||||
parser.add_argument("--server", default=None, help="Path to tsl-server binary")
|
||||
parser.add_argument("--no-validate", action="store_true", help="Skip response validation")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
request_dir = repo_root / "lsp-server" / "test" / "request_json"
|
||||
sequence_path = request_dir / "sequence.txt"
|
||||
|
||||
fixtures_dir = repo_root / "lsp-server" / "test" / "test_provider" / "fixtures"
|
||||
main_unit = fixtures_dir / "main_unit.tsf"
|
||||
rename_case = fixtures_dir / "rename_case.tsl"
|
||||
workspace_dir = fixtures_dir / "workspace"
|
||||
|
||||
if not request_dir.exists():
|
||||
raise RuntimeError(f"request_json not found: {request_dir}")
|
||||
|
||||
server_path = Path(args.server) if args.server else (repo_root / "vscode" / "bin" / "tsl-server")
|
||||
if not server_path.exists():
|
||||
raise RuntimeError(f"tsl-server not found: {server_path}")
|
||||
|
||||
replacements = {
|
||||
"{{WORKSPACE_PATH}}": str(workspace_dir.resolve()),
|
||||
"{{WORKSPACE_URI}}": path_to_uri(workspace_dir),
|
||||
"{{MAIN_UNIT_URI}}": path_to_uri(main_unit),
|
||||
"{{RENAME_CASE_URI}}": path_to_uri(rename_case),
|
||||
"{{MAIN_UNIT_TEXT}}": json.dumps(main_unit.read_text()),
|
||||
}
|
||||
|
||||
files = load_sequence(sequence_path)
|
||||
messages = load_messages(request_dir, replacements, files)
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[str(server_path), "--log=off", "--log-stderr"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
responses = []
|
||||
by_id = {}
|
||||
buffer = b""
|
||||
|
||||
try:
|
||||
for message in messages:
|
||||
if proc.stdin is None:
|
||||
raise RuntimeError("tsl-server stdin is not available")
|
||||
proc.stdin.write(build_payload(message))
|
||||
proc.stdin.flush()
|
||||
|
||||
if "id" not in message:
|
||||
continue
|
||||
|
||||
expected_id = str(message["id"])
|
||||
while expected_id not in by_id:
|
||||
response, buffer = read_response(proc.stdout, buffer, timeout_seconds=10)
|
||||
responses.append(response)
|
||||
if "id" in response:
|
||||
by_id[str(response["id"])] = response
|
||||
|
||||
if proc.stdin:
|
||||
proc.stdin.close()
|
||||
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
proc.wait(timeout=5)
|
||||
raise
|
||||
|
||||
if proc.returncode not in (0, None):
|
||||
sys.stderr.write(proc.stderr.read().decode("utf-8", errors="replace"))
|
||||
raise RuntimeError(f"tsl-server exited with code {proc.returncode}")
|
||||
|
||||
if not args.no_validate:
|
||||
validate_responses(by_id)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
cmake_minimum_required(VERSION 4.0)
|
||||
|
||||
project(test_provider LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
|
||||
|
||||
find_package(glaze CONFIG REQUIRED)
|
||||
find_package(spdlog CONFIG REQUIRED)
|
||||
find_package(fmt CONFIG REQUIRED)
|
||||
find_package(Taskflow CONFIG REQUIRED)
|
||||
find_package(tree-sitter CONFIG REQUIRED)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
find_package(Threads REQUIRED)
|
||||
endif()
|
||||
|
||||
set(SOURCES
|
||||
main.cc
|
||||
test_main.cppm
|
||||
../test_lsp_any/test_framework.cppm
|
||||
fixtures.cppm
|
||||
completion_test.cppm
|
||||
json_flow_test.cppm
|
||||
definitions_test.cppm
|
||||
provider_misc_test.cppm
|
||||
provider_surface_test.cppm
|
||||
../../src/tree-sitter/parser.c
|
||||
../../src/tree-sitter/scanner.c)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${SOURCES})
|
||||
if(TARGET std_module)
|
||||
add_dependencies(${PROJECT_NAME} std_module)
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src)
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE
|
||||
FILE_SET cxx_modules TYPE CXX_MODULES
|
||||
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../test_lsp_any
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../test_lsp_any/test_framework.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/fixtures.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/completion_test.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/definitions_test.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/provider_misc_test.cppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/provider_surface_test.cppm
|
||||
../../src/bridge/glaze.cppm
|
||||
../../src/bridge/spdlog.cppm
|
||||
../../src/bridge/taskflow.cppm
|
||||
../../src/bridge/tree_sitter.cppm
|
||||
../../src/utils/string.cppm
|
||||
../../src/utils/text_coordinates.cppm
|
||||
../../src/core/dispacther.cppm
|
||||
../../src/scheduler/async_executor.cppm
|
||||
../../src/manager/event_bus.cppm
|
||||
../../src/manager/events.cppm
|
||||
../../src/manager/detail/text_document.cppm
|
||||
../../src/manager/document.cppm
|
||||
../../src/manager/parser.cppm
|
||||
../../src/manager/symbol.cppm
|
||||
../../src/manager/manager_hub.cppm
|
||||
../../src/language/ast/ast.cppm
|
||||
../../src/language/ast/types.cppm
|
||||
../../src/language/ast/deserializer.cppm
|
||||
../../src/language/ast/deserializer_impl.cppm
|
||||
../../src/language/ast/ts_utils.cppm
|
||||
../../src/language/ast/detail.cppm
|
||||
../../src/language/symbol/types.cppm
|
||||
../../src/language/symbol/internal/builder.cppm
|
||||
../../src/language/symbol/internal/store.cppm
|
||||
../../src/language/symbol/internal/table.cppm
|
||||
../../src/language/symbol/index/coordinator.cppm
|
||||
../../src/language/symbol/index/location.cppm
|
||||
../../src/language/symbol/index/scope.cppm
|
||||
../../src/language/symbol/symbol.cppm
|
||||
../../src/language/semantic/interface.cppm
|
||||
../../src/language/semantic/semantic.cppm
|
||||
../../src/language/semantic/analyzer.cppm
|
||||
../../src/language/semantic/semantic_model.cppm
|
||||
../../src/language/semantic/type_system.cppm
|
||||
../../src/language/semantic/name_resolver.cppm
|
||||
../../src/language/semantic/token_collector.cppm
|
||||
../../src/language/semantic/graph/call.cppm
|
||||
../../src/language/semantic/graph/reference.cppm
|
||||
../../src/language/semantic/graph/inheritance.cppm
|
||||
../../src/language/keyword/repo.cppm
|
||||
../../src/codec/common.cppm
|
||||
../../src/codec/transformer.cppm
|
||||
../../src/codec/facade.cppm
|
||||
../../src/protocol/common/basic_types.cppm
|
||||
../../src/protocol/common/message.cppm
|
||||
../../src/protocol/common/registration.cppm
|
||||
../../src/protocol/window/progress.cppm
|
||||
../../src/protocol/initialize/configuration.cppm
|
||||
../../src/protocol/initialize/capabilities.cppm
|
||||
../../src/protocol/workspace/workspace.cppm
|
||||
../../src/protocol/workspace/file_operations.cppm
|
||||
../../src/protocol/workspace/notebook.cppm
|
||||
../../src/protocol/text_document/document_sync.cppm
|
||||
../../src/protocol/text_document/completion.cppm
|
||||
../../src/protocol/text_document/code_actions.cppm
|
||||
../../src/protocol/text_document/diagnostics.cppm
|
||||
../../src/protocol/text_document/document_features.cppm
|
||||
../../src/protocol/text_document/formatting.cppm
|
||||
../../src/protocol/text_document/inline_features.cppm
|
||||
../../src/protocol/text_document/navigation.cppm
|
||||
../../src/protocol/text_document/rename.cppm
|
||||
../../src/protocol/text_document/semantic_tokens.cppm
|
||||
../../src/protocol/text_document/signature_help.cppm
|
||||
../../src/protocol/text_document/symbols.cppm
|
||||
../../src/protocol/types.cppm
|
||||
../../src/protocol/protocol.cppm
|
||||
../../src/provider/base/interface.cppm
|
||||
../../src/provider/text_document/completion.cppm
|
||||
../../src/provider/text_document/definition.cppm
|
||||
../../src/provider/text_document/did_open.cppm
|
||||
../../src/provider/text_document/did_change.cppm
|
||||
../../src/provider/text_document/did_close.cppm
|
||||
../../src/provider/text_document/rename.cppm
|
||||
../../src/provider/text_document/references.cppm
|
||||
../../src/provider/text_document/semantic_tokens.cppm
|
||||
../../src/provider/completion_item/resolve.cppm
|
||||
../../src/provider/initialize/initialize.cppm
|
||||
../../src/provider/initialized/initialized.cppm
|
||||
../../src/provider/shutdown/shutdown.cppm
|
||||
../../src/provider/exit/exit.cppm
|
||||
../../src/provider/cancel_request/cancel_request.cppm
|
||||
../../src/provider/trace/set_trace.cppm
|
||||
../../src/provider/client/register_capability.cppm
|
||||
../../src/provider/client/unregister_capability.cppm
|
||||
../../src/provider/workspace/symbol.cppm
|
||||
../../src/provider/call_hierarchy/incoming_calls.cppm
|
||||
../../src/provider/call_hierarchy/outgoing_calls.cppm
|
||||
../../src/provider/code_action/resolve.cppm
|
||||
../../src/provider/code_lens/resolve.cppm
|
||||
../../src/provider/document_link/resolve.cppm
|
||||
../../src/provider/inlay_hint/resolve.cppm
|
||||
../../src/provider/telemetry/event.cppm
|
||||
../../src/provider/text_document/code_action.cppm
|
||||
../../src/provider/text_document/code_lens.cppm
|
||||
../../src/provider/text_document/color_presentation.cppm
|
||||
../../src/provider/text_document/diagnostic.cppm
|
||||
../../src/provider/text_document/document_color.cppm
|
||||
../../src/provider/text_document/document_highlight.cppm
|
||||
../../src/provider/text_document/document_link.cppm
|
||||
../../src/provider/text_document/document_symbol.cppm
|
||||
../../src/provider/text_document/folding_range.cppm
|
||||
../../src/provider/text_document/formatting.cppm
|
||||
../../src/provider/text_document/hover.cppm
|
||||
../../src/provider/text_document/implementation.cppm
|
||||
../../src/provider/text_document/inlay_hint.cppm
|
||||
../../src/provider/text_document/inline_value.cppm
|
||||
../../src/provider/text_document/linked_editing_range.cppm
|
||||
../../src/provider/text_document/moniker.cppm
|
||||
../../src/provider/text_document/on_type_formatting.cppm
|
||||
../../src/provider/text_document/prepare_call_hierarchy.cppm
|
||||
../../src/provider/text_document/prepare_rename.cppm
|
||||
../../src/provider/text_document/prepare_type_hierarchy.cppm
|
||||
../../src/provider/text_document/publish_diagnostics.cppm
|
||||
../../src/provider/text_document/range_formatting.cppm
|
||||
../../src/provider/text_document/selection_range.cppm
|
||||
../../src/provider/text_document/signature_help.cppm
|
||||
../../src/provider/text_document/type_definition.cppm
|
||||
../../src/provider/type_hierarchy/subtypes.cppm
|
||||
../../src/provider/type_hierarchy/supertypes.cppm
|
||||
../../src/provider/window/log_message.cppm
|
||||
../../src/provider/window/show_document.cppm
|
||||
../../src/provider/window/show_message.cppm
|
||||
../../src/provider/window/show_message_request.cppm
|
||||
../../src/provider/window/work_done_progress_create.cppm
|
||||
../../src/provider/workspace/apply_edit.cppm
|
||||
../../src/provider/workspace/code_lens_refresh.cppm
|
||||
../../src/provider/workspace/configuration.cppm
|
||||
../../src/provider/workspace/diagnostic.cppm
|
||||
../../src/provider/workspace/diagnostic_refresh.cppm
|
||||
../../src/provider/workspace/did_change_configuration.cppm
|
||||
../../src/provider/workspace/did_change_watched_files.cppm
|
||||
../../src/provider/workspace/did_change_workspace_folders.cppm
|
||||
../../src/provider/workspace/did_create_files.cppm
|
||||
../../src/provider/workspace/did_delete_files.cppm
|
||||
../../src/provider/workspace/did_rename_files.cppm
|
||||
../../src/provider/workspace/execute_command.cppm
|
||||
../../src/provider/workspace/inlay_hint_refresh.cppm
|
||||
../../src/provider/workspace/inline_value_refresh.cppm
|
||||
../../src/provider/workspace/semantic_tokens_refresh.cppm
|
||||
../../src/provider/workspace/will_create_files.cppm
|
||||
../../src/provider/workspace/will_delete_files.cppm
|
||||
../../src/provider/workspace/will_rename_files.cppm
|
||||
../../src/provider/workspace/workspace_folders.cppm
|
||||
../../src/provider/workspace_symbol/resolve.cppm)
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_HEADER_ONLY FMT_HEADER_ONLY)
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE glaze::glaze Taskflow::Taskflow spdlog::spdlog_header_only
|
||||
fmt::fmt-header-only tree-sitter::tree-sitter
|
||||
$<$<PLATFORM_ID:Linux>:Threads::Threads>)
|
||||
|
||||
target_compile_options(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE -Wall -Wextra -Wpedantic $<$<CONFIG:Debug>:-g -O0>
|
||||
-Wno-import-implementation-partition-unit-in-interface-unit
|
||||
$<$<CONFIG:Release>:-O3>)
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.completion;
|
||||
|
||||
import std;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.provider.text_document.completion;
|
||||
import lsp.provider.completion_item.resolve;
|
||||
import lsp.core.dispacther;
|
||||
import lsp.manager.manager_hub;
|
||||
import lsp.scheduler.async_executor;
|
||||
import lsp.protocol;
|
||||
import lsp.codec.facade;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
class CompletionTests
|
||||
{
|
||||
public:
|
||||
static void Register(TestRunner& runner);
|
||||
|
||||
private:
|
||||
static TestResult TestClassMethodCompletion();
|
||||
static TestResult TestNewCompletion();
|
||||
static TestResult TestUnitScopedNewCompletion();
|
||||
static TestResult TestCreateObjectCompletion();
|
||||
static TestResult TestCreateObjectQuotedCompletion();
|
||||
static TestResult TestCreateObjectQualifiedCompletion();
|
||||
static TestResult TestUnitContextCompletion();
|
||||
static TestResult TestUnitMemberCompletion();
|
||||
static TestResult TestObjectMemberCompletion();
|
||||
static TestResult TestObjectMemberQualifiedTypeCompletion();
|
||||
static TestResult TestFunctionCompletion();
|
||||
static TestResult TestKeywordCompletion();
|
||||
static TestResult TestClassContextCompletion();
|
||||
static TestResult TestUnitScopedNewAliasCompletion();
|
||||
static TestResult TestCompletionResolveNewSnippet();
|
||||
static TestResult TestCompletionResolveCreateObjectSnippet();
|
||||
};
|
||||
}
|
||||
|
||||
namespace lsp::test::provider
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct ProviderEnv
|
||||
{
|
||||
scheduler::AsyncExecutor scheduler{ 1 };
|
||||
manager::ManagerHub hub{};
|
||||
core::ExecutionContext context;
|
||||
|
||||
ProviderEnv()
|
||||
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
|
||||
{
|
||||
hub.Initialize();
|
||||
}
|
||||
};
|
||||
|
||||
protocol::ResponseMessage ParseResponse(const std::string& json)
|
||||
{
|
||||
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
|
||||
if (!parsed)
|
||||
{
|
||||
throw std::runtime_error("Failed to deserialize response");
|
||||
}
|
||||
return *parsed;
|
||||
}
|
||||
|
||||
protocol::Position FindPosition(const std::string& content, const std::string& marker)
|
||||
{
|
||||
auto pos = content.find(marker);
|
||||
assertTrue(pos != std::string::npos, "Marker not found in fixture");
|
||||
protocol::Position result{};
|
||||
result.line = 0;
|
||||
result.character = 0;
|
||||
for (std::size_t i = 0; i < pos; ++i)
|
||||
{
|
||||
if (content[i] == '\n')
|
||||
{
|
||||
result.line++;
|
||||
result.character = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.character++;
|
||||
}
|
||||
}
|
||||
result.character += static_cast<std::uint32_t>(marker.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
|
||||
{
|
||||
protocol::DidOpenTextDocumentParams open_params;
|
||||
open_params.textDocument.uri = uri;
|
||||
open_params.textDocument.languageId = "tsl";
|
||||
open_params.textDocument.version = version;
|
||||
open_params.textDocument.text = text;
|
||||
hub.documents().OpenDocument(open_params);
|
||||
}
|
||||
|
||||
std::vector<protocol::CompletionItem> RequestCompletion(ProviderEnv& env,
|
||||
const std::string& uri,
|
||||
const std::string& content,
|
||||
const std::string& marker)
|
||||
{
|
||||
protocol::CompletionParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, marker);
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "c1";
|
||||
request.method = "textDocument/completion";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::Completion handler;
|
||||
auto json = handler.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
if (!response.result.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
auto list = codec::FromLSPAny.template operator()<protocol::CompletionList>(response.result.value());
|
||||
return list.items;
|
||||
}
|
||||
|
||||
bool HasLabel(const std::vector<protocol::CompletionItem>& items, const std::string& label)
|
||||
{
|
||||
return std::any_of(items.begin(), items.end(), [&](const auto& item) {
|
||||
return item.label == label;
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<protocol::CompletionItem> FindItem(const std::vector<protocol::CompletionItem>& items,
|
||||
const std::string& label)
|
||||
{
|
||||
auto it = std::find_if(items.begin(), items.end(), [&](const auto& item) {
|
||||
return item.label == label;
|
||||
});
|
||||
if (it == items.end())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
|
||||
void CompletionTests::Register(TestRunner& runner)
|
||||
{
|
||||
runner.addTest("completion class(Widget).", TestClassMethodCompletion);
|
||||
runner.addTest("completion new Widget", TestNewCompletion);
|
||||
runner.addTest("completion new unit(MainUnit).Widget", TestUnitScopedNewCompletion);
|
||||
runner.addTest("completion createobject", TestCreateObjectCompletion);
|
||||
runner.addTest("completion createobject quoted", TestCreateObjectQuotedCompletion);
|
||||
runner.addTest("completion createobject qualified", TestCreateObjectQualifiedCompletion);
|
||||
runner.addTest("completion unit(", TestUnitContextCompletion);
|
||||
runner.addTest("completion unit member", TestUnitMemberCompletion);
|
||||
runner.addTest("completion object member", TestObjectMemberCompletion);
|
||||
runner.addTest("completion object member qualified type", TestObjectMemberQualifiedTypeCompletion);
|
||||
runner.addTest("completion function prefix", TestFunctionCompletion);
|
||||
runner.addTest("completion keyword prefix", TestKeywordCompletion);
|
||||
runner.addTest("completion class(", TestClassContextCompletion);
|
||||
runner.addTest("completion new alias", TestUnitScopedNewAliasCompletion);
|
||||
runner.addTest("completion resolve new snippet", TestCompletionResolveNewSnippet);
|
||||
runner.addTest("completion resolve createobject snippet", TestCompletionResolveCreateObjectSnippet);
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestClassMethodCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "class(Widget).Sta");
|
||||
assertTrue(HasLabel(items, "StaticFoo"), "class() should suggest static methods");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestNewCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "new Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "new context should suggest classes");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestUnitScopedNewCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "new unit(MainUnit).Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "unit scoped new should suggest unit classes");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestCreateObjectCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "createobject(Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "createobject should suggest classes");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestCreateObjectQuotedCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "createobject(\"Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "quoted createobject should suggest classes");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestCreateObjectQualifiedCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "createobject(MainUnit.Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "qualified createobject should suggest classes");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestUnitContextCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "unit(Main");
|
||||
assertTrue(HasLabel(items, "MainUnit"), "unit context should list visible units");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestUnitMemberCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "unit(MainUnit).Uni");
|
||||
assertTrue(HasLabel(items, "UnitFunc"), "unit member completion should list functions");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestObjectMemberCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "obj.Fo");
|
||||
assertTrue(HasLabel(items, "Foo"), "object member completion should list instance methods");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestObjectMemberQualifiedTypeCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace")));
|
||||
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "unit_inst.Wor");
|
||||
assertTrue(HasLabel(items, "Work"), "qualified type should resolve workspace class members");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestFunctionCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "UnitF");
|
||||
assertTrue(HasLabel(items, "UnitFunc"), "function prefix should return UnitFunc");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestKeywordCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "fun");
|
||||
assertTrue(HasLabel(items, "function"), "keyword completion should include 'function'");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestClassContextCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "class(Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "class context should suggest classes with static methods");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestUnitScopedNewAliasCompletion()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "new MainUnit.Wid");
|
||||
assertTrue(HasLabel(items, "Widget"), "alias scoped new should suggest classes");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestCompletionResolveNewSnippet()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "new Wid");
|
||||
auto item = FindItem(items, "Widget");
|
||||
assertTrue(item.has_value(), "Expected Widget completion");
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "r1";
|
||||
request.method = "completionItem/resolve";
|
||||
request.params = codec::ToLSPAny(*item);
|
||||
|
||||
::lsp::provider::completion_item::Resolve resolver;
|
||||
auto json = resolver.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(response.result.value());
|
||||
|
||||
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
|
||||
assertTrue(resolved.insertText.value().find("Widget(") == 0, "Resolved new snippet should start with Widget(");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult CompletionTests::TestCompletionResolveCreateObjectSnippet()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
auto items = RequestCompletion(env, uri, content, "createobject(\"Wid");
|
||||
auto item = FindItem(items, "Widget");
|
||||
assertTrue(item.has_value(), "Expected Widget completion");
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "r2";
|
||||
request.method = "completionItem/resolve";
|
||||
request.params = codec::ToLSPAny(*item);
|
||||
|
||||
::lsp::provider::completion_item::Resolve resolver;
|
||||
auto json = resolver.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(response.result.value());
|
||||
|
||||
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
|
||||
auto snippet = resolved.insertText.value();
|
||||
assertTrue(!snippet.empty() && snippet.front() == 'W', "Resolved createobject snippet should not add quote");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.definitions;
|
||||
|
||||
import std;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.provider.text_document.definition;
|
||||
import lsp.core.dispacther;
|
||||
import lsp.manager.manager_hub;
|
||||
import lsp.scheduler.async_executor;
|
||||
import lsp.protocol;
|
||||
import lsp.codec.facade;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
class DefinitionTests
|
||||
{
|
||||
public:
|
||||
static void Register(TestRunner& runner);
|
||||
|
||||
private:
|
||||
static TestResult TestDefinitionResolvesInDocument();
|
||||
static TestResult TestDefinitionResolvesInWorkspaceIndex();
|
||||
static TestResult TestDefinitionResolvesInSystemIndex();
|
||||
};
|
||||
}
|
||||
|
||||
namespace lsp::test::provider
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct ProviderEnv
|
||||
{
|
||||
scheduler::AsyncExecutor scheduler{ 1 };
|
||||
manager::ManagerHub hub{};
|
||||
core::ExecutionContext context;
|
||||
|
||||
ProviderEnv()
|
||||
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
|
||||
{
|
||||
hub.Initialize();
|
||||
}
|
||||
};
|
||||
|
||||
protocol::ResponseMessage ParseResponse(const std::string& json)
|
||||
{
|
||||
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
|
||||
if (!parsed)
|
||||
{
|
||||
throw std::runtime_error("Failed to deserialize response");
|
||||
}
|
||||
return *parsed;
|
||||
}
|
||||
|
||||
protocol::Position FindPosition(const std::string& content, const std::string& marker)
|
||||
{
|
||||
auto pos = content.find(marker);
|
||||
assertTrue(pos != std::string::npos, "Marker not found in fixture");
|
||||
protocol::Position result{};
|
||||
result.line = 0;
|
||||
result.character = 0;
|
||||
for (std::size_t i = 0; i < pos; ++i)
|
||||
{
|
||||
if (content[i] == '\n')
|
||||
{
|
||||
result.line++;
|
||||
result.character = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.character++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
|
||||
{
|
||||
protocol::DidOpenTextDocumentParams open_params;
|
||||
open_params.textDocument.uri = uri;
|
||||
open_params.textDocument.languageId = "tsl";
|
||||
open_params.textDocument.version = version;
|
||||
open_params.textDocument.text = text;
|
||||
hub.documents().OpenDocument(open_params);
|
||||
}
|
||||
|
||||
protocol::Location ExtractLocation(const protocol::ResponseMessage& response)
|
||||
{
|
||||
if (!response.result.has_value())
|
||||
{
|
||||
throw std::runtime_error("Expected definition result");
|
||||
}
|
||||
return codec::FromLSPAny.template operator()<protocol::Location>(response.result.value());
|
||||
}
|
||||
|
||||
bool LocationMatchesLine(const protocol::Location& location, std::uint32_t line)
|
||||
{
|
||||
return location.range.start.line == line;
|
||||
}
|
||||
}
|
||||
|
||||
void DefinitionTests::Register(TestRunner& runner)
|
||||
{
|
||||
runner.addTest("definition resolves in document", TestDefinitionResolvesInDocument);
|
||||
runner.addTest("definition resolves in workspace index", TestDefinitionResolvesInWorkspaceIndex);
|
||||
runner.addTest("definition resolves in system index", TestDefinitionResolvesInSystemIndex);
|
||||
}
|
||||
|
||||
TestResult DefinitionTests::TestDefinitionResolvesInDocument()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
ProviderEnv env;
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::DefinitionParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, "UnitFunc(1);");
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "1";
|
||||
request.method = "textDocument/definition";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::Definition handler;
|
||||
auto json = handler.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
auto location = ExtractLocation(response);
|
||||
|
||||
auto def_pos = FindPosition(content, "function UnitFunc");
|
||||
assertTrue(LocationMatchesLine(location, def_pos.line), "Definition should resolve in document");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult DefinitionTests::TestDefinitionResolvesInWorkspaceIndex()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
ProviderEnv env;
|
||||
auto workspace_root = FixturePath("workspace");
|
||||
env.hub.symbols().LoadWorkspace(ToUri(workspace_root));
|
||||
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::DefinitionParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, "WorkspaceFunc()");
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "2";
|
||||
request.method = "textDocument/definition";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::Definition handler;
|
||||
auto json = handler.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
auto location = ExtractLocation(response);
|
||||
|
||||
auto expected = ReadTextFile(FixturePath("workspace/workspace_script.tsl"));
|
||||
auto expected_pos = FindPosition(expected, "function WorkspaceFunc");
|
||||
assertTrue(LocationMatchesLine(location, expected_pos.line), "Definition should resolve in workspace index");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult DefinitionTests::TestDefinitionResolvesInSystemIndex()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
ProviderEnv env;
|
||||
auto system_root = FixturePath("system");
|
||||
env.hub.symbols().LoadSystemLibrary(system_root.string());
|
||||
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::DefinitionParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, "SystemUnit");
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "3";
|
||||
request.method = "textDocument/definition";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::Definition handler;
|
||||
auto json = handler.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
auto location = ExtractLocation(response);
|
||||
|
||||
auto expected = ReadTextFile(FixturePath("system/SystemUnit.tsf"));
|
||||
auto expected_pos = FindPosition(expected, "unit SystemUnit");
|
||||
assertTrue(LocationMatchesLine(location, expected_pos.line), "Definition should resolve in system index");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.fixtures;
|
||||
|
||||
import std;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
inline std::string& ExecutablePathStorage()
|
||||
{
|
||||
static std::string value;
|
||||
return value;
|
||||
}
|
||||
|
||||
inline void SetExecutablePath(std::string value)
|
||||
{
|
||||
ExecutablePathStorage() = std::move(value);
|
||||
}
|
||||
|
||||
inline const std::string& ExecutablePath()
|
||||
{
|
||||
return ExecutablePathStorage();
|
||||
}
|
||||
|
||||
inline std::filesystem::path FixturesRoot()
|
||||
{
|
||||
return std::filesystem::path(__FILE__).parent_path() / "fixtures";
|
||||
}
|
||||
|
||||
inline std::filesystem::path FixturePath(const std::string& name)
|
||||
{
|
||||
return FixturesRoot() / name;
|
||||
}
|
||||
|
||||
inline std::string ToUri(const std::filesystem::path& path)
|
||||
{
|
||||
auto absolute = std::filesystem::absolute(path).generic_string();
|
||||
#ifdef _WIN32
|
||||
std::string normalized;
|
||||
normalized.reserve(absolute.size());
|
||||
for (char ch : absolute)
|
||||
{
|
||||
normalized.push_back(ch == '\\' ? '/' : ch);
|
||||
}
|
||||
absolute = normalized;
|
||||
#endif
|
||||
if (!absolute.empty() && absolute.front() != '/')
|
||||
{
|
||||
absolute.insert(absolute.begin(), '/');
|
||||
}
|
||||
return "file://" + absolute;
|
||||
}
|
||||
|
||||
inline std::string ReadTextFile(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
throw std::runtime_error("Failed to open fixture: " + path.string());
|
||||
}
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
unit MainUnit;
|
||||
interface
|
||||
|
||||
uses WorkspaceUnit, SystemUnit;
|
||||
|
||||
type Widget = class
|
||||
public
|
||||
function Create(a: integer; b: string = "x");
|
||||
function Foo(x: integer): integer;
|
||||
class function StaticFoo(y: integer): integer;
|
||||
property Prop: integer read FProp;
|
||||
private
|
||||
FProp: integer;
|
||||
FieldVal: string;
|
||||
end;
|
||||
|
||||
type Plain = class
|
||||
public
|
||||
function Create();
|
||||
end;
|
||||
|
||||
function UnitFunc(a: integer): integer;
|
||||
var UnitVar: integer;
|
||||
|
||||
implementation
|
||||
|
||||
function Widget.Create(a: integer; b: string = "x");
|
||||
begin
|
||||
end;
|
||||
|
||||
function Widget.Foo(x: integer): integer;
|
||||
begin
|
||||
return x;
|
||||
end;
|
||||
|
||||
class function Widget.StaticFoo(y: integer): integer;
|
||||
begin
|
||||
return y;
|
||||
end;
|
||||
|
||||
function Plain.Create();
|
||||
begin
|
||||
end;
|
||||
|
||||
function UnitFunc(a: integer): integer;
|
||||
begin
|
||||
return a;
|
||||
end;
|
||||
|
||||
function TestCompletions(obj: Widget; aliased: MainUnit.Widget; unit_inst: WorkspaceUnit.WorkspaceUnitClass);
|
||||
begin
|
||||
obj := new Widget;
|
||||
aliased := createobject("MainUnit.Widget");
|
||||
unit_inst := createobject("WorkspaceUnit.WorkspaceUnitClass");
|
||||
class(Widget).Sta;
|
||||
new Wid;
|
||||
new unit(MainUnit).Wid;
|
||||
new MainUnit.Wid;
|
||||
createobject(Wid);
|
||||
createobject("Wid");
|
||||
createobject(MainUnit.Wid);
|
||||
unit(MainUnit).Uni;
|
||||
MainUnit.Uni;
|
||||
obj.Fo;
|
||||
aliased.Fo;
|
||||
unit_inst.Wor;
|
||||
fun;
|
||||
UnitF;
|
||||
end;
|
||||
|
||||
procedure TestDefinitions();
|
||||
begin
|
||||
UnitFunc(1);
|
||||
WorkspaceFunc();
|
||||
SystemUnit;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
var target: integer;
|
||||
target := target + 1;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
unit SystemUnit;
|
||||
interface
|
||||
|
||||
type
|
||||
SystemClass = class
|
||||
public
|
||||
function Create(x: integer);
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
function SystemClass.Create(x: integer);
|
||||
begin
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
unit WorkspaceUnit;
|
||||
interface
|
||||
|
||||
type
|
||||
WorkspaceUnitClass = class
|
||||
public
|
||||
function Create();
|
||||
function Work(): integer;
|
||||
end;
|
||||
|
||||
function WorkspaceUnitFunc(): integer;
|
||||
|
||||
implementation
|
||||
|
||||
function WorkspaceUnitClass.Create();
|
||||
begin
|
||||
end;
|
||||
|
||||
function WorkspaceUnitClass.Work(): integer;
|
||||
begin
|
||||
return 9;
|
||||
end;
|
||||
|
||||
function WorkspaceUnitFunc(): integer;
|
||||
begin
|
||||
return 7;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
type WorkspaceClass = class
|
||||
public
|
||||
function Create(a: integer);
|
||||
function Work(): integer;
|
||||
end;
|
||||
|
||||
function WorkspaceFunc(): integer;
|
||||
|
||||
function WorkspaceClass.Create(a: integer);
|
||||
begin
|
||||
end;
|
||||
|
||||
function WorkspaceClass.Work(): integer;
|
||||
begin
|
||||
return 0;
|
||||
end;
|
||||
|
||||
function WorkspaceFunc(): integer;
|
||||
begin
|
||||
return 100;
|
||||
end;
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.json_flow;
|
||||
|
||||
import std;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.provider.initialize.initialize;
|
||||
import lsp.provider.text_document.did_open;
|
||||
import lsp.provider.text_document.completion;
|
||||
import lsp.provider.completion_item.resolve;
|
||||
import lsp.provider.text_document.definition;
|
||||
import lsp.core.dispacther;
|
||||
import lsp.manager.manager_hub;
|
||||
import lsp.scheduler.async_executor;
|
||||
import lsp.protocol;
|
||||
import lsp.codec.facade;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
class JsonFlowTests
|
||||
{
|
||||
public:
|
||||
static void Register(TestRunner& runner);
|
||||
|
||||
private:
|
||||
static TestResult TestInitializeCompletionResolveDefinition();
|
||||
};
|
||||
}
|
||||
|
||||
namespace lsp::test::provider
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct ProviderEnv
|
||||
{
|
||||
scheduler::AsyncExecutor scheduler{ 1 };
|
||||
manager::ManagerHub hub{};
|
||||
core::ExecutionContext context;
|
||||
|
||||
ProviderEnv()
|
||||
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
|
||||
{
|
||||
hub.Initialize();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
std::string SerializeOrThrow(const T& obj)
|
||||
{
|
||||
auto json = codec::Serialize(obj);
|
||||
assertTrue(json.has_value(), "Failed to serialize LSP JSON");
|
||||
return json.value();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T DeserializeOrThrow(const std::string& json)
|
||||
{
|
||||
auto parsed = codec::Deserialize<T>(json);
|
||||
assertTrue(parsed.has_value(), "Failed to deserialize LSP JSON");
|
||||
return parsed.value();
|
||||
}
|
||||
|
||||
protocol::ResponseMessage ParseResponse(const std::string& json)
|
||||
{
|
||||
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
|
||||
assertTrue(parsed.has_value(), "Failed to deserialize response");
|
||||
return parsed.value();
|
||||
}
|
||||
|
||||
protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker)
|
||||
{
|
||||
auto pos = content.find(marker);
|
||||
assertTrue(pos != std::string::npos, "Marker not found in fixture");
|
||||
protocol::Position result{};
|
||||
result.line = 0;
|
||||
result.character = 0;
|
||||
for (std::size_t i = 0; i < pos; ++i)
|
||||
{
|
||||
if (content[i] == '\n')
|
||||
{
|
||||
result.line++;
|
||||
result.character = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.character++;
|
||||
}
|
||||
}
|
||||
if (after_marker)
|
||||
{
|
||||
result.character += static_cast<std::uint32_t>(marker.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<protocol::CompletionItem> FindItem(const std::vector<protocol::CompletionItem>& items,
|
||||
const std::string& label)
|
||||
{
|
||||
auto it = std::find_if(items.begin(), items.end(), [&](const auto& item) {
|
||||
return item.label == label;
|
||||
});
|
||||
if (it == items.end())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
|
||||
void JsonFlowTests::Register(TestRunner& runner)
|
||||
{
|
||||
runner.addTest("json flow initialize->completion->resolve->definition", TestInitializeCompletionResolveDefinition);
|
||||
}
|
||||
|
||||
TestResult JsonFlowTests::TestInitializeCompletionResolveDefinition()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto workspace_uri = ToUri(FixturePath("workspace"));
|
||||
protocol::LSPArray folders;
|
||||
folders.emplace_back(protocol::LSPObject{
|
||||
{ "uri", workspace_uri },
|
||||
{ "name", "workspace" }
|
||||
});
|
||||
|
||||
protocol::LSPObject init_params;
|
||||
init_params["trace"] = protocol::string(protocol::TraceValueLiterals::Off);
|
||||
init_params["workspaceFolders"] = folders;
|
||||
|
||||
protocol::RequestMessage init_request;
|
||||
init_request.id = "init";
|
||||
init_request.method = "initialize";
|
||||
init_request.params = protocol::LSPAny(init_params);
|
||||
|
||||
auto init_json = SerializeOrThrow(init_request);
|
||||
auto parsed_init = DeserializeOrThrow<protocol::RequestMessage>(init_json);
|
||||
::lsp::provider::Initialize init_provider;
|
||||
auto init_response_json = init_provider.ProvideResponse(parsed_init, env.context);
|
||||
auto init_response = ParseResponse(init_response_json);
|
||||
assertTrue(init_response.result.has_value(), "Initialize should return result");
|
||||
env.scheduler.WaitAll();
|
||||
|
||||
auto path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
|
||||
protocol::LSPObject text_doc{
|
||||
{ "uri", uri },
|
||||
{ "languageId", "tsl" },
|
||||
{ "version", 1 },
|
||||
{ "text", content }
|
||||
};
|
||||
protocol::NotificationMessage open_message;
|
||||
open_message.method = "textDocument/didOpen";
|
||||
open_message.params = protocol::LSPAny(protocol::LSPObject{ { "textDocument", protocol::LSPAny(text_doc) } });
|
||||
|
||||
auto open_json = SerializeOrThrow(open_message);
|
||||
auto parsed_open = DeserializeOrThrow<protocol::NotificationMessage>(open_json);
|
||||
::lsp::provider::text_document::DidOpen open_provider;
|
||||
open_provider.HandleNotification(parsed_open, env.context);
|
||||
|
||||
auto completion_pos = FindPosition(content, "new Wid", true);
|
||||
protocol::LSPObject completion_params{
|
||||
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
|
||||
{ "position", protocol::LSPObject{
|
||||
{ "line", completion_pos.line },
|
||||
{ "character", completion_pos.character } } }
|
||||
};
|
||||
protocol::RequestMessage completion_request;
|
||||
completion_request.id = "c1";
|
||||
completion_request.method = "textDocument/completion";
|
||||
completion_request.params = protocol::LSPAny(completion_params);
|
||||
|
||||
auto completion_json = SerializeOrThrow(completion_request);
|
||||
auto parsed_completion = DeserializeOrThrow<protocol::RequestMessage>(completion_json);
|
||||
::lsp::provider::text_document::Completion completion_provider;
|
||||
auto completion_response_json = completion_provider.ProvideResponse(parsed_completion, env.context);
|
||||
auto completion_response = ParseResponse(completion_response_json);
|
||||
auto list = codec::FromLSPAny.template operator()<protocol::CompletionList>(completion_response.result.value());
|
||||
auto item = FindItem(list.items, "Widget");
|
||||
assertTrue(item.has_value(), "Expected Widget completion");
|
||||
|
||||
protocol::RequestMessage resolve_request;
|
||||
resolve_request.id = "r1";
|
||||
resolve_request.method = "completionItem/resolve";
|
||||
resolve_request.params = codec::ToLSPAny(*item);
|
||||
|
||||
auto resolve_json = SerializeOrThrow(resolve_request);
|
||||
auto parsed_resolve = DeserializeOrThrow<protocol::RequestMessage>(resolve_json);
|
||||
::lsp::provider::completion_item::Resolve resolve_provider;
|
||||
auto resolve_response_json = resolve_provider.ProvideResponse(parsed_resolve, env.context);
|
||||
auto resolve_response = ParseResponse(resolve_response_json);
|
||||
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(resolve_response.result.value());
|
||||
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
|
||||
|
||||
auto def_pos = FindPosition(content, "UnitFunc(1);", false);
|
||||
protocol::LSPObject definition_params{
|
||||
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
|
||||
{ "position", protocol::LSPObject{
|
||||
{ "line", def_pos.line },
|
||||
{ "character", def_pos.character } } }
|
||||
};
|
||||
protocol::RequestMessage def_request;
|
||||
def_request.id = "d1";
|
||||
def_request.method = "textDocument/definition";
|
||||
def_request.params = protocol::LSPAny(definition_params);
|
||||
|
||||
auto def_json = SerializeOrThrow(def_request);
|
||||
auto parsed_def = DeserializeOrThrow<protocol::RequestMessage>(def_json);
|
||||
::lsp::provider::text_document::Definition def_provider;
|
||||
auto def_response_json = def_provider.ProvideResponse(parsed_def, env.context);
|
||||
auto def_response = ParseResponse(def_response_json);
|
||||
auto location = codec::FromLSPAny.template operator()<protocol::Location>(def_response.result.value());
|
||||
|
||||
auto expected_pos = FindPosition(content, "function UnitFunc", false);
|
||||
assertTrue(location.range.start.line == expected_pos.line, "Definition should resolve in document");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import lsp.test.provider.main;
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
return Run(argc, argv);
|
||||
}
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.misc;
|
||||
|
||||
import std;
|
||||
|
||||
import spdlog;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.provider.initialize.initialize;
|
||||
import lsp.provider.initialized.initialized;
|
||||
import lsp.provider.text_document.did_open;
|
||||
import lsp.provider.text_document.did_change;
|
||||
import lsp.provider.text_document.did_close;
|
||||
import lsp.provider.text_document.rename;
|
||||
import lsp.provider.text_document.references;
|
||||
import lsp.provider.text_document.semantic_tokens;
|
||||
import lsp.provider.workspace.symbol;
|
||||
import lsp.provider.client.register_capability;
|
||||
import lsp.provider.client.unregister_capability;
|
||||
import lsp.provider.shutdown.shutdown;
|
||||
import lsp.provider.cancel_request.cancel_request;
|
||||
import lsp.provider.trace.set_trace;
|
||||
import lsp.provider.exit.exit;
|
||||
import lsp.core.dispacther;
|
||||
import lsp.manager.manager_hub;
|
||||
import lsp.manager.symbol;
|
||||
import lsp.scheduler.async_executor;
|
||||
import lsp.protocol;
|
||||
import lsp.codec.facade;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
class ProviderMiscTests
|
||||
{
|
||||
public:
|
||||
static void Register(TestRunner& runner);
|
||||
|
||||
private:
|
||||
static TestResult TestInitializeProvider();
|
||||
static TestResult TestInitializedNotification();
|
||||
static TestResult TestDidOpenDidChangeDidClose();
|
||||
static TestResult TestRenameProvider();
|
||||
static TestResult TestRenameInvalidName();
|
||||
static TestResult TestReferencesProvider();
|
||||
static TestResult TestWorkspaceSymbolProvider();
|
||||
static TestResult TestSemanticTokensProvider();
|
||||
static TestResult TestRegisterCapabilityProvider();
|
||||
static TestResult TestUnregisterCapabilityProvider();
|
||||
static TestResult TestShutdownProvider();
|
||||
static TestResult TestCancelRequestProvider();
|
||||
static TestResult TestSetTraceProvider();
|
||||
static TestResult TestExitProvider();
|
||||
};
|
||||
|
||||
int RunExitProviderChild();
|
||||
}
|
||||
|
||||
namespace lsp::test::provider
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct ProviderEnv
|
||||
{
|
||||
std::vector<core::ServerLifecycleEvent> events;
|
||||
scheduler::AsyncExecutor scheduler{ 1 };
|
||||
manager::ManagerHub hub{};
|
||||
core::ExecutionContext context;
|
||||
|
||||
ProviderEnv()
|
||||
: context([this](core::ServerLifecycleEvent event) { events.push_back(event); }, scheduler, hub)
|
||||
{
|
||||
hub.Initialize();
|
||||
}
|
||||
};
|
||||
|
||||
protocol::ResponseMessage ParseResponse(const std::string& json)
|
||||
{
|
||||
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
|
||||
if (!parsed)
|
||||
{
|
||||
throw std::runtime_error("Failed to deserialize response");
|
||||
}
|
||||
return *parsed;
|
||||
}
|
||||
|
||||
protocol::Position FindPosition(const std::string& content, const std::string& marker)
|
||||
{
|
||||
auto pos = content.find(marker);
|
||||
assertTrue(pos != std::string::npos, "Marker not found in fixture");
|
||||
protocol::Position result{};
|
||||
result.line = 0;
|
||||
result.character = 0;
|
||||
for (std::size_t i = 0; i < pos; ++i)
|
||||
{
|
||||
if (content[i] == '\n')
|
||||
{
|
||||
result.line++;
|
||||
result.character = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.character++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
|
||||
{
|
||||
protocol::DidOpenTextDocumentParams open_params;
|
||||
open_params.textDocument.uri = uri;
|
||||
open_params.textDocument.languageId = "tsl";
|
||||
open_params.textDocument.version = version;
|
||||
open_params.textDocument.text = text;
|
||||
hub.documents().OpenDocument(open_params);
|
||||
}
|
||||
}
|
||||
|
||||
void ProviderMiscTests::Register(TestRunner& runner)
|
||||
{
|
||||
runner.addTest("initialize provider", TestInitializeProvider);
|
||||
runner.addTest("initialized notification", TestInitializedNotification);
|
||||
runner.addTest("didOpen/didChange/didClose", TestDidOpenDidChangeDidClose);
|
||||
runner.addTest("rename provider", TestRenameProvider);
|
||||
runner.addTest("rename invalid name", TestRenameInvalidName);
|
||||
runner.addTest("references provider", TestReferencesProvider);
|
||||
runner.addTest("workspace symbol provider", TestWorkspaceSymbolProvider);
|
||||
runner.addTest("semantic tokens provider", TestSemanticTokensProvider);
|
||||
runner.addTest("register capability provider", TestRegisterCapabilityProvider);
|
||||
runner.addTest("unregister capability provider", TestUnregisterCapabilityProvider);
|
||||
runner.addTest("shutdown provider", TestShutdownProvider);
|
||||
runner.addTest("cancel request provider", TestCancelRequestProvider);
|
||||
runner.addTest("setTrace provider", TestSetTraceProvider);
|
||||
runner.addTest("exit provider", TestExitProvider);
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestInitializeProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
protocol::InitializeParams params;
|
||||
params.trace = protocol::TraceValueLiterals::Off;
|
||||
params.workspaceFolders = std::vector<protocol::WorkspaceFolder>{
|
||||
{ .uri = ToUri(FixturePath("workspace")), .name = "workspace" }
|
||||
};
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "init";
|
||||
request.method = "initialize";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::Initialize provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.result.has_value(), "Initialize should return result");
|
||||
assertTrue(response.result.value().Is<protocol::LSPObject>(), "Initialize result should be an object");
|
||||
const auto& result_obj = response.result.value().Get<protocol::LSPObject>();
|
||||
auto server_info_it = result_obj.find("serverInfo");
|
||||
assertTrue(server_info_it != result_obj.end(), "Initialize should return serverInfo");
|
||||
assertTrue(server_info_it->second.Is<protocol::LSPObject>(), "serverInfo should be an object");
|
||||
const auto& server_info = server_info_it->second.Get<protocol::LSPObject>();
|
||||
auto name_it = server_info.find("name");
|
||||
assertTrue(name_it != server_info.end(), "serverInfo should include name");
|
||||
assertTrue(name_it->second.Is<protocol::string>(), "serverInfo.name should be string");
|
||||
assertTrue(!name_it->second.Get<protocol::string>().empty(), "Initialize should set server name");
|
||||
|
||||
auto capabilities_it = result_obj.find("capabilities");
|
||||
assertTrue(capabilities_it != result_obj.end(), "Initialize should return capabilities");
|
||||
assertTrue(capabilities_it->second.Is<protocol::LSPObject>(), "capabilities should be an object");
|
||||
const auto& capabilities = capabilities_it->second.Get<protocol::LSPObject>();
|
||||
assertTrue(capabilities.find("textDocumentSync") != capabilities.end(),
|
||||
"Initialize should set textDocumentSync");
|
||||
auto completion_it = capabilities.find("completionProvider");
|
||||
assertTrue(completion_it != capabilities.end(), "Initialize should set completionProvider");
|
||||
if (completion_it != capabilities.end() && completion_it->second.Is<protocol::LSPObject>())
|
||||
{
|
||||
const auto& completion = completion_it->second.Get<protocol::LSPObject>();
|
||||
auto resolve_it = completion.find("resolveProvider");
|
||||
assertTrue(resolve_it != completion.end(), "Initialize should include resolveProvider");
|
||||
assertTrue(resolve_it->second.Is<protocol::boolean>() && resolve_it->second.Get<protocol::boolean>(),
|
||||
"Initialize should enable completion resolve");
|
||||
}
|
||||
|
||||
env.scheduler.WaitAll();
|
||||
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Module);
|
||||
bool found_workspace = std::any_of(indexed.begin(), indexed.end(), [](const manager::Symbol::IndexedSymbol& item) {
|
||||
return item.name == "WorkspaceUnit";
|
||||
});
|
||||
assertTrue(found_workspace, "Workspace symbols should be indexed");
|
||||
|
||||
assertTrue(!env.events.empty(), "Initialize should emit lifecycle event");
|
||||
assertTrue(env.events.back() == core::ServerLifecycleEvent::kInitialized, "Initialize should emit initialized");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestInitializedNotification()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
::lsp::provider::Initialized provider;
|
||||
protocol::NotificationMessage notification;
|
||||
notification.method = "initialized";
|
||||
provider.HandleNotification(notification, env.context);
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestDidOpenDidChangeDidClose()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto path = FixturePath("rename_case.tsl");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
|
||||
protocol::DidOpenTextDocumentParams open_params;
|
||||
open_params.textDocument.uri = uri;
|
||||
open_params.textDocument.languageId = "tsl";
|
||||
open_params.textDocument.version = 1;
|
||||
open_params.textDocument.text = content;
|
||||
|
||||
protocol::NotificationMessage open_msg;
|
||||
open_msg.method = "textDocument/didOpen";
|
||||
open_msg.params = codec::ToLSPAny(open_params);
|
||||
|
||||
::lsp::provider::text_document::DidOpen open_provider;
|
||||
open_provider.HandleNotification(open_msg, env.context);
|
||||
auto stored = env.hub.documents().GetContent(uri);
|
||||
assertTrue(stored.has_value(), "Document should be opened");
|
||||
|
||||
protocol::DidChangeTextDocumentParams change_params;
|
||||
change_params.textDocument.uri = uri;
|
||||
change_params.textDocument.version = 2;
|
||||
protocol::TextDocumentContentChangeEvent change;
|
||||
change.range.start.line = 0;
|
||||
change.range.start.character = 0;
|
||||
change.range.end.line = 100;
|
||||
change.range.end.character = 0;
|
||||
change.text = "var replaced: integer;";
|
||||
change_params.contentChanges.push_back(change);
|
||||
|
||||
protocol::NotificationMessage change_msg;
|
||||
change_msg.method = "textDocument/didChange";
|
||||
change_msg.params = codec::ToLSPAny(change_params);
|
||||
|
||||
::lsp::provider::text_document::DidChange change_provider;
|
||||
change_provider.HandleNotification(change_msg, env.context);
|
||||
auto updated = env.hub.documents().GetContent(uri);
|
||||
assertTrue(updated.has_value() && updated.value() == change.text, "Document should be updated");
|
||||
|
||||
protocol::DidCloseTextDocumentParams close_params;
|
||||
close_params.textDocument.uri = uri;
|
||||
|
||||
protocol::NotificationMessage close_msg;
|
||||
close_msg.method = "textDocument/didClose";
|
||||
close_msg.params = codec::ToLSPAny(close_params);
|
||||
|
||||
::lsp::provider::text_document::DidClose close_provider;
|
||||
close_provider.HandleNotification(close_msg, env.context);
|
||||
auto closed = env.hub.documents().GetContent(uri);
|
||||
assertFalse(closed.has_value(), "Document should be closed");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestRenameProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto path = FixturePath("rename_case.tsl");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::RenameParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, "target := target");
|
||||
params.newName = "renamed";
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "rename";
|
||||
request.method = "textDocument/rename";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::Rename provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.result.has_value(), "Rename should return workspace edit");
|
||||
auto edit = codec::FromLSPAny.template operator()<protocol::WorkspaceEdit>(response.result.value());
|
||||
auto it = edit.changes.find(uri);
|
||||
assertTrue(it != edit.changes.end(), "Rename should include edits for document");
|
||||
assertEqual(std::size_t(3), it->second.size(), "Rename should edit all occurrences");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestRenameInvalidName()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto path = FixturePath("rename_case.tsl");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::RenameParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, "target := target");
|
||||
params.newName = "1bad";
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "rename_invalid";
|
||||
request.method = "textDocument/rename";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::Rename provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.error.has_value(), "Invalid rename should return error");
|
||||
assertEqual(static_cast<int>(protocol::ErrorCodes::InvalidParams),
|
||||
static_cast<int>(response.error->code),
|
||||
"Invalid rename should return invalid params");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestReferencesProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto path = FixturePath("rename_case.tsl");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::ReferenceParams params;
|
||||
params.textDocument.uri = uri;
|
||||
params.position = FindPosition(content, "target := target");
|
||||
params.context.includeDeclaration = true;
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "refs";
|
||||
request.method = "textDocument/references";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::References provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
auto locations = codec::FromLSPAny.template operator()<std::vector<protocol::Location>>(response.result.value());
|
||||
assertEqual(std::size_t(0), locations.size(), "References provider currently returns empty list");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestWorkspaceSymbolProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
protocol::LSPObject params;
|
||||
params["query"] = "Widget";
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "ws_symbol";
|
||||
request.method = "workspace/symbol";
|
||||
request.params = protocol::LSPAny(params);
|
||||
|
||||
::lsp::provider::workspace::Symbol provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.error.has_value(), "Workspace symbol should return error");
|
||||
assertEqual(static_cast<int>(protocol::ErrorCodes::MethodNotFound),
|
||||
static_cast<int>(response.error->code),
|
||||
"Workspace symbol should return MethodNotFound");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestSemanticTokensProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
protocol::LSPObject params;
|
||||
params["textDocument"] = protocol::LSPObject{
|
||||
{ "uri", ToUri(FixturePath("rename_case.tsl")) }
|
||||
};
|
||||
params["range"] = protocol::LSPObject{
|
||||
{ "start", protocol::LSPObject{ { "line", 0 }, { "character", 0 } } },
|
||||
{ "end", protocol::LSPObject{ { "line", 0 }, { "character", 1 } } }
|
||||
};
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "sem";
|
||||
request.method = "textDocument/semanticTokens/range";
|
||||
request.params = protocol::LSPAny(params);
|
||||
|
||||
::lsp::provider::text_document::SemanticTokensRange provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.error.has_value(), "Semantic tokens range should return error");
|
||||
assertEqual(static_cast<int>(protocol::ErrorCodes::MethodNotFound),
|
||||
static_cast<int>(response.error->code),
|
||||
"Semantic tokens range should return MethodNotFound");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestRegisterCapabilityProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
protocol::RegistrationParams params;
|
||||
protocol::RequestMessage request;
|
||||
request.id = "reg";
|
||||
request.method = "client/registerCapability";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::client::RegisterCapability provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.result == std::nullopt, "Register capability should return null");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestUnregisterCapabilityProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
protocol::UnregistrationParams params;
|
||||
protocol::RequestMessage request;
|
||||
request.id = "unreg";
|
||||
request.method = "client/unregisterCapability";
|
||||
request.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::client::UnregisterCapability provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(response.result == std::nullopt, "Unregister capability should return null");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestShutdownProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto path = FixturePath("rename_case.tsl");
|
||||
auto content = ReadTextFile(path);
|
||||
auto uri = ToUri(path);
|
||||
OpenDocument(env.hub, uri, content, 1);
|
||||
|
||||
protocol::RequestMessage request;
|
||||
request.id = "shutdown";
|
||||
request.method = "shutdown";
|
||||
|
||||
::lsp::provider::Shutdown provider;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
auto response = ParseResponse(json);
|
||||
assertTrue(!response.error.has_value(), "Shutdown should not return error");
|
||||
assertTrue(env.events.size() >= 1, "Shutdown should emit lifecycle event");
|
||||
assertTrue(env.events.back() == core::ServerLifecycleEvent::kShuttingDown, "Shutdown should emit shutting down");
|
||||
assertFalse(env.hub.documents().GetContent(uri).has_value(), "Shutdown should clear documents");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestCancelRequestProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
std::atomic<bool> started{ false };
|
||||
env.scheduler.Submit("cancel_me", [&started]() -> std::optional<std::string> {
|
||||
started.store(true);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
return std::string("done");
|
||||
});
|
||||
|
||||
while (!started.load())
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
protocol::CancelParams params;
|
||||
params.id = std::string("cancel_me");
|
||||
protocol::NotificationMessage notification;
|
||||
notification.method = "$/cancelRequest";
|
||||
notification.params = codec::ToLSPAny(params);
|
||||
|
||||
::lsp::provider::CancelRequest provider;
|
||||
provider.HandleNotification(notification, env.context);
|
||||
env.scheduler.WaitAll();
|
||||
|
||||
auto stats = env.scheduler.GetStatistics();
|
||||
assertEqual(std::size_t(1), static_cast<std::size_t>(stats.cancelled),
|
||||
"CancelRequest should mark task cancelled");
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestSetTraceProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
ProviderEnv env;
|
||||
|
||||
auto prev = spdlog::get_level();
|
||||
|
||||
protocol::NotificationMessage notification;
|
||||
notification.method = "$/setTrace";
|
||||
|
||||
protocol::SetTraceParams params;
|
||||
params.value = protocol::TraceValueLiterals::Messages;
|
||||
notification.params = codec::ToLSPAny(params);
|
||||
::lsp::provider::SetTrace provider;
|
||||
provider.HandleNotification(notification, env.context);
|
||||
assertTrue(spdlog::get_level() == spdlog::level::debug, "SetTrace messages should set debug level");
|
||||
|
||||
params.value = protocol::TraceValueLiterals::Verbose;
|
||||
notification.params = codec::ToLSPAny(params);
|
||||
provider.HandleNotification(notification, env.context);
|
||||
assertTrue(spdlog::get_level() == spdlog::level::trace, "SetTrace verbose should set trace level");
|
||||
|
||||
params.value = protocol::TraceValueLiterals::Off;
|
||||
notification.params = codec::ToLSPAny(params);
|
||||
provider.HandleNotification(notification, env.context);
|
||||
assertTrue(spdlog::get_level() == spdlog::level::info, "SetTrace off should set info level");
|
||||
|
||||
spdlog::set_level(prev);
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderMiscTests::TestExitProvider()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
auto exe = ExecutablePath();
|
||||
assertTrue(!exe.empty(), "ExecutablePath should be set");
|
||||
std::string command = "\"" + exe + "\" --exit-provider";
|
||||
int code = std::system(command.c_str());
|
||||
assertEqual(0, code, "Exit should return code 0");
|
||||
return result;
|
||||
}
|
||||
|
||||
int RunExitProviderChild()
|
||||
{
|
||||
ProviderEnv env;
|
||||
::lsp::provider::Exit provider;
|
||||
protocol::NotificationMessage notification;
|
||||
notification.method = "exit";
|
||||
provider.HandleNotification(notification, env.context);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.surface;
|
||||
|
||||
import std;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.codec.facade;
|
||||
import lsp.protocol;
|
||||
import lsp.core.dispacther;
|
||||
import lsp.manager.manager_hub;
|
||||
import lsp.scheduler.async_executor;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
import lsp.provider.cancel_request.cancel_request;
|
||||
import lsp.provider.client.register_capability;
|
||||
import lsp.provider.client.unregister_capability;
|
||||
import lsp.provider.code_action.resolve;
|
||||
import lsp.provider.code_lens.resolve;
|
||||
import lsp.provider.completion_item.resolve;
|
||||
import lsp.provider.document_link.resolve;
|
||||
import lsp.provider.exit.exit;
|
||||
import lsp.provider.initialize.initialize;
|
||||
import lsp.provider.initialized.initialized;
|
||||
import lsp.provider.inlay_hint.resolve;
|
||||
import lsp.provider.shutdown.shutdown;
|
||||
import lsp.provider.telemetry.event;
|
||||
import lsp.provider.trace.set_trace;
|
||||
import lsp.provider.call_hierarchy.incoming_calls;
|
||||
import lsp.provider.call_hierarchy.outgoing_calls;
|
||||
import lsp.provider.type_hierarchy.supertypes;
|
||||
import lsp.provider.type_hierarchy.subtypes;
|
||||
import lsp.provider.window.log_message;
|
||||
import lsp.provider.window.show_document;
|
||||
import lsp.provider.window.show_message;
|
||||
import lsp.provider.window.show_message_request;
|
||||
import lsp.provider.window.work_done_progress_create;
|
||||
import lsp.provider.workspace.apply_edit;
|
||||
import lsp.provider.workspace.code_lens_refresh;
|
||||
import lsp.provider.workspace.configuration;
|
||||
import lsp.provider.workspace.diagnostic;
|
||||
import lsp.provider.workspace.diagnostic_refresh;
|
||||
import lsp.provider.workspace.did_change_configuration;
|
||||
import lsp.provider.workspace.did_change_watched_files;
|
||||
import lsp.provider.workspace.did_change_workspace_folders;
|
||||
import lsp.provider.workspace.did_create_files;
|
||||
import lsp.provider.workspace.did_delete_files;
|
||||
import lsp.provider.workspace.did_rename_files;
|
||||
import lsp.provider.workspace.execute_command;
|
||||
import lsp.provider.workspace.inlay_hint_refresh;
|
||||
import lsp.provider.workspace.inline_value_refresh;
|
||||
import lsp.provider.workspace.semantic_tokens_refresh;
|
||||
import lsp.provider.workspace.symbol;
|
||||
import lsp.provider.workspace.will_create_files;
|
||||
import lsp.provider.workspace.will_delete_files;
|
||||
import lsp.provider.workspace.will_rename_files;
|
||||
import lsp.provider.workspace.workspace_folders;
|
||||
import lsp.provider.workspace_symbol.resolve;
|
||||
import lsp.provider.text_document.code_action;
|
||||
import lsp.provider.text_document.code_lens;
|
||||
import lsp.provider.text_document.color_presentation;
|
||||
import lsp.provider.text_document.completion;
|
||||
import lsp.provider.text_document.definition;
|
||||
import lsp.provider.text_document.diagnostic;
|
||||
import lsp.provider.text_document.did_change;
|
||||
import lsp.provider.text_document.did_close;
|
||||
import lsp.provider.text_document.did_open;
|
||||
import lsp.provider.text_document.document_color;
|
||||
import lsp.provider.text_document.document_highlight;
|
||||
import lsp.provider.text_document.document_link;
|
||||
import lsp.provider.text_document.document_symbol;
|
||||
import lsp.provider.text_document.folding_range;
|
||||
import lsp.provider.text_document.formatting;
|
||||
import lsp.provider.text_document.hover;
|
||||
import lsp.provider.text_document.implementation;
|
||||
import lsp.provider.text_document.inlay_hint;
|
||||
import lsp.provider.text_document.inline_value;
|
||||
import lsp.provider.text_document.linked_editing_range;
|
||||
import lsp.provider.text_document.moniker;
|
||||
import lsp.provider.text_document.on_type_formatting;
|
||||
import lsp.provider.text_document.prepare_call_hierarchy;
|
||||
import lsp.provider.text_document.prepare_rename;
|
||||
import lsp.provider.text_document.prepare_type_hierarchy;
|
||||
import lsp.provider.text_document.publish_diagnostics;
|
||||
import lsp.provider.text_document.range_formatting;
|
||||
import lsp.provider.text_document.references;
|
||||
import lsp.provider.text_document.rename;
|
||||
import lsp.provider.text_document.selection_range;
|
||||
import lsp.provider.text_document.semantic_tokens;
|
||||
import lsp.provider.text_document.signature_help;
|
||||
import lsp.provider.text_document.type_definition;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
class ProviderSurfaceTests
|
||||
{
|
||||
public:
|
||||
static void Register(TestRunner& runner);
|
||||
|
||||
private:
|
||||
static TestResult TestProviderMetadata();
|
||||
static TestResult TestRequestProviderResponses();
|
||||
static TestResult TestNotificationProviderHandlers();
|
||||
};
|
||||
}
|
||||
|
||||
namespace lsp::test::provider
|
||||
{
|
||||
namespace
|
||||
{
|
||||
namespace codec = lsp::codec;
|
||||
namespace provider = lsp::provider;
|
||||
|
||||
struct ProviderEnv
|
||||
{
|
||||
scheduler::AsyncExecutor scheduler{ 1 };
|
||||
manager::ManagerHub hub{};
|
||||
core::ExecutionContext context;
|
||||
|
||||
ProviderEnv()
|
||||
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
|
||||
{
|
||||
hub.Initialize();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Provider>
|
||||
void CheckProviderMetadata(const std::string& expected_method,
|
||||
const std::string& expected_name)
|
||||
{
|
||||
Provider provider;
|
||||
assertEqual(expected_method, provider.GetMethod(),
|
||||
"GetMethod mismatch for " + expected_name);
|
||||
assertEqual(expected_name, provider.GetProviderName(),
|
||||
"GetProviderName mismatch for " + expected_name);
|
||||
}
|
||||
|
||||
template<typename Provider>
|
||||
void CheckRequestResponse()
|
||||
{
|
||||
ProviderEnv env;
|
||||
Provider provider;
|
||||
protocol::RequestMessage request;
|
||||
request.id = "probe";
|
||||
request.method = provider.GetMethod();
|
||||
request.params = std::nullopt;
|
||||
auto json = provider.ProvideResponse(request, env.context);
|
||||
assertTrue(!json.empty(), "ProvideResponse should return JSON for " + provider.GetProviderName());
|
||||
auto parsed = codec::Deserialize<protocol::LSPAny>(json);
|
||||
assertTrue(parsed.has_value(), "ProvideResponse should return valid JSON for " + provider.GetProviderName());
|
||||
}
|
||||
|
||||
template<typename Provider>
|
||||
void CheckNotificationHandler(const std::optional<protocol::LSPAny>& params)
|
||||
{
|
||||
ProviderEnv env;
|
||||
Provider provider;
|
||||
protocol::NotificationMessage notification;
|
||||
notification.method = provider.GetMethod();
|
||||
notification.params = params;
|
||||
provider.HandleNotification(notification, env.context);
|
||||
}
|
||||
}
|
||||
|
||||
void ProviderSurfaceTests::Register(TestRunner& runner)
|
||||
{
|
||||
runner.addTest("provider metadata", TestProviderMetadata);
|
||||
runner.addTest("provider request responses", TestRequestProviderResponses);
|
||||
runner.addTest("provider notification handlers", TestNotificationProviderHandlers);
|
||||
}
|
||||
|
||||
TestResult ProviderSurfaceTests::TestProviderMetadata()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
CheckProviderMetadata<provider::Initialize>("initialize", "Initialize");
|
||||
CheckProviderMetadata<provider::Shutdown>("shutdown", "Shutdown");
|
||||
CheckProviderMetadata<provider::completion_item::Resolve>("completionItem/resolve", "CompletionItemResolve");
|
||||
CheckProviderMetadata<provider::text_document::Completion>("textDocument/completion", "TextDocumentCompletion");
|
||||
CheckProviderMetadata<provider::text_document::Definition>("textDocument/definition", "TextDocumentDefinition");
|
||||
CheckProviderMetadata<provider::text_document::Rename>("textDocument/rename", "TextDocumentRename");
|
||||
CheckProviderMetadata<provider::text_document::References>("textDocument/references", "TextDocumentReferences");
|
||||
CheckProviderMetadata<provider::text_document::SemanticTokensRange>("textDocument/semanticTokens/range",
|
||||
"TextDocumentSemanticTokensRange");
|
||||
CheckProviderMetadata<provider::text_document::SemanticTokensFull>("textDocument/semanticTokens/full",
|
||||
"SemanticTokensFull");
|
||||
CheckProviderMetadata<provider::text_document::SemanticTokensFullDelta>("textDocument/semanticTokens/full/delta",
|
||||
"SemanticTokensFullDelta");
|
||||
CheckProviderMetadata<provider::text_document::Hover>("textDocument/hover", "TextDocumentHover");
|
||||
CheckProviderMetadata<provider::text_document::Formatting>("textDocument/formatting", "TextDocumentFormatting");
|
||||
CheckProviderMetadata<provider::text_document::RangeFormatting>("textDocument/rangeFormatting",
|
||||
"TextDocumentRangeFormatting");
|
||||
CheckProviderMetadata<provider::text_document::OnTypeFormatting>("textDocument/onTypeFormatting",
|
||||
"TextDocumentOnTypeFormatting");
|
||||
CheckProviderMetadata<provider::text_document::DocumentSymbol>("textDocument/documentSymbol",
|
||||
"TextDocumentDocumentSymbol");
|
||||
CheckProviderMetadata<provider::text_document::DocumentLink>("textDocument/documentLink",
|
||||
"TextDocumentDocumentLink");
|
||||
CheckProviderMetadata<provider::text_document::DocumentHighlight>("textDocument/documentHighlight",
|
||||
"TextDocumentDocumentHighlight");
|
||||
CheckProviderMetadata<provider::text_document::DocumentColor>("textDocument/documentColor",
|
||||
"TextDocumentDocumentColor");
|
||||
CheckProviderMetadata<provider::text_document::ColorPresentation>("textDocument/colorPresentation",
|
||||
"TextDocumentColorPresentation");
|
||||
CheckProviderMetadata<provider::text_document::CodeLens>("textDocument/codeLens", "TextDocumentCodeLens");
|
||||
CheckProviderMetadata<provider::text_document::CodeAction>("textDocument/codeAction", "TextDocumentCodeAction");
|
||||
CheckProviderMetadata<provider::text_document::PrepareTypeHierarchy>("textDocument/prepareTypeHierarchy",
|
||||
"TextDocumentPrepareTypeHierarchy");
|
||||
CheckProviderMetadata<provider::text_document::PrepareRename>("textDocument/prepareRename",
|
||||
"TextDocumentPrepareRename");
|
||||
CheckProviderMetadata<provider::text_document::PrepareCallHierarchy>("textDocument/prepareCallHierarchy",
|
||||
"TextDocumentPrepareCallHierarchy");
|
||||
CheckProviderMetadata<provider::text_document::TypeDefinition>("textDocument/typeDefinition",
|
||||
"TextDocumentTypeDefinition");
|
||||
CheckProviderMetadata<provider::text_document::Implementation>("textDocument/implementation",
|
||||
"TextDocumentImplementation");
|
||||
CheckProviderMetadata<provider::text_document::SelectionRange>("textDocument/selectionRange",
|
||||
"TextDocumentSelectionRange");
|
||||
CheckProviderMetadata<provider::text_document::SignatureHelp>("textDocument/signatureHelp",
|
||||
"TextDocumentSignatureHelp");
|
||||
CheckProviderMetadata<provider::text_document::InlayHint>("textDocument/inlayHint", "TextDocumentInlayHint");
|
||||
CheckProviderMetadata<provider::text_document::InlineValue>("textDocument/inlineValue",
|
||||
"TextDocumentInlineValue");
|
||||
CheckProviderMetadata<provider::text_document::LinkedEditingRange>("textDocument/linkedEditingRange",
|
||||
"TextDocumentLinkedEditingRange");
|
||||
CheckProviderMetadata<provider::text_document::Moniker>("textDocument/moniker", "TextDocumentMoniker");
|
||||
CheckProviderMetadata<provider::text_document::Diagnostic>("textDocument/diagnostic",
|
||||
"TextDocumentDiagnostic");
|
||||
CheckProviderMetadata<provider::text_document::FoldingRange>("textDocument/foldingRange",
|
||||
"TextDocumentFoldingRange");
|
||||
CheckProviderMetadata<provider::code_action::Resolve>("codeAction/resolve", "CodeActionResolve");
|
||||
CheckProviderMetadata<provider::code_lens::Resolve>("codeLens/resolve", "CodeLensResolve");
|
||||
CheckProviderMetadata<provider::document_link::Resolve>("documentLink/resolve", "DocumentLinkResolve");
|
||||
CheckProviderMetadata<provider::inlay_hint::Resolve>("inlayHint/resolve", "InlayHintResolve");
|
||||
CheckProviderMetadata<provider::workspace_symbol::Resolve>("workspaceSymbol/resolve", "WorkspaceSymbResolve");
|
||||
CheckProviderMetadata<provider::type_hierarchy::Supertypes>("typeHierarchy/supertypes", "TypeHierarchySupertypes");
|
||||
CheckProviderMetadata<provider::type_hierarchy::Subtypes>("typeHierarchy/subtypes", "WorkspaceSubtypes");
|
||||
CheckProviderMetadata<provider::call_hierarchy::IncomingCalls>("callHierarchy/incomingCalls",
|
||||
"CallHierarchyIncomingCalls");
|
||||
CheckProviderMetadata<provider::call_hierarchy::OutgoingCalls>("callHierarchy/outgoingCalls",
|
||||
"CallHierarchyOutgoingCalls");
|
||||
CheckProviderMetadata<provider::workspace::ApplyEdit>("workspace/applyEdit", "WorkspaceApplyEdit");
|
||||
CheckProviderMetadata<provider::workspace::Configuration>("workspace/configuration", "WorkspaceConfiguration");
|
||||
CheckProviderMetadata<provider::workspace::Diagnostic>("workspace/diagnostic", "WorkspaceDiagnostic");
|
||||
CheckProviderMetadata<provider::workspace::DiagnosticRefresh>("workspace/diagnostic/refresh",
|
||||
"WorkspaceDiagnosticRefresh");
|
||||
CheckProviderMetadata<provider::workspace::ExecuteCommand>("workspace/executeCommand",
|
||||
"WorkspaceExecuteCommand");
|
||||
CheckProviderMetadata<provider::workspace::WorkspaceFolders>("workspace/workspaceFolders",
|
||||
"WorkspaceWorkspaceFolders");
|
||||
CheckProviderMetadata<provider::workspace::WillCreateFiles>("workspace/willCreateFiles", "WorkspaceWillCreateFiles");
|
||||
CheckProviderMetadata<provider::workspace::WillDeleteFiles>("workspace/willDeleteFiles", "WorkspaceWillDeleteFiles");
|
||||
CheckProviderMetadata<provider::workspace::WillRenameFiles>("workspace/willRenameFiles", "WorkspaceWillRenameFiles");
|
||||
CheckProviderMetadata<provider::workspace::Symbol>("workspace/symbol", "WorkSpaceSymbol");
|
||||
CheckProviderMetadata<provider::workspace::SemanticTokensRefresh>("workspace/semanticTokens/refresh",
|
||||
"WorkspaceSemanticTokensRefresh");
|
||||
CheckProviderMetadata<provider::workspace::InlineValueRefresh>("workspace/inlineValue/refresh",
|
||||
"WorkspaceInlineValueRefresh");
|
||||
CheckProviderMetadata<provider::workspace::InlayHintRefresh>("workspace/inlayHint/refresh",
|
||||
"WorkspaceInlayHintRefresh");
|
||||
CheckProviderMetadata<provider::workspace::CodeLensRefresh>("workspace/codeLens/refresh",
|
||||
"WorkspaceCodeLensRefresh");
|
||||
CheckProviderMetadata<provider::workspace::DidChangeConfiguration>("workspace/didChangeConfiguration",
|
||||
"WorkspaceDidChangeConfiguration");
|
||||
CheckProviderMetadata<provider::workspace::DidChangeWatchedFiles>("workspace/didChangeWatchedFiles",
|
||||
"WorkspaceDidChangeWatchedFiles");
|
||||
CheckProviderMetadata<provider::workspace::DidChangeWorkspaceFolders>("workspace/didChangeWorkspaceFolders",
|
||||
"WorkspaceDidChangeWorkspaceFolders");
|
||||
CheckProviderMetadata<provider::workspace::DidCreateFiles>("workspace/didCreateFiles", "WorkspaceDidCreateFiles");
|
||||
CheckProviderMetadata<provider::workspace::DidDeleteFiles>("workspace/didDeleteFiles", "WorkspaceDidDeleteFiles");
|
||||
CheckProviderMetadata<provider::workspace::DidRenameFiles>("workspace/didRenameFiles", "WorkspaceDidRenameFiles");
|
||||
CheckProviderMetadata<provider::window::ShowMessageRequest>("window/showMessageRequest",
|
||||
"WindowShowMessageRequest");
|
||||
CheckProviderMetadata<provider::window::ShowDocument>("window/showDocument", "WindowShowDocument");
|
||||
CheckProviderMetadata<provider::window::WorkDoneProgressCreate>("window/workDoneProgress/create",
|
||||
"WindowWorkDoneProgressCreate");
|
||||
CheckProviderMetadata<provider::client::RegisterCapability>("client/registerCapability",
|
||||
"ClientRegisterCapability");
|
||||
CheckProviderMetadata<provider::client::UnregisterCapability>("client/unregisterCapability",
|
||||
"ClientUnregisterCapability");
|
||||
CheckProviderMetadata<provider::Initialized>("initialized", "Initialized");
|
||||
CheckProviderMetadata<provider::Exit>("exit", "Exit");
|
||||
CheckProviderMetadata<provider::CancelRequest>("$/cancelRequest", "CancelRequest");
|
||||
CheckProviderMetadata<provider::SetTrace>("$/setTrace", "SetTrace");
|
||||
CheckProviderMetadata<provider::text_document::DidOpen>("textDocument/didOpen", "TextDocumentDidOpen");
|
||||
CheckProviderMetadata<provider::text_document::DidChange>("textDocument/didChange", "TextDocumentDidChange");
|
||||
CheckProviderMetadata<provider::text_document::DidClose>("textDocument/didClose", "TextDocumentDidClose");
|
||||
CheckProviderMetadata<provider::text_document::PublishDiagnostics>("textDocument/publishDiagnostics",
|
||||
"TextDocumentPublishDiagnostics");
|
||||
CheckProviderMetadata<provider::window::ShowMessage>("window/showMessage", "WindowShowMessage");
|
||||
CheckProviderMetadata<provider::window::LogMessage>("window/logMessage", "WindowLogMessage");
|
||||
CheckProviderMetadata<provider::telemetry::Event>("telemetry/event", "TelemetryEvent");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderSurfaceTests::TestRequestProviderResponses()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
CheckRequestResponse<provider::Shutdown>();
|
||||
CheckRequestResponse<provider::completion_item::Resolve>();
|
||||
CheckRequestResponse<provider::text_document::Completion>();
|
||||
CheckRequestResponse<provider::text_document::Definition>();
|
||||
CheckRequestResponse<provider::text_document::Rename>();
|
||||
CheckRequestResponse<provider::text_document::References>();
|
||||
CheckRequestResponse<provider::text_document::SemanticTokensRange>();
|
||||
CheckRequestResponse<provider::text_document::SemanticTokensFull>();
|
||||
CheckRequestResponse<provider::text_document::SemanticTokensFullDelta>();
|
||||
CheckRequestResponse<provider::text_document::Hover>();
|
||||
CheckRequestResponse<provider::text_document::Formatting>();
|
||||
CheckRequestResponse<provider::text_document::RangeFormatting>();
|
||||
CheckRequestResponse<provider::text_document::OnTypeFormatting>();
|
||||
CheckRequestResponse<provider::text_document::DocumentSymbol>();
|
||||
CheckRequestResponse<provider::text_document::DocumentLink>();
|
||||
CheckRequestResponse<provider::text_document::DocumentHighlight>();
|
||||
CheckRequestResponse<provider::text_document::DocumentColor>();
|
||||
CheckRequestResponse<provider::text_document::ColorPresentation>();
|
||||
CheckRequestResponse<provider::text_document::CodeLens>();
|
||||
CheckRequestResponse<provider::text_document::CodeAction>();
|
||||
CheckRequestResponse<provider::text_document::PrepareTypeHierarchy>();
|
||||
CheckRequestResponse<provider::text_document::PrepareRename>();
|
||||
CheckRequestResponse<provider::text_document::PrepareCallHierarchy>();
|
||||
CheckRequestResponse<provider::text_document::TypeDefinition>();
|
||||
CheckRequestResponse<provider::text_document::Implementation>();
|
||||
CheckRequestResponse<provider::text_document::SelectionRange>();
|
||||
CheckRequestResponse<provider::text_document::SignatureHelp>();
|
||||
CheckRequestResponse<provider::text_document::InlayHint>();
|
||||
CheckRequestResponse<provider::text_document::InlineValue>();
|
||||
CheckRequestResponse<provider::text_document::LinkedEditingRange>();
|
||||
CheckRequestResponse<provider::text_document::Moniker>();
|
||||
CheckRequestResponse<provider::text_document::Diagnostic>();
|
||||
CheckRequestResponse<provider::text_document::FoldingRange>();
|
||||
CheckRequestResponse<provider::code_action::Resolve>();
|
||||
CheckRequestResponse<provider::code_lens::Resolve>();
|
||||
CheckRequestResponse<provider::document_link::Resolve>();
|
||||
CheckRequestResponse<provider::inlay_hint::Resolve>();
|
||||
CheckRequestResponse<provider::workspace_symbol::Resolve>();
|
||||
CheckRequestResponse<provider::type_hierarchy::Supertypes>();
|
||||
CheckRequestResponse<provider::type_hierarchy::Subtypes>();
|
||||
CheckRequestResponse<provider::call_hierarchy::IncomingCalls>();
|
||||
CheckRequestResponse<provider::call_hierarchy::OutgoingCalls>();
|
||||
CheckRequestResponse<provider::workspace::ApplyEdit>();
|
||||
CheckRequestResponse<provider::workspace::Configuration>();
|
||||
CheckRequestResponse<provider::workspace::Diagnostic>();
|
||||
CheckRequestResponse<provider::workspace::DiagnosticRefresh>();
|
||||
CheckRequestResponse<provider::workspace::ExecuteCommand>();
|
||||
CheckRequestResponse<provider::workspace::WorkspaceFolders>();
|
||||
CheckRequestResponse<provider::workspace::WillCreateFiles>();
|
||||
CheckRequestResponse<provider::workspace::WillDeleteFiles>();
|
||||
CheckRequestResponse<provider::workspace::WillRenameFiles>();
|
||||
CheckRequestResponse<provider::workspace::Symbol>();
|
||||
CheckRequestResponse<provider::workspace::SemanticTokensRefresh>();
|
||||
CheckRequestResponse<provider::workspace::InlineValueRefresh>();
|
||||
CheckRequestResponse<provider::workspace::InlayHintRefresh>();
|
||||
CheckRequestResponse<provider::workspace::CodeLensRefresh>();
|
||||
CheckRequestResponse<provider::window::ShowMessageRequest>();
|
||||
CheckRequestResponse<provider::window::ShowDocument>();
|
||||
CheckRequestResponse<provider::window::WorkDoneProgressCreate>();
|
||||
CheckRequestResponse<provider::client::RegisterCapability>();
|
||||
CheckRequestResponse<provider::client::UnregisterCapability>();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TestResult ProviderSurfaceTests::TestNotificationProviderHandlers()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
CheckNotificationHandler<provider::Initialized>(std::nullopt);
|
||||
CheckNotificationHandler<provider::Exit>(std::nullopt);
|
||||
CheckNotificationHandler<provider::text_document::PublishDiagnostics>(std::nullopt);
|
||||
CheckNotificationHandler<provider::window::ShowMessage>(std::nullopt);
|
||||
CheckNotificationHandler<provider::window::LogMessage>(std::nullopt);
|
||||
CheckNotificationHandler<provider::telemetry::Event>(std::nullopt);
|
||||
CheckNotificationHandler<provider::workspace::DidChangeConfiguration>(std::nullopt);
|
||||
CheckNotificationHandler<provider::workspace::DidChangeWatchedFiles>(std::nullopt);
|
||||
CheckNotificationHandler<provider::workspace::DidChangeWorkspaceFolders>(std::nullopt);
|
||||
CheckNotificationHandler<provider::workspace::DidCreateFiles>(std::nullopt);
|
||||
CheckNotificationHandler<provider::workspace::DidDeleteFiles>(std::nullopt);
|
||||
CheckNotificationHandler<provider::workspace::DidRenameFiles>(std::nullopt);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.server_json;
|
||||
|
||||
import std;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.codec.facade;
|
||||
import lsp.protocol;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
export namespace lsp::test::provider
|
||||
{
|
||||
class ServerJsonTests
|
||||
{
|
||||
public:
|
||||
static void Register(TestRunner& runner);
|
||||
|
||||
private:
|
||||
static TestResult TestServerJsonFlow();
|
||||
};
|
||||
}
|
||||
|
||||
namespace lsp::test::provider
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string SerializeOrThrow(const auto& obj)
|
||||
{
|
||||
auto json = codec::Serialize(obj);
|
||||
assertTrue(json.has_value(), "Failed to serialize LSP JSON");
|
||||
return json.value();
|
||||
}
|
||||
|
||||
protocol::ResponseMessage DeserializeResponseOrThrow(const std::string& json)
|
||||
{
|
||||
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
|
||||
assertTrue(parsed.has_value(), "Failed to deserialize response JSON");
|
||||
return parsed.value();
|
||||
}
|
||||
|
||||
protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker)
|
||||
{
|
||||
auto pos = content.find(marker);
|
||||
assertTrue(pos != std::string::npos, "Marker not found in fixture");
|
||||
protocol::Position result{};
|
||||
result.line = 0;
|
||||
result.character = 0;
|
||||
for (std::size_t i = 0; i < pos; ++i)
|
||||
{
|
||||
if (content[i] == '\n')
|
||||
{
|
||||
result.line++;
|
||||
result.character = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.character++;
|
||||
}
|
||||
}
|
||||
if (after_marker)
|
||||
{
|
||||
result.character += static_cast<std::uint32_t>(marker.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AppendLspMessage(std::string& out, const std::string& body)
|
||||
{
|
||||
out += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";
|
||||
out += body;
|
||||
}
|
||||
|
||||
std::size_t ParseContentLength(const std::string& header)
|
||||
{
|
||||
std::istringstream stream(header);
|
||||
std::string line;
|
||||
while (std::getline(stream, line))
|
||||
{
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
{
|
||||
line.pop_back();
|
||||
}
|
||||
if (line.rfind("Content-Length:", 0) == 0)
|
||||
{
|
||||
auto value = line.substr(std::strlen("Content-Length:"));
|
||||
std::size_t start = value.find_first_not_of(' ');
|
||||
if (start != std::string::npos)
|
||||
{
|
||||
value = value.substr(start);
|
||||
}
|
||||
return static_cast<std::size_t>(std::stoul(value));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<protocol::ResponseMessage> ParseResponses(const std::string& data)
|
||||
{
|
||||
std::vector<protocol::ResponseMessage> responses;
|
||||
std::size_t pos = 0;
|
||||
while (pos < data.size())
|
||||
{
|
||||
auto header_end = data.find("\r\n\r\n", pos);
|
||||
if (header_end == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
auto header = data.substr(pos, header_end - pos);
|
||||
auto length = ParseContentLength(header);
|
||||
if (length == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
auto body_start = header_end + 4;
|
||||
if (body_start + length > data.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
auto body = data.substr(body_start, length);
|
||||
responses.push_back(DeserializeResponseOrThrow(body));
|
||||
pos = body_start + length;
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
protocol::CompletionItem BuildResolveItem(const std::string& uri)
|
||||
{
|
||||
protocol::CompletionItem item;
|
||||
item.label = "Widget";
|
||||
protocol::LSPObject data;
|
||||
data["ctx"] = "new";
|
||||
data["class"] = "Widget";
|
||||
data["unit"] = "MainUnit";
|
||||
data["uri"] = uri;
|
||||
item.data = std::move(data);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
void ServerJsonTests::Register(TestRunner& runner)
|
||||
{
|
||||
runner.addTest("server json flow", TestServerJsonFlow);
|
||||
}
|
||||
|
||||
TestResult ServerJsonTests::TestServerJsonFlow()
|
||||
{
|
||||
TestResult result{ "", true, "ok" };
|
||||
|
||||
#ifdef _WIN32
|
||||
return result;
|
||||
#endif
|
||||
|
||||
auto exe_path = ExecutablePath();
|
||||
assertTrue(!exe_path.empty(), "ExecutablePath should be set");
|
||||
|
||||
std::filesystem::path server_path = std::filesystem::path(exe_path).parent_path() / "../../src/tsl-server";
|
||||
server_path = server_path.lexically_normal();
|
||||
assertTrue(std::filesystem::exists(server_path), "tsl-server binary not found");
|
||||
|
||||
auto source_path = FixturePath("main_unit.tsf");
|
||||
auto content = ReadTextFile(source_path);
|
||||
auto uri = ToUri(source_path);
|
||||
|
||||
auto workspace_uri = ToUri(FixturePath("workspace"));
|
||||
|
||||
protocol::LSPArray workspace_folders;
|
||||
workspace_folders.emplace_back(protocol::LSPObject{
|
||||
{ "uri", workspace_uri },
|
||||
{ "name", "workspace" }
|
||||
});
|
||||
|
||||
protocol::LSPObject init_params;
|
||||
init_params["trace"] = protocol::string(protocol::TraceValueLiterals::Off);
|
||||
init_params["workspaceFolders"] = workspace_folders;
|
||||
|
||||
protocol::RequestMessage init_request;
|
||||
init_request.id = "1";
|
||||
init_request.method = "initialize";
|
||||
init_request.params = protocol::LSPAny(init_params);
|
||||
|
||||
protocol::NotificationMessage initialized;
|
||||
initialized.method = "initialized";
|
||||
|
||||
protocol::NotificationMessage did_open;
|
||||
did_open.method = "textDocument/didOpen";
|
||||
did_open.params = protocol::LSPAny(protocol::LSPObject{
|
||||
{ "textDocument", protocol::LSPObject{
|
||||
{ "uri", uri },
|
||||
{ "languageId", "tsl" },
|
||||
{ "version", 1 },
|
||||
{ "text", content } } }
|
||||
});
|
||||
|
||||
auto completion_pos = FindPosition(content, "new Wid", true);
|
||||
protocol::RequestMessage completion_request;
|
||||
completion_request.id = "2";
|
||||
completion_request.method = "textDocument/completion";
|
||||
completion_request.params = protocol::LSPAny(protocol::LSPObject{
|
||||
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
|
||||
{ "position", protocol::LSPObject{
|
||||
{ "line", completion_pos.line },
|
||||
{ "character", completion_pos.character } } }
|
||||
});
|
||||
|
||||
protocol::RequestMessage resolve_request;
|
||||
resolve_request.id = "3";
|
||||
resolve_request.method = "completionItem/resolve";
|
||||
resolve_request.params = codec::ToLSPAny(BuildResolveItem(uri));
|
||||
|
||||
auto def_pos = FindPosition(content, "UnitFunc(1);", false);
|
||||
protocol::RequestMessage definition_request;
|
||||
definition_request.id = "4";
|
||||
definition_request.method = "textDocument/definition";
|
||||
definition_request.params = protocol::LSPAny(protocol::LSPObject{
|
||||
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
|
||||
{ "position", protocol::LSPObject{
|
||||
{ "line", def_pos.line },
|
||||
{ "character", def_pos.character } } }
|
||||
});
|
||||
|
||||
protocol::RequestMessage shutdown_request;
|
||||
shutdown_request.id = "5";
|
||||
shutdown_request.method = "shutdown";
|
||||
|
||||
protocol::NotificationMessage exit_notification;
|
||||
exit_notification.method = "exit";
|
||||
|
||||
std::string input_payload;
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(init_request));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(initialized));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(did_open));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(completion_request));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(resolve_request));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(definition_request));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(shutdown_request));
|
||||
AppendLspMessage(input_payload, SerializeOrThrow(exit_notification));
|
||||
|
||||
auto temp_dir = std::filesystem::temp_directory_path();
|
||||
auto nonce = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
auto input_path = temp_dir / ("tsl_lsp_input_" + nonce + ".txt");
|
||||
auto output_path = temp_dir / ("tsl_lsp_output_" + nonce + ".txt");
|
||||
|
||||
{
|
||||
std::ofstream input_file(input_path, std::ios::binary);
|
||||
input_file << input_payload;
|
||||
}
|
||||
|
||||
std::string command = "\"" + server_path.string() + "\" --log=off --use-stdio < \"" +
|
||||
input_path.string() + "\" > \"" + output_path.string() + "\"";
|
||||
int exit_code = std::system(command.c_str());
|
||||
assertEqual(0, exit_code, "tsl-server should exit successfully");
|
||||
|
||||
std::string output;
|
||||
{
|
||||
std::ifstream output_file(output_path, std::ios::binary);
|
||||
output.assign(std::istreambuf_iterator<char>(output_file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::filesystem::remove(input_path);
|
||||
std::filesystem::remove(output_path);
|
||||
|
||||
auto responses = ParseResponses(output);
|
||||
std::unordered_map<std::string, protocol::ResponseMessage> by_id;
|
||||
for (const auto& response : responses)
|
||||
{
|
||||
if (response.id.has_value())
|
||||
{
|
||||
by_id[codec::debug::GetIdString(response.id.value())] = response;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(by_id.contains("1"), "Missing initialize response");
|
||||
assertTrue(by_id.contains("2"), "Missing completion response");
|
||||
assertTrue(by_id.contains("3"), "Missing resolve response");
|
||||
assertTrue(by_id.contains("4"), "Missing definition response");
|
||||
assertTrue(by_id.contains("5"), "Missing shutdown response");
|
||||
|
||||
auto completion = codec::FromLSPAny.template operator()<protocol::CompletionList>(by_id["2"].result.value());
|
||||
auto item_it = std::find_if(completion.items.begin(), completion.items.end(), [](const auto& item) {
|
||||
return item.label == "Widget";
|
||||
});
|
||||
assertTrue(item_it != completion.items.end(), "Completion response should include Widget");
|
||||
|
||||
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(by_id["3"].result.value());
|
||||
assertTrue(resolved.insertText.has_value(), "Resolve response should include insertText");
|
||||
|
||||
auto location = codec::FromLSPAny.template operator()<protocol::Location>(by_id["4"].result.value());
|
||||
auto expected = FindPosition(content, "function UnitFunc", false);
|
||||
assertTrue(location.range.start.line == expected.line, "Definition should resolve in document");
|
||||
|
||||
assertTrue(!by_id["5"].error.has_value(), "Shutdown response should not contain error");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
module;
|
||||
|
||||
export module lsp.test.provider.main;
|
||||
|
||||
import std;
|
||||
|
||||
import lsp.test.framework;
|
||||
import lsp.test.provider.completion;
|
||||
import lsp.test.provider.definitions;
|
||||
import lsp.test.provider.json_flow;
|
||||
import lsp.test.provider.misc;
|
||||
import lsp.test.provider.surface;
|
||||
import lsp.test.provider.fixtures;
|
||||
|
||||
export int Run(int argc, char** argv)
|
||||
{
|
||||
if (argc > 0 && argv && argv[0])
|
||||
{
|
||||
lsp::test::provider::SetExecutablePath(std::filesystem::absolute(argv[0]).string());
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (std::string_view(argv[i]) == "--exit-provider")
|
||||
{
|
||||
return lsp::test::provider::RunExitProviderChild();
|
||||
}
|
||||
}
|
||||
|
||||
lsp::test::TestRunner runner;
|
||||
|
||||
std::cout << "\n========================================" << std::endl;
|
||||
std::cout << " LSP Provider Test Suite" << std::endl;
|
||||
std::cout << "========================================\n"
|
||||
<< std::endl;
|
||||
|
||||
std::cout << "Registering tests..." << std::endl;
|
||||
std::cout << " - Completion tests" << std::endl;
|
||||
lsp::test::provider::CompletionTests::Register(runner);
|
||||
std::cout << " - Definition tests" << std::endl;
|
||||
lsp::test::provider::DefinitionTests::Register(runner);
|
||||
std::cout << " - JSON flow tests" << std::endl;
|
||||
lsp::test::provider::JsonFlowTests::Register(runner);
|
||||
std::cout << " - Other provider tests" << std::endl;
|
||||
lsp::test::provider::ProviderMiscTests::Register(runner);
|
||||
std::cout << " - Provider surface tests" << std::endl;
|
||||
lsp::test::provider::ProviderSurfaceTests::Register(runner);
|
||||
|
||||
runner.runAllTests();
|
||||
return runner.getFailedCount();
|
||||
}
|
||||
Loading…
Reference in New Issue