yams/tests/plugins/plugin_export_surface_catch2_test.cpp
Trevon d398556ee5
fix(tests): replace frozen expectations and harden release gates (#323)
* test(search,daemon,storage): assert behaviour instead of frozen literals

Several suites encoded exact internal values that legitimate tuning and
ranking changes are allowed to move: SCIENTIFIC-era profile weights and
rrfK values, corpus-statistic figures, and hardcoded timeout/cancellation
numbers. They failed on every platform after those changes landed, so the
real behaviour regressions they should catch were hidden behind noise.

Replace the frozen literals with contract assertions: weights are finite,
non-negative and normalize, state selection still holds, capability-gated
sources stay zeroed, and hybrid results keep matching canonical hash/path
sets. Deadlines are not widened and the daemon paths they cover are
unchanged.

Also add independent corpus and adapter oracles, deterministic collector
cancellation and late-result gates, and synchronize ServiceManager
shutdown observation, so the tests fail for the behaviour they name.

The portability allowlist keeps its two reviewed raw test-env reads; only
their line numbers moved.

Signed-off-by: trvon <git@trevon.dev>

* ci(tests): summarise Meson failures without flooding the log

A failing test run printed the whole Meson suite output, so the actual
failing test names and their assertion messages were buried and the
uploaded logs were dominated by passing-test chatter.

Add a bounded reporter that extracts the failing test names plus their
surrounding diagnostics, keeps an overall cap on emitted lines, and is
unit-tested against representative Meson output. Wire it into the Tests
workflow so failures stay readable.

Signed-off-by: trvon <git@trevon.dev>

* build(deb): validate candidate identity, mapping and upgrade ordering

The Debian packaging scripts took filenames and versions on trust, so a
misnamed candidate or a wrong package ordering could pass validation while
the real install path was never exercised.

Fail closed on candidate identity, version and architecture, verify the
package-to-source mapping, seed the legacy-only layout, wait on the actual
services, and assert install/upgrade/remove ordering for both Debian and
RPM in an offline container. Cases that cannot be proven on a host are
reported as skipped rather than silently passing.

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>
2026-10-19 19:16:57 -06:00

103 lines
3.7 KiB
C++

// Plugin export surface: a plugin's dynamic symbol table is its ABI. Only the
// yams_plugin_* C entry points may be visible; statically linked third-party
// code (OpenSSL, libcurl, zlib, spdlog, ...) and plugin internals must not be,
// otherwise a host that loads the plugin with RTLD_GLOBAL, or another library
// in the process, can bind to the plugin's private copies.
#include <catch2/catch_test_macros.hpp>
#include <yams/compat/dlfcn.h>
#include <string>
#include <vector>
namespace {
struct PluginHandle {
explicit PluginHandle(const char* path) : handle(dlopen(path, RTLD_LAZY | RTLD_LOCAL)) {}
~PluginHandle() {
if (handle) {
dlclose(handle);
}
}
PluginHandle(const PluginHandle&) = delete;
PluginHandle& operator=(const PluginHandle&) = delete;
void* handle;
};
const std::vector<std::string> kRequiredEntryPoints = {
"yams_plugin_get_abi_version",
"yams_plugin_get_name",
"yams_plugin_get_version",
"yams_plugin_get_manifest_json",
"yams_plugin_init",
"yams_plugin_shutdown",
"yams_plugin_get_interface",
"yams_plugin_get_health_json",
};
void requireEntryPoints(void* handle) {
for (const auto& name : kRequiredEntryPoints) {
INFO("entry point " << name);
CHECK(dlsym(handle, name.c_str()) != nullptr);
}
}
void requireHidden(void* handle, const std::vector<std::string>& names) {
#ifndef _WIN32
// POSIX dlsym also searches dependencies. A system zlib symbol is not an
// export from the plugin's private copy; compare the defining images.
auto* entry = dlsym(handle, "yams_plugin_get_abi_version");
REQUIRE(entry != nullptr);
Dl_info pluginImage{};
REQUIRE(dladdr(entry, &pluginImage) != 0);
#endif
for (const auto& name : names) {
INFO("symbol must not be exported by the plugin: " << name);
auto* symbol = dlsym(handle, name.c_str());
#ifdef _WIN32
// GetProcAddress checks the DLL's export table, not its dependencies.
CHECK(symbol == nullptr);
#else
bool definedByPlugin = false;
std::string definingImage;
if (symbol != nullptr) {
Dl_info symbolImage{};
REQUIRE(dladdr(symbol, &symbolImage) != 0);
definedByPlugin = symbolImage.dli_fbase == pluginImage.dli_fbase;
if (symbolImage.dli_fname != nullptr) {
definingImage = symbolImage.dli_fname;
}
}
CAPTURE(definingImage);
CHECK_FALSE(definedByPlugin);
#endif
}
}
} // namespace
#ifdef YAMS_S3_PLUGIN_PATH
TEST_CASE("S3 plugin exports only the plugin C ABI", "[plugins][s3][abi][catch2]") {
PluginHandle plugin(YAMS_S3_PLUGIN_PATH);
REQUIRE(plugin.handle != nullptr);
requireEntryPoints(plugin.handle);
// Legacy object-storage factory used by ObjectStoragePluginLoader.
CHECK(dlsym(plugin.handle, "yams_plugin_create_object_storage") != nullptr);
CHECK(dlsym(plugin.handle, "yams_plugin_destroy_object_storage") != nullptr);
// Statically linked libcurl, OpenSSL and zlib stay private.
requireHidden(plugin.handle, {"curl_easy_init", "curl_global_init", "SSL_new", "SSL_CTX_new",
"EVP_sha256", "OPENSSL_init_ssl", "deflate", "inflate"});
}
#endif
#ifdef YAMS_ZYP_PLUGIN_PATH
TEST_CASE("zyp plugin exports only the plugin C ABI", "[plugins][zyp][abi][catch2]") {
PluginHandle plugin(YAMS_ZYP_PLUGIN_PATH);
REQUIRE(plugin.handle != nullptr);
requireEntryPoints(plugin.handle);
// The zpdf wrapper and PDF metadata parser are implementation details.
requireHidden(plugin.handle,
{"_ZN4yams3zyp10TextBufferD1Ev", "_ZN4yams3zyp8DocumentC1EPv",
"_ZN4yams3zyp15extractMetadataESt4spanIKhLm18446744073709551615EE"});
}
#endif