- Implemented the OpenAI-compatible AI provider plugin, including configuration, chat, and chat_stream functionalities. - Added support for SSE streaming and tool calls. - Integrated Boost.JSON for JSON handling. - Created CMake configuration for the plugin. - Added error handling and logging throughout the plugin.
326 lines
15 KiB
C++
326 lines
15 KiB
C++
/*
|
||
* @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 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_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;
|
||
}
|
||
}
|