🧪 test: add LSP json fixtures and provider suite

This commit is contained in:
csh 2025-12-21 15:39:33 +08:00
parent 53f4588c53
commit bb0cbde79c
33 changed files with 2934 additions and 20 deletions

View File

@ -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()

View File

@ -0,0 +1,7 @@
{
"jsonrpc": "2.0",
"method": "$/cancelRequest",
"params": {
"id": "nonexistent"
}
}

View File

@ -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
}
}
}
}

View File

@ -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}}"
}
}
}

View File

@ -0,0 +1,14 @@
{
"jsonrpc": "2.0",
"id": 4,
"method": "textDocument/definition",
"params": {
"textDocument": {
"uri": "{{MAIN_UNIT_URI}}"
},
"position": {
"line": 72,
"character": 2
}
}
}

View File

@ -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": " "
}
]
}
}
}

View File

@ -0,0 +1,9 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didClose",
"params": {
"textDocument": {
"uri": "{{MAIN_UNIT_URI}}"
}
}
}

View File

@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "{{MAIN_UNIT_URI}}",
"languageId": "tsl",
"version": 1,
"text": {{MAIN_UNIT_TEXT}}
}
}
}

View File

@ -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}}
}
}
}
}

View File

@ -0,0 +1,4 @@
{
"jsonrpc": "2.0",
"method": "exit"
}

View File

@ -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"
}
]

View File

@ -0,0 +1,5 @@
{
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
}

View File

@ -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
}
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -0,0 +1,7 @@
{
"jsonrpc": "2.0",
"method": "$/setTrace",
"params": {
"value": "verbose"
}
}

View File

@ -0,0 +1,5 @@
{
"jsonrpc": "2.0",
"id": 7,
"method": "shutdown"
}

View File

@ -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())

View File

