feat: Add LSP plugin unit tests and frontend common initialization library
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / Sanitizer (ASan+UBSan) / ubuntu-24.04 (push) Has been cancelled
CI / Coverage (gcovr) / ubuntu-24.04 (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled

- Introduced `dstalk_lsp_plugin_test` for testing LSP plugin functionalities including `lsp_trim`, `lsp_frame_message`, and `lsp_parse_content_length`.
- Created `dstalk_frontend_common` static library to encapsulate shared initialization logic for frontend components (CLI, GUI, Web).
- Implemented configuration file discovery and service querying in `dstalk_frontend_init`.
- Added internal headers for LSP and Anthropic plugins to facilitate unit testing.
- Established a mailroom system for asynchronous message passing between stateless agents, enhancing coordination and context management.
This commit is contained in:
2026-06-01 08:51:40 +08:00
parent 8faa02c3d5
commit c0af9c65c7
17 changed files with 1235 additions and 69 deletions

View File

@@ -212,6 +212,40 @@ target_link_libraries(dstalk_network_plugin_test
add_test(NAME dstalk_network_plugin_test COMMAND dstalk_network_plugin_test)
# ============================================================
# dstalk_lsp_plugin_test — LSP 插件单元测试 (GoogleTest, 新增)
# 覆盖: lsp_trim / lsp_frame_message / lsp_parse_content_length
# ============================================================
add_executable(dstalk_lsp_plugin_test
lsp_plugin_test.cpp
${CMAKE_SOURCE_DIR}/plugins_base/lsp/src/lsp_plugin.cpp
)
target_include_directories(dstalk_lsp_plugin_test
PRIVATE
${CMAKE_SOURCE_DIR}/dstalk_core/include
${CMAKE_SOURCE_DIR}/plugins_base/lsp/src
)
target_compile_definitions(dstalk_lsp_plugin_test
PRIVATE
BOOST_JSON_HEADER_ONLY
BOOST_ALL_NO_LIB
)
target_compile_features(dstalk_lsp_plugin_test
PRIVATE cxx_std_17
)
target_link_libraries(dstalk_lsp_plugin_test
PRIVATE
dstalk
boost::boost
)
add_test(NAME dstalk_lsp_plugin_test COMMAND dstalk_lsp_plugin_test)
# ============================================================
# coverage — gcovr 覆盖率报告 (HTML + 终端摘要)
# 用法: cmake --build <dir> --target coverage

153
tests/lsp_plugin_test.cpp Normal file
View File

@@ -0,0 +1,153 @@
// ============================================================================
// lsp_plugin_test.cpp — LSP 插件单元测试 (轻量 CHECK 宏,离线环境无 GoogleTest)
// ============================================================================
// 测试 LSP 插件的可独立验证功能:
// - lsp_trim: 字符串 trim 逻辑
// - lsp_frame_message: Content-Length header 构建
// - lsp_parse_content_length: Content-Length header 解析
// ============================================================================
#include "lsp_internal.hpp"
#include "dstalk/dstalk_host.h"
#include <cstdarg>
#include <cstring>
#include <iostream>
#include <string>
static int g_failures = 0;
// Lightweight assertion macros (matches project pattern used by other tests)
#define CHECK(cond, msg) do { \
if (!(cond)) { \
std::cerr << "[FAIL] " << (msg) << "\n"; \
++g_failures; \
} \
} while (0)
#define CHECK_EQ(actual, expected, msg) do { \
auto _a = (actual); \
auto _e = (expected); \
if (!(_a == _e)) { \
std::cerr << "[FAIL] " << (msg) \
<< " (got=" << _a << " expected=" << _e << ")\n"; \
++g_failures; \
} \
} while (0)
// ----------------------------------------------------------------------------
// lsp_trim
// ----------------------------------------------------------------------------
static void test_lsp_trim() {
CHECK_EQ(lsp_trim(""), std::string(""), "trim empty string");
CHECK_EQ(lsp_trim("hello"), std::string("hello"), "trim no whitespace");
CHECK_EQ(lsp_trim(" hello"), std::string("hello"), "trim leading spaces");
CHECK_EQ(lsp_trim("hello "), std::string("hello"), "trim trailing spaces");
CHECK_EQ(lsp_trim(" hello "), std::string("hello"), "trim both sides");
CHECK_EQ(lsp_trim("\t\n\rhello\t\n\r"), std::string("hello"), "trim tabs/newlines");
CHECK_EQ(lsp_trim(" \t\n\r "), std::string(""), "trim only whitespace");
CHECK_EQ(lsp_trim("A"), std::string("A"), "trim single char");
CHECK_EQ(lsp_trim(" hello world "), std::string("hello world"),
"trim preserves internal whitespace");
}
// ----------------------------------------------------------------------------
// lsp_frame_message
// ----------------------------------------------------------------------------
static void test_lsp_frame_message() {
{
std::string frame = lsp_frame_message("");
CHECK(frame.find("Content-Length: 0") != std::string::npos,
"frame empty body has Content-Length: 0");
CHECK(frame.find("\r\n\r\n") != std::string::npos,
"frame empty body has header separator");
CHECK(frame.find("\r\n\r\n") + 4 == frame.size(),
"frame empty body ends right after separator");
}
{
std::string body = "{\"jsonrpc\":\"2.0\"}";
std::string frame = lsp_frame_message(body);
CHECK(frame.find("Content-Length: " + std::to_string(body.size())) != std::string::npos,
"frame simple body Content-Length matches");
CHECK(frame.find(body) != std::string::npos, "frame simple body present");
}
{
std::string body = "line1\nline2\nline3";
std::string frame = lsp_frame_message(body);
CHECK(frame.find("Content-Length: " + std::to_string(body.size())) != std::string::npos,
"frame multiline body Content-Length matches");
CHECK(frame.find(body) != std::string::npos, "frame multiline body present");
}
{
std::string body(std::string("\x00\x01\x02\x03\xFF", 5));
std::string frame = lsp_frame_message(body);
CHECK(frame.find("Content-Length: 5") != std::string::npos,
"frame binary body Content-Length: 5");
}
}
// ----------------------------------------------------------------------------
// lsp_parse_content_length
// ----------------------------------------------------------------------------
static void test_lsp_parse_content_length() {
CHECK_EQ(lsp_parse_content_length("Content-Length: 1234"), 1234, "parse valid header");
CHECK_EQ(lsp_parse_content_length(" Content-Length: 42"), 42, "parse leading spaces");
CHECK_EQ(lsp_parse_content_length("content-length: 99"), 99, "parse lowercase");
CHECK_EQ(lsp_parse_content_length("CONTENT-LENGTH: 77"), 77, "parse uppercase");
CHECK_EQ(lsp_parse_content_length("Content-Length: 0"), 0, "parse zero length");
CHECK_EQ(lsp_parse_content_length("Content-Length: 1048576"), 1048576, "parse large value");
CHECK_EQ(lsp_parse_content_length(""), -1, "parse empty string");
CHECK_EQ(lsp_parse_content_length("Content-Lengthh: 10"), -1, "parse misspelled (extra h)");
CHECK_EQ(lsp_parse_content_length("ContentLength: 10"), -1, "parse misspelled (no hyphen)");
CHECK_EQ(lsp_parse_content_length("Content-Length 10"), -1, "parse missing colon");
CHECK_EQ(lsp_parse_content_length("Content-Length: abc"), -1, "parse non-numeric");
CHECK_EQ(lsp_parse_content_length("Content-Length: -5"), -5, "parse negative");
CHECK_EQ(lsp_parse_content_length("Content-Length: 999999999999"), -1, "parse overflow");
CHECK_EQ(lsp_parse_content_length(std::string("\x00\x01\xFF", 3)), -1, "parse garbage input");
CHECK_EQ(lsp_parse_content_length("Content-Length: 1234abc"), 1234, "parse trailing garbage");
CHECK_EQ(lsp_parse_content_length("Content-Type: application/vscode-jsonrpc"), -1,
"parse other LSP header returns -1");
}
// ----------------------------------------------------------------------------
// frame + parse round-trip
// ----------------------------------------------------------------------------
static void test_round_trip() {
{
std::string body = "{\"jsonrpc\":\"2.0\",\"method\":\"initialize\"}";
std::string frame = lsp_frame_message(body);
size_t header_end = frame.find("\r\n\r\n");
CHECK(header_end != std::string::npos, "round-trip simple finds header end");
if (header_end != std::string::npos) {
std::string header_block = frame.substr(0, header_end);
CHECK_EQ(lsp_parse_content_length(header_block), static_cast<int>(body.size()),
"round-trip simple Content-Length matches body size");
}
}
{
std::string body;
std::string frame = lsp_frame_message(body);
size_t header_end = frame.find("\r\n\r\n");
CHECK(header_end != std::string::npos, "round-trip empty finds header end");
if (header_end != std::string::npos) {
std::string header_block = frame.substr(0, header_end);
CHECK_EQ(lsp_parse_content_length(header_block), 0,
"round-trip empty Content-Length is 0");
}
}
}
int main() {
test_lsp_trim();
test_lsp_frame_message();
test_lsp_parse_content_length();
test_round_trip();
if (g_failures == 0) {
std::cout << "lsp_plugin_test: all checks passed\n";
return 0;
}
std::cerr << "lsp_plugin_test: " << g_failures << " check(s) failed\n";
return 1;
}