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

332 lines
12 KiB
C++

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<std::string> ParseBodies(const std::string& data)
{
std::vector<std::string> bodies;
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;
}
bodies.push_back(data.substr(body_start, length));
pos = body_start + length;
}
return bodies;
}
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 bodies = ParseBodies(output);
std::unordered_map<std::string, protocol::ResponseMessage> by_id;
bool saw_diagnostics = false;
for (const auto& body : bodies)
{
auto any = codec::Deserialize<protocol::LSPAny>(body);
if (!any.has_value() || !any->Is<protocol::LSPObject>())
{
continue;
}
const auto& obj = any->Get<protocol::LSPObject>();
const bool has_id = obj.contains("id");
const bool has_method = obj.contains("method");
const bool has_result = obj.contains("result");
const bool has_error = obj.contains("error");
if (has_id && (has_result || has_error))
{
auto response = DeserializeResponseOrThrow(body);
if (response.id.has_value())
{
by_id[codec::debug::GetIdString(response.id.value())] = std::move(response);
}
continue;
}
if (has_method && !has_id)
{
auto notification = codec::Deserialize<protocol::NotificationMessage>(body);
if (notification && notification->method == "textDocument/publishDiagnostics" && notification->params.has_value())
{
const auto& diag_params = notification->params->Get<protocol::LSPObject>();
auto uri_it = diag_params.find("uri");
if (uri_it != diag_params.end() && uri_it->second.Is<protocol::string>() && uri_it->second.Get<protocol::string>() == uri)
{
saw_diagnostics = true;
}
}
}
}
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(!saw_diagnostics, "Server should not publish diagnostics during staged rollout");
assertTrue(!by_id["5"].error.has_value(), "Shutdown response should not contain error");
return result;
}
}