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