Files
dstalk/tests/plugin_loader_test.cpp
XiuChengWu 8faa02c3d5 W17: extract ai_common shared module + fix anthropic data race + brace bugs
- New plugins_upper/ai_common/ static library: shared PluginConfig, ToolCallAccum,
  StreamContext, secure_zero, extract_host_port, serialize_tool_calls, free_chat_result
- Refactored openai/anthropic plugins to use dstalk_ai:: namespace from ai_common
- Fixed anthropic g_config raw pointer → std::atomic (data race)
- Added SSE parse error counter with threshold abort (kMaxSseParseErrors=5)
- Fixed missing closing brace in both plugins' error-body catch block
- Updated test targets: ai_common include path + link, using namespace dstalk_ai
- plugin_loader_test: added stub_unreg + service_registry.cpp for unregister_service
- Includes pre-existing uncommitted changes from prior waves

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-31 16:58:25 +08:00

327 lines
15 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* @file plugin_loader_test.cpp
* @brief PluginLoader safety regression tests (W20.3): path validation,
* ABI checks, next_id_ atomicity, failure-path logging with mock host API.
* PluginLoader 安全回归测试 (W20.3)路径验证、ABI 检查、next_id_ 原子性、失败路径日志(使用 mock host API
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "plugin_loader.hpp"
#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
namespace fs = std::filesystem;
// ---- 轻量断言 ----
static int g_failures = 0;
// Lightweight assertion macro: increments g_failures counter on failure
#define CHECK(cond, msg) do { \
if (cond) { \
std::cout << "[OK] " << (msg) << "\n"; \
} else { \
std::cerr << "[FAIL] " << (msg) << "\n"; \
g_failures++; \
} \
} while (0)
// ============================================================================
// Mock host_api — 捕获 log 调用以验证失败路径日志 (F-18.3-2)
// Mock host_api — captures log calls to verify failure-path logging (F-18.3-2)
// ============================================================================
static int g_log_call_count = 0;
static int g_last_severity = 0;
static char g_last_log_msg[1024] = {0};
// Mock host_api::log implementation: counts calls and captures last severity+message
// Mock host_api::log 实现:计数调用并捕获最后的 severity+message
static void mock_log(int level, const char* fmt, ...) {
g_log_call_count++;
g_last_severity = level;
va_list args;
va_start(args, fmt);
vsnprintf(g_last_log_msg, sizeof(g_last_log_msg), fmt, args);
va_end(args);
}
// Stub host_api functions: return failure/default for all operations except log
// Stub host_api 函数:除 log 外所有操作均返回失败/默认值
static int stub_reg(const char*, int, void*) { return -1; }
static void* stub_query(const char*, int) { return nullptr; }
static void stub_unreg(const char*) {}
static int stub_sub(int, dstalk_event_handler_fn, void*) { return -1; }
static int stub_emit(int, const void*) { return -1; }
static void stub_unsub(int) {}
static const char* stub_cget(const char*) { return nullptr; }
static int stub_cset(const char*, const char*) { return -1; }
static void* stub_alloc(size_t) { return nullptr; }
static void stub_free(void*) {}
static char* stub_strdup(const char*) { return nullptr; }
// Mock host_api vtable: all stubs except mock_log for capturing error-path diagnostics
// Mock host_api 虚表:除 mock_log 外全部 stub用于捕获错误路径诊断
static dstalk_host_api_t g_mock_host_api = {
stub_reg, stub_query, stub_unreg,
stub_sub, stub_emit, stub_unsub,
stub_cget, stub_cset,
mock_log,
stub_alloc, stub_free, stub_strdup
};
// Reset log capture state between tests
// 重置日志捕获状态(测试间使用)
static void reset_log_state() {
g_log_call_count = 0;
g_last_severity = 0;
g_last_log_msg[0] = '\0';
}
// Get the absolute path to the plugin tier directory (plugins_base / middle / upper)
// 获取插件层级目录 (plugins_base / middle / upper) 的绝对路径
static fs::path get_plugins_dir() {
#ifdef DSTALK_TEST_PLUGINS_DIR
return fs::path(DSTALK_TEST_PLUGINS_DIR);
#else
return fs::current_path().parent_path() / "plugins";
#endif
}
// PluginLoader 回归测试 (W20.3)F-18.3-3 路径验证拒绝、F-18.3-4 next_id_ 唯一性+单调性+并发、
// F-18.3-2 失败路径日志,以及边界情况(空 loader、无效操作
// PluginLoader regression tests (W20.3): F-18.3-3 path validation rejection,
// F-18.3-4 next_id_ uniqueness+monotonic+concurrent, F-18.3-2 failure-path logging,
// and edge cases (empty loader, invalid operations).
int main()
{
std::cout << "=== dstalk plugin_loader regression tests (W20.3) ===\n\n";
// ========================================================================
// Block 1: 路径验证 — 拒绝非法路径 (F-18.3-3)
// Block 1: Path validation — reject illegal paths (F-18.3-3)
// ========================================================================
std::cout << "--- Block 1: Path validation — rejection ---\n";
{
dstalk::PluginLoader loader;
// T1.1: nullptr / null pointer
CHECK(loader.load_plugin(nullptr) == -1,
"T1.1: nullptr path returns -1");
// T1.2: 非法扩展名 .txt / illegal .txt extension
CHECK(loader.load_plugin("plugins/test.txt") == -1,
"T1.2: .txt extension rejected");
// T1.3: 路径含 .. 遍历 / path contains .. traversal
CHECK(loader.load_plugin("../plugins/test.dll") == -1,
"T1.3: ../ traversal rejected");
// T1.4: 不在插件 tier 目录下 / not under plugin tier dir
auto tmp = fs::temp_directory_path() / "dstalk_test_no_plugins" / "test.dll";
CHECK(loader.load_plugin(tmp.string().c_str()) == -1,
"T1.4: path not under plugin tier dir rejected");
// T1.5: 路径中间的 .. 段 / .. segment in middle of path
CHECK(loader.load_plugin("plugins/../secret/test.dll") == -1,
"T1.5: .. in middle of path rejected");
// T1.6: 无扩展名 / no extension
CHECK(loader.load_plugin("plugins/test") == -1,
"T1.6: no extension rejected");
// T1.7: 合法扩展名但不在插件 tier 目录下 / valid extension but not under plugin tier dir
CHECK(loader.load_plugin("/etc/someconfig.so") == -1,
"T1.7: .so extension but not under plugin tier dir rejected");
}
// ========================================================================
// Block 2: 合法路径 — 成功加载 + next_id_ 验证 (F-18.3-4)
// Block 2: Valid path — successful load + ID uniqueness (F-18.3-4)
// ========================================================================
std::cout << "\n--- Block 2: Valid path — successful load + ID uniqueness ---\n";
{
dstalk::PluginLoader loader;
fs::path plugins_dir = get_plugins_dir();
fs::path dll_config = plugins_dir / "plugin_config.dll";
fs::path dll_fileio = plugins_dir / "plugin_file_io.dll";
bool have_plugins = fs::exists(dll_config) && fs::exists(dll_fileio);
if (!have_plugins) {
std::cout << "[WARN] Plugin DLLs not found at " << plugins_dir.string()
<< " — skipping Block 2\n";
} else {
// T2.1: 加载第一个插件 / load first plugin
int id1 = loader.load_plugin(dll_config.string().c_str());
CHECK(id1 >= 1, "T2.1: first plugin loaded with positive ID");
std::cout << " id1 = " << id1 << "\n";
// T2.2: 加载第二个不同插件 / load second (different) plugin
int id2 = loader.load_plugin(dll_fileio.string().c_str());
CHECK(id2 >= 1, "T2.2: second plugin loaded with positive ID");
std::cout << " id2 = " << id2 << "\n";
// T2.3: ID 唯一 / IDs are unique
CHECK(id1 != id2, "T2.3: IDs are unique (next_id_ atomicity)");
// T2.4: ID 单调递增 / IDs monotonically increasing
CHECK(id2 > id1, "T2.4: IDs monotonically increasing");
// T2.5: get_plugin 可查询到已加载插件 / get_plugin can find loaded plugin
const dstalk::PluginInfo* info1 = loader.get_plugin(id1);
CHECK(info1 != nullptr, "T2.5: get_plugin(id1) returns non-null");
if (info1) {
CHECK(!info1->name.empty(), "T2.6: plugin has non-empty name");
std::cout << " plugin1 name: " << info1->name << "\n";
}
// T2.7: get_plugin 对无效 ID 返回 nullptr / get_plugin returns nullptr for invalid ID
CHECK(loader.get_plugin(99999) == nullptr,
"T2.7: get_plugin(invalid_id) returns nullptr");
// T2.8: 卸载后 get_plugin 返回 nullptr / get_plugin returns nullptr after unload
int ret = loader.unload_plugin(id1);
CHECK(ret == 0, "T2.8: unload_plugin returns 0");
CHECK(loader.get_plugin(id1) == nullptr,
"T2.9: get_plugin returns nullptr after unload");
// 清理 / cleanup
loader.unload_plugin(id2);
}
}
// ========================================================================
// Block 3: next_id_ 原子性 — 多线程并发加载 (F-18.3-4)
// Block 3: next_id_ atomicity — concurrent loads (F-18.3-4)
// ========================================================================
std::cout << "\n--- Block 3: next_id_ atomicity — concurrent loads ---\n";
{
dstalk::PluginLoader loader;
fs::path plugins_dir = get_plugins_dir();
std::vector<fs::path> dlls;
for (auto name : {"plugin_config.dll", "plugin_file_io.dll",
"plugin_context.dll", "plugin_session.dll"}) {
fs::path p = plugins_dir / name;
if (fs::exists(p)) dlls.push_back(p);
}
if (dlls.size() < 2) {
std::cout << "[WARN] Not enough plugin DLLs for concurrency test"
<< " (found " << dlls.size() << ") — skipping Block 3\n";
} else {
std::vector<int> ids(dlls.size(), -1);
std::vector<std::thread> threads;
for (size_t i = 0; i < dlls.size(); ++i) {
threads.emplace_back([&loader, &ids, i, &dlls]() {
ids[i] = loader.load_plugin(dlls[i].string().c_str());
});
}
for (auto& t : threads) t.join();
// 验证: 所有 load 成功, ID 唯一且 > 0 / Verify: all loads succeed, IDs unique and > 0
std::vector<int> valid_ids;
for (size_t i = 0; i < ids.size(); ++i) {
CHECK(ids[i] >= 1, "T3." + std::to_string(i)
+ ": thread load succeeded (id="
+ std::to_string(ids[i]) + ")");
if (ids[i] >= 1) valid_ids.push_back(ids[i]);
}
// 去重后大小应等于成功加载数 / dedup size should equal successful load count
std::sort(valid_ids.begin(), valid_ids.end());
auto dup = std::unique(valid_ids.begin(), valid_ids.end());
size_t unique_count = std::distance(valid_ids.begin(), dup);
CHECK(unique_count == valid_ids.size(),
"T3.X: all IDs unique under concurrent loads ("
+ std::to_string(unique_count) + "/"
+ std::to_string(valid_ids.size()) + ")");
// 清理 / cleanup
for (int id : valid_ids) loader.unload_plugin(id);
}
}
// ========================================================================
// Block 4: 失败路径日志 — host_api->log 被调用 (F-18.3-2)
// Block 4: Failure-path logging — host_api->log is called (F-18.3-2)
// ========================================================================
std::cout << "\n--- Block 4: Failure-path logging (host_api->log) ---\n";
{
dstalk::PluginLoader loader;
// 4.1: 无 host_api 时 load_plugin 失败不崩溃 / load_plugin fails without crash when no host_api
reset_log_state();
int id = loader.load_plugin("bad_ext.noext");
CHECK(id == -1, "T4.1: load_plugin with invalid ext returns -1 (no host_api)");
CHECK(g_log_call_count == 0,
"T4.2: log NOT called when host_api_ is null");
// 4.2: 设置 mock host_api 后验证 log 被调用 / set mock host_api and verify log is called
int init_ret = loader.initialize_all(&g_mock_host_api);
CHECK(init_ret == 0, "T4.3: initialize_all with mock host_api returns 0");
reset_log_state();
id = loader.load_plugin("bad_ext.noext");
CHECK(id == -1, "T4.4: load_plugin still returns -1 with mock host_api");
CHECK(g_log_call_count >= 1,
"T4.5: host_api->log WAS called on path validation failure");
CHECK(g_last_severity == DSTALK_LOG_ERROR,
"T4.6: log severity is DSTALK_LOG_ERROR");
std::cout << " log msg: " << g_last_log_msg << "\n";
// 4.3: LoadLibrary 失败也触发 log (文件不存在) / LoadLibrary failure also triggers log (file missing)
reset_log_state();
fs::path missing = get_plugins_dir() / "nonexistent_plugin.dll";
id = loader.load_plugin(missing.string().c_str());
CHECK(id == -1, "T4.7: missing DLL returns -1");
CHECK(g_log_call_count >= 1,
"T4.8: host_api->log called on LoadLibrary failure");
std::cout << " log msg: " << g_last_log_msg << "\n";
}
// ========================================================================
// Block 5: 边界 — 空 loader / 无效操作
// Block 5: Edge cases — empty loader / invalid operations
// ========================================================================
std::cout << "\n--- Block 5: Edge cases — empty loader / invalid op ---\n";
{
dstalk::PluginLoader loader;
// T5.1: unload 不存在的 ID 返回 -1 / unload non-existent ID returns -1
CHECK(loader.unload_plugin(42) == -1,
"T5.1: unload_plugin(nonexistent) returns -1");
// T5.2: 空 PluginLoader 的 list_plugins 返回 "[]" / empty PluginLoader list_plugins returns "[]"
std::string json = loader.list_plugins();
CHECK(!json.empty(), "T5.2: list_plugins returns non-empty string");
CHECK(json == "[]", "T5.3: empty loader produces empty JSON array");
std::cout << " list_plugins (empty): " << json << "\n";
// T5.3: get_plugin 在空 loader 上返回 nullptr / get_plugin on empty loader returns nullptr
CHECK(loader.get_plugin(1) == nullptr,
"T5.4: get_plugin on empty loader returns nullptr");
}
// ========================================================================
// 结果 / Result
// ========================================================================
std::cout << "\n";
if (g_failures == 0) {
std::cout << "=== All plugin_loader regression tests passed ===\n";
return 0;
} else {
std::cerr << "=== " << g_failures << " test(s) FAILED ===\n";
return 1;
}
}