332 lines
12 KiB
C++
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;
|
|
}
|
|
}
|