yams/tests/unit/extraction/external_plugin_extractor_test.cpp

209 lines
7.4 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025 Trevon Sides
#include <filesystem>
#include <string>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "yams/extraction/external_plugin_extractor.hpp"
using namespace yams::extraction;
namespace fs = std::filesystem;
namespace {
// Helper to get platform-appropriate python executable
std::string getPythonExecutable() {
#ifdef _WIN32
return "python"; // Windows uses 'python' not 'python3'
#else
return "/usr/bin/env"; // Unix uses env to find python3
#endif
}
std::vector<std::string> getPythonArgs() {
#ifdef _WIN32
return {"-u"}; // Just unbuffered flag, python is the executable
#else
return {"python3", "-u"}; // Use env to find python3
#endif
}
} // namespace
TEST_CASE("ExternalPluginExtractorConfig builder pattern", "[extraction][external_plugin]") {
auto config = ExternalPluginExtractorConfig{}
.with_executable("/usr/bin/python3")
.with_arg("-u")
.with_arg("plugin.py")
.with_env("DEBUG", "1")
.in_directory("/tmp")
.with_timeout(std::chrono::milliseconds(2000))
.with_startup_timeout(std::chrono::milliseconds(3000));
REQUIRE(config.executable() == "/usr/bin/python3");
REQUIRE(config.args().size() == 2);
REQUIRE(config.args()[0] == "-u");
REQUIRE(config.args()[1] == "plugin.py");
REQUIRE(config.env().at("DEBUG") == "1");
REQUIRE(config.working_directory() == "/tmp");
REQUIRE(config.timeout() == std::chrono::milliseconds(2000));
REQUIRE(config.startup_timeout() == std::chrono::milliseconds(3000));
}
TEST_CASE("ExternalPluginExtractorConfig with_args batch", "[extraction][external_plugin]") {
std::vector<std::string> args = {"-u", "--verbose", "plugin.py"};
auto config = ExternalPluginExtractorConfig{}
.with_executable("/usr/bin/python3")
.with_args(std::move(args));
REQUIRE(config.args().size() == 3);
REQUIRE(config.args()[0] == "-u");
REQUIRE(config.args()[1] == "--verbose");
REQUIRE(config.args()[2] == "plugin.py");
}
TEST_CASE("ExternalPluginExtractor full lifecycle", "[extraction][external_plugin]") {
// Get test fixture path - navigate from source file location
// __FILE__ is tests/unit/extraction/external_plugin_extractor_test.cpp
// Navigate up 3 levels to tests/, then into fixtures/
auto plugin_path = fs::path(__FILE__).parent_path().parent_path().parent_path() / "fixtures" /
"mock_plugin.py";
INFO("Plugin path: " << plugin_path.string());
REQUIRE(fs::exists(plugin_path));
// Create extractor with platform-appropriate python
auto config = ExternalPluginExtractorConfig{}
.with_executable(getPythonExecutable())
.with_timeout(std::chrono::milliseconds(3000));
for (const auto& arg : getPythonArgs()) {
config = std::move(config).with_arg(arg);
}
config = std::move(config).with_arg(plugin_path.string());
auto extractor = ExternalPluginExtractor(std::move(config));
SECTION("initialization") {
auto result = extractor.init();
REQUIRE(result.has_value());
REQUIRE(extractor.is_ready());
REQUIRE(extractor.name() == "mock_plugin");
REQUIRE(extractor.version() == "1.0.0");
auto manifest = extractor.manifest();
REQUIRE(manifest.name == "mock_plugin");
REQUIRE(manifest.version == "1.0.0");
REQUIRE_FALSE(manifest.supported_mime_types.empty());
}
SECTION("health check") {
REQUIRE(extractor.init().has_value());
auto health_result = extractor.health_check();
REQUIRE(health_result.has_value());
}
SECTION("supports() method") {
REQUIRE(extractor.init().has_value());
REQUIRE(extractor.supports("text/plain", "txt"));
REQUIRE_FALSE(extractor.supports("application/json", "json"));
REQUIRE_FALSE(extractor.supports("image/png", "png"));
}
SECTION("extractText() method") {
REQUIRE(extractor.init().has_value());
std::string test_content = "Hello, World!";
std::vector<std::byte> content_bytes;
content_bytes.reserve(test_content.size());
for (char c : test_content) {
content_bytes.push_back(static_cast<std::byte>(c));
}
auto result = extractor.extractText(content_bytes, "text/plain", "txt");
REQUIRE(result.has_value());
REQUIRE_FALSE(result.value().empty());
}
SECTION("graceful shutdown") {
REQUIRE(extractor.init().has_value());
REQUIRE(extractor.is_ready());
auto shutdown_result = extractor.shutdown();
REQUIRE(shutdown_result.has_value());
REQUIRE_FALSE(extractor.is_ready());
}
}
TEST_CASE("ExternalPluginExtractor multiple extractions", "[extraction][external_plugin]") {
// Navigate from source file location to fixtures directory
auto plugin_path = fs::path(__FILE__).parent_path().parent_path().parent_path() / "fixtures" /
"mock_plugin.py";
INFO("Plugin path: " << plugin_path.string());
REQUIRE(fs::exists(plugin_path));
auto config = ExternalPluginExtractorConfig{}
.with_executable(getPythonExecutable())
.with_timeout(std::chrono::milliseconds(3000));
for (const auto& arg : getPythonArgs()) {
config = std::move(config).with_arg(arg);
}
config = std::move(config).with_arg(plugin_path.string());
auto extractor = ExternalPluginExtractor(std::move(config));
REQUIRE(extractor.init().has_value());
// Multiple extractions
for (int i = 0; i < 5; ++i) {
std::string test_content = "Test content " + std::to_string(i);
std::vector<std::byte> content_bytes;
content_bytes.reserve(test_content.size());
for (char c : test_content) {
content_bytes.push_back(static_cast<std::byte>(c));
}
auto result = extractor.extractText(content_bytes, "text/plain", "txt");
REQUIRE(result.has_value());
}
}
TEST_CASE("ExternalPluginExtractor invalid executable", "[extraction][external_plugin]") {
auto config = ExternalPluginExtractorConfig{}.with_executable("/nonexistent/plugin");
auto extractor = ExternalPluginExtractor(std::move(config));
auto result = extractor.init();
REQUIRE_FALSE(result.has_value());
REQUIRE_FALSE(extractor.is_ready());
}
TEST_CASE("ExternalPluginExtractor RAII cleanup", "[extraction][external_plugin]") {
// Navigate from source file location to fixtures directory
auto plugin_path = fs::path(__FILE__).parent_path().parent_path().parent_path() / "fixtures" /
"mock_plugin.py";
INFO("Plugin path: " << plugin_path.string());
REQUIRE(fs::exists(plugin_path));
{
auto config = ExternalPluginExtractorConfig{}.with_executable(getPythonExecutable());
for (const auto& arg : getPythonArgs()) {
config = std::move(config).with_arg(arg);
}
config = std::move(config).with_arg(plugin_path.string());
auto extractor = ExternalPluginExtractor(std::move(config));
REQUIRE(extractor.init().has_value());
REQUIRE(extractor.is_ready());
// Destructor should clean up
}
// If we get here without hanging, RAII worked
REQUIRE(true);
}