@ -0,0 +1,212 @@
cmake_minimum_required(VERSION 4.0)
project(test_provider LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
find_package(glaze CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(fmt CONFIG REQUIRED)
find_package(Taskflow CONFIG REQUIRED)
find_package(tree-sitter CONFIG REQUIRED)
if(UNIX AND NOT APPLE)
find_package(Threads REQUIRED)
endif()
set(SOURCES
main.cc
test_main.cppm
../test_lsp_any/test_framework.cppm
fixtures.cppm
completion_test.cppm
json_flow_test.cppm
definitions_test.cppm
provider_misc_test.cppm
provider_surface_test.cppm
../../src/tree-sitter/parser.c
../../src/tree-sitter/scanner.c)
add_executable(${PROJECT_NAME} ${SOURCES})
if(TARGET std_module)
add_dependencies(${PROJECT_NAME} std_module)
endif()
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../../src)
target_sources(
${PROJECT_NAME}
PRIVATE
FILE_SET cxx_modules TYPE CXX_MODULES
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../test_lsp_any
${CMAKE_CURRENT_SOURCE_DIR}/../../src
FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cppm
${CMAKE_CURRENT_SOURCE_DIR}/../test_lsp_any/test_framework.cppm
${CMAKE_CURRENT_SOURCE_DIR}/fixtures.cppm
${CMAKE_CURRENT_SOURCE_DIR}/completion_test.cppm
${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm
${CMAKE_CURRENT_SOURCE_DIR}/definitions_test.cppm
${CMAKE_CURRENT_SOURCE_DIR}/provider_misc_test.cppm
${CMAKE_CURRENT_SOURCE_DIR}/provider_surface_test.cppm
../../src/bridge/glaze.cppm
../../src/bridge/spdlog.cppm
../../src/bridge/taskflow.cppm
../../src/bridge/tree_sitter.cppm
../../src/utils/string.cppm
../../src/utils/text_coordinates.cppm
../../src/core/dispacther.cppm
../../src/scheduler/async_executor.cppm
../../src/manager/event_bus.cppm
../../src/manager/events.cppm
../../src/manager/detail/text_document.cppm
../../src/manager/document.cppm
../../src/manager/parser.cppm
../../src/manager/symbol.cppm
../../src/manager/manager_hub.cppm
../../src/language/ast/ast.cppm
../../src/language/ast/types.cppm
../../src/language/ast/deserializer.cppm
../../src/language/ast/deserializer_impl.cppm
../../src/language/ast/ts_utils.cppm
../../src/language/ast/detail.cppm
../../src/language/symbol/types.cppm
../../src/language/symbol/internal/builder.cppm
../../src/language/symbol/internal/store.cppm
../../src/language/symbol/internal/table.cppm
../../src/language/symbol/index/coordinator.cppm
../../src/language/symbol/index/location.cppm
../../src/language/symbol/index/scope.cppm
../../src/language/symbol/symbol.cppm
../../src/language/semantic/interface.cppm
../../src/language/semantic/semantic.cppm
../../src/language/semantic/analyzer.cppm
../../src/language/semantic/semantic_model.cppm
../../src/language/semantic/type_system.cppm
../../src/language/semantic/name_resolver.cppm
../../src/language/semantic/token_collector.cppm
../../src/language/semantic/graph/call.cppm
../../src/language/semantic/graph/reference.cppm
../../src/language/semantic/graph/inheritance.cppm
../../src/language/keyword/repo.cppm
../../src/codec/common.cppm
../../src/codec/transformer.cppm
../../src/codec/facade.cppm
../../src/protocol/common/basic_types.cppm
../../src/protocol/common/message.cppm
../../src/protocol/common/registration.cppm
../../src/protocol/window/progress.cppm
../../src/protocol/initialize/configuration.cppm
../../src/protocol/initialize/capabilities.cppm
../../src/protocol/workspace/workspace.cppm
../../src/protocol/workspace/file_operations.cppm
../../src/protocol/workspace/notebook.cppm
../../src/protocol/text_document/document_sync.cppm
../../src/protocol/text_document/completion.cppm
../../src/protocol/text_document/code_actions.cppm
../../src/protocol/text_document/diagnostics.cppm
../../src/protocol/text_document/document_features.cppm
../../src/protocol/text_document/formatting.cppm
../../src/protocol/text_document/inline_features.cppm
../../src/protocol/text_document/navigation.cppm
../../src/protocol/text_document/rename.cppm
../../src/protocol/text_document/semantic_tokens.cppm
../../src/protocol/text_document/signature_help.cppm
../../src/protocol/text_document/symbols.cppm
../../src/protocol/types.cppm
../../src/protocol/protocol.cppm
../../src/provider/base/interface.cppm
../../src/provider/text_document/completion.cppm
../../src/provider/text_document/definition.cppm
../../src/provider/text_document/did_open.cppm
../../src/provider/text_document/did_change.cppm
../../src/provider/text_document/did_close.cppm
../../src/provider/text_document/rename.cppm
../../src/provider/text_document/references.cppm
../../src/provider/text_document/semantic_tokens.cppm
../../src/provider/completion_item/resolve.cppm
../../src/provider/initialize/initialize.cppm
../../src/provider/initialized/initialized.cppm
../../src/provider/shutdown/shutdown.cppm
../../src/provider/exit/exit.cppm
../../src/provider/cancel_request/cancel_request.cppm
../../src/provider/trace/set_trace.cppm
../../src/provider/client/register_capability.cppm
../../src/provider/client/unregister_capability.cppm
../../src/provider/workspace/symbol.cppm
../../src/provider/call_hierarchy/incoming_calls.cppm
../../src/provider/call_hierarchy/outgoing_calls.cppm
../../src/provider/code_action/resolve.cppm
../../src/provider/code_lens/resolve.cppm
../../src/provider/document_link/resolve.cppm
../../src/provider/inlay_hint/resolve.cppm
../../src/provider/telemetry/event.cppm
../../src/provider/text_document/code_action.cppm
../../src/provider/text_document/code_lens.cppm
../../src/provider/text_document/color_presentation.cppm
../../src/provider/text_document/diagnostic.cppm
../../src/provider/text_document/document_color.cppm
../../src/provider/text_document/document_highlight.cppm
../../src/provider/text_document/document_link.cppm
../../src/provider/text_document/document_symbol.cppm
../../src/provider/text_document/folding_range.cppm
../../src/provider/text_document/formatting.cppm
../../src/provider/text_document/hover.cppm
../../src/provider/text_document/implementation.cppm
../../src/provider/text_document/inlay_hint.cppm
../../src/provider/text_document/inline_value.cppm
../../src/provider/text_document/linked_editing_range.cppm
../../src/provider/text_document/moniker.cppm
../../src/provider/text_document/on_type_formatting.cppm
../../src/provider/text_document/prepare_call_hierarchy.cppm
../../src/provider/text_document/prepare_rename.cppm
../../src/provider/text_document/prepare_type_hierarchy.cppm
../../src/provider/text_document/publish_diagnostics.cppm
../../src/provider/text_document/range_formatting.cppm
../../src/provider/text_document/selection_range.cppm
../../src/provider/text_document/signature_help.cppm
../../src/provider/text_document/type_definition.cppm
../../src/provider/type_hierarchy/subtypes.cppm
../../src/provider/type_hierarchy/supertypes.cppm
../../src/provider/window/log_message.cppm
../../src/provider/window/show_document.cppm
../../src/provider/window/show_message.cppm
../../src/provider/window/show_message_request.cppm
../../src/provider/window/work_done_progress_create.cppm
../../src/provider/workspace/apply_edit.cppm
../../src/provider/workspace/code_lens_refresh.cppm
../../src/provider/workspace/configuration.cppm
../../src/provider/workspace/diagnostic.cppm
../../src/provider/workspace/diagnostic_refresh.cppm
../../src/provider/workspace/did_change_configuration.cppm
../../src/provider/workspace/did_change_watched_files.cppm
../../src/provider/workspace/did_change_workspace_folders.cppm
../../src/provider/workspace/did_create_files.cppm
../../src/provider/workspace/did_delete_files.cppm
../../src/provider/workspace/did_rename_files.cppm
../../src/provider/workspace/execute_command.cppm
../../src/provider/workspace/inlay_hint_refresh.cppm
../../src/provider/workspace/inline_value_refresh.cppm
../../src/provider/workspace/semantic_tokens_refresh.cppm
../../src/provider/workspace/will_create_files.cppm
../../src/provider/workspace/will_delete_files.cppm
../../src/provider/workspace/will_rename_files.cppm
../../src/provider/workspace/workspace_folders.cppm
../../src/provider/workspace_symbol/resolve.cppm)
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_HEADER_ONLY FMT_HEADER_ONLY)
target_link_libraries(
${PROJECT_NAME}
PRIVATE glaze::glaze Taskflow::Taskflow spdlog::spdlog_header_only
fmt::fmt-header-only tree-sitter::tree-sitter
$<$<PLATFORM_ID:Linux>:Threads::Threads>)
target_compile_options(
${PROJECT_NAME}
PRIVATE -Wall -Wextra -Wpedantic $<$<CONFIG:Debug>:-g -O0>
-Wno-import-implementation-partition-unit-in-interface-unit
$<$<CONFIG:Release>:-O3>)

View File

@ -0,0 +1,424 @@
module;
export module lsp.test.provider.completion;
import std;
import lsp.test.framework;
import lsp.provider.text_document.completion;
import lsp.provider.completion_item.resolve;
import lsp.core.dispacther;
import lsp.manager.manager_hub;
import lsp.scheduler.async_executor;
import lsp.protocol;
import lsp.codec.facade;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class CompletionTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestClassMethodCompletion();
static TestResult TestNewCompletion();
static TestResult TestUnitScopedNewCompletion();
static TestResult TestCreateObjectCompletion();
static TestResult TestCreateObjectQuotedCompletion();
static TestResult TestCreateObjectQualifiedCompletion();
static TestResult TestUnitContextCompletion();
static TestResult TestUnitMemberCompletion();
static TestResult TestObjectMemberCompletion();
static TestResult TestObjectMemberQualifiedTypeCompletion();
static TestResult TestFunctionCompletion();
static TestResult TestKeywordCompletion();
static TestResult TestClassContextCompletion();
static TestResult TestUnitScopedNewAliasCompletion();
static TestResult TestCompletionResolveNewSnippet();
static TestResult TestCompletionResolveCreateObjectSnippet();
};
}
namespace lsp::test::provider
{
namespace
{
struct ProviderEnv
{
scheduler::AsyncExecutor scheduler{ 1 };
manager::ManagerHub hub{};
core::ExecutionContext context;
ProviderEnv()
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
{
hub.Initialize();
}
};
protocol::ResponseMessage ParseResponse(const std::string& json)
{
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
if (!parsed)
{
throw std::runtime_error("Failed to deserialize response");
}
return *parsed;
}
protocol::Position FindPosition(const std::string& content, const std::string& marker)
{
auto pos = content.find(marker);
assertTrue(pos != std::string::npos, "Marker not found in fixture");
protocol::Position result{};
result.line = 0;
result.character = 0;
for (std::size_t i = 0; i < pos; ++i)
{
if (content[i] == '\n')
{
result.line++;
result.character = 0;
}
else
{
result.character++;
}
}
result.character += static_cast<std::uint32_t>(marker.size());
return result;
}
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
{
protocol::DidOpenTextDocumentParams open_params;
open_params.textDocument.uri = uri;
open_params.textDocument.languageId = "tsl";
open_params.textDocument.version = version;
open_params.textDocument.text = text;
hub.documents().OpenDocument(open_params);
}
std::vector<protocol::CompletionItem> RequestCompletion(ProviderEnv& env,
const std::string& uri,
const std::string& content,
const std::string& marker)
{
protocol::CompletionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, marker);
protocol::RequestMessage request;
request.id = "c1";
request.method = "textDocument/completion";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Completion handler;
auto json = handler.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
if (!response.result.has_value())
{
return {};
}
auto list = codec::FromLSPAny.template operator()<protocol::CompletionList>(response.result.value());
return list.items;
}
bool HasLabel(const std::vector<protocol::CompletionItem>& items, const std::string& label)
{
return std::any_of(items.begin(), items.end(), [&](const auto& item) {
return item.label == label;
});
}
std::optional<protocol::CompletionItem> FindItem(const std::vector<protocol::CompletionItem>& items,
const std::string& label)
{
auto it = std::find_if(items.begin(), items.end(), [&](const auto& item) {
return item.label == label;
});
if (it == items.end())
{
return std::nullopt;
}
return *it;
}
}
void CompletionTests::Register(TestRunner& runner)
{
runner.addTest("completion class(Widget).", TestClassMethodCompletion);
runner.addTest("completion new Widget", TestNewCompletion);
runner.addTest("completion new unit(MainUnit).Widget", TestUnitScopedNewCompletion);
runner.addTest("completion createobject", TestCreateObjectCompletion);
runner.addTest("completion createobject quoted", TestCreateObjectQuotedCompletion);
runner.addTest("completion createobject qualified", TestCreateObjectQualifiedCompletion);
runner.addTest("completion unit(", TestUnitContextCompletion);
runner.addTest("completion unit member", TestUnitMemberCompletion);
runner.addTest("completion object member", TestObjectMemberCompletion);
runner.addTest("completion object member qualified type", TestObjectMemberQualifiedTypeCompletion);
runner.addTest("completion function prefix", TestFunctionCompletion);
runner.addTest("completion keyword prefix", TestKeywordCompletion);
runner.addTest("completion class(", TestClassContextCompletion);
runner.addTest("completion new alias", TestUnitScopedNewAliasCompletion);
runner.addTest("completion resolve new snippet", TestCompletionResolveNewSnippet);
runner.addTest("completion resolve createobject snippet", TestCompletionResolveCreateObjectSnippet);
}
TestResult CompletionTests::TestClassMethodCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "class(Widget).Sta");
assertTrue(HasLabel(items, "StaticFoo"), "class() should suggest static methods");
return result;
}
TestResult CompletionTests::TestNewCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new Wid");
assertTrue(HasLabel(items, "Widget"), "new context should suggest classes");
return result;
}
TestResult CompletionTests::TestUnitScopedNewCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new unit(MainUnit).Wid");
assertTrue(HasLabel(items, "Widget"), "unit scoped new should suggest unit classes");
return result;
}
TestResult CompletionTests::TestCreateObjectCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(Wid");
assertTrue(HasLabel(items, "Widget"), "createobject should suggest classes");
return result;
}
TestResult CompletionTests::TestCreateObjectQuotedCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(\"Wid");
assertTrue(HasLabel(items, "Widget"), "quoted createobject should suggest classes");
return result;
}
TestResult CompletionTests::TestCreateObjectQualifiedCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(MainUnit.Wid");
assertTrue(HasLabel(items, "Widget"), "qualified createobject should suggest classes");
return result;
}
TestResult CompletionTests::TestUnitContextCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "unit(Main");
assertTrue(HasLabel(items, "MainUnit"), "unit context should list visible units");
return result;
}
TestResult CompletionTests::TestUnitMemberCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "unit(MainUnit).Uni");
assertTrue(HasLabel(items, "UnitFunc"), "unit member completion should list functions");
return result;
}
TestResult CompletionTests::TestObjectMemberCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "obj.Fo");
assertTrue(HasLabel(items, "Foo"), "object member completion should list instance methods");
return result;
}
TestResult CompletionTests::TestObjectMemberQualifiedTypeCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace")));
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "unit_inst.Wor");
assertTrue(HasLabel(items, "Work"), "qualified type should resolve workspace class members");
return result;
}
TestResult CompletionTests::TestFunctionCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "UnitF");
assertTrue(HasLabel(items, "UnitFunc"), "function prefix should return UnitFunc");
return result;
}
TestResult CompletionTests::TestKeywordCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "fun");
assertTrue(HasLabel(items, "function"), "keyword completion should include 'function'");
return result;
}
TestResult CompletionTests::TestClassContextCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "class(Wid");
assertTrue(HasLabel(items, "Widget"), "class context should suggest classes with static methods");
return result;
}
TestResult CompletionTests::TestUnitScopedNewAliasCompletion()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new MainUnit.Wid");
assertTrue(HasLabel(items, "Widget"), "alias scoped new should suggest classes");
return result;
}
TestResult CompletionTests::TestCompletionResolveNewSnippet()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "new Wid");
auto item = FindItem(items, "Widget");
assertTrue(item.has_value(), "Expected Widget completion");
protocol::RequestMessage request;
request.id = "r1";
request.method = "completionItem/resolve";
request.params = codec::ToLSPAny(*item);
::lsp::provider::completion_item::Resolve resolver;
auto json = resolver.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(response.result.value());
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
assertTrue(resolved.insertText.value().find("Widget(") == 0, "Resolved new snippet should start with Widget(");
return result;
}
TestResult CompletionTests::TestCompletionResolveCreateObjectSnippet()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
auto items = RequestCompletion(env, uri, content, "createobject(\"Wid");
auto item = FindItem(items, "Widget");
assertTrue(item.has_value(), "Expected Widget completion");
protocol::RequestMessage request;
request.id = "r2";
request.method = "completionItem/resolve";
request.params = codec::ToLSPAny(*item);
::lsp::provider::completion_item::Resolve resolver;
auto json = resolver.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(response.result.value());
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
auto snippet = resolved.insertText.value();
assertTrue(!snippet.empty() && snippet.front() == 'W', "Resolved createobject snippet should not add quote");
return result;
}
}

View File

