tsl-devkit/lsp-server/test/test_provider/provider_misc_test.cppm

555 lines
22 KiB
C++

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