yams/tests/plugins/symbol_extractor_edge_cases_catch2_test.cpp

168 lines
5.7 KiB
C++

#include <cstring>
#include <optional>
#include <string>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <yams/compat/dlfcn.h>
#include "../common/plugins_catch2.h"
#include <yams/plugins/symbol_extractor_v1.h>
namespace {
struct PluginAPI {
void* handle{};
yams_symbol_extractor_v1* api{};
~PluginAPI() { /* avoid dlclose in tests to prevent teardown crashes */ }
};
std::optional<PluginAPI> loadPlugin() {
#ifdef PLUGIN_PATH
const char* libpath = PLUGIN_PATH;
#else
const char* libpath = nullptr;
#endif
PluginAPI p;
if (!libpath)
return std::nullopt;
p.handle = dlopen(libpath, RTLD_LAZY | RTLD_LOCAL);
if (!p.handle)
return std::nullopt;
auto getabi = (int (*)())dlsym(p.handle, "yams_plugin_get_abi_version");
auto getiface =
(int (*)(const char*, uint32_t, void**))dlsym(p.handle, "yams_plugin_get_interface");
auto init = (int (*)(const char*, const void*))dlsym(p.handle, "yams_plugin_init");
if (!getabi || !getiface || !init)
return std::nullopt;
if (getabi() <= 0)
return std::nullopt;
if (init("{\"languages\":[\"cpp\",\"python\",\"rust\",\"go\"]}", nullptr) != 0)
return std::nullopt;
void* ptr = nullptr;
int rc = getiface(YAMS_IFACE_SYMBOL_EXTRACTOR_V1, YAMS_IFACE_SYMBOL_EXTRACTOR_V1_VERSION, &ptr);
if (rc != 0 || !ptr)
return std::nullopt;
p.api = reinterpret_cast<yams_symbol_extractor_v1*>(ptr);
return p.api ? std::optional<PluginAPI>(std::move(p)) : std::nullopt;
}
} // namespace
// Unicode identifiers (Python) — ensure extractor handles UTF-8 content
TEST_CASE("SymbolExtractorPlugins_EdgeCases.UnicodeIdentifiers",
"[plugin][symbolextractorplugins_edgecases]") {
auto plug = loadPlugin();
REQUIRE(plug.has_value());
auto* api = plug->api;
REQUIRE(api != nullptr);
const char* py_code =
"\n# -*- coding: utf-8 -*-\n\nclass 解析器:\n def 寿司(self, 値):\n return 値\n";
yams_symbol_extraction_result_v1* out = nullptr;
int rc = api->extract_symbols(api->self, py_code, std::strlen(py_code), "/tmp/unicode.py",
"python", &out);
PLUGIN_MISSING_SKIP(rc, out, "Python grammar not available");
REQUIRE(rc == 0);
REQUIRE(out != nullptr);
// At minimum, we should extract a class or a function/method
bool saw_symbol = false;
bool saw_unicode_name = false;
for (size_t i = 0; i < out->symbol_count; ++i) {
const char* name = out->symbols[i].name ? out->symbols[i].name : "";
const char* kind = out->symbols[i].kind ? out->symbols[i].kind : "";
if (*kind)
saw_symbol = true;
if (std::strcmp(name, "寿司") == 0 || std::strcmp(name, "解析器") == 0) {
saw_unicode_name = true;
}
}
CHECK(saw_symbol);
// Unicode capture may depend on grammar support; do not hard fail
// but prefer to see at least one unicode name captured when available
if (saw_symbol) {
CHECK(saw_unicode_name);
}
api->free_result(api->self, out);
}
// Empty input — should return an error (graceful)
TEST_CASE("SymbolExtractorPlugins_EdgeCases.EmptyFile",
"[plugin][symbolextractorplugins_edgecases]") {
auto plug = loadPlugin();
REQUIRE(plug.has_value());
auto* api = plug->api;
REQUIRE(api != nullptr);
const char* empty = "";
yams_symbol_extraction_result_v1* out = nullptr;
int rc = api->extract_symbols(api->self, empty, 0, "/tmp/empty.c", "c", &out);
// Empty content is an invalid input; either error or empty result is acceptable
if (rc == 0) {
REQUIRE(out != nullptr);
api->free_result(api->self, out);
} else {
// Prefer explicit error message
CHECK(out != nullptr);
if (out) {
CHECK(out->error != nullptr);
api->free_result(api->self, out);
}
}
}
// Large input (~1MB) — ensure no crash and reasonable runtime
TEST_CASE("SymbolExtractorPlugins_EdgeCases.HugeFileOneMB",
"[plugin][symbolextractorplugins_edgecases]") {
auto plug = loadPlugin();
REQUIRE(plug.has_value());
auto* api = plug->api;
REQUIRE(api != nullptr);
std::string unit = "int f(int x){return x+1;}\n";
std::string big;
big.reserve(1024 * 1024 + 1024);
while (big.size() < 1024 * 1024)
big += unit;
yams_symbol_extraction_result_v1* out = nullptr;
int rc = api->extract_symbols(api->self, big.data(), big.size(), "/tmp/huge.c", "c", &out);
PLUGIN_MISSING_SKIP(rc, out, "C grammar not available");
REQUIRE((rc == 0 || rc == YAMS_PLUGIN_ERR_INVALID));
if (rc == 0) {
REQUIRE(out != nullptr);
// Expect at least one function detected
CHECK(out->symbol_count >= 1u);
api->free_result(api->self, out);
} else if (out) {
api->free_result(api->self, out);
}
}
// Binary-ish content — ensure graceful handling
TEST_CASE("SymbolExtractorPlugins_EdgeCases.BinaryContent",
"[plugin][symbolextractorplugins_edgecases]") {
auto plug = loadPlugin();
REQUIRE(plug.has_value());
auto* api = plug->api;
REQUIRE(api != nullptr);
std::string bytes(4096, '\xff'); // not valid UTF-8
yams_symbol_extraction_result_v1* out = nullptr;
int rc = api->extract_symbols(api->self, bytes.data(), bytes.size(), "/tmp/bin.go", "go", &out);
PLUGIN_MISSING_SKIP(rc, out, "Go grammar not available");
// Either parse to zero symbols or return an error
if (rc == 0) {
REQUIRE(out != nullptr);
// Likely no symbols
CHECK(out->symbol_count >= 0u);
api->free_result(api->self, out);
} else {
if (out)
api->free_result(api->self, out);
}
}