@ -0,0 +1,205 @@
module;
export module lsp.test.provider.definitions;
import std;
import lsp.test.framework;
import lsp.provider.text_document.definition;
import lsp.core.dispacther;
import lsp.manager.manager_hub;
import lsp.scheduler.async_executor;
import lsp.protocol;
import lsp.codec.facade;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class DefinitionTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestDefinitionResolvesInDocument();
static TestResult TestDefinitionResolvesInWorkspaceIndex();
static TestResult TestDefinitionResolvesInSystemIndex();
};
}
namespace lsp::test::provider
{
namespace
{
struct ProviderEnv
{
scheduler::AsyncExecutor scheduler{ 1 };
manager::ManagerHub hub{};
core::ExecutionContext context;
ProviderEnv()
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
{
hub.Initialize();
}
};
protocol::ResponseMessage ParseResponse(const std::string& json)
{
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
if (!parsed)
{
throw std::runtime_error("Failed to deserialize response");
}
return *parsed;
}
protocol::Position FindPosition(const std::string& content, const std::string& marker)
{
auto pos = content.find(marker);
assertTrue(pos != std::string::npos, "Marker not found in fixture");
protocol::Position result{};
result.line = 0;
result.character = 0;
for (std::size_t i = 0; i < pos; ++i)
{
if (content[i] == '\n')
{
result.line++;
result.character = 0;
}
else
{
result.character++;
}
}
return result;
}
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
{
protocol::DidOpenTextDocumentParams open_params;
open_params.textDocument.uri = uri;
open_params.textDocument.languageId = "tsl";
open_params.textDocument.version = version;
open_params.textDocument.text = text;
hub.documents().OpenDocument(open_params);
}
protocol::Location ExtractLocation(const protocol::ResponseMessage& response)
{
if (!response.result.has_value())
{
throw std::runtime_error("Expected definition result");
}
return codec::FromLSPAny.template operator()<protocol::Location>(response.result.value());
}
bool LocationMatchesLine(const protocol::Location& location, std::uint32_t line)
{
return location.range.start.line == line;
}
}
void DefinitionTests::Register(TestRunner& runner)
{
runner.addTest("definition resolves in document", TestDefinitionResolvesInDocument);
runner.addTest("definition resolves in workspace index", TestDefinitionResolvesInWorkspaceIndex);
runner.addTest("definition resolves in system index", TestDefinitionResolvesInSystemIndex);
}
TestResult DefinitionTests::TestDefinitionResolvesInDocument()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DefinitionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "UnitFunc(1);");
protocol::RequestMessage request;
request.id = "1";
request.method = "textDocument/definition";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Definition handler;
auto json = handler.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto location = ExtractLocation(response);
auto def_pos = FindPosition(content, "function UnitFunc");
assertTrue(LocationMatchesLine(location, def_pos.line), "Definition should resolve in document");
return result;
}
TestResult DefinitionTests::TestDefinitionResolvesInWorkspaceIndex()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto workspace_root = FixturePath("workspace");
env.hub.symbols().LoadWorkspace(ToUri(workspace_root));
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DefinitionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "WorkspaceFunc()");
protocol::RequestMessage request;
request.id = "2";
request.method = "textDocument/definition";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Definition handler;
auto json = handler.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto location = ExtractLocation(response);
auto expected = ReadTextFile(FixturePath("workspace/workspace_script.tsl"));
auto expected_pos = FindPosition(expected, "function WorkspaceFunc");
assertTrue(LocationMatchesLine(location, expected_pos.line), "Definition should resolve in workspace index");
return result;
}
TestResult DefinitionTests::TestDefinitionResolvesInSystemIndex()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto system_root = FixturePath("system");
env.hub.symbols().LoadSystemLibrary(system_root.string());
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::DefinitionParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "SystemUnit");
protocol::RequestMessage request;
request.id = "3";
request.method = "textDocument/definition";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Definition handler;
auto json = handler.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto location = ExtractLocation(response);
auto expected = ReadTextFile(FixturePath("system/SystemUnit.tsf"));
auto expected_pos = FindPosition(expected, "unit SystemUnit");
assertTrue(LocationMatchesLine(location, expected_pos.line), "Definition should resolve in system index");
return result;
}
}

View File

@ -0,0 +1,63 @@
module;
export module lsp.test.provider.fixtures;
import std;
export namespace lsp::test::provider
{
inline std::string& ExecutablePathStorage()
{
static std::string value;
return value;
}
inline void SetExecutablePath(std::string value)
{
ExecutablePathStorage() = std::move(value);
}
inline const std::string& ExecutablePath()
{
return ExecutablePathStorage();
}
inline std::filesystem::path FixturesRoot()
{
return std::filesystem::path(__FILE__).parent_path() / "fixtures";
}
inline std::filesystem::path FixturePath(const std::string& name)
{
return FixturesRoot() / name;
}
inline std::string ToUri(const std::filesystem::path& path)
{
auto absolute = std::filesystem::absolute(path).generic_string();
#ifdef _WIN32
std::string normalized;
normalized.reserve(absolute.size());
for (char ch : absolute)
{
normalized.push_back(ch == '\\' ? '/' : ch);
}
absolute = normalized;
#endif
if (!absolute.empty() && absolute.front() != '/')
{
absolute.insert(absolute.begin(), '/');
}
return "file://" + absolute;
}
inline std::string ReadTextFile(const std::filesystem::path& path)
{
std::ifstream file(path, std::ios::binary);
if (!file.is_open())
{
throw std::runtime_error("Failed to open fixture: " + path.string());
}
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}
}

View File

@ -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.

View File

@ -0,0 +1,2 @@
var target: integer;
target := target + 1;

View File

@ -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.

View File

@ -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.

View File

@ -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;

View File

@ -0,0 +1,222 @@
module;
export module lsp.test.provider.json_flow;
import std;
import lsp.test.framework;
import lsp.provider.initialize.initialize;
import lsp.provider.text_document.did_open;
import lsp.provider.text_document.completion;
import lsp.provider.completion_item.resolve;
import lsp.provider.text_document.definition;
import lsp.core.dispacther;
import lsp.manager.manager_hub;
import lsp.scheduler.async_executor;
import lsp.protocol;
import lsp.codec.facade;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class JsonFlowTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestInitializeCompletionResolveDefinition();
};
}
namespace lsp::test::provider
{
namespace
{
struct ProviderEnv
{
scheduler::AsyncExecutor scheduler{ 1 };
manager::ManagerHub hub{};
core::ExecutionContext context;
ProviderEnv()
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
{
hub.Initialize();
}
};
template<typename T>
std::string SerializeOrThrow(const T& obj)
{
auto json = codec::Serialize(obj);
assertTrue(json.has_value(), "Failed to serialize LSP JSON");
return json.value();
}
template<typename T>
T DeserializeOrThrow(const std::string& json)
{
auto parsed = codec::Deserialize<T>(json);
assertTrue(parsed.has_value(), "Failed to deserialize LSP JSON");
return parsed.value();
}
protocol::ResponseMessage ParseResponse(const std::string& json)
{
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
assertTrue(parsed.has_value(), "Failed to deserialize response");
return parsed.value();
}
protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker)
{
auto pos = content.find(marker);
assertTrue(pos != std::string::npos, "Marker not found in fixture");
protocol::Position result{};
result.line = 0;
result.character = 0;
for (std::size_t i = 0; i < pos; ++i)
{
if (content[i] == '\n')
{
result.line++;
result.character = 0;
}
else
{
result.character++;
}
}
if (after_marker)
{
result.character += static_cast<std::uint32_t>(marker.size());
}
return result;
}
std::optional<protocol::CompletionItem> FindItem(const std::vector<protocol::CompletionItem>& items,
const std::string& label)
{
auto it = std::find_if(items.begin(), items.end(), [&](const auto& item) {
return item.label == label;
});
if (it == items.end())
{
return std::nullopt;
}
return *it;
}
}
void JsonFlowTests::Register(TestRunner& runner)
{
runner.addTest("json flow initialize->completion->resolve->definition", TestInitializeCompletionResolveDefinition);
}
TestResult JsonFlowTests::TestInitializeCompletionResolveDefinition()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto workspace_uri = ToUri(FixturePath("workspace"));
protocol::LSPArray folders;
folders.emplace_back(protocol::LSPObject{
{ "uri", workspace_uri },
{ "name", "workspace" }
});
protocol::LSPObject init_params;
init_params["trace"] = protocol::string(protocol::TraceValueLiterals::Off);
init_params["workspaceFolders"] = folders;
protocol::RequestMessage init_request;
init_request.id = "init";
init_request.method = "initialize";
init_request.params = protocol::LSPAny(init_params);
auto init_json = SerializeOrThrow(init_request);
auto parsed_init = DeserializeOrThrow<protocol::RequestMessage>(init_json);
::lsp::provider::Initialize init_provider;
auto init_response_json = init_provider.ProvideResponse(parsed_init, env.context);
auto init_response = ParseResponse(init_response_json);
assertTrue(init_response.result.has_value(), "Initialize should return result");
env.scheduler.WaitAll();
auto path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
protocol::LSPObject text_doc{
{ "uri", uri },
{ "languageId", "tsl" },
{ "version", 1 },
{ "text", content }
};
protocol::NotificationMessage open_message;
open_message.method = "textDocument/didOpen";
open_message.params = protocol::LSPAny(protocol::LSPObject{ { "textDocument", protocol::LSPAny(text_doc) } });
auto open_json = SerializeOrThrow(open_message);
auto parsed_open = DeserializeOrThrow<protocol::NotificationMessage>(open_json);
::lsp::provider::text_document::DidOpen open_provider;
open_provider.HandleNotification(parsed_open, env.context);
auto completion_pos = FindPosition(content, "new Wid", true);
protocol::LSPObject completion_params{
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
{ "position", protocol::LSPObject{
{ "line", completion_pos.line },
{ "character", completion_pos.character } } }
};
protocol::RequestMessage completion_request;
completion_request.id = "c1";
completion_request.method = "textDocument/completion";
completion_request.params = protocol::LSPAny(completion_params);
auto completion_json = SerializeOrThrow(completion_request);
auto parsed_completion = DeserializeOrThrow<protocol::RequestMessage>(completion_json);
::lsp::provider::text_document::Completion completion_provider;
auto completion_response_json = completion_provider.ProvideResponse(parsed_completion, env.context);
auto completion_response = ParseResponse(completion_response_json);
auto list = codec::FromLSPAny.template operator()<protocol::CompletionList>(completion_response.result.value());
auto item = FindItem(list.items, "Widget");
assertTrue(item.has_value(), "Expected Widget completion");
protocol::RequestMessage resolve_request;
resolve_request.id = "r1";
resolve_request.method = "completionItem/resolve";
resolve_request.params = codec::ToLSPAny(*item);
auto resolve_json = SerializeOrThrow(resolve_request);
auto parsed_resolve = DeserializeOrThrow<protocol::RequestMessage>(resolve_json);
::lsp::provider::completion_item::Resolve resolve_provider;
auto resolve_response_json = resolve_provider.ProvideResponse(parsed_resolve, env.context);
auto resolve_response = ParseResponse(resolve_response_json);
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(resolve_response.result.value());
assertTrue(resolved.insertText.has_value(), "Resolved item should have insertText");
auto def_pos = FindPosition(content, "UnitFunc(1);", false);
protocol::LSPObject definition_params{
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
{ "position", protocol::LSPObject{
{ "line", def_pos.line },
{ "character", def_pos.character } } }
};
protocol::RequestMessage def_request;
def_request.id = "d1";
def_request.method = "textDocument/definition";
def_request.params = protocol::LSPAny(definition_params);
auto def_json = SerializeOrThrow(def_request);
auto parsed_def = DeserializeOrThrow<protocol::RequestMessage>(def_json);
::lsp::provider::text_document::Definition def_provider;
auto def_response_json = def_provider.ProvideResponse(parsed_def, env.context);
auto def_response = ParseResponse(def_response_json);
auto location = codec::FromLSPAny.template operator()<protocol::Location>(def_response.result.value());
auto expected_pos = FindPosition(content, "function UnitFunc", false);
assertTrue(location.range.start.line == expected_pos.line, "Definition should resolve in document");
return result;
}
}

