555 lines
22 KiB
C++
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;
|
|
}
|
|
}
|