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