#include #include #include #include #include #include #include #include "../../common/test_helpers_catch2.h" extern "C" { #include #include } namespace { /** * @brief RAII wrapper for plugin handle */ struct PluginHandle { void* handle{nullptr}; PluginHandle() = default; PluginHandle(const PluginHandle&) = delete; PluginHandle& operator=(const PluginHandle&) = delete; PluginHandle(PluginHandle&& o) noexcept : handle(o.handle) { o.handle = nullptr; } PluginHandle& operator=(PluginHandle&& o) noexcept { if (this != &o) { if (handle) dlclose(handle); handle = o.handle; o.handle = nullptr; } return *this; } ~PluginHandle() { if (handle) dlclose(handle); } }; /** * @brief Helper to load plugin and fetch onnx_request_v1 interface */ yams_onnx_request_v1* load_req_iface(PluginHandle& ph) { #if defined(_WIN32) const char* so = "builddir/plugins/onnx/yams_onnx_plugin.dll"; #elif defined(__APPLE__) const char* so = "builddir/plugins/onnx/libyams_onnx_plugin.dylib"; #else const char* so = "builddir/plugins/onnx/libyams_onnx_plugin.so"; #endif ph.handle = dlopen(so, RTLD_LAZY | RTLD_LOCAL); if (!ph.handle) return nullptr; auto getabi = (int (*)())dlsym(ph.handle, "yams_plugin_get_abi_version"); auto getiface = (int (*)(const char*, uint32_t, void**))dlsym(ph.handle, "yams_plugin_get_interface"); if (!getabi || !getiface) return nullptr; void* p = nullptr; int rc = getiface(YAMS_IFACE_ONNX_REQUEST_V1, YAMS_IFACE_ONNX_REQUEST_V1_VERSION, &p); if (rc != 0 || !p) return nullptr; return reinterpret_cast(p); } } // namespace TEST_CASE("ONNX Request plugin interface", "[plugins][onnx][request]") { PluginHandle ph; auto* req = load_req_iface(ph); if (!req) { SKIP("ONNX plugin not available - skipping test"); } SECTION("embedding request without inputs returns error") { char* out = nullptr; int rc = req->process_json(req->self, "{}", &out); REQUIRE((rc == 0)); REQUIRE((out != nullptr)); auto j = nlohmann::json::parse(out); req->free_string(req->self, out); CHECK(j.contains("error")); } SECTION("embedding request with batch returns correct shape") { // Use mock mode to avoid heavy model load. yams::test::ScopedEnvVar skipLoading{"YAMS_SKIP_MODEL_LOADING", std::string{"1"}}; std::string body = R"({"task":"embedding","model_id":"e5-small","inputs":["a","b"]})"; char* out = nullptr; int rc = req->process_json(req->self, body.c_str(), &out); REQUIRE((rc == 0)); REQUIRE((out != nullptr)); auto j = nlohmann::json::parse(out, nullptr, false); req->free_string(req->self, out); REQUIRE_FALSE(j.is_discarded()); if (j.contains("error")) { SKIP("Model not available: " + j.dump()); } CHECK((j.value("task", "") == "embedding")); CHECK((j.value("batch", 0) == 2)); } }