diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 11aca7dac6..c7b6d6ff6e 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -36,24 +36,3 @@ orcaslicer_copy_test_dlls(${_TEST_NAME}_tests) orcaslicer_stage_test_python_runtime(${_TEST_NAME}_tests) orcaslicer_discover_tests(${_TEST_NAME}_tests) - -# why: the loader runs on a detached worker thread, so its Python interpreter -# ownership model cannot share the embedded interpreter in the main test binary. -add_executable(printer_agent_plugin_tests test_printer_agent_plugin.cpp) - -if (MSVC) - target_link_libraries(printer_agent_plugin_tests Setupapi.lib) -endif () - -target_link_libraries(printer_agent_plugin_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2) -set_property(TARGET printer_agent_plugin_tests PROPERTY FOLDER "tests") - -# why: both targets' post-build steps stage/clear the same python/ directory -# beside the shared test output dir; serialize them so the copies can't race. -add_dependencies(printer_agent_plugin_tests ${_TEST_NAME}_tests) - -orcaslicer_copy_test_dlls(printer_agent_plugin_tests) - -orcaslicer_stage_test_python_runtime(printer_agent_plugin_tests) - -orcaslicer_discover_tests(printer_agent_plugin_tests) diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 63a4e6827b..3166426d5b 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -73,6 +74,96 @@ class EchoPackage(orca.base): orca.register_capability(Echo) )PY"; +// A minimal printer-agent plugin exposing exactly one PrinterConnection capability. +const char* const PRINTER_AGENT_PLUGIN_SOURCE = R"PY(# /// script +# requires-python = ">=3.12" +# dependencies = [] +# +# [tool.orcaslicer.plugin] +# name = "Lifecycle Test Agent" +# description = "Minimal printer-agent plugin for the lifecycle test." +# author = "tests" +# version = "1.0.0" +# type = "printer-connection" +# /// +import orca + + +class LifecycleTestAgentCapability(orca.printer_agent.PrinterAgentBase): + def get_name(self): + return "Lifecycle Test Agent" + + def get_agent_info(self): + return orca.printer_agent.AgentInfo( + id="lifecycle-test-agent", + name="Lifecycle Test Agent", + version="1.0.0", + description="Lifecycle test printer agent", + ) + + +@orca.plugin +class LifecycleTestPlugin(orca.base): + def register_capabilities(self): + orca.register_capability(LifecycleTestAgentCapability) +)PY"; + +// In production GUI_App::init_plugin_gui_wiring subscribes the agent-registry +// callbacks. The test binary has no GUI, so install the same unload-side wiring +// once so these tests exercise the production deregister-on-unload path. +// +// The load-side (register) wiring is deliberately not installed: duplicate-id +// coverage registers agents manually in a deterministic order instead. +void install_agent_registry_wiring() +{ + static bool installed = false; + if (installed) + return; + installed = true; + + PluginManager& manager = PluginManager::instance(); + manager.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + manager.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) { + if (capability.type == PluginCapabilityType::PrinterConnection) + NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); + }); +} + +// Duplicate-id coverage needs two distinct plugins with different package keys +// and classes while both return the same AgentInfo.id. +std::string make_agent_plugin_source(const std::string& suffix, + const std::string& display_name, + const std::string& agent_id) +{ + return std::string{} + + "# /// script\n" + + "# requires-python = \">=3.12\"\n" + + "# dependencies = []\n" + + "#\n" + + "# [tool.orcaslicer.plugin]\n" + + "# name = \"" + display_name + "\"\n" + + "# description = \"Duplicate-id fake printer-agent plugin.\"\n" + + "# author = \"tests\"\n" + + "# version = \"1.0.0\"\n" + + "# type = \"printer-connection\"\n" + + "# ///\n" + + "import orca\n" + + "\n\n" + + "class Cap" + suffix + "(orca.printer_agent.PrinterAgentBase):\n" + + " def get_name(self):\n" + + " return \"" + display_name + "\"\n" + + "\n" + + " def get_agent_info(self):\n" + + " return orca.printer_agent.AgentInfo(\n" + + " id=\"" + agent_id + "\", name=\"" + display_name + "\",\n" + + " version=\"1.0.0\", description=\"duplicate id test\")\n" + + "\n\n" + + "@orca.plugin\n" + + "class Plugin" + suffix + "(orca.base):\n" + + " def register_capabilities(self):\n" + + " orca.register_capability(Cap" + suffix + ")\n"; +} + // Writes {data_dir}/orca_plugins//.py and returns the plugin directory. fs::path write_plugin(const ScopedDataDir& data_dir_guard, const std::string& stem, const std::string& source) { @@ -152,6 +243,157 @@ TEST_CASE("A discovered script plugin loads and materializes its capability", "[ manager.unload_plugin("Echo_Plugin"); } +TEST_CASE("A printer-agent plugin registers and deregisters with its lifecycle", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + ScopedDataDir data_dir_guard("printer-agent-load"); + write_plugin(data_dir_guard, "LifecycleTestAgent", PRINTER_AGENT_PLUGIN_SOURCE); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor; + REQUIRE(manager.try_get_valid_plugin_descriptor("LifecycleTestAgent", descriptor)); + + std::string error; + REQUIRE(load_and_wait(manager, "LifecycleTestAgent", error)); + INFO("load error: " << error); + + const auto capabilities = manager.get_plugin_capabilities( + "LifecycleTestAgent", PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities.size() == 1); + NetworkAgentFactory::register_python_printer_agent("LifecycleTestAgent", capabilities.front()->name()); + + CHECK(NetworkAgentFactory::is_printer_agent_registered("lifecycle-test-agent")); + REQUIRE(manager.unload_plugin("LifecycleTestAgent")); + CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered("lifecycle-test-agent")); +} + +TEST_CASE("A duplicate printer-agent id is rejected without clobbering its owner", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + const std::string key_a = "DuplicateIdAgentA"; + const std::string key_b = "DuplicateIdAgentB"; + const std::string dup_id = "duplicate-id-agent"; + + ScopedDataDir data_dir_guard("printer-agent-duplicate"); + write_plugin(data_dir_guard, key_a, make_agent_plugin_source("A", "Duplicate Id Agent A", dup_id)); + write_plugin(data_dir_guard, key_b, make_agent_plugin_source("B", "Duplicate Id Agent B", dup_id)); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor_a; + PluginDescriptor descriptor_b; + REQUIRE(manager.try_get_valid_plugin_descriptor(key_a, descriptor_a)); + REQUIRE(manager.try_get_valid_plugin_descriptor(key_b, descriptor_b)); + + std::string error; + REQUIRE(load_and_wait(manager, key_a, error)); + REQUIRE(load_and_wait(manager, key_b, error)); + + const auto capabilities_a = manager.get_plugin_capabilities(key_a, PluginCapabilityType::PrinterConnection); + const auto capabilities_b = manager.get_plugin_capabilities(key_b, PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities_a.size() == 1); + REQUIRE(capabilities_b.size() == 1); + + NetworkAgentFactory::register_python_printer_agent(key_a, capabilities_a.front()->name()); + NetworkAgentFactory::register_python_printer_agent(key_b, capabilities_b.front()->name()); + + CHECK(NetworkAgentFactory::is_printer_agent_registered(dup_id)); + const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(dup_id); + REQUIRE(info != nullptr); + CHECK(info->plugin_identifier.find(key_a) != std::string::npos); + CHECK(info->plugin_identifier.find(key_b) == std::string::npos); + + REQUIRE(manager.unload_plugin(key_a)); + CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(dup_id)); + manager.unload_plugin(key_b); +} + +TEST_CASE("A printer-agent plugin cannot claim a built-in agent id", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + NetworkAgentFactory::register_all_agents(); + REQUIRE(NetworkAgentFactory::is_printer_agent_registered(BBL_PRINTER_AGENT_ID)); + + const std::string plugin_key = "BuiltinClashAgent"; + ScopedDataDir data_dir_guard("printer-agent-builtin-clash"); + write_plugin(data_dir_guard, plugin_key, + make_agent_plugin_source("Clash", "Builtin Clash", BBL_PRINTER_AGENT_ID)); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor; + REQUIRE(manager.try_get_valid_plugin_descriptor(plugin_key, descriptor)); + + std::string error; + REQUIRE(load_and_wait(manager, plugin_key, error)); + const auto capabilities = manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities.size() == 1); + NetworkAgentFactory::register_python_printer_agent(plugin_key, capabilities.front()->name()); + + const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(BBL_PRINTER_AGENT_ID); + REQUIRE(info != nullptr); + CHECK_FALSE(info->is_plugin()); + CHECK(info->plugin_identifier.find(plugin_key) == std::string::npos); + + manager.unload_plugin(plugin_key); +} + +TEST_CASE("Re-registering a printer-agent capability preserves its registration", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + const std::string plugin_key = "ReRegisterAgent"; + const std::string agent_id = "re-register-agent"; + + ScopedDataDir data_dir_guard("printer-agent-reregister"); + write_plugin(data_dir_guard, plugin_key, + make_agent_plugin_source("Re", "Re Register", agent_id)); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor; + REQUIRE(manager.try_get_valid_plugin_descriptor(plugin_key, descriptor)); + + std::string error; + REQUIRE(load_and_wait(manager, plugin_key, error)); + const auto capabilities = manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities.size() == 1); + + NetworkAgentFactory::register_python_printer_agent(plugin_key, capabilities.front()->name()); + REQUIRE(NetworkAgentFactory::is_printer_agent_registered(agent_id)); + const PrinterAgentInfo* first = NetworkAgentFactory::get_printer_agent_info(agent_id); + REQUIRE(first != nullptr); + const std::string owner = first->plugin_identifier; + + NetworkAgentFactory::register_python_printer_agent(plugin_key, capabilities.front()->name()); + CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id)); + const PrinterAgentInfo* second = NetworkAgentFactory::get_printer_agent_info(agent_id); + REQUIRE(second != nullptr); + CHECK(second->plugin_identifier == owner); + + manager.unload_plugin(plugin_key); +} + TEST_CASE("Plugin manager can initialize again after shutdown", "[PluginLifecycle][Python]") { ScopedPluginManager plugin_system; diff --git a/tests/slic3rutils/test_printer_agent_plugin.cpp b/tests/slic3rutils/test_printer_agent_plugin.cpp deleted file mode 100644 index 70c290a795..0000000000 --- a/tests/slic3rutils/test_printer_agent_plugin.cpp +++ /dev/null @@ -1,414 +0,0 @@ -#include - -#include -#include -#include // for set_data_dir - -#include -#include - -#include - -#include -#include -#include -#include -#include -#include - -using namespace Slic3r; -namespace fs = boost::filesystem; - -namespace { - -// why: embedding the fake plugin keeps this test self-contained. The PEP 723 -// block declares a printer-connection plugin, and the decorated plugin package -// registers one PrinterAgentBase capability whose AgentInfo.id is the registry -// key asserted below. -constexpr const char* kFakePluginSource = R"PY(# /// script -# requires-python = ">=3.12" -# dependencies = [] -# -# [tool.orcaslicer.plugin] -# name = "Lifecycle Test Agent" -# description = "Minimal printer-agent plugin for the lifecycle test." -# author = "tests" -# version = "1.0.0" -# type = "printer-connection" -# /// -import orca - - -class LifecycleTestAgentCapability(orca.printer_agent.PrinterAgentBase): - def get_name(self): - return "Lifecycle Test Agent" - - def get_agent_info(self): - return orca.printer_agent.AgentInfo( - id="lifecycle-test-agent", - name="Lifecycle Test Agent", - version="1.0.0", - description="Lifecycle test printer agent", - ) - - -@orca.plugin -class LifecycleTestPlugin(orca.base): - def register_capabilities(self): - orca.register_capability(LifecycleTestAgentCapability) -)PY"; - -// why: in production GUI_App::init_plugin_gui_wiring subscribes the agent-registry -// callbacks; the test binary has no GUI, so install the same UNLOAD-side wiring -// once so the tests exercise the production deregister-on-unload path. -// note: the load-side (register) wiring is deliberately NOT installed - the two -// concurrent load_plugin calls in the duplicate-id test would race for the id; -// the tests register manually, in a deterministic order, instead. -void install_agent_registry_wiring() -{ - static bool installed = false; - if (installed) - return; - installed = true; - - PluginManager& mgr = PluginManager::instance(); - mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); - mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) { - if (capability.type == PluginCapabilityType::PrinterConnection) - NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); - }); -} - -} // namespace - -// =========================================================================== -// PRINTER-AGENT PLUGIN LIFECYCLE: load, register, unload, deregister. -// -// This test uses its own executable because load_plugin runs on a detached worker -// thread that needs the GIL released on the main thread (the PythonInterpreter -// model). slic3rutils_tests' other Python tests hold the GIL on the main thread -// via a bare scoped_interpreter; the two models can't share one process. -// -// When bundled Python is unavailable in the test environment, the test is -// skipped so source-only or partially staged builds can still run the rest of -// the suite. -// =========================================================================== -TEST_CASE("plugin lifecycle: printer-agent load registers and unload deregisters", "[plugin][lifecycle][Python]") -{ - const std::string plugin_key = "LifecycleTestAgent"; // entry-file stem - const std::string agent_id = "lifecycle-test-agent"; // AgentInfo.id from the plugin - - // Stage a throwaway data directory. Plugins are discovered under - // /orca_plugins, so this controls which plugin is loaded. - const fs::path data_dir = fs::temp_directory_path() / "orca-plugin-lifecycle-test"; - const fs::path plugin_dir = data_dir / "orca_plugins" / plugin_key; - { - boost::system::error_code ec; - fs::remove_all(data_dir, ec); // clear any stale run - } - fs::create_directories(plugin_dir); - { - std::ofstream out((plugin_dir / (plugin_key + ".py")).string(), std::ios::binary); - out << kFakePluginSource; - } - // why: best-effort cleanup even if an assertion throws. - struct DirGuard - { - fs::path p; - ~DirGuard() - { - boost::system::error_code ec; - fs::remove_all(p, ec); - } - } guard{data_dir}; - - Slic3r::set_data_dir(data_dir.string()); - - // Initialize the plugin system on this thread. If the bundled Python home - // is not reachable, skip gracefully. - PluginManager& mgr = PluginManager::instance(); - if (!mgr.initialize()) - SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)"); - install_agent_registry_wiring(); - - // Discover synchronously so the catalog holds the descriptor before loading. - mgr.discover_plugins(/*async=*/false, /*clear=*/true); - INFO("expected plugin_key (entry-file stem): " << plugin_key); - PluginDescriptor descriptor; - REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor)); - - // Load on the worker thread and block until it finishes. - std::string error; - mgr.load_plugin(plugin_key, /*skip_deps=*/true); - const bool loaded = mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error); - INFO("plugin load error: " << error); - REQUIRE(loaded); - REQUIRE(mgr.is_plugin_loaded(plugin_key)); - - // Resolve the one PrinterConnection capability and register it as an agent. - auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); - REQUIRE(caps.size() == 1); - REQUIRE(caps[0] != nullptr); - NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name()); - - // The AgentInfo.id returned by the plugin is now in the registry. - CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id)); - - // Unloading the plugin deregisters its Python-backed agent. - REQUIRE(mgr.unload_plugin(plugin_key)); - - // The registry no longer contains the agent id after unload. - CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(agent_id)); -} - -namespace { - -// why: duplicate-id coverage needs two distinct plugins with different package -// keys and classes while both return the same AgentInfo.id. -std::string make_agent_plugin_source(const std::string& suffix, const std::string& display_name, const std::string& agent_id) -{ - return std::string{} - + "# /// script\n" - + "# requires-python = \">=3.12\"\n" - + "# dependencies = []\n" - + "#\n" - + "# [tool.orcaslicer.plugin]\n" - + "# name = \"" + display_name + "\"\n" - + "# description = \"Duplicate-id fake printer-agent plugin.\"\n" - + "# author = \"tests\"\n" - + "# version = \"1.0.0\"\n" - + "# type = \"printer-connection\"\n" - + "# ///\n" - + "import orca\n" - + "\n\n" - + "class Cap" + suffix + "(orca.printer_agent.PrinterAgentBase):\n" - + " def get_name(self):\n" - + " return \"" + display_name + "\"\n" - + "\n" - + " def get_agent_info(self):\n" - + " return orca.printer_agent.AgentInfo(\n" - + " id=\"" + agent_id + "\", name=\"" + display_name + "\",\n" - + " version=\"1.0.0\", description=\"duplicate id test\")\n" - + "\n\n" - + "@orca.plugin\n" - + "class Plugin" + suffix + "(orca.base):\n" - + " def register_capabilities(self):\n" - + " orca.register_capability(Cap" + suffix + ")\n"; -} - -void stage_plugin(const fs::path& data_dir, const std::string& plugin_key, const std::string& source) -{ - const fs::path dir = data_dir / "orca_plugins" / plugin_key; - fs::create_directories(dir); - std::ofstream out((dir / (plugin_key + ".py")).string(), std::ios::binary); - out << source; -} - -} // namespace - -// =========================================================================== -// DUPLICATE PRINTER-AGENT IDS -// When two loaded plugins return the same AgentInfo.id, the first registration -// keeps ownership. Unloading it removes the id, and the rejected plugin is not -// promoted automatically. Manual re-registration is required. -// =========================================================================== -TEST_CASE("duplicate agent id is rejected and the winner is not clobbered", "[plugin][lifecycle][Python]") -{ - const std::string key_a = "DuplicateIdAgentA"; // winner, registered first - const std::string key_b = "DuplicateIdAgentB"; // duplicate, rejected - const std::string dup_id = "duplicate-id-agent"; // both plugins return this AgentInfo.id - - const fs::path data_dir = fs::temp_directory_path() / "orca-duplicate-agent-test"; - { - boost::system::error_code ec; - fs::remove_all(data_dir, ec); - } - stage_plugin(data_dir, key_a, make_agent_plugin_source("A", "Duplicate Id Agent A", dup_id)); - stage_plugin(data_dir, key_b, make_agent_plugin_source("B", "Duplicate Id Agent B", dup_id)); - struct DirGuard - { - fs::path p; - ~DirGuard() - { - boost::system::error_code ec; - fs::remove_all(p, ec); - } - } guard{data_dir}; - - Slic3r::set_data_dir(data_dir.string()); - - PluginManager& mgr = PluginManager::instance(); - if (!mgr.initialize()) - SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)"); - install_agent_registry_wiring(); - - mgr.discover_plugins(/*async=*/false, /*clear=*/true); - PluginDescriptor descriptor_a; - PluginDescriptor descriptor_b; - REQUIRE(mgr.try_get_valid_plugin_descriptor(key_a, descriptor_a)); - REQUIRE(mgr.try_get_valid_plugin_descriptor(key_b, descriptor_b)); - - std::string error; - mgr.load_plugin(key_a, /*skip_deps=*/true); - mgr.load_plugin(key_b, /*skip_deps=*/true); - const bool loaded_a = mgr.wait_for_plugin_load(key_a, std::chrono::seconds(60), error); - INFO("plugin A load error: " << error); - REQUIRE(loaded_a); - const bool loaded_b = mgr.wait_for_plugin_load(key_b, std::chrono::seconds(60), error); - INFO("plugin B load error: " << error); - REQUIRE(loaded_b); - - auto caps_a = mgr.get_plugin_capabilities(key_a, PluginCapabilityType::PrinterConnection); - auto caps_b = mgr.get_plugin_capabilities(key_b, PluginCapabilityType::PrinterConnection); - REQUIRE(caps_a.size() == 1); - REQUIRE(caps_b.size() == 1); - - // Register A first as the owner, then B with the duplicate id. - NetworkAgentFactory::register_python_printer_agent(key_a, caps_a[0]->name()); - NetworkAgentFactory::register_python_printer_agent(key_b, caps_b[0]->name()); - - // The shared id is registered, and still owned by A; B did not replace it. - CHECK(NetworkAgentFactory::is_printer_agent_registered(dup_id)); - const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(dup_id); - REQUIRE(info != nullptr); - CHECK(info->plugin_identifier.find(key_a) != std::string::npos); // owned by A - CHECK(info->plugin_identifier.find(key_b) == std::string::npos); // B never took ownership - - // Unload the owner. The id is not promoted to the still loaded duplicate. - REQUIRE(mgr.unload_plugin(key_a)); - CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(dup_id)); - - mgr.unload_plugin(key_b); // the duplicate was loaded but never registered -} - -// =========================================================================== -// NATIVE (BUILT-IN) AGENT ID COLLISION -// A plugin may not hijack a built-in agent id (e.g. "bbl"). The built-in keeps -// ownership and the plugin's agent is rejected. -// =========================================================================== -TEST_CASE("printer-agent plugin cannot claim a built-in agent id", "[plugin][lifecycle][Python]") -{ - // Register the native built-ins so "bbl"/"orca" occupy the registry. - NetworkAgentFactory::register_all_agents(); - REQUIRE(NetworkAgentFactory::is_printer_agent_registered(BBL_PRINTER_AGENT_ID)); - - const std::string plugin_key = "BuiltinClashAgent"; - - const fs::path data_dir = fs::temp_directory_path() / "orca-builtin-clash-test"; - { - boost::system::error_code ec; - fs::remove_all(data_dir, ec); - } - stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Clash", "Builtin Clash", BBL_PRINTER_AGENT_ID)); - struct DirGuard - { - fs::path p; - ~DirGuard() - { - boost::system::error_code ec; - fs::remove_all(p, ec); - } - } guard{data_dir}; - - Slic3r::set_data_dir(data_dir.string()); - - PluginManager& mgr = PluginManager::instance(); - if (!mgr.initialize()) - SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)"); - install_agent_registry_wiring(); - - mgr.discover_plugins(/*async=*/false, /*clear=*/true); - PluginDescriptor descriptor; - REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor)); - - std::string error; - mgr.load_plugin(plugin_key, /*skip_deps=*/true); - REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error)); - - auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); - REQUIRE(caps.size() == 1); - NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name()); - - // "bbl" is still the native built-in, not the plugin. - const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(BBL_PRINTER_AGENT_ID); - REQUIRE(info != nullptr); - CHECK_FALSE(info->is_plugin()); - CHECK(info->plugin_identifier.find(plugin_key) == std::string::npos); - - mgr.unload_plugin(plugin_key); -} - -// =========================================================================== -// RE-REGISTERING THE SAME CAPABILITY IS A REFRESH, NOT A DUPLICATE -// The guard rejects only a DIFFERENT owner. The same capability registering its -// own id again must stay registered (insert_or_assign refreshes it in place). -// =========================================================================== -TEST_CASE("re-registering the same capability keeps its agent registered", "[plugin][lifecycle][Python]") -{ - const std::string plugin_key = "ReRegisterAgent"; - const std::string agent_id = "re-register-agent"; - - const fs::path data_dir = fs::temp_directory_path() / "orca-re-register-test"; - { - boost::system::error_code ec; - fs::remove_all(data_dir, ec); - } - stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Re", "Re Register", agent_id)); - struct DirGuard - { - fs::path p; - ~DirGuard() - { - boost::system::error_code ec; - fs::remove_all(p, ec); - } - } guard{data_dir}; - - Slic3r::set_data_dir(data_dir.string()); - - PluginManager& mgr = PluginManager::instance(); - if (!mgr.initialize()) - SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)"); - install_agent_registry_wiring(); - - mgr.discover_plugins(/*async=*/false, /*clear=*/true); - PluginDescriptor descriptor; - REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor)); - - std::string error; - mgr.load_plugin(plugin_key, /*skip_deps=*/true); - REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error)); - - auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); - REQUIRE(caps.size() == 1); - - NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name()); - REQUIRE(NetworkAgentFactory::is_printer_agent_registered(agent_id)); - const PrinterAgentInfo* first = NetworkAgentFactory::get_printer_agent_info(agent_id); - REQUIRE(first != nullptr); - const std::string owner = first->plugin_identifier; - - // The same capability registering again is a refresh: still registered, same owner, not rejected. - NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name()); - CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id)); - const PrinterAgentInfo* second = NetworkAgentFactory::get_printer_agent_info(agent_id); - REQUIRE(second != nullptr); - CHECK(second->plugin_identifier == owner); - - mgr.unload_plugin(plugin_key); -} - -// why: this binary embeds CPython through the PythonInterpreter singleton, which -// lives for the full process. Normal static destruction can tear Python down -// while C++ objects still hold Python handles, producing a Windows heap -// corruption after the assertions have finished. The app has an ordered shutdown -// path; this test harness does not, so it returns the Catch2 result through -// _Exit after flushing output. -int main(int argc, char* argv[]) -{ - const int result = Catch::Session().run(argc, argv); - std::cout.flush(); - std::cerr.flush(); - std::fflush(nullptr); - std::_Exit(result); -}