View File

@ -0,0 +1,6 @@
import lsp.test.provider.main;
int main(int argc, char** argv)
{
return Run(argc, argv);
}

View File

@ -0,0 +1,554 @@
module;
export module lsp.test.provider.misc;
import std;
import spdlog;
import lsp.test.framework;
import lsp.provider.initialize.initialize;
import lsp.provider.initialized.initialized;
import lsp.provider.text_document.did_open;
import lsp.provider.text_document.did_change;
import lsp.provider.text_document.did_close;
import lsp.provider.text_document.rename;
import lsp.provider.text_document.references;
import lsp.provider.text_document.semantic_tokens;
import lsp.provider.workspace.symbol;
import lsp.provider.client.register_capability;
import lsp.provider.client.unregister_capability;
import lsp.provider.shutdown.shutdown;
import lsp.provider.cancel_request.cancel_request;
import lsp.provider.trace.set_trace;
import lsp.provider.exit.exit;
import lsp.core.dispacther;
import lsp.manager.manager_hub;
import lsp.manager.symbol;
import lsp.scheduler.async_executor;
import lsp.protocol;
import lsp.codec.facade;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class ProviderMiscTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestInitializeProvider();
static TestResult TestInitializedNotification();
static TestResult TestDidOpenDidChangeDidClose();
static TestResult TestRenameProvider();
static TestResult TestRenameInvalidName();
static TestResult TestReferencesProvider();
static TestResult TestWorkspaceSymbolProvider();
static TestResult TestSemanticTokensProvider();
static TestResult TestRegisterCapabilityProvider();
static TestResult TestUnregisterCapabilityProvider();
static TestResult TestShutdownProvider();
static TestResult TestCancelRequestProvider();
static TestResult TestSetTraceProvider();
static TestResult TestExitProvider();
};
int RunExitProviderChild();
}
namespace lsp::test::provider
{
namespace
{
struct ProviderEnv
{
std::vector<core::ServerLifecycleEvent> events;
scheduler::AsyncExecutor scheduler{ 1 };
manager::ManagerHub hub{};
core::ExecutionContext context;
ProviderEnv()
: context([this](core::ServerLifecycleEvent event) { events.push_back(event); }, scheduler, hub)
{
hub.Initialize();
}
};
protocol::ResponseMessage ParseResponse(const std::string& json)
{
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
if (!parsed)
{
throw std::runtime_error("Failed to deserialize response");
}
return *parsed;
}
protocol::Position FindPosition(const std::string& content, const std::string& marker)
{
auto pos = content.find(marker);
assertTrue(pos != std::string::npos, "Marker not found in fixture");
protocol::Position result{};
result.line = 0;
result.character = 0;
for (std::size_t i = 0; i < pos; ++i)
{
if (content[i] == '\n')
{
result.line++;
result.character = 0;
}
else
{
result.character++;
}
}
return result;
}
void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version)
{
protocol::DidOpenTextDocumentParams open_params;
open_params.textDocument.uri = uri;
open_params.textDocument.languageId = "tsl";
open_params.textDocument.version = version;
open_params.textDocument.text = text;
hub.documents().OpenDocument(open_params);
}
}
void ProviderMiscTests::Register(TestRunner& runner)
{
runner.addTest("initialize provider", TestInitializeProvider);
runner.addTest("initialized notification", TestInitializedNotification);
runner.addTest("didOpen/didChange/didClose", TestDidOpenDidChangeDidClose);
runner.addTest("rename provider", TestRenameProvider);
runner.addTest("rename invalid name", TestRenameInvalidName);
runner.addTest("references provider", TestReferencesProvider);
runner.addTest("workspace symbol provider", TestWorkspaceSymbolProvider);
runner.addTest("semantic tokens provider", TestSemanticTokensProvider);
runner.addTest("register capability provider", TestRegisterCapabilityProvider);
runner.addTest("unregister capability provider", TestUnregisterCapabilityProvider);
runner.addTest("shutdown provider", TestShutdownProvider);
runner.addTest("cancel request provider", TestCancelRequestProvider);
runner.addTest("setTrace provider", TestSetTraceProvider);
runner.addTest("exit provider", TestExitProvider);
}
TestResult ProviderMiscTests::TestInitializeProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::InitializeParams params;
params.trace = protocol::TraceValueLiterals::Off;
params.workspaceFolders = std::vector<protocol::WorkspaceFolder>{
{ .uri = ToUri(FixturePath("workspace")), .name = "workspace" }
};
protocol::RequestMessage request;
request.id = "init";
request.method = "initialize";
request.params = codec::ToLSPAny(params);
::lsp::provider::Initialize provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Initialize should return result");
assertTrue(response.result.value().Is<protocol::LSPObject>(), "Initialize result should be an object");
const auto& result_obj = response.result.value().Get<protocol::LSPObject>();
auto server_info_it = result_obj.find("serverInfo");
assertTrue(server_info_it != result_obj.end(), "Initialize should return serverInfo");
assertTrue(server_info_it->second.Is<protocol::LSPObject>(), "serverInfo should be an object");
const auto& server_info = server_info_it->second.Get<protocol::LSPObject>();
auto name_it = server_info.find("name");
assertTrue(name_it != server_info.end(), "serverInfo should include name");
assertTrue(name_it->second.Is<protocol::string>(), "serverInfo.name should be string");
assertTrue(!name_it->second.Get<protocol::string>().empty(), "Initialize should set server name");
auto capabilities_it = result_obj.find("capabilities");
assertTrue(capabilities_it != result_obj.end(), "Initialize should return capabilities");
assertTrue(capabilities_it->second.Is<protocol::LSPObject>(), "capabilities should be an object");
const auto& capabilities = capabilities_it->second.Get<protocol::LSPObject>();
assertTrue(capabilities.find("textDocumentSync") != capabilities.end(),
"Initialize should set textDocumentSync");
auto completion_it = capabilities.find("completionProvider");
assertTrue(completion_it != capabilities.end(), "Initialize should set completionProvider");
if (completion_it != capabilities.end() && completion_it->second.Is<protocol::LSPObject>())
{
const auto& completion = completion_it->second.Get<protocol::LSPObject>();
auto resolve_it = completion.find("resolveProvider");
assertTrue(resolve_it != completion.end(), "Initialize should include resolveProvider");
assertTrue(resolve_it->second.Is<protocol::boolean>() && resolve_it->second.Get<protocol::boolean>(),
"Initialize should enable completion resolve");
}
env.scheduler.WaitAll();
auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Module);
bool found_workspace = std::any_of(indexed.begin(), indexed.end(), [](const manager::Symbol::IndexedSymbol& item) {
return item.name == "WorkspaceUnit";
});
assertTrue(found_workspace, "Workspace symbols should be indexed");
assertTrue(!env.events.empty(), "Initialize should emit lifecycle event");
assertTrue(env.events.back() == core::ServerLifecycleEvent::kInitialized, "Initialize should emit initialized");
return result;
}
TestResult ProviderMiscTests::TestInitializedNotification()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
::lsp::provider::Initialized provider;
protocol::NotificationMessage notification;
notification.method = "initialized";
provider.HandleNotification(notification, env.context);
return result;
}
TestResult ProviderMiscTests::TestDidOpenDidChangeDidClose()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
protocol::DidOpenTextDocumentParams open_params;
open_params.textDocument.uri = uri;
open_params.textDocument.languageId = "tsl";
open_params.textDocument.version = 1;
open_params.textDocument.text = content;
protocol::NotificationMessage open_msg;
open_msg.method = "textDocument/didOpen";
open_msg.params = codec::ToLSPAny(open_params);
::lsp::provider::text_document::DidOpen open_provider;
open_provider.HandleNotification(open_msg, env.context);
auto stored = env.hub.documents().GetContent(uri);
assertTrue(stored.has_value(), "Document should be opened");
protocol::DidChangeTextDocumentParams change_params;
change_params.textDocument.uri = uri;
change_params.textDocument.version = 2;
protocol::TextDocumentContentChangeEvent change;
change.range.start.line = 0;
change.range.start.character = 0;
change.range.end.line = 100;
change.range.end.character = 0;
change.text = "var replaced: integer;";
change_params.contentChanges.push_back(change);
protocol::NotificationMessage change_msg;
change_msg.method = "textDocument/didChange";
change_msg.params = codec::ToLSPAny(change_params);
::lsp::provider::text_document::DidChange change_provider;
change_provider.HandleNotification(change_msg, env.context);
auto updated = env.hub.documents().GetContent(uri);
assertTrue(updated.has_value() && updated.value() == change.text, "Document should be updated");
protocol::DidCloseTextDocumentParams close_params;
close_params.textDocument.uri = uri;
protocol::NotificationMessage close_msg;
close_msg.method = "textDocument/didClose";
close_msg.params = codec::ToLSPAny(close_params);
::lsp::provider::text_document::DidClose close_provider;
close_provider.HandleNotification(close_msg, env.context);
auto closed = env.hub.documents().GetContent(uri);
assertFalse(closed.has_value(), "Document should be closed");
return result;
}
TestResult ProviderMiscTests::TestRenameProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RenameParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
params.newName = "renamed";
protocol::RequestMessage request;
request.id = "rename";
request.method = "textDocument/rename";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Rename provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result.has_value(), "Rename should return workspace edit");
auto edit = codec::FromLSPAny.template operator()<protocol::WorkspaceEdit>(response.result.value());
auto it = edit.changes.find(uri);
assertTrue(it != edit.changes.end(), "Rename should include edits for document");
assertEqual(std::size_t(3), it->second.size(), "Rename should edit all occurrences");
return result;
}
TestResult ProviderMiscTests::TestRenameInvalidName()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RenameParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
params.newName = "1bad";
protocol::RequestMessage request;
request.id = "rename_invalid";
request.method = "textDocument/rename";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::Rename provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.error.has_value(), "Invalid rename should return error");
assertEqual(static_cast<int>(protocol::ErrorCodes::InvalidParams),
static_cast<int>(response.error->code),
"Invalid rename should return invalid params");
return result;
}
TestResult ProviderMiscTests::TestReferencesProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::ReferenceParams params;
params.textDocument.uri = uri;
params.position = FindPosition(content, "target := target");
params.context.includeDeclaration = true;
protocol::RequestMessage request;
request.id = "refs";
request.method = "textDocument/references";
request.params = codec::ToLSPAny(params);
::lsp::provider::text_document::References provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
auto locations = codec::FromLSPAny.template operator()<std::vector<protocol::Location>>(response.result.value());
assertEqual(std::size_t(0), locations.size(), "References provider currently returns empty list");
return result;
}
TestResult ProviderMiscTests::TestWorkspaceSymbolProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject params;
params["query"] = "Widget";
protocol::RequestMessage request;
request.id = "ws_symbol";
request.method = "workspace/symbol";
request.params = protocol::LSPAny(params);
::lsp::provider::workspace::Symbol provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.error.has_value(), "Workspace symbol should return error");
assertEqual(static_cast<int>(protocol::ErrorCodes::MethodNotFound),
static_cast<int>(response.error->code),
"Workspace symbol should return MethodNotFound");
return result;
}
TestResult ProviderMiscTests::TestSemanticTokensProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::LSPObject params;
params["textDocument"] = protocol::LSPObject{
{ "uri", ToUri(FixturePath("rename_case.tsl")) }
};
params["range"] = protocol::LSPObject{
{ "start", protocol::LSPObject{ { "line", 0 }, { "character", 0 } } },
{ "end", protocol::LSPObject{ { "line", 0 }, { "character", 1 } } }
};
protocol::RequestMessage request;
request.id = "sem";
request.method = "textDocument/semanticTokens/range";
request.params = protocol::LSPAny(params);
::lsp::provider::text_document::SemanticTokensRange provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.error.has_value(), "Semantic tokens range should return error");
assertEqual(static_cast<int>(protocol::ErrorCodes::MethodNotFound),
static_cast<int>(response.error->code),
"Semantic tokens range should return MethodNotFound");
return result;
}
TestResult ProviderMiscTests::TestRegisterCapabilityProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::RegistrationParams params;
protocol::RequestMessage request;
request.id = "reg";
request.method = "client/registerCapability";
request.params = codec::ToLSPAny(params);
::lsp::provider::client::RegisterCapability provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result == std::nullopt, "Register capability should return null");
return result;
}
TestResult ProviderMiscTests::TestUnregisterCapabilityProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
protocol::UnregistrationParams params;
protocol::RequestMessage request;
request.id = "unreg";
request.method = "client/unregisterCapability";
request.params = codec::ToLSPAny(params);
::lsp::provider::client::UnregisterCapability provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(response.result == std::nullopt, "Unregister capability should return null");
return result;
}
TestResult ProviderMiscTests::TestShutdownProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto path = FixturePath("rename_case.tsl");
auto content = ReadTextFile(path);
auto uri = ToUri(path);
OpenDocument(env.hub, uri, content, 1);
protocol::RequestMessage request;
request.id = "shutdown";
request.method = "shutdown";
::lsp::provider::Shutdown provider;
auto json = provider.ProvideResponse(request, env.context);
auto response = ParseResponse(json);
assertTrue(!response.error.has_value(), "Shutdown should not return error");
assertTrue(env.events.size() >= 1, "Shutdown should emit lifecycle event");
assertTrue(env.events.back() == core::ServerLifecycleEvent::kShuttingDown, "Shutdown should emit shutting down");
assertFalse(env.hub.documents().GetContent(uri).has_value(), "Shutdown should clear documents");
return result;
}
TestResult ProviderMiscTests::TestCancelRequestProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
std::atomic<bool> started{ false };
env.scheduler.Submit("cancel_me", [&started]() -> std::optional<std::string> {
started.store(true);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return std::string("done");
});
while (!started.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
protocol::CancelParams params;
params.id = std::string("cancel_me");
protocol::NotificationMessage notification;
notification.method = "$/cancelRequest";
notification.params = codec::ToLSPAny(params);
::lsp::provider::CancelRequest provider;
provider.HandleNotification(notification, env.context);
env.scheduler.WaitAll();
auto stats = env.scheduler.GetStatistics();
assertEqual(std::size_t(1), static_cast<std::size_t>(stats.cancelled),
"CancelRequest should mark task cancelled");
return result;
}
TestResult ProviderMiscTests::TestSetTraceProvider()
{
TestResult result{ "", true, "ok" };
ProviderEnv env;
auto prev = spdlog::get_level();
protocol::NotificationMessage notification;
notification.method = "$/setTrace";
protocol::SetTraceParams params;
params.value = protocol::TraceValueLiterals::Messages;
notification.params = codec::ToLSPAny(params);
::lsp::provider::SetTrace provider;
provider.HandleNotification(notification, env.context);
assertTrue(spdlog::get_level() == spdlog::level::debug, "SetTrace messages should set debug level");
params.value = protocol::TraceValueLiterals::Verbose;
notification.params = codec::ToLSPAny(params);
provider.HandleNotification(notification, env.context);
assertTrue(spdlog::get_level() == spdlog::level::trace, "SetTrace verbose should set trace level");
params.value = protocol::TraceValueLiterals::Off;
notification.params = codec::ToLSPAny(params);
provider.HandleNotification(notification, env.context);
assertTrue(spdlog::get_level() == spdlog::level::info, "SetTrace off should set info level");
spdlog::set_level(prev);
return result;
}
TestResult ProviderMiscTests::TestExitProvider()
{
TestResult result{ "", true, "ok" };
auto exe = ExecutablePath();
assertTrue(!exe.empty(), "ExecutablePath should be set");
std::string command = "\"" + exe + "\" --exit-provider";
int code = std::system(command.c_str());
assertEqual(0, code, "Exit should return code 0");
return result;
}
int RunExitProviderChild()
{
ProviderEnv env;
::lsp::provider::Exit provider;
protocol::NotificationMessage notification;
notification.method = "exit";
provider.HandleNotification(notification, env.context);
return 1;
}
}

