#include #include #include #include #include #include #include #include "../common/plugins_catch2.h" #include #include namespace { struct PluginAPI { void* handle{}; yams_symbol_extractor_v1* api{}; PluginAPI() = default; PluginAPI(const PluginAPI&) = delete; PluginAPI& operator=(const PluginAPI&) = delete; PluginAPI(PluginAPI&& other) noexcept : handle(other.handle), api(other.api) { other.handle = nullptr; other.api = nullptr; } PluginAPI& operator=(PluginAPI&& other) noexcept { if (this != &other) { if (handle) { /* skip dlclose in tests to avoid teardown segfaults */ } handle = other.handle; api = other.api; other.handle = nullptr; other.api = nullptr; } return *this; } ~PluginAPI() { /* avoid dlclose in tests to prevent teardown crashes */ } }; std::optional load_extractor(const char* so_path) { PluginAPI p; p.handle = dlopen(so_path, RTLD_LAZY | RTLD_LOCAL); if (!p.handle) { fprintf(stderr, "dlopen failed for %s: %s\n", so_path, dlerror()); 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) { fprintf(stderr, "Symbol lookup failed: getabi=%p getiface=%p init=%p\n", (void*)getabi, (void*)getiface, (void*)init); return std::nullopt; } CHECK(getabi() > 0); CHECK(0 == init("{\n \"languages\": [\"cpp\", \"python\", \"rust\", \"go\", \"javascript\", " "\"typescript\"]\n}", nullptr)); void* ptr = nullptr; int rc = getiface(YAMS_IFACE_SYMBOL_EXTRACTOR_V1, YAMS_IFACE_SYMBOL_EXTRACTOR_V1_VERSION, &ptr); if (rc != 0 || !ptr) { fprintf(stderr, "get_interface failed: rc=%d ptr=%p\n", rc, ptr); return std::nullopt; } p.api = reinterpret_cast(ptr); return p.api ? std::optional(std::move(p)) : std::nullopt; } void assert_has_kind(yams_symbol_extraction_result_v1* out, const char* kind, size_t min_count = 1) { size_t n = 0; for (size_t i = 0; i < out->symbol_count; ++i) { if (out->symbols[i].kind && std::strcmp(out->symbols[i].kind, kind) == 0) ++n; } CHECK(n >= min_count); } } // namespace // Helper to load the plugin (refactored from test code) std::optional loadPlugin() { std::vector libnames; #ifdef _WIN32 libnames = {"yams_symbol_extractor.dll"}; #elif defined(__APPLE__) libnames = {"yams_symbol_extractor.dylib", "yams_symbol_extractor.so"}; #else libnames = {"yams_symbol_extractor.so"}; #endif auto try_fallback_paths = [&](std::optional& plug) { const char* buildroot_env = std::getenv("MESON_BUILD_ROOT"); std::vector paths; for (const char* libname : libnames) { if (buildroot_env && *buildroot_env) { std::string br(buildroot_env); paths.push_back(br + "/plugins/symbol_extractor_treesitter/" + libname); paths.push_back(br + "/tools/yams.d/plugins/symbol_extractor_treesitter/" + libname); } paths.push_back(std::string("plugins/symbol_extractor_treesitter/") + libname); paths.push_back(std::string("tools/yams.d/plugins/symbol_extractor_treesitter/") + libname); paths.push_back(std::string("../plugins/symbol_extractor_treesitter/") + libname); paths.push_back(std::string("../tools/yams.d/plugins/symbol_extractor_treesitter/") + libname); } for (const auto& cand : paths) { plug = load_extractor(cand.c_str()); if (plug.has_value()) break; } }; #ifndef PLUGIN_PATH std::optional plug; try_fallback_paths(plug); return plug; #else std::optional plug = load_extractor(PLUGIN_PATH); if (!plug.has_value()) { try_fallback_paths(plug); } return plug; #endif } TEST_CASE("SymbolExtractorPlugins.RustAndGoAndJS", "[plugin][symbolextractorplugins]") { auto plug = loadPlugin(); REQUIRE(plug.has_value()); fprintf(stderr, "Plugin loaded successfully\n"); auto* api = plug->api; fprintf(stderr, "api pointer=%p\n", (void*)api); if (!api) { fprintf(stderr, "ERROR: api is null!\n"); FAIL("unreachable"); } // Safely access struct fields uint32_t ver = 0; memcpy(&ver, &api->abi_version, sizeof(uint32_t)); fprintf(stderr, " api->abi_version=%u\n", ver); void* self_ptr = nullptr; memcpy(&self_ptr, &api->self, sizeof(void*)); fprintf(stderr, " api->self=%p\n", self_ptr); void* extract_ptr = nullptr; memcpy(&extract_ptr, &api->extract_symbols, sizeof(void*)); fprintf(stderr, " api->extract_symbols=%p\n", extract_ptr); struct Case { const char* lang; const char* path; const char* code; const char* want_kind; } cases[] = { {"c", "/tmp/t.c", "int add(int x, int y) { return x + y; }", "function"}, {"cpp", "/tmp/t.cpp", "class MyClass { public: void method() {} };", "class"}, {"python", "/tmp/t.py", "def greet(name):\n return f'Hello {name}'", "function"}, {"rust", "/tmp/t.rs", "fn hello(x:i32)->i32{ x+1 }", "function"}, {"go", "/tmp/t.go", "package main\nfunc Hello(x int) int { return x+1 }", "function"}, {"javascript", "/tmp/t.js", "function foo(a){ return a+1 }", "function"}, {"typescript", "/tmp/t.ts", "function foo(a:number){ return a+1 }", "function"}, {"java", "/tmp/T.java", "public class Test { public void run() {} }", "class"}, {"csharp", "/tmp/T.cs", "public class Test { public void Run() {} }", "class"}, {"php", "/tmp/t.php", "extract_symbols=%p, api->self=%p\n", (void*)api, (void*)api->extract_symbols, api->self); int rc = api->extract_symbols(api->self, c.code, std::strlen(c.code), c.path, c.lang, &out); fprintf(stderr, " extract_symbols returned: rc=%d, out=%p\n", rc, (void*)out); // Allow environments without grammars to skip gracefully PLUGIN_MISSING_SKIP(rc, out, "symbol grammar not available"); REQUIRE(rc == 0); REQUIRE(out != nullptr); assert_has_kind(out, c.want_kind, 1); api->free_result(api->self, out); } } // Test: Multiple symbols extraction TEST_CASE("SymbolExtractorPlugins.ExtractMultipleSymbols", "[plugin][symbolextractorplugins]") { auto plug = loadPlugin(); REQUIRE(plug.has_value()); auto* api = plug->api; REQUIRE(api != nullptr); // C++ code with multiple symbols const char* cpp_code = R"( class Calculator { public: int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } }; void helper() {} )"; yams_symbol_extraction_result_v1* out = nullptr; int rc = api->extract_symbols(api->self, cpp_code, std::strlen(cpp_code), "/tmp/calc.cpp", "cpp", &out); PLUGIN_MISSING_SKIP(rc, out, "C++ grammar not available"); REQUIRE(rc == 0); REQUIRE(out != nullptr); // Should have at least the class and some methods/functions INFO("Expected at least 1 symbol"); REQUIRE(out->symbol_count >= 1u); // Verify we have at least one class or function bool found_symbol = false; for (size_t i = 0; i < out->symbol_count; i++) { std::string kind = out->symbols[i].kind ? out->symbols[i].kind : ""; if (kind == "class" || kind == "function" || kind == "method") { found_symbol = true; break; } } INFO("Expected at least one class/function/method"); CHECK(found_symbol); api->free_result(api->self, out); } // Test: Python class and methods TEST_CASE("SymbolExtractorPlugins.PythonClassExtraction", "[plugin][symbolextractorplugins]") { auto plug = loadPlugin(); REQUIRE(plug.has_value()); auto* api = plug->api; REQUIRE(api != nullptr); const char* py_code = R"( class DataProcessor: def __init__(self, name): self.name = name def process(self, data): return data.upper() )"; yams_symbol_extraction_result_v1* out = nullptr; int rc = api->extract_symbols(api->self, py_code, std::strlen(py_code), "/tmp/processor.py", "python", &out); PLUGIN_MISSING_SKIP(rc, out, "Python grammar not available"); REQUIRE(rc == 0); REQUIRE(out != nullptr); INFO("No symbols extracted"); REQUIRE(out->symbol_count > 0u); // Should have class and methods bool has_class = false; bool has_init = false; bool has_process = false; for (size_t i = 0; i < out->symbol_count; i++) { std::string kind = out->symbols[i].kind ? out->symbols[i].kind : ""; std::string name = out->symbols[i].name ? out->symbols[i].name : ""; if (kind == "class" && name == "DataProcessor") has_class = true; if (name == "__init__") has_init = true; if (name == "process") has_process = true; } INFO("Class 'DataProcessor' not found"); CHECK(has_class); INFO("No methods found"); CHECK((has_init || has_process)); api->free_result(api->self, out); } // Test: Error handling for invalid input TEST_CASE("SymbolExtractorPlugins.ErrorHandling", "[plugin][symbolextractorplugins]") { auto plug = loadPlugin(); REQUIRE(plug.has_value()); auto* api = plug->api; REQUIRE(api != nullptr); // Test with invalid/incomplete code const char* bad_code = "int foo( { // incomplete"; yams_symbol_extraction_result_v1* out = nullptr; int rc = api->extract_symbols(api->self, bad_code, std::strlen(bad_code), "/tmp/bad.c", "c", &out); PLUGIN_MISSING_SKIP(rc, out, "C grammar not available"); // Should either succeed with 0 symbols or return error if (rc == 0) { REQUIRE(out != nullptr); // Parser might recover and extract nothing - that's ok api->free_result(api->self, out); } else { // Error is acceptable for invalid syntax CHECK(rc != 0); if (out) { INFO("Error code but no error message"); CHECK(out->error != nullptr); api->free_result(api->self, out); } } }