#include #include #include #include #include #include #include #include #include using namespace Slic3r; namespace py = pybind11; // why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space. TEST_CASE("unit: BBL AMS gcode builders preserve command bytes", "[unit][bbl]") { CHECK(BBLPrinterAgent::ams_refresh_rfid_gcode("123") == "M620 R123 \n"); CHECK(BBLPrinterAgent::ams_calibrate_gcode(123) == "M620 C123 \n"); CHECK(BBLPrinterAgent::ams_select_tray_gcode("123") == "M620 P123 \n"); } // why: an agent without a Bambu-dialect translation must refuse these commands before any network or wx path. TEST_CASE("unit: default AMS commands report not supported", "[unit][moonraker]") { MoonrakerPrinterAgent agent(""); CHECK(agent.command_ams_refresh_rfid("dev", "123", 1, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); CHECK(agent.command_ams_calibrate("dev", 1, 2, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); CHECK(agent.command_ams_select_tray("dev", "123", 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); } TEST_CASE("unit: Moonraker light name matching", "[unit][moonraker]") { CHECK(moonraker_is_light_name("caselight")); CHECK(moonraker_is_light_name("LED_STRIP")); CHECK_FALSE(moonraker_is_light_name("beeper")); CHECK(moonraker_is_light_name("FLASHLIGHT_SWITCH")); CHECK(moonraker_is_light_name("MODLELIGHT_SWITCH")); } // =========================================================================== // UNIT - handle_request's not-supported default. // The agent is the only thing that knows what it can translate, so an untranslated // command has to say so instead of returning success and letting the UI believe the // control worked. Guards the inverse too: the pushing namespace is genuinely // satisfied by the websocket status stream, and it re-fires from the keepalive timer // roughly once a second, so it must stay a success or it would raise a dialog on a // timer. Only branches that touch neither the network nor wx are exercised. // =========================================================================== TEST_CASE("unit: Moonraker reports untranslated commands as not supported", "[unit][moonraker]") { MoonrakerPrinterAgent agent(""); CHECK(agent.send_message("dev", R"({"print":{"command":"ams_change_filament"}})", 0, 0) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); CHECK(agent.send_message("dev", R"({"system":{"command":"set_door_stat"}})", 0, 0) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); CHECK(agent.send_message("dev", R"({"xcam":{"command":"xcam_control_set"}})", 0, 0) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); CHECK(agent.send_message("dev", R"({"pushing":{"command":"pushall"}})", 0, 0) == BAMBU_NETWORK_SUCCESS); CHECK(agent.send_message("dev", R"({"pushing":{"command":"start"}})", 0, 0) == BAMBU_NETWORK_SUCCESS); // why: malformed input is a different failure than an untranslated command, and the // default must not swallow it into a misleading not-supported verdict. CHECK(agent.send_message("dev", "{not json", 0, 0) == BAMBU_NETWORK_ERR_INVALID_RESULT); } // =========================================================================== // UNIT - printer-agent registry duplicate handling. // Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in // or previously registered agent. // =========================================================================== TEST_CASE("unit: printer-agent registry register / lookup / duplicate-reject", "[registry][unit]") { // why: the registry is process-global state shared by the test binary, and // Catch2 may run cases in any order. Use an id that cannot collide with // built-ins or other cases. Avoid SECTIONs because each section re-runs the // body and would register the same id twice. const std::string id = "orca-test::registry-probe-7f3a"; auto stub_factory = [](std::shared_ptr, const std::string&) -> std::shared_ptr { return nullptr; }; REQUIRE_FALSE(NetworkAgentFactory::is_printer_agent_registered(id)); REQUIRE(NetworkAgentFactory::register_printer_agent(id, "Registry Probe", stub_factory)); REQUIRE(NetworkAgentFactory::is_printer_agent_registered(id)); // Re-registering the same id is rejected and does not replace the entry. REQUIRE_FALSE(NetworkAgentFactory::register_printer_agent(id, "Impostor", stub_factory)); // The first registration's display name survives the rejected duplicate. const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(id); REQUIRE(info != nullptr); CHECK(info->display_name == "Registry Probe"); // It appears exactly once in the UI-population list. auto agents = NetworkAgentFactory::get_registered_printer_agents(); int count = 0; for (const auto& a : agents) if (a.id == id) ++count; CHECK(count == 1); } // =========================================================================== // INTEGRATION - the orca.printer_agent Python binding surface. // Boots the embedded interpreter and asserts the C++ to Python contract that // every printer-agent plugin subclasses. If a binding is renamed or removed, // plugins fail at runtime even though C++ still compiles. // =========================================================================== namespace { void ensure_python_initialized() { // why: the `orca` module is embedded in this binary, so a bare interpreter // can import it without a bundled Python home. The app interpreter expects // that deployed layout, which is not present beside this test binary. if (!Py_IsInitialized()) { static py::scoped_interpreter interpreter; (void) interpreter; } } py::module_ import_orca_module() { ensure_python_initialized(); // Force PythonPluginBridge.cpp into the binary so the embedded // PYBIND11_EMBEDDED_MODULE(orca, ...) registration (incl. printer_agent) exists. (void) Slic3r::PythonPluginBridge::instance(); return py::module_::import("orca"); } } // namespace TEST_CASE("integration: orca.printer_agent binding surface", "[integration][Python]") { py::module_ orca = import_orca_module(); REQUIRE(py::hasattr(orca, "printer_agent")); py::object pa = orca.attr("printer_agent"); // The base class every printer-agent plugin subclasses. REQUIRE(py::hasattr(pa, "PrinterAgentBase")); py::object base = pa.attr("PrinterAgentBase"); for (const char* method : { "get_agent_info", "connect_printer", "disconnect_printer", "send_message", "start_discovery", "bind_detect", "start_print", "get_filament_sync_mode" }) { CAPTURE(method); CHECK(py::hasattr(base, method)); } // AgentInfo value type - the registry identity the host reads (id is the key). REQUIRE(py::hasattr(pa, "AgentInfo")); py::object info = pa.attr("AgentInfo")("moonraker", "Moonraker", "1.0", "test agent"); CHECK(info.attr("id").cast() == "moonraker"); CHECK(info.attr("name").cast() == "Moonraker"); // FilamentSyncMode enum the host queries to pick pull vs subscription. REQUIRE(py::hasattr(pa, "FilamentSyncMode")); py::object mode = pa.attr("FilamentSyncMode"); CHECK(py::hasattr(mode, "Pull")); CHECK(py::hasattr(mode, "Subscription")); CHECK(py::hasattr(mode, "None_")); // Plugin-type enum exposed at module root (host reads it without the GIL). CHECK(py::hasattr(orca, "PluginType")); } // =========================================================================== // INTEGRATION - plugin-registration API and discovery-context guards. // These are the symbols every plugin package uses: the @orca.plugin decorator, // orca.base, orca.register_capability, and the capability base modules. Checking // them in the lightweight embedded-interpreter test catches binding breakage // before the plugin-loader test needs to run. // =========================================================================== TEST_CASE("integration: orca plugin-registration API surface + discovery-context guards", "[integration][Python]") { py::module_ orca = import_orca_module(); // Module-level surface every plugin package relies on. // note: no "gcode" module here - this branch has no G-code capability module; // PostProcessing exists only as a PluginType value. for (const char* name : { "plugin", "register_capability", "base", "PythonPluginBase", "PluginType", "PluginResult", "script", "printer_agent", "host" }) { CAPTURE(name); CHECK(py::hasattr(orca, name)); } // Plugin package base and capability base contract. CHECK(py::hasattr(orca.attr("base"), "register_capabilities")); py::object cap_base = orca.attr("PythonPluginBase"); for (const char* method : { "get_name", "get_type", "on_load", "on_unload" }) { CAPTURE(method); CHECK(py::hasattr(cap_base, method)); } // The script capability module exposes its own base class. CHECK(py::hasattr(orca.attr("script"), "ScriptPluginCapabilityBase")); // PluginType enum carries the values that route a capability, including PrinterConnection. // note: no PostProcessing value on this branch's binding. py::object types = orca.attr("PluginType"); for (const char* value : { "PrinterConnection", "Script" }) { CAPTURE(value); CHECK(py::hasattr(types, value)); } // note: this is testing behavior, not normal plugin loading. // These APIs should only work while Orca is actively loading a plugin. try { // Calls Python's orca.register_capability(0) from C++. // 0 is intentionally bogus. The important part is that there is no active // plugin load context, so the function should reject the call immediately. orca.attr("register_capability")(py::int_(0)); // If the call above does NOT throw, the test fails here. FAIL("register_capability outside discovery context must raise"); } catch (const py::error_already_set& error) { // pybind11 wraps Python exceptions as py::error_already_set. // This checks the Python exception type is ValueError. CHECK(error.matches(PyExc_ValueError)); } try { // This is the function behind @orca.plugin. // Same logic as above. orca.attr("plugin")(py::int_(0)); FAIL("@orca.plugin outside discovery context must raise"); } catch (const py::error_already_set& error) { CHECK(error.matches(PyExc_ValueError)); } }