From bb0cbde79c8175eb534461c6ef4073cac66dd99b Mon Sep 17 00:00:00 2001 From: csh Date: Sun, 21 Dec 2025 15:39:33 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20test:=20add=20LSP=20json=20fixtu?= =?UTF-8?q?res=20and=20provider=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lsp-server/test/CMakeLists.txt | 4 + .../test/request_json/cancelRequest.json | 7 + lsp-server/test/request_json/completion.json | 10 +- .../test/request_json/completion_resolve.json | 14 + lsp-server/test/request_json/definition.json | 14 + lsp-server/test/request_json/didChange.json | 16 +- lsp-server/test/request_json/didClose.json | 9 + lsp-server/test/request_json/didOpen.json | 12 + lsp-server/test/request_json/didiOpen.json | 6 +- lsp-server/test/request_json/exit.json | 4 + lsp-server/test/request_json/initialize.json | 8 +- lsp-server/test/request_json/initialized.json | 5 + lsp-server/test/request_json/references.json | 17 + lsp-server/test/request_json/rename.json | 15 + lsp-server/test/request_json/sequence.txt | 14 + lsp-server/test/request_json/setTrace.json | 7 + lsp-server/test/request_json/shutdown.json | 5 + lsp-server/test/run_lsp_json_tests.py | 221 +++++++ lsp-server/test/test_provider/CMakeLists.txt | 212 +++++++ .../test/test_provider/completion_test.cppm | 424 ++++++++++++++ .../test/test_provider/definitions_test.cppm | 205 +++++++ lsp-server/test/test_provider/fixtures.cppm | 63 ++ .../test/test_provider/fixtures/main_unit.tsf | 78 +++ .../test_provider/fixtures/rename_case.tsl | 2 + .../fixtures/system/SystemUnit.tsf | 16 + .../fixtures/workspace/WorkspaceUnit.tsf | 29 + .../fixtures/workspace/workspace_script.tsl | 21 + .../test/test_provider/json_flow_test.cppm | 222 +++++++ lsp-server/test/test_provider/main.cc | 6 + .../test_provider/provider_misc_test.cppm | 554 ++++++++++++++++++ .../test_provider/provider_surface_test.cppm | 385 ++++++++++++ .../test/test_provider/server_json_test.cppm | 298 ++++++++++ lsp-server/test/test_provider/test_main.cppm | 51 ++ 33 files changed, 2934 insertions(+), 20 deletions(-) create mode 100644 lsp-server/test/request_json/cancelRequest.json create mode 100644 lsp-server/test/request_json/completion_resolve.json create mode 100644 lsp-server/test/request_json/definition.json create mode 100644 lsp-server/test/request_json/didClose.json create mode 100644 lsp-server/test/request_json/didOpen.json create mode 100644 lsp-server/test/request_json/exit.json create mode 100644 lsp-server/test/request_json/initialized.json create mode 100644 lsp-server/test/request_json/references.json create mode 100644 lsp-server/test/request_json/rename.json create mode 100644 lsp-server/test/request_json/sequence.txt create mode 100644 lsp-server/test/request_json/setTrace.json create mode 100644 lsp-server/test/request_json/shutdown.json create mode 100644 lsp-server/test/run_lsp_json_tests.py create mode 100644 lsp-server/test/test_provider/CMakeLists.txt create mode 100644 lsp-server/test/test_provider/completion_test.cppm create mode 100644 lsp-server/test/test_provider/definitions_test.cppm create mode 100644 lsp-server/test/test_provider/fixtures.cppm create mode 100644 lsp-server/test/test_provider/fixtures/main_unit.tsf create mode 100644 lsp-server/test/test_provider/fixtures/rename_case.tsl create mode 100644 lsp-server/test/test_provider/fixtures/system/SystemUnit.tsf create mode 100644 lsp-server/test/test_provider/fixtures/workspace/WorkspaceUnit.tsf create mode 100644 lsp-server/test/test_provider/fixtures/workspace/workspace_script.tsl create mode 100644 lsp-server/test/test_provider/json_flow_test.cppm create mode 100644 lsp-server/test/test_provider/main.cc create mode 100644 lsp-server/test/test_provider/provider_misc_test.cppm create mode 100644 lsp-server/test/test_provider/provider_surface_test.cppm create mode 100644 lsp-server/test/test_provider/server_json_test.cppm create mode 100644 lsp-server/test/test_provider/test_main.cppm diff --git a/lsp-server/test/CMakeLists.txt b/lsp-server/test/CMakeLists.txt index a0fa9ca..ade2597 100644 --- a/lsp-server/test/CMakeLists.txt +++ b/lsp-server/test/CMakeLists.txt @@ -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() diff --git a/lsp-server/test/request_json/cancelRequest.json b/lsp-server/test/request_json/cancelRequest.json new file mode 100644 index 0000000..fbe3cb8 --- /dev/null +++ b/lsp-server/test/request_json/cancelRequest.json @@ -0,0 +1,7 @@ +{ + "jsonrpc": "2.0", + "method": "$/cancelRequest", + "params": { + "id": "nonexistent" + } +} diff --git a/lsp-server/test/request_json/completion.json b/lsp-server/test/request_json/completion.json index f81d66c..aa61a69 100644 --- a/lsp-server/test/request_json/completion.json +++ b/lsp-server/test/request_json/completion.json @@ -1,17 +1,17 @@ { "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 } } -} \ No newline at end of file +} diff --git a/lsp-server/test/request_json/completion_resolve.json b/lsp-server/test/request_json/completion_resolve.json new file mode 100644 index 0000000..01e7ed4 --- /dev/null +++ b/lsp-server/test/request_json/completion_resolve.json @@ -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}}" + } + } +} diff --git a/lsp-server/test/request_json/definition.json b/lsp-server/test/request_json/definition.json new file mode 100644 index 0000000..01cbca2 --- /dev/null +++ b/lsp-server/test/request_json/definition.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": 4, + "method": "textDocument/definition", + "params": { + "textDocument": { + "uri": "{{MAIN_UNIT_URI}}" + }, + "position": { + "line": 72, + "character": 2 + } + } +} diff --git a/lsp-server/test/request_json/didChange.json b/lsp-server/test/request_json/didChange.json index c1dfcc9..f2163ee 100644 --- a/lsp-server/test/request_json/didChange.json +++ b/lsp-server/test/request_json/didChange.json @@ -3,24 +3,24 @@ "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": " " } ] } -} \ No newline at end of file +} diff --git a/lsp-server/test/request_json/didClose.json b/lsp-server/test/request_json/didClose.json new file mode 100644 index 0000000..2f67ff2 --- /dev/null +++ b/lsp-server/test/request_json/didClose.json @@ -0,0 +1,9 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "{{MAIN_UNIT_URI}}" + } + } +} diff --git a/lsp-server/test/request_json/didOpen.json b/lsp-server/test/request_json/didOpen.json new file mode 100644 index 0000000..53504c0 --- /dev/null +++ b/lsp-server/test/request_json/didOpen.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "{{MAIN_UNIT_URI}}", + "languageId": "tsl", + "version": 1, + "text": {{MAIN_UNIT_TEXT}} + } + } +} diff --git a/lsp-server/test/request_json/didiOpen.json b/lsp-server/test/request_json/didiOpen.json index 3c03adf..53504c0 100644 --- a/lsp-server/test/request_json/didiOpen.json +++ b/lsp-server/test/request_json/didiOpen.json @@ -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}} } } -} \ No newline at end of file +} diff --git a/lsp-server/test/request_json/exit.json b/lsp-server/test/request_json/exit.json new file mode 100644 index 0000000..f7a9e50 --- /dev/null +++ b/lsp-server/test/request_json/exit.json @@ -0,0 +1,4 @@ +{ + "jsonrpc": "2.0", + "method": "exit" +} diff --git a/lsp-server/test/request_json/initialize.json b/lsp-server/test/request_json/initialize.json index 459a639..2e22946 100644 --- a/lsp-server/test/request_json/initialize.json +++ b/lsp-server/test/request_json/initialize.json @@ -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" } ] diff --git a/lsp-server/test/request_json/initialized.json b/lsp-server/test/request_json/initialized.json new file mode 100644 index 0000000..a09692d --- /dev/null +++ b/lsp-server/test/request_json/initialized.json @@ -0,0 +1,5 @@ +{ + "jsonrpc": "2.0", + "method": "initialized", + "params": {} +} diff --git a/lsp-server/test/request_json/references.json b/lsp-server/test/request_json/references.json new file mode 100644 index 0000000..41b84fe --- /dev/null +++ b/lsp-server/test/request_json/references.json @@ -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 + } + } +} diff --git a/lsp-server/test/request_json/rename.json b/lsp-server/test/request_json/rename.json new file mode 100644 index 0000000..b5235bc --- /dev/null +++ b/lsp-server/test/request_json/rename.json @@ -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" + } +} diff --git a/lsp-server/test/request_json/sequence.txt b/lsp-server/test/request_json/sequence.txt new file mode 100644 index 0000000..c2664fb --- /dev/null +++ b/lsp-server/test/request_json/sequence.txt @@ -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 diff --git a/lsp-server/test/request_json/setTrace.json b/lsp-server/test/request_json/setTrace.json new file mode 100644 index 0000000..50d1dff --- /dev/null +++ b/lsp-server/test/request_json/setTrace.json @@ -0,0 +1,7 @@ +{ + "jsonrpc": "2.0", + "method": "$/setTrace", + "params": { + "value": "verbose" + } +} diff --git a/lsp-server/test/request_json/shutdown.json b/lsp-server/test/request_json/shutdown.json new file mode 100644 index 0000000..28aa46a --- /dev/null +++ b/lsp-server/test/request_json/shutdown.json @@ -0,0 +1,5 @@ +{ + "jsonrpc": "2.0", + "id": 7, + "method": "shutdown" +} diff --git a/lsp-server/test/run_lsp_json_tests.py b/lsp-server/test/run_lsp_json_tests.py new file mode 100644 index 0000000..9ed4b1c --- /dev/null +++ b/lsp-server/test/run_lsp_json_tests.py @@ -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()) diff --git a/lsp-server/test/test_provider/CMakeLists.txt b/lsp-server/test/test_provider/CMakeLists.txt new file mode 100644 index 0000000..7edb248 --- /dev/null +++ b/lsp-server/test/test_provider/CMakeLists.txt @@ -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 + $<$:Threads::Threads>) + +target_compile_options( + ${PROJECT_NAME} + PRIVATE -Wall -Wextra -Wpedantic $<$:-g -O0> + -Wno-import-implementation-partition-unit-in-interface-unit + $<$:-O3>) diff --git a/lsp-server/test/test_provider/completion_test.cppm b/lsp-server/test/test_provider/completion_test.cppm new file mode 100644 index 0000000..8b6ff73 --- /dev/null +++ b/lsp-server/test/test_provider/completion_test.cppm @@ -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(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(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 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()(response.result.value()); + return list.items; + } + + bool HasLabel(const std::vector& items, const std::string& label) + { + return std::any_of(items.begin(), items.end(), [&](const auto& item) { + return item.label == label; + }); + } + + std::optional FindItem(const std::vector& 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()(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()(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; + } +} diff --git a/lsp-server/test/test_provider/definitions_test.cppm b/lsp-server/test/test_provider/definitions_test.cppm new file mode 100644 index 0000000..87ba6ac --- /dev/null +++ b/lsp-server/test/test_provider/definitions_test.cppm @@ -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(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()(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; + } +} diff --git a/lsp-server/test/test_provider/fixtures.cppm b/lsp-server/test/test_provider/fixtures.cppm new file mode 100644 index 0000000..2e33df0 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures.cppm @@ -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(file)), std::istreambuf_iterator()); + } +} diff --git a/lsp-server/test/test_provider/fixtures/main_unit.tsf b/lsp-server/test/test_provider/fixtures/main_unit.tsf new file mode 100644 index 0000000..a0e4c0e --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/main_unit.tsf @@ -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. diff --git a/lsp-server/test/test_provider/fixtures/rename_case.tsl b/lsp-server/test/test_provider/fixtures/rename_case.tsl new file mode 100644 index 0000000..9f0992a --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/rename_case.tsl @@ -0,0 +1,2 @@ +var target: integer; +target := target + 1; diff --git a/lsp-server/test/test_provider/fixtures/system/SystemUnit.tsf b/lsp-server/test/test_provider/fixtures/system/SystemUnit.tsf new file mode 100644 index 0000000..43121a4 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/system/SystemUnit.tsf @@ -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. diff --git a/lsp-server/test/test_provider/fixtures/workspace/WorkspaceUnit.tsf b/lsp-server/test/test_provider/fixtures/workspace/WorkspaceUnit.tsf new file mode 100644 index 0000000..34f14e2 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/workspace/WorkspaceUnit.tsf @@ -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. diff --git a/lsp-server/test/test_provider/fixtures/workspace/workspace_script.tsl b/lsp-server/test/test_provider/fixtures/workspace/workspace_script.tsl new file mode 100644 index 0000000..215f9c8 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/workspace/workspace_script.tsl @@ -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; diff --git a/lsp-server/test/test_provider/json_flow_test.cppm b/lsp-server/test/test_provider/json_flow_test.cppm new file mode 100644 index 0000000..8567946 --- /dev/null +++ b/lsp-server/test/test_provider/json_flow_test.cppm @@ -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 + std::string SerializeOrThrow(const T& obj) + { + auto json = codec::Serialize(obj); + assertTrue(json.has_value(), "Failed to serialize LSP JSON"); + return json.value(); + } + + template + T DeserializeOrThrow(const std::string& json) + { + auto parsed = codec::Deserialize(json); + assertTrue(parsed.has_value(), "Failed to deserialize LSP JSON"); + return parsed.value(); + } + + protocol::ResponseMessage ParseResponse(const std::string& json) + { + auto parsed = codec::Deserialize(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(marker.size()); + } + return result; + } + + std::optional FindItem(const std::vector& 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(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(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(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()(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(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()(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(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()(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; + } +} diff --git a/lsp-server/test/test_provider/main.cc b/lsp-server/test/test_provider/main.cc new file mode 100644 index 0000000..f63d383 --- /dev/null +++ b/lsp-server/test/test_provider/main.cc @@ -0,0 +1,6 @@ +import lsp.test.provider.main; + +int main(int argc, char** argv) +{ + return Run(argc, argv); +} diff --git a/lsp-server/test/test_provider/provider_misc_test.cppm b/lsp-server/test/test_provider/provider_misc_test.cppm new file mode 100644 index 0000000..9a87d24 --- /dev/null +++ b/lsp-server/test/test_provider/provider_misc_test.cppm @@ -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 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(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{ + { .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(), "Initialize result should be an object"); + const auto& result_obj = response.result.value().Get(); + 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(), "serverInfo should be an object"); + const auto& server_info = server_info_it->second.Get(); + auto name_it = server_info.find("name"); + assertTrue(name_it != server_info.end(), "serverInfo should include name"); + assertTrue(name_it->second.Is(), "serverInfo.name should be string"); + assertTrue(!name_it->second.Get().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(), "capabilities should be an object"); + const auto& capabilities = capabilities_it->second.Get(); + 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()) + { + const auto& completion = completion_it->second.Get(); + auto resolve_it = completion.find("resolveProvider"); + assertTrue(resolve_it != completion.end(), "Initialize should include resolveProvider"); + assertTrue(resolve_it->second.Is() && resolve_it->second.Get(), + "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()(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(protocol::ErrorCodes::InvalidParams), + static_cast(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()>(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(protocol::ErrorCodes::MethodNotFound), + static_cast(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(protocol::ErrorCodes::MethodNotFound), + static_cast(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 started{ false }; + env.scheduler.Submit("cancel_me", [&started]() -> std::optional { + 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(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; + } +} diff --git a/lsp-server/test/test_provider/provider_surface_test.cppm b/lsp-server/test/test_provider/provider_surface_test.cppm new file mode 100644 index 0000000..87bd3c8 --- /dev/null +++ b/lsp-server/test/test_provider/provider_surface_test.cppm @@ -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 + 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 + 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(json); + assertTrue(parsed.has_value(), "ProvideResponse should return valid JSON for " + provider.GetProviderName()); + } + + template + void CheckNotificationHandler(const std::optional& 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("initialize", "Initialize"); + CheckProviderMetadata("shutdown", "Shutdown"); + CheckProviderMetadata("completionItem/resolve", "CompletionItemResolve"); + CheckProviderMetadata("textDocument/completion", "TextDocumentCompletion"); + CheckProviderMetadata("textDocument/definition", "TextDocumentDefinition"); + CheckProviderMetadata("textDocument/rename", "TextDocumentRename"); + CheckProviderMetadata("textDocument/references", "TextDocumentReferences"); + CheckProviderMetadata("textDocument/semanticTokens/range", + "TextDocumentSemanticTokensRange"); + CheckProviderMetadata("textDocument/semanticTokens/full", + "SemanticTokensFull"); + CheckProviderMetadata("textDocument/semanticTokens/full/delta", + "SemanticTokensFullDelta"); + CheckProviderMetadata("textDocument/hover", "TextDocumentHover"); + CheckProviderMetadata("textDocument/formatting", "TextDocumentFormatting"); + CheckProviderMetadata("textDocument/rangeFormatting", + "TextDocumentRangeFormatting"); + CheckProviderMetadata("textDocument/onTypeFormatting", + "TextDocumentOnTypeFormatting"); + CheckProviderMetadata("textDocument/documentSymbol", + "TextDocumentDocumentSymbol"); + CheckProviderMetadata("textDocument/documentLink", + "TextDocumentDocumentLink"); + CheckProviderMetadata("textDocument/documentHighlight", + "TextDocumentDocumentHighlight"); + CheckProviderMetadata("textDocument/documentColor", + "TextDocumentDocumentColor"); + CheckProviderMetadata("textDocument/colorPresentation", + "TextDocumentColorPresentation"); + CheckProviderMetadata("textDocument/codeLens", "TextDocumentCodeLens"); + CheckProviderMetadata("textDocument/codeAction", "TextDocumentCodeAction"); + CheckProviderMetadata("textDocument/prepareTypeHierarchy", + "TextDocumentPrepareTypeHierarchy"); + CheckProviderMetadata("textDocument/prepareRename", + "TextDocumentPrepareRename"); + CheckProviderMetadata("textDocument/prepareCallHierarchy", + "TextDocumentPrepareCallHierarchy"); + CheckProviderMetadata("textDocument/typeDefinition", + "TextDocumentTypeDefinition"); + CheckProviderMetadata("textDocument/implementation", + "TextDocumentImplementation"); + CheckProviderMetadata("textDocument/selectionRange", + "TextDocumentSelectionRange"); + CheckProviderMetadata("textDocument/signatureHelp", + "TextDocumentSignatureHelp"); + CheckProviderMetadata("textDocument/inlayHint", "TextDocumentInlayHint"); + CheckProviderMetadata("textDocument/inlineValue", + "TextDocumentInlineValue"); + CheckProviderMetadata("textDocument/linkedEditingRange", + "TextDocumentLinkedEditingRange"); + CheckProviderMetadata("textDocument/moniker", "TextDocumentMoniker"); + CheckProviderMetadata("textDocument/diagnostic", + "TextDocumentDiagnostic"); + CheckProviderMetadata("textDocument/foldingRange", + "TextDocumentFoldingRange"); + CheckProviderMetadata("codeAction/resolve", "CodeActionResolve"); + CheckProviderMetadata("codeLens/resolve", "CodeLensResolve"); + CheckProviderMetadata("documentLink/resolve", "DocumentLinkResolve"); + CheckProviderMetadata("inlayHint/resolve", "InlayHintResolve"); + CheckProviderMetadata("workspaceSymbol/resolve", "WorkspaceSymbResolve"); + CheckProviderMetadata("typeHierarchy/supertypes", "TypeHierarchySupertypes"); + CheckProviderMetadata("typeHierarchy/subtypes", "WorkspaceSubtypes"); + CheckProviderMetadata("callHierarchy/incomingCalls", + "CallHierarchyIncomingCalls"); + CheckProviderMetadata("callHierarchy/outgoingCalls", + "CallHierarchyOutgoingCalls"); + CheckProviderMetadata("workspace/applyEdit", "WorkspaceApplyEdit"); + CheckProviderMetadata("workspace/configuration", "WorkspaceConfiguration"); + CheckProviderMetadata("workspace/diagnostic", "WorkspaceDiagnostic"); + CheckProviderMetadata("workspace/diagnostic/refresh", + "WorkspaceDiagnosticRefresh"); + CheckProviderMetadata("workspace/executeCommand", + "WorkspaceExecuteCommand"); + CheckProviderMetadata("workspace/workspaceFolders", + "WorkspaceWorkspaceFolders"); + CheckProviderMetadata("workspace/willCreateFiles", "WorkspaceWillCreateFiles"); + CheckProviderMetadata("workspace/willDeleteFiles", "WorkspaceWillDeleteFiles"); + CheckProviderMetadata("workspace/willRenameFiles", "WorkspaceWillRenameFiles"); + CheckProviderMetadata("workspace/symbol", "WorkSpaceSymbol"); + CheckProviderMetadata("workspace/semanticTokens/refresh", + "WorkspaceSemanticTokensRefresh"); + CheckProviderMetadata("workspace/inlineValue/refresh", + "WorkspaceInlineValueRefresh"); + CheckProviderMetadata("workspace/inlayHint/refresh", + "WorkspaceInlayHintRefresh"); + CheckProviderMetadata("workspace/codeLens/refresh", + "WorkspaceCodeLensRefresh"); + CheckProviderMetadata("workspace/didChangeConfiguration", + "WorkspaceDidChangeConfiguration"); + CheckProviderMetadata("workspace/didChangeWatchedFiles", + "WorkspaceDidChangeWatchedFiles"); + CheckProviderMetadata("workspace/didChangeWorkspaceFolders", + "WorkspaceDidChangeWorkspaceFolders"); + CheckProviderMetadata("workspace/didCreateFiles", "WorkspaceDidCreateFiles"); + CheckProviderMetadata("workspace/didDeleteFiles", "WorkspaceDidDeleteFiles"); + CheckProviderMetadata("workspace/didRenameFiles", "WorkspaceDidRenameFiles"); + CheckProviderMetadata("window/showMessageRequest", + "WindowShowMessageRequest"); + CheckProviderMetadata("window/showDocument", "WindowShowDocument"); + CheckProviderMetadata("window/workDoneProgress/create", + "WindowWorkDoneProgressCreate"); + CheckProviderMetadata("client/registerCapability", + "ClientRegisterCapability"); + CheckProviderMetadata("client/unregisterCapability", + "ClientUnregisterCapability"); + CheckProviderMetadata("initialized", "Initialized"); + CheckProviderMetadata("exit", "Exit"); + CheckProviderMetadata("$/cancelRequest", "CancelRequest"); + CheckProviderMetadata("$/setTrace", "SetTrace"); + CheckProviderMetadata("textDocument/didOpen", "TextDocumentDidOpen"); + CheckProviderMetadata("textDocument/didChange", "TextDocumentDidChange"); + CheckProviderMetadata("textDocument/didClose", "TextDocumentDidClose"); + CheckProviderMetadata("textDocument/publishDiagnostics", + "TextDocumentPublishDiagnostics"); + CheckProviderMetadata("window/showMessage", "WindowShowMessage"); + CheckProviderMetadata("window/logMessage", "WindowLogMessage"); + CheckProviderMetadata("telemetry/event", "TelemetryEvent"); + + return result; + } + + TestResult ProviderSurfaceTests::TestRequestProviderResponses() + { + TestResult result{ "", true, "ok" }; + + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + + return result; + } + + TestResult ProviderSurfaceTests::TestNotificationProviderHandlers() + { + TestResult result{ "", true, "ok" }; + + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + + return result; + } +} diff --git a/lsp-server/test/test_provider/server_json_test.cppm b/lsp-server/test/test_provider/server_json_test.cppm new file mode 100644 index 0000000..4cf1875 --- /dev/null +++ b/lsp-server/test/test_provider/server_json_test.cppm @@ -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(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(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::stoul(value)); + } + } + return 0; + } + + std::vector ParseResponses(const std::string& data) + { + std::vector 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(output_file), std::istreambuf_iterator()); + } + + std::filesystem::remove(input_path); + std::filesystem::remove(output_path); + + auto responses = ParseResponses(output); + std::unordered_map 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()(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()(by_id["3"].result.value()); + assertTrue(resolved.insertText.has_value(), "Resolve response should include insertText"); + + auto location = codec::FromLSPAny.template operator()(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; + } +} diff --git a/lsp-server/test/test_provider/test_main.cppm b/lsp-server/test/test_provider/test_main.cppm new file mode 100644 index 0000000..7107cad --- /dev/null +++ b/lsp-server/test/test_provider/test_main.cppm @@ -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(); +}