// ============================================================================ // plugin_loader_test.cpp — PluginLoader 安全回归测试 // ============================================================================ // W20.3 (qa-xu 徐磊): 覆盖 W19 修复的 5 条发现 (F-18.3-1~5) // - F-18.3-3: 路径验证 (lexically_normal + 扩展名 + 目录约束) // - F-18.3-4: next_id_ atomic 唯一性 + 单调递增 // - F-18.3-2: host_api_->log 调用 (mock 验证) // - F-18.3-1: try/catch 异常安全边界 (间接: 注入 mock 不崩溃) // ============================================================================ #include "plugin_loader.hpp" #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; // ---- 轻量断言 ---- static int g_failures = 0; #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) // ============================================================================ static int g_log_call_count = 0; static int g_last_severity = 0; static char g_last_log_msg[1024] = {0}; 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); } 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; } 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 }; static void reset_log_state() { g_log_call_count = 0; g_last_severity = 0; g_last_log_msg[0] = '\0'; } // ============================================================================ // Helper: 获取已构建的 plugins/ 目录绝对路径 // ============================================================================ 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 } // ============================================================================ int main() { std::cout << "=== dstalk plugin_loader regression tests (W20.3) ===\n\n"; // ======================================================================== // Block 1: 路径验证 — 拒绝非法路径 (F-18.3-3) // ======================================================================== std::cout << "--- Block 1: Path validation — rejection ---\n"; { dstalk::PluginLoader loader; // T1.1: nullptr CHECK(loader.load_plugin(nullptr) == -1, "T1.1: nullptr path returns -1"); // T1.2: 非法扩展名 .txt CHECK(loader.load_plugin("plugins/test.txt") == -1, "T1.2: .txt extension rejected"); // T1.3: 路径含 .. 遍历 CHECK(loader.load_plugin("../plugins/test.dll") == -1, "T1.3: ../ traversal rejected"); // T1.4: 不在 plugins/ 目录下 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 plugins/ dir rejected"); // T1.5: 路径中间的 .. 段 CHECK(loader.load_plugin("plugins/../secret/test.dll") == -1, "T1.5: .. in middle of path rejected"); // T1.6: 无扩展名 CHECK(loader.load_plugin("plugins/test") == -1, "T1.6: no extension rejected"); // T1.7: 合法扩展名但不在 plugins/ 下 CHECK(loader.load_plugin("/etc/someconfig.so") == -1, "T1.7: .so extension but not under plugins/ rejected"); } // ======================================================================== // Block 2: 合法路径 — 成功加载 + next_id_ 验证 (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: 加载第一个插件 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: 加载第二个不同插件 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 唯一 CHECK(id1 != id2, "T2.3: IDs are unique (next_id_ atomicity)"); // T2.4: ID 单调递增 CHECK(id2 > id1, "T2.4: IDs monotonically increasing"); // T2.5: get_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 CHECK(loader.get_plugin(99999) == nullptr, "T2.7: get_plugin(invalid_id) returns nullptr"); // T2.8: 卸载后 get_plugin 返回 nullptr 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"); // 清理 loader.unload_plugin(id2); } } // ======================================================================== // Block 3: next_id_ 原子性 — 多线程并发加载 (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 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 ids(dlls.size(), -1); std::vector 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 std::vector 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]); } // 去重后大小应等于成功加载数 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()) + ")"); // 清理 for (int id : valid_ids) loader.unload_plugin(id); } } // ======================================================================== // Block 4: 失败路径日志 — host_api->log 被调用 (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 失败不崩溃 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 被调用 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 (文件不存在) 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 / 无效操作 // ======================================================================== std::cout << "\n--- Block 5: Edge cases — empty loader / invalid op ---\n"; { dstalk::PluginLoader loader; // T5.1: unload 不存在的 ID 返回 -1 CHECK(loader.unload_plugin(42) == -1, "T5.1: unload_plugin(nonexistent) returns -1"); // T5.2: 空 PluginLoader 的 list_plugins 返回 "[]" 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 CHECK(loader.get_plugin(1) == nullptr, "T5.4: get_plugin on empty loader returns nullptr"); } // ======================================================================== // 结果 // ======================================================================== 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; } }