View File

@ -0,0 +1,385 @@
module;
export module lsp.test.provider.surface;
import std;
import lsp.test.framework;
import lsp.codec.facade;
import lsp.protocol;
import lsp.core.dispacther;
import lsp.manager.manager_hub;
import lsp.scheduler.async_executor;
import lsp.test.provider.fixtures;
import lsp.provider.cancel_request.cancel_request;
import lsp.provider.client.register_capability;
import lsp.provider.client.unregister_capability;
import lsp.provider.code_action.resolve;
import lsp.provider.code_lens.resolve;
import lsp.provider.completion_item.resolve;
import lsp.provider.document_link.resolve;
import lsp.provider.exit.exit;
import lsp.provider.initialize.initialize;
import lsp.provider.initialized.initialized;
import lsp.provider.inlay_hint.resolve;
import lsp.provider.shutdown.shutdown;
import lsp.provider.telemetry.event;
import lsp.provider.trace.set_trace;
import lsp.provider.call_hierarchy.incoming_calls;
import lsp.provider.call_hierarchy.outgoing_calls;
import lsp.provider.type_hierarchy.supertypes;
import lsp.provider.type_hierarchy.subtypes;
import lsp.provider.window.log_message;
import lsp.provider.window.show_document;
import lsp.provider.window.show_message;
import lsp.provider.window.show_message_request;
import lsp.provider.window.work_done_progress_create;
import lsp.provider.workspace.apply_edit;
import lsp.provider.workspace.code_lens_refresh;
import lsp.provider.workspace.configuration;
import lsp.provider.workspace.diagnostic;
import lsp.provider.workspace.diagnostic_refresh;
import lsp.provider.workspace.did_change_configuration;
import lsp.provider.workspace.did_change_watched_files;
import lsp.provider.workspace.did_change_workspace_folders;
import lsp.provider.workspace.did_create_files;
import lsp.provider.workspace.did_delete_files;
import lsp.provider.workspace.did_rename_files;
import lsp.provider.workspace.execute_command;
import lsp.provider.workspace.inlay_hint_refresh;
import lsp.provider.workspace.inline_value_refresh;
import lsp.provider.workspace.semantic_tokens_refresh;
import lsp.provider.workspace.symbol;
import lsp.provider.workspace.will_create_files;
import lsp.provider.workspace.will_delete_files;
import lsp.provider.workspace.will_rename_files;
import lsp.provider.workspace.workspace_folders;
import lsp.provider.workspace_symbol.resolve;
import lsp.provider.text_document.code_action;
import lsp.provider.text_document.code_lens;
import lsp.provider.text_document.color_presentation;
import lsp.provider.text_document.completion;
import lsp.provider.text_document.definition;
import lsp.provider.text_document.diagnostic;
import lsp.provider.text_document.did_change;
import lsp.provider.text_document.did_close;
import lsp.provider.text_document.did_open;
import lsp.provider.text_document.document_color;
import lsp.provider.text_document.document_highlight;
import lsp.provider.text_document.document_link;
import lsp.provider.text_document.document_symbol;
import lsp.provider.text_document.folding_range;
import lsp.provider.text_document.formatting;
import lsp.provider.text_document.hover;
import lsp.provider.text_document.implementation;
import lsp.provider.text_document.inlay_hint;
import lsp.provider.text_document.inline_value;
import lsp.provider.text_document.linked_editing_range;
import lsp.provider.text_document.moniker;
import lsp.provider.text_document.on_type_formatting;
import lsp.provider.text_document.prepare_call_hierarchy;
import lsp.provider.text_document.prepare_rename;
import lsp.provider.text_document.prepare_type_hierarchy;
import lsp.provider.text_document.publish_diagnostics;
import lsp.provider.text_document.range_formatting;
import lsp.provider.text_document.references;
import lsp.provider.text_document.rename;
import lsp.provider.text_document.selection_range;
import lsp.provider.text_document.semantic_tokens;
import lsp.provider.text_document.signature_help;
import lsp.provider.text_document.type_definition;
export namespace lsp::test::provider
{
class ProviderSurfaceTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestProviderMetadata();
static TestResult TestRequestProviderResponses();
static TestResult TestNotificationProviderHandlers();
};
}
namespace lsp::test::provider
{
namespace
{
namespace codec = lsp::codec;
namespace provider = lsp::provider;
struct ProviderEnv
{
scheduler::AsyncExecutor scheduler{ 1 };
manager::ManagerHub hub{};
core::ExecutionContext context;
ProviderEnv()
: context([](core::ServerLifecycleEvent) {}, scheduler, hub)
{
hub.Initialize();
}
};
template<typename Provider>
void CheckProviderMetadata(const std::string& expected_method,
const std::string& expected_name)
{
Provider provider;
assertEqual(expected_method, provider.GetMethod(),
"GetMethod mismatch for " + expected_name);
assertEqual(expected_name, provider.GetProviderName(),
"GetProviderName mismatch for " + expected_name);
}
template<typename Provider>
void CheckRequestResponse()
{
ProviderEnv env;
Provider provider;
protocol::RequestMessage request;
request.id = "probe";
request.method = provider.GetMethod();
request.params = std::nullopt;
auto json = provider.ProvideResponse(request, env.context);
assertTrue(!json.empty(), "ProvideResponse should return JSON for " + provider.GetProviderName());
auto parsed = codec::Deserialize<protocol::LSPAny>(json);
assertTrue(parsed.has_value(), "ProvideResponse should return valid JSON for " + provider.GetProviderName());
}
template<typename Provider>
void CheckNotificationHandler(const std::optional<protocol::LSPAny>& params)
{
ProviderEnv env;
Provider provider;
protocol::NotificationMessage notification;
notification.method = provider.GetMethod();
notification.params = params;
provider.HandleNotification(notification, env.context);
}
}
void ProviderSurfaceTests::Register(TestRunner& runner)
{
runner.addTest("provider metadata", TestProviderMetadata);
runner.addTest("provider request responses", TestRequestProviderResponses);
runner.addTest("provider notification handlers", TestNotificationProviderHandlers);
}
TestResult ProviderSurfaceTests::TestProviderMetadata()
{
TestResult result{ "", true, "ok" };
CheckProviderMetadata<provider::Initialize>("initialize", "Initialize");
CheckProviderMetadata<provider::Shutdown>("shutdown", "Shutdown");
CheckProviderMetadata<provider::completion_item::Resolve>("completionItem/resolve", "CompletionItemResolve");
CheckProviderMetadata<provider::text_document::Completion>("textDocument/completion", "TextDocumentCompletion");
CheckProviderMetadata<provider::text_document::Definition>("textDocument/definition", "TextDocumentDefinition");
CheckProviderMetadata<provider::text_document::Rename>("textDocument/rename", "TextDocumentRename");
CheckProviderMetadata<provider::text_document::References>("textDocument/references", "TextDocumentReferences");
CheckProviderMetadata<provider::text_document::SemanticTokensRange>("textDocument/semanticTokens/range",
"TextDocumentSemanticTokensRange");
CheckProviderMetadata<provider::text_document::SemanticTokensFull>("textDocument/semanticTokens/full",
"SemanticTokensFull");
CheckProviderMetadata<provider::text_document::SemanticTokensFullDelta>("textDocument/semanticTokens/full/delta",
"SemanticTokensFullDelta");
CheckProviderMetadata<provider::text_document::Hover>("textDocument/hover", "TextDocumentHover");
CheckProviderMetadata<provider::text_document::Formatting>("textDocument/formatting", "TextDocumentFormatting");
CheckProviderMetadata<provider::text_document::RangeFormatting>("textDocument/rangeFormatting",
"TextDocumentRangeFormatting");
CheckProviderMetadata<provider::text_document::OnTypeFormatting>("textDocument/onTypeFormatting",
"TextDocumentOnTypeFormatting");
CheckProviderMetadata<provider::text_document::DocumentSymbol>("textDocument/documentSymbol",
"TextDocumentDocumentSymbol");
CheckProviderMetadata<provider::text_document::DocumentLink>("textDocument/documentLink",
"TextDocumentDocumentLink");
CheckProviderMetadata<provider::text_document::DocumentHighlight>("textDocument/documentHighlight",
"TextDocumentDocumentHighlight");
CheckProviderMetadata<provider::text_document::DocumentColor>("textDocument/documentColor",
"TextDocumentDocumentColor");
CheckProviderMetadata<provider::text_document::ColorPresentation>("textDocument/colorPresentation",
"TextDocumentColorPresentation");
CheckProviderMetadata<provider::text_document::CodeLens>("textDocument/codeLens", "TextDocumentCodeLens");
CheckProviderMetadata<provider::text_document::CodeAction>("textDocument/codeAction", "TextDocumentCodeAction");
CheckProviderMetadata<provider::text_document::PrepareTypeHierarchy>("textDocument/prepareTypeHierarchy",
"TextDocumentPrepareTypeHierarchy");
CheckProviderMetadata<provider::text_document::PrepareRename>("textDocument/prepareRename",
"TextDocumentPrepareRename");
CheckProviderMetadata<provider::text_document::PrepareCallHierarchy>("textDocument/prepareCallHierarchy",
"TextDocumentPrepareCallHierarchy");
CheckProviderMetadata<provider::text_document::TypeDefinition>("textDocument/typeDefinition",
"TextDocumentTypeDefinition");
CheckProviderMetadata<provider::text_document::Implementation>("textDocument/implementation",
"TextDocumentImplementation");
CheckProviderMetadata<provider::text_document::SelectionRange>("textDocument/selectionRange",
"TextDocumentSelectionRange");
CheckProviderMetadata<provider::text_document::SignatureHelp>("textDocument/signatureHelp",
"TextDocumentSignatureHelp");
CheckProviderMetadata<provider::text_document::InlayHint>("textDocument/inlayHint", "TextDocumentInlayHint");
CheckProviderMetadata<provider::text_document::InlineValue>("textDocument/inlineValue",
"TextDocumentInlineValue");
CheckProviderMetadata<provider::text_document::LinkedEditingRange>("textDocument/linkedEditingRange",
"TextDocumentLinkedEditingRange");
CheckProviderMetadata<provider::text_document::Moniker>("textDocument/moniker", "TextDocumentMoniker");
CheckProviderMetadata<provider::text_document::Diagnostic>("textDocument/diagnostic",
"TextDocumentDiagnostic");
CheckProviderMetadata<provider::text_document::FoldingRange>("textDocument/foldingRange",
"TextDocumentFoldingRange");
CheckProviderMetadata<provider::code_action::Resolve>("codeAction/resolve", "CodeActionResolve");
CheckProviderMetadata<provider::code_lens::Resolve>("codeLens/resolve", "CodeLensResolve");
CheckProviderMetadata<provider::document_link::Resolve>("documentLink/resolve", "DocumentLinkResolve");
CheckProviderMetadata<provider::inlay_hint::Resolve>("inlayHint/resolve", "InlayHintResolve");
CheckProviderMetadata<provider::workspace_symbol::Resolve>("workspaceSymbol/resolve", "WorkspaceSymbResolve");
CheckProviderMetadata<provider::type_hierarchy::Supertypes>("typeHierarchy/supertypes", "TypeHierarchySupertypes");
CheckProviderMetadata<provider::type_hierarchy::Subtypes>("typeHierarchy/subtypes", "WorkspaceSubtypes");
CheckProviderMetadata<provider::call_hierarchy::IncomingCalls>("callHierarchy/incomingCalls",
"CallHierarchyIncomingCalls");
CheckProviderMetadata<provider::call_hierarchy::OutgoingCalls>("callHierarchy/outgoingCalls",
"CallHierarchyOutgoingCalls");
CheckProviderMetadata<provider::workspace::ApplyEdit>("workspace/applyEdit", "WorkspaceApplyEdit");
CheckProviderMetadata<provider::workspace::Configuration>("workspace/configuration", "WorkspaceConfiguration");
CheckProviderMetadata<provider::workspace::Diagnostic>("workspace/diagnostic", "WorkspaceDiagnostic");
CheckProviderMetadata<provider::workspace::DiagnosticRefresh>("workspace/diagnostic/refresh",
"WorkspaceDiagnosticRefresh");
CheckProviderMetadata<provider::workspace::ExecuteCommand>("workspace/executeCommand",
"WorkspaceExecuteCommand");
CheckProviderMetadata<provider::workspace::WorkspaceFolders>("workspace/workspaceFolders",
"WorkspaceWorkspaceFolders");
CheckProviderMetadata<provider::workspace::WillCreateFiles>("workspace/willCreateFiles", "WorkspaceWillCreateFiles");
CheckProviderMetadata<provider::workspace::WillDeleteFiles>("workspace/willDeleteFiles", "WorkspaceWillDeleteFiles");
CheckProviderMetadata<provider::workspace::WillRenameFiles>("workspace/willRenameFiles", "WorkspaceWillRenameFiles");
CheckProviderMetadata<provider::workspace::Symbol>("workspace/symbol", "WorkSpaceSymbol");
CheckProviderMetadata<provider::workspace::SemanticTokensRefresh>("workspace/semanticTokens/refresh",
"WorkspaceSemanticTokensRefresh");
CheckProviderMetadata<provider::workspace::InlineValueRefresh>("workspace/inlineValue/refresh",
"WorkspaceInlineValueRefresh");
CheckProviderMetadata<provider::workspace::InlayHintRefresh>("workspace/inlayHint/refresh",
"WorkspaceInlayHintRefresh");
CheckProviderMetadata<provider::workspace::CodeLensRefresh>("workspace/codeLens/refresh",
"WorkspaceCodeLensRefresh");
CheckProviderMetadata<provider::workspace::DidChangeConfiguration>("workspace/didChangeConfiguration",
"WorkspaceDidChangeConfiguration");
CheckProviderMetadata<provider::workspace::DidChangeWatchedFiles>("workspace/didChangeWatchedFiles",
"WorkspaceDidChangeWatchedFiles");
CheckProviderMetadata<provider::workspace::DidChangeWorkspaceFolders>("workspace/didChangeWorkspaceFolders",
"WorkspaceDidChangeWorkspaceFolders");
CheckProviderMetadata<provider::workspace::DidCreateFiles>("workspace/didCreateFiles", "WorkspaceDidCreateFiles");
CheckProviderMetadata<provider::workspace::DidDeleteFiles>("workspace/didDeleteFiles", "WorkspaceDidDeleteFiles");
CheckProviderMetadata<provider::workspace::DidRenameFiles>("workspace/didRenameFiles", "WorkspaceDidRenameFiles");
CheckProviderMetadata<provider::window::ShowMessageRequest>("window/showMessageRequest",
"WindowShowMessageRequest");
CheckProviderMetadata<provider::window::ShowDocument>("window/showDocument", "WindowShowDocument");
CheckProviderMetadata<provider::window::WorkDoneProgressCreate>("window/workDoneProgress/create",
"WindowWorkDoneProgressCreate");
CheckProviderMetadata<provider::client::RegisterCapability>("client/registerCapability",
"ClientRegisterCapability");
CheckProviderMetadata<provider::client::UnregisterCapability>("client/unregisterCapability",
"ClientUnregisterCapability");
CheckProviderMetadata<provider::Initialized>("initialized", "Initialized");
CheckProviderMetadata<provider::Exit>("exit", "Exit");
CheckProviderMetadata<provider::CancelRequest>("$/cancelRequest", "CancelRequest");
CheckProviderMetadata<provider::SetTrace>("$/setTrace", "SetTrace");
CheckProviderMetadata<provider::text_document::DidOpen>("textDocument/didOpen", "TextDocumentDidOpen");
CheckProviderMetadata<provider::text_document::DidChange>("textDocument/didChange", "TextDocumentDidChange");
CheckProviderMetadata<provider::text_document::DidClose>("textDocument/didClose", "TextDocumentDidClose");
CheckProviderMetadata<provider::text_document::PublishDiagnostics>("textDocument/publishDiagnostics",
"TextDocumentPublishDiagnostics");
CheckProviderMetadata<provider::window::ShowMessage>("window/showMessage", "WindowShowMessage");
CheckProviderMetadata<provider::window::LogMessage>("window/logMessage", "WindowLogMessage");
CheckProviderMetadata<provider::telemetry::Event>("telemetry/event", "TelemetryEvent");
return result;
}
TestResult ProviderSurfaceTests::TestRequestProviderResponses()
{
TestResult result{ "", true, "ok" };
CheckRequestResponse<provider::Shutdown>();
CheckRequestResponse<provider::completion_item::Resolve>();
CheckRequestResponse<provider::text_document::Completion>();
CheckRequestResponse<provider::text_document::Definition>();
CheckRequestResponse<provider::text_document::Rename>();
CheckRequestResponse<provider::text_document::References>();
CheckRequestResponse<provider::text_document::SemanticTokensRange>();
CheckRequestResponse<provider::text_document::SemanticTokensFull>();
CheckRequestResponse<provider::text_document::SemanticTokensFullDelta>();
CheckRequestResponse<provider::text_document::Hover>();
CheckRequestResponse<provider::text_document::Formatting>();
CheckRequestResponse<provider::text_document::RangeFormatting>();
CheckRequestResponse<provider::text_document::OnTypeFormatting>();
CheckRequestResponse<provider::text_document::DocumentSymbol>();
CheckRequestResponse<provider::text_document::DocumentLink>();
CheckRequestResponse<provider::text_document::DocumentHighlight>();
CheckRequestResponse<provider::text_document::DocumentColor>();
CheckRequestResponse<provider::text_document::ColorPresentation>();
CheckRequestResponse<provider::text_document::CodeLens>();
CheckRequestResponse<provider::text_document::CodeAction>();
CheckRequestResponse<provider::text_document::PrepareTypeHierarchy>();
CheckRequestResponse<provider::text_document::PrepareRename>();
CheckRequestResponse<provider::text_document::PrepareCallHierarchy>();
CheckRequestResponse<provider::text_document::TypeDefinition>();
CheckRequestResponse<provider::text_document::Implementation>();
CheckRequestResponse<provider::text_document::SelectionRange>();
CheckRequestResponse<provider::text_document::SignatureHelp>();
CheckRequestResponse<provider::text_document::InlayHint>();
CheckRequestResponse<provider::text_document::InlineValue>();
CheckRequestResponse<provider::text_document::LinkedEditingRange>();
CheckRequestResponse<provider::text_document::Moniker>();
CheckRequestResponse<provider::text_document::Diagnostic>();
CheckRequestResponse<provider::text_document::FoldingRange>();
CheckRequestResponse<provider::code_action::Resolve>();
CheckRequestResponse<provider::code_lens::Resolve>();
CheckRequestResponse<provider::document_link::Resolve>();
CheckRequestResponse<provider::inlay_hint::Resolve>();
CheckRequestResponse<provider::workspace_symbol::Resolve>();
CheckRequestResponse<provider::type_hierarchy::Supertypes>();
CheckRequestResponse<provider::type_hierarchy::Subtypes>();
CheckRequestResponse<provider::call_hierarchy::IncomingCalls>();
CheckRequestResponse<provider::call_hierarchy::OutgoingCalls>();
CheckRequestResponse<provider::workspace::ApplyEdit>();
CheckRequestResponse<provider::workspace::Configuration>();
CheckRequestResponse<provider::workspace::Diagnostic>();
CheckRequestResponse<provider::workspace::DiagnosticRefresh>();
CheckRequestResponse<provider::workspace::ExecuteCommand>();
CheckRequestResponse<provider::workspace::WorkspaceFolders>();
CheckRequestResponse<provider::workspace::WillCreateFiles>();
CheckRequestResponse<provider::workspace::WillDeleteFiles>();
CheckRequestResponse<provider::workspace::WillRenameFiles>();
CheckRequestResponse<provider::workspace::Symbol>();
CheckRequestResponse<provider::workspace::SemanticTokensRefresh>();
CheckRequestResponse<provider::workspace::InlineValueRefresh>();
CheckRequestResponse<provider::workspace::InlayHintRefresh>();
CheckRequestResponse<provider::workspace::CodeLensRefresh>();
CheckRequestResponse<provider::window::ShowMessageRequest>();
CheckRequestResponse<provider::window::ShowDocument>();
CheckRequestResponse<provider::window::WorkDoneProgressCreate>();
CheckRequestResponse<provider::client::RegisterCapability>();
CheckRequestResponse<provider::client::UnregisterCapability>();
return result;
}
TestResult ProviderSurfaceTests::TestNotificationProviderHandlers()
{
TestResult result{ "", true, "ok" };
CheckNotificationHandler<provider::Initialized>(std::nullopt);
CheckNotificationHandler<provider::Exit>(std::nullopt);
CheckNotificationHandler<provider::text_document::PublishDiagnostics>(std::nullopt);
CheckNotificationHandler<provider::window::ShowMessage>(std::nullopt);
CheckNotificationHandler<provider::window::LogMessage>(std::nullopt);
CheckNotificationHandler<provider::telemetry::Event>(std::nullopt);
CheckNotificationHandler<provider::workspace::DidChangeConfiguration>(std::nullopt);
CheckNotificationHandler<provider::workspace::DidChangeWatchedFiles>(std::nullopt);
CheckNotificationHandler<provider::workspace::DidChangeWorkspaceFolders>(std::nullopt);
CheckNotificationHandler<provider::workspace::DidCreateFiles>(std::nullopt);
CheckNotificationHandler<provider::workspace::DidDeleteFiles>(std::nullopt);
CheckNotificationHandler<provider::workspace::DidRenameFiles>(std::nullopt);
return result;
}
}

View File

@ -0,0 +1,298 @@
module;
export module lsp.test.provider.server_json;
import std;
import lsp.test.framework;
import lsp.codec.facade;
import lsp.protocol;
import lsp.test.provider.fixtures;
export namespace lsp::test::provider
{
class ServerJsonTests
{
public:
static void Register(TestRunner& runner);
private:
static TestResult TestServerJsonFlow();
};
}
namespace lsp::test::provider
{
namespace
{
std::string SerializeOrThrow(const auto& obj)
{
auto json = codec::Serialize(obj);
assertTrue(json.has_value(), "Failed to serialize LSP JSON");
return json.value();
}
protocol::ResponseMessage DeserializeResponseOrThrow(const std::string& json)
{
auto parsed = codec::Deserialize<protocol::ResponseMessage>(json);
assertTrue(parsed.has_value(), "Failed to deserialize response JSON");
return parsed.value();
}
protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker)
{
auto pos = content.find(marker);
assertTrue(pos != std::string::npos, "Marker not found in fixture");
protocol::Position result{};
result.line = 0;
result.character = 0;
for (std::size_t i = 0; i < pos; ++i)
{
if (content[i] == '\n')
{
result.line++;
result.character = 0;
}
else
{
result.character++;
}
}
if (after_marker)
{
result.character += static_cast<std::uint32_t>(marker.size());
}
return result;
}
void AppendLspMessage(std::string& out, const std::string& body)
{
out += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";
out += body;
}
std::size_t ParseContentLength(const std::string& header)
{
std::istringstream stream(header);
std::string line;
while (std::getline(stream, line))
{
if (!line.empty() && line.back() == '\r')
{
line.pop_back();
}
if (line.rfind("Content-Length:", 0) == 0)
{
auto value = line.substr(std::strlen("Content-Length:"));
std::size_t start = value.find_first_not_of(' ');
if (start != std::string::npos)
{
value = value.substr(start);
}
return static_cast<std::size_t>(std::stoul(value));
}
}
return 0;
}
std::vector<protocol::ResponseMessage> ParseResponses(const std::string& data)
{
std::vector<protocol::ResponseMessage> responses;
std::size_t pos = 0;
while (pos < data.size())
{
auto header_end = data.find("\r\n\r\n", pos);
if (header_end == std::string::npos)
{
break;
}
auto header = data.substr(pos, header_end - pos);
auto length = ParseContentLength(header);
if (length == 0)
{
break;
}
auto body_start = header_end + 4;
if (body_start + length > data.size())
{
break;
}
auto body = data.substr(body_start, length);
responses.push_back(DeserializeResponseOrThrow(body));
pos = body_start + length;
}
return responses;
}
protocol::CompletionItem BuildResolveItem(const std::string& uri)
{
protocol::CompletionItem item;
item.label = "Widget";
protocol::LSPObject data;
data["ctx"] = "new";
data["class"] = "Widget";
data["unit"] = "MainUnit";
data["uri"] = uri;
item.data = std::move(data);
return item;
}
}
void ServerJsonTests::Register(TestRunner& runner)
{
runner.addTest("server json flow", TestServerJsonFlow);
}
TestResult ServerJsonTests::TestServerJsonFlow()
{
TestResult result{ "", true, "ok" };
#ifdef _WIN32
return result;
#endif
auto exe_path = ExecutablePath();
assertTrue(!exe_path.empty(), "ExecutablePath should be set");
std::filesystem::path server_path = std::filesystem::path(exe_path).parent_path() / "../../src/tsl-server";
server_path = server_path.lexically_normal();
assertTrue(std::filesystem::exists(server_path), "tsl-server binary not found");
auto source_path = FixturePath("main_unit.tsf");
auto content = ReadTextFile(source_path);
auto uri = ToUri(source_path);
auto workspace_uri = ToUri(FixturePath("workspace"));
protocol::LSPArray workspace_folders;
workspace_folders.emplace_back(protocol::LSPObject{
{ "uri", workspace_uri },
{ "name", "workspace" }
});
protocol::LSPObject init_params;
init_params["trace"] = protocol::string(protocol::TraceValueLiterals::Off);
init_params["workspaceFolders"] = workspace_folders;
protocol::RequestMessage init_request;
init_request.id = "1";
init_request.method = "initialize";
init_request.params = protocol::LSPAny(init_params);
protocol::NotificationMessage initialized;
initialized.method = "initialized";
protocol::NotificationMessage did_open;
did_open.method = "textDocument/didOpen";
did_open.params = protocol::LSPAny(protocol::LSPObject{
{ "textDocument", protocol::LSPObject{
{ "uri", uri },
{ "languageId", "tsl" },
{ "version", 1 },
{ "text", content } } }
});
auto completion_pos = FindPosition(content, "new Wid", true);
protocol::RequestMessage completion_request;
completion_request.id = "2";
completion_request.method = "textDocument/completion";
completion_request.params = protocol::LSPAny(protocol::LSPObject{
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
{ "position", protocol::LSPObject{
{ "line", completion_pos.line },
{ "character", completion_pos.character } } }
});
protocol::RequestMessage resolve_request;
resolve_request.id = "3";
resolve_request.method = "completionItem/resolve";
resolve_request.params = codec::ToLSPAny(BuildResolveItem(uri));
auto def_pos = FindPosition(content, "UnitFunc(1);", false);
protocol::RequestMessage definition_request;
definition_request.id = "4";
definition_request.method = "textDocument/definition";
definition_request.params = protocol::LSPAny(protocol::LSPObject{
{ "textDocument", protocol::LSPObject{ { "uri", uri } } },
{ "position", protocol::LSPObject{
{ "line", def_pos.line },
{ "character", def_pos.character } } }
});
protocol::RequestMessage shutdown_request;
shutdown_request.id = "5";
shutdown_request.method = "shutdown";
protocol::NotificationMessage exit_notification;
exit_notification.method = "exit";
std::string input_payload;
AppendLspMessage(input_payload, SerializeOrThrow(init_request));
AppendLspMessage(input_payload, SerializeOrThrow(initialized));
AppendLspMessage(input_payload, SerializeOrThrow(did_open));
AppendLspMessage(input_payload, SerializeOrThrow(completion_request));
AppendLspMessage(input_payload, SerializeOrThrow(resolve_request));
AppendLspMessage(input_payload, SerializeOrThrow(definition_request));
AppendLspMessage(input_payload, SerializeOrThrow(shutdown_request));
AppendLspMessage(input_payload, SerializeOrThrow(exit_notification));
auto temp_dir = std::filesystem::temp_directory_path();
auto nonce = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
auto input_path = temp_dir / ("tsl_lsp_input_" + nonce + ".txt");
auto output_path = temp_dir / ("tsl_lsp_output_" + nonce + ".txt");
{
std::ofstream input_file(input_path, std::ios::binary);
input_file << input_payload;
}
std::string command = "\"" + server_path.string() + "\" --log=off --use-stdio < \"" +
input_path.string() + "\" > \"" + output_path.string() + "\"";
int exit_code = std::system(command.c_str());
assertEqual(0, exit_code, "tsl-server should exit successfully");
std::string output;
{
std::ifstream output_file(output_path, std::ios::binary);
output.assign(std::istreambuf_iterator<char>(output_file), std::istreambuf_iterator<char>());
}
std::filesystem::remove(input_path);
std::filesystem::remove(output_path);
auto responses = ParseResponses(output);
std::unordered_map<std::string, protocol::ResponseMessage> by_id;
for (const auto& response : responses)
{
if (response.id.has_value())
{
by_id[codec::debug::GetIdString(response.id.value())] = response;
}
}
assertTrue(by_id.contains("1"), "Missing initialize response");
assertTrue(by_id.contains("2"), "Missing completion response");
assertTrue(by_id.contains("3"), "Missing resolve response");
assertTrue(by_id.contains("4"), "Missing definition response");
assertTrue(by_id.contains("5"), "Missing shutdown response");
auto completion = codec::FromLSPAny.template operator()<protocol::CompletionList>(by_id["2"].result.value());
auto item_it = std::find_if(completion.items.begin(), completion.items.end(), [](const auto& item) {
return item.label == "Widget";
});
assertTrue(item_it != completion.items.end(), "Completion response should include Widget");
auto resolved = codec::FromLSPAny.template operator()<protocol::CompletionItem>(by_id["3"].result.value());
assertTrue(resolved.insertText.has_value(), "Resolve response should include insertText");
auto location = codec::FromLSPAny.template operator()<protocol::Location>(by_id["4"].result.value());
auto expected = FindPosition(content, "function UnitFunc", false);
assertTrue(location.range.start.line == expected.line, "Definition should resolve in document");
assertTrue(!by_id["5"].error.has_value(), "Shutdown response should not contain error");
return result;
}
}

View File

@ -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();
}