From 418623e2c567cff4f0c654271ad9d1ece54ef0b6 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 5 Oct 2026 16:37:13 +0800 Subject: [PATCH] chore: remove orcasonar test changes (cherry picked from commit c6d3e755ecc0599e6fce88aa8a6736f9a70eb97f) --- tests/slic3rutils/CMakeLists.txt | 2 - tests/slic3rutils/orca_mqtt_mock_broker.hpp | 325 ------------------ .../slic3rutils/test_orca_mqtt_connection.cpp | 243 ------------- tests/slic3rutils/test_orca_printer_agent.cpp | 204 ----------- 4 files changed, 774 deletions(-) delete mode 100644 tests/slic3rutils/orca_mqtt_mock_broker.hpp delete mode 100644 tests/slic3rutils/test_orca_mqtt_connection.cpp delete mode 100644 tests/slic3rutils/test_orca_printer_agent.cpp diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 7d2087f783..5d0f6eca76 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -26,8 +26,6 @@ add_executable(${_TEST_NAME}_tests test_plugin_status.cpp test_printer_agent.cpp test_qidi_printer_agent.cpp - test_orca_mqtt_connection.cpp - test_orca_printer_agent.cpp test_plugin_install.cpp test_plugin_lifecycle.cpp test_plugin_printer_agent.cpp diff --git a/tests/slic3rutils/orca_mqtt_mock_broker.hpp b/tests/slic3rutils/orca_mqtt_mock_broker.hpp deleted file mode 100644 index 555a4a9a05..0000000000 --- a/tests/slic3rutils/orca_mqtt_mock_broker.hpp +++ /dev/null @@ -1,325 +0,0 @@ -#pragma once - -// In-process plaintext MQTT-over-WebSocket broker for the OrcaMqtt tests. -// -// It speaks just enough of MQTT 3.1.1 to drive OrcaMqttConnection / -// OrcaPrinterAgent end to end without a real network: CONNECT/CONNACK, -// SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK, client PUBLISH (QoS 0), PINGREQ and -// DISCONNECT. The outbound PUBLISH frame is built with the production -// OrcaMqttConnection::make_publish_packet() so the tests never depend on a -// second, hand-rolled MQTT encoder. - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace orca_mqtt_test { - -namespace net = boost::asio; -namespace beast = boost::beast; -namespace ws = boost::beast::websocket; -using tcp = boost::asio::ip::tcp; - -// Decode an MQTT remaining-length varint starting at packet[offset]. -// Returns {value, bytes_consumed}; bytes_consumed == 0 means malformed. -inline std::pair mqtt_decode_remaining_length(const std::string& packet, std::size_t offset) -{ - std::size_t value = 0; - std::size_t multiplier = 1; - std::size_t used = 0; - while (offset + used < packet.size() && used < 4) { - const std::uint8_t byte = static_cast(packet[offset + used]); - value += static_cast(byte & 0x7f) * multiplier; - multiplier *= 128; - ++used; - if ((byte & 0x80) == 0) - return {value, used}; - } - return {0, 0}; -} - -inline bool mqtt_topic_is_request(const std::string& topic) -{ - static const std::string suffix = "/request"; - return topic.size() >= suffix.size() && - topic.compare(topic.size() - suffix.size(), suffix.size(), suffix) == 0; -} - -class MockBroker -{ -public: - // refuse_auth: answer every CONNECT with CONNACK rc 5 (not authorized) and - // close, so the reconnect/refusal paths can be exercised. - explicit MockBroker(bool refuse_auth = false) : m_refuse_auth(refuse_auth), m_acceptor(m_io) - { - const tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), 0); - m_acceptor.open(endpoint.protocol()); - m_acceptor.set_option(net::socket_base::reuse_address(true)); - m_acceptor.bind(endpoint); - m_acceptor.listen(net::socket_base::max_listen_connections); - m_port = std::to_string(m_acceptor.local_endpoint().port()); - // why: a non-blocking acceptor lets the accept loop poll a stop flag, so - // the destructor never has to interrupt a blocking accept(). - m_acceptor.non_blocking(true); - m_thread = std::thread([this] { run(); }); - } - - ~MockBroker() - { - m_stopping.store(true); - drop_client(); // unblocks the worker's blocking read - if (m_thread.joinable()) - m_thread.join(); - boost::system::error_code ec; - m_acceptor.close(ec); // after join: the acceptor is worker-owned - m_io.stop(); - } - - MockBroker(const MockBroker&) = delete; - MockBroker& operator=(const MockBroker&) = delete; - - std::string ws_url() const { return "ws://127.0.0.1:" + m_port + "/mqtt"; } - - std::pair host_port() const { return {std::string("127.0.0.1"), m_port}; } - - // Server -> client PUBLISH on device//report. - void push_report(const std::string& dev_id, const std::string& payload) - { - const std::vector packet = - Slic3r::OrcaMqttConnection::make_publish_packet("device/" + dev_id + "/report", payload); - std::lock_guard lock(m_mutex); - if (!m_stream || !m_stream_ready) - return; - boost::system::error_code ec; - m_stream->binary(true); - m_stream->write(net::buffer(packet), ec); // a vanished client is not a test failure - } - - // Force-close the live client socket; the worker's read returns an error and - // the accept loop picks up the client's reconnect. - void drop_client() - { - std::lock_guard lock(m_mutex); - close_client_locked(); - } - - // Payloads the client PUBLISHed to any device//request topic. - std::vector received_requests() const - { - std::lock_guard lock(m_mutex); - return m_received_requests; - } - - // MQTT CONNECTs seen; increments again after a reconnect. - int connect_count() const { return m_connect_count.load(); } - - // MQTT PINGREQs seen while the client has no other traffic. - int ping_count() const { return m_ping_count.load(); } - -private: - void run() - { - try { - while (!m_stopping.load()) { - tcp::socket socket(m_io); - boost::system::error_code ec; - m_acceptor.accept(socket, ec); - if (ec == net::error::would_block || ec == net::error::try_again) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - continue; - } - if (ec) - return; - try { - serve(std::move(socket)); - } catch (...) { - // a client dying mid-session must not take the broker down - } - std::lock_guard lock(m_mutex); - close_client_locked(); - m_stream.reset(); - } - } catch (...) { - // never let an exception escape the broker thread - } - } - - void serve(tcp::socket socket) - { - ws::stream* stream = nullptr; - { - std::lock_guard lock(m_mutex); - m_stream.emplace(std::move(socket)); - m_stream_ready = false; - stream = &*m_stream; - } - // why: no io_context is ever run here, so a tcp_stream timer would never - // fire; the sync operations below carry no timeout of their own. - beast::get_lowest_layer(*stream).expires_never(); - stream->set_option(ws::stream_base::decorator( - [](ws::response_type& res) { res.set("Sec-WebSocket-Protocol", "mqtt"); })); - - boost::system::error_code ec; - stream->accept(ec); - if (ec) - return; - stream->binary(true); - { - std::lock_guard lock(m_mutex); - m_stream_ready = true; - } - read_loop(*stream); - } - - // The client sends every MQTT packet as one binary WebSocket message, so one - // read yields exactly one packet. - void read_loop(ws::stream& stream) - { - beast::flat_buffer buffer; - while (!m_stopping.load()) { - boost::system::error_code ec; - buffer.clear(); - stream.read(buffer, ec); - if (ec) - return; - const std::string packet = beast::buffers_to_string(buffer.data()); - if (packet.empty()) - continue; - if (!handle_packet(stream, packet)) - return; - } - } - - // Returns false when the session must be closed. - bool handle_packet(ws::stream& stream, const std::string& packet) - { - switch (static_cast(packet[0]) & 0xf0) { - case 0x10: { // CONNECT - ++m_connect_count; - if (m_refuse_auth) { - write_packet(stream, {0x20, 0x02, 0x00, 0x05}); // CONNACK not authorized - return false; - } - write_packet(stream, {0x20, 0x02, 0x00, 0x00}); // CONNACK accepted - return true; - } - case 0x80: { // SUBSCRIBE (0x82) - packet id follows the remaining-length varint - const auto id = packet_id(packet); - if (id) - write_packet(stream, {0x90, 0x03, id->first, id->second, 0x00}); // SUBACK, QoS 0 - return true; - } - case 0xa0: { // UNSUBSCRIBE (0xa2) - const auto id = packet_id(packet); - if (id) - write_packet(stream, {0xb0, 0x02, id->first, id->second}); // UNSUBACK - return true; - } - case 0x30: { // PUBLISH, QoS 0 (no packet identifier) - record_publish(packet); - return true; - } - case 0xc0: // PINGREQ - ++m_ping_count; - write_packet(stream, {0xd0, 0x00}); - return true; - case 0xe0: // DISCONNECT - return false; - default: - return true; - } - } - - // The two packet-identifier bytes sitting right after the remaining-length varint. - static std::optional> packet_id(const std::string& packet) - { - const auto varint = mqtt_decode_remaining_length(packet, 1); - if (varint.second == 0) - return std::nullopt; - const std::size_t pos = 1 + varint.second; - if (pos + 2 > packet.size()) - return std::nullopt; - return std::make_pair(static_cast(packet[pos]), static_cast(packet[pos + 1])); - } - - void record_publish(const std::string& packet) - { - const auto varint = mqtt_decode_remaining_length(packet, 1); - if (varint.second == 0) - return; - std::size_t pos = 1 + varint.second; - if (pos + 2 > packet.size()) - return; - const std::size_t topic_len = (static_cast(static_cast(packet[pos])) << 8) | - static_cast(packet[pos + 1]); - pos += 2; - if (pos + topic_len > packet.size()) - return; - const std::string topic = packet.substr(pos, topic_len); - pos += topic_len; - const std::size_t end = std::min(packet.size(), 1 + varint.second + varint.first); - if (end < pos) - return; - if (!mqtt_topic_is_request(topic)) - return; - std::lock_guard lock(m_mutex); - m_received_requests.push_back(packet.substr(pos, end - pos)); - } - - // Every write - the worker's own replies and push_report() from the test - // thread - is serialised by m_mutex. The production client keeps all of its - // WebSocket operations on its MQTT worker instead. - void write_packet(ws::stream& stream, const std::vector& packet) - { - std::lock_guard lock(m_mutex); - boost::system::error_code ec; - stream.binary(true); - stream.write(net::buffer(packet), ec); - } - - void close_client_locked() - { - if (!m_stream) - return; - m_stream_ready = false; - boost::system::error_code ec; - auto& socket = beast::get_lowest_layer(*m_stream).socket(); - if (socket.cancel(ec)) - return; - // shutdown() before close() is what actually wakes a blocking read on the - // worker thread; close() alone does not on POSIX. - if (socket.shutdown(tcp::socket::shutdown_both, ec)) - return; - if (socket.close(ec)) - return; - } - - const bool m_refuse_auth; - net::io_context m_io; - tcp::acceptor m_acceptor; - std::string m_port; - std::thread m_thread; - std::atomic_bool m_stopping{false}; - std::atomic m_connect_count{0}; - std::atomic m_ping_count{0}; - mutable std::mutex m_mutex; - std::optional> m_stream; // guarded by m_mutex - bool m_stream_ready = false; // guarded by m_mutex - std::vector m_received_requests; // guarded by m_mutex -}; - -} // namespace orca_mqtt_test diff --git a/tests/slic3rutils/test_orca_mqtt_connection.cpp b/tests/slic3rutils/test_orca_mqtt_connection.cpp deleted file mode 100644 index 1bb4983901..0000000000 --- a/tests/slic3rutils/test_orca_mqtt_connection.cpp +++ /dev/null @@ -1,243 +0,0 @@ -#include -#include - -#include "orca_mqtt_mock_broker.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -using Slic3r::OrcaMqttConnection; - -// Offset of the CONNECT variable header: 1 (fixed header) + N remaining-length varint bytes. -static size_t mqtt_varheader_offset(const std::vector& p) { - size_t i = 1; - while (i < p.size() && (p[i] & 0x80)) ++i; // skip varint continuation bytes - return i + 1; // + the final varint byte -} - -TEST_CASE("OrcaMqtt parse_endpoint handles ws and wss", "[OrcaMqtt]") { - OrcaMqttConnection::Endpoint ep; - - REQUIRE(OrcaMqttConnection::parse_endpoint("ws://printer.local:8280/mqtt", ep)); - CHECK(ep.host == "printer.local"); - CHECK(ep.port == "8280"); - CHECK(ep.target == "/mqtt"); - - REQUIRE(OrcaMqttConnection::parse_endpoint("ws://10.0.0.5/mqtt", ep)); - CHECK(ep.port == "80"); - - REQUIRE(OrcaMqttConnection::parse_endpoint("wss://api.example.com/api/v1/printers/abc/mqtt", ep)); - CHECK(ep.host == "api.example.com"); - CHECK(ep.port == "443"); - CHECK(ep.target == "/api/v1/printers/abc/mqtt"); - - CHECK_FALSE(OrcaMqttConnection::parse_endpoint("http://x/y", ep)); -} - -TEST_CASE("OrcaMqtt CONNECT packet - no auth (cloud form)", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_connect_packet("OrcaSlicer", "", "", 300); - REQUIRE(p.size() >= 12); - CHECK(p[0] == 0x10); // CONNECT fixed header - const size_t v = mqtt_varheader_offset(p); - CHECK(p[v + 0] == 0x00); CHECK(p[v + 1] == 0x04); // protocol name length - CHECK(p[v + 2] == 'M'); CHECK(p[v + 3] == 'Q'); - CHECK(p[v + 4] == 'T'); CHECK(p[v + 5] == 'T'); - CHECK(p[v + 6] == 0x04); // protocol level 3.1.1 - CHECK(p[v + 7] == 0x02); // connect flags: clean session only - CHECK(((p[v + 8] << 8) | p[v + 9]) == 300); // keepalive -} - -TEST_CASE("OrcaMqtt CONNECT packet - username/password (LAN form)", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_connect_packet("orcaslicer-lan-x", "orcasonar", "code123", 60); - CHECK(p[0] == 0x10); - const size_t v = mqtt_varheader_offset(p); - CHECK(p[v + 7] == (0x02 | 0x80 | 0x40)); // clean session + username + password flags - const std::string blob(p.begin(), p.end()); - CHECK(blob.find("orcaslicer-lan-x") != std::string::npos); - CHECK(blob.find("orcasonar") != std::string::npos); - CHECK(blob.find("code123") != std::string::npos); -} - -// Auth precedence (spec O3): when a bearer_provider is configured, connect_and_read -// passes empty CONNECT credentials, so the packet must carry clean-session only and -// no username/password flags or payload fields. (The precedence branch itself lives -// in connect_and_read; the [.integration] cloud-style round trip exercises it live.) -TEST_CASE("OrcaMqtt CONNECT omits creds when a bearer is configured", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_connect_packet("cid", "", "", 60); - const size_t v = mqtt_varheader_offset(p); - CHECK(p[v + 7] == 0x02); // clean session only: no 0x80 / 0x40 - const std::string blob(p.begin(), p.end()); - CHECK(blob.find("orcasonar") == std::string::npos); -} - -TEST_CASE("OrcaMqtt topic helpers", "[OrcaMqtt]") { - CHECK(OrcaMqttConnection::request_topic("abc") == "device/abc/request"); - CHECK(OrcaMqttConnection::report_topic("abc") == "device/abc/report"); -} - -TEST_CASE("OrcaMqtt PUBLISH packet QoS0", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_publish_packet("device/abc/request", "{\"ok\":1}"); - CHECK((p[0] & 0xf0) == 0x30); // PUBLISH - CHECK((p[0] & 0x06) == 0x00); // QoS 0 - const std::string blob(p.begin(), p.end()); - CHECK(blob.find("device/abc/request") != std::string::npos); - CHECK(blob.find("{\"ok\":1}") != std::string::npos); -} - -TEST_CASE("OrcaMqtt SUBSCRIBE packet", "[OrcaMqtt]") { - auto p = OrcaMqttConnection::make_subscribe_packet(7, "device/abc/report", 1); - CHECK(p[0] == 0x82); // SUBSCRIBE + reserved bit - const size_t v = mqtt_varheader_offset(p); - CHECK(((p[v] << 8) | p[v + 1]) == 7); // packet id - CHECK(p.back() == 1); // requested QoS -} - -TEST_CASE("OrcaMqtt send_request refuses when not connected", "[OrcaMqtt]") { - OrcaMqttConnection conn; - CHECK_FALSE(conn.send_request("abc", "{\"pushing\":{\"command\":\"pushall\",\"sequence_id\":\"20001\"}}")); -} - -TEST_CASE("OrcaMqtt start takes a Config", "[OrcaMqtt]") { - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; - cfg.url = "ws://127.0.0.1:1/mqtt"; // nothing listening - cfg.keepalive_seconds = 42; - // start() returns false (no server) but must compile with the Config overload - const bool ok = conn.start(cfg, [](auto, auto){}, [](bool, bool){}); - CHECK_FALSE(ok); - CHECK(conn.last_connack_rc() == -1); - conn.stop(); -} - -TEST_CASE("MockBroker starts and reports a url", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker b; - CHECK(b.ws_url().rfind("ws://127.0.0.1:", 0) == 0); - CHECK(b.connect_count() == 0); -} - -// --- End-to-end integration: OrcaMqttConnection against the in-process MockBroker. -// All hidden behind [.integration] (run explicitly). These prove a LAN-style config -// (CONNECT username/password) and a cloud-style config (bearer on the WS upgrade, -// no CONNECT creds) drive the *same* OrcaMqttConnection code path with identical -// assertions. - -static void run_round_trip(bool use_tls_flag_only) { - orca_mqtt_test::MockBroker broker; - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; - cfg.url = broker.ws_url(); // plaintext regardless - cfg.use_tls = false; // the mock is plaintext; the flag path is unit-tested elsewhere - if (use_tls_flag_only) cfg.bearer_provider = []{ return std::string("tok"); }; - else { cfg.username = "orcasonar"; cfg.password = "code"; } - - // A mutex + condition_variable rather than a promise: the handler runs on the MQTT - // worker thread and a second inbound message would throw std::future_error there. - std::mutex got_mutex; - std::condition_variable got_cv; - bool got_any = false; - std::string got_id, got_payload; - - REQUIRE(conn.start(cfg, - [&](const std::string& id, const std::string& payload){ - { - std::lock_guard l(got_mutex); - if (got_any) return; // keep the first message only - got_any = true; got_id = id; got_payload = payload; - } - got_cv.notify_all(); - }, - [](bool,bool){})); - REQUIRE(conn.subscribe("dev-1")); - REQUIRE(conn.send_request("dev-1", R"({"pushing":{"command":"pushall","sequence_id":"20001"}})")); - - broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20001","result":"success"}})"); - std::string id, payload; - { - std::unique_lock l(got_mutex); - REQUIRE(got_cv.wait_for(l, std::chrono::seconds(3), [&]{ return got_any; })); - id = got_id; payload = got_payload; - } - CHECK(id == "dev-1"); - CHECK(payload.find("push_status") != std::string::npos); - - // the client's command reached the broker on the request topic. The mock records - // the PUBLISH on its own read-loop thread, so poll rather than check immediately. - std::vector reqs; - for (int i = 0; i < 200; ++i) { - reqs = broker.received_requests(); - if (!reqs.empty()) break; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - REQUIRE(reqs.size() >= 1); - CHECK(reqs.front().find("pushall") != std::string::npos); - conn.stop(); -} - -TEST_CASE("OrcaMqtt round-trip — LAN-style config", "[OrcaMqtt][.integration]") { run_round_trip(false); } -TEST_CASE("OrcaMqtt round-trip — cloud-style config", "[OrcaMqtt][.integration]") { run_round_trip(true); } - -TEST_CASE("OrcaMqtt keepalive runs while the connection is idle", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker broker; - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; - cfg.url = broker.ws_url(); - cfg.keepalive_seconds = 2; - - REQUIRE(conn.start(cfg, [](const std::string&, const std::string&) {}, [](bool, bool) {})); - for (int i = 0; i < 200 && broker.ping_count() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - CHECK(broker.ping_count() > 0); - conn.stop(); -} - -TEST_CASE("OrcaMqtt reconnects and re-subscribes after a socket drop", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker broker; - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "p"; - - std::mutex m; std::vector got; - REQUIRE(conn.start(cfg, - [&](const std::string&, const std::string& p){ std::lock_guard l(m); got.push_back(p); }, - [](bool,bool){})); - REQUIRE(conn.subscribe("dev-1")); - - broker.drop_client(); - // the worker reconnects with ~1s backoff - for (int i = 0; i < 300 && broker.connect_count() < 2; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - CHECK(broker.connect_count() >= 2); - - // a report after the reconnect must still be delivered -> the SUBSCRIBE was re-sent - broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20002"}})"); - bool delivered = false; - for (int i = 0; i < 200 && !delivered; ++i) { - { std::lock_guard l(m); delivered = !got.empty(); } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - CHECK(delivered); - conn.stop(); -} - -TEST_CASE("OrcaMqtt auth rejection is terminal (no retry storm)", "[OrcaMqtt][.integration]") { - orca_mqtt_test::MockBroker broker(/*refuse_auth=*/true); - OrcaMqttConnection conn; - OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "bad"; - - const bool ok = conn.start(cfg, [](const std::string&, const std::string&){}, [](bool,bool){}); - CHECK_FALSE(ok); - CHECK(conn.last_connack_rc() == 5); - // worker must have stopped itself (rc 5 is terminal) — give it a moment - for (int i = 0; i < 100 && conn.is_running(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - CHECK_FALSE(conn.is_running()); - // and it must NOT have hammered the broker with retries - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - CHECK(broker.connect_count() <= 2); - conn.stop(); -} diff --git a/tests/slic3rutils/test_orca_printer_agent.cpp b/tests/slic3rutils/test_orca_printer_agent.cpp deleted file mode 100644 index 7ffc4f3d2f..0000000000 --- a/tests/slic3rutils/test_orca_printer_agent.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include -#include -#include - -#include "orca_mqtt_mock_broker.hpp" - -#include -#include -#include -#include -#include -#include - -using Slic3r::OrcaPrinterAgent; - -namespace { -// Probe exposes the protected internals the tests drive. -struct Probe : OrcaPrinterAgent { - using OrcaPrinterAgent::OrcaPrinterAgent; - using OrcaPrinterAgent::deliver_to_sink; - using OrcaPrinterAgent::parse_lan_endpoint; - using OrcaPrinterAgent::make_lan_client_id; - using OrcaPrinterAgent::lan_connection_target; -}; -} - -TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaPrinterAgent]") { - Probe agent("/tmp"); - std::string got_id, got_payload; - agent.set_on_message_fn([&](std::string id, std::string p){ got_id = std::move(id); got_payload = std::move(p); }); - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})", /*local=*/false); - CHECK(got_id == "dev-1"); - CHECK(got_payload.find("push_status") != std::string::npos); -} - -TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto push_status frames", "[OrcaPrinterAgent]") { - Probe agent("/tmp"); - std::string last_payload; - agent.set_on_message_fn([&](std::string, std::string p){ last_payload = std::move(p); }); - - // Before any capabilities reply, a push_status frame is forwarded untouched. - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})", /*local=*/false); - CHECK(last_payload.find("nozzle_diameter") == std::string::npos); - - // The get_capabilities reply is forwarded verbatim; its topology nozzle diameter - // is cached for the device. - agent.deliver_to_sink( - "dev-1", - R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})", - /*local=*/false); - CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos); - CHECK(last_payload.find("\"print\"") == std::string::npos); - - // Later push_status frames for that device get the cached diameter plus a neutral - // nozzle_type, so MachineObject::parse_json's legacy nozzle parser can run. - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":20}})", /*local=*/false); - CHECK(last_payload.find("\"nozzle_diameter\":0.4") != std::string::npos); - CHECK(last_payload.find("\"nozzle_type\":\"N/A\"") != std::string::npos); - - // A different device is unaffected. - agent.deliver_to_sink("dev-2", R"({"print":{"command":"push_status"}})", /*local=*/false); - CHECK(last_payload.find("nozzle_diameter") == std::string::npos); - - // A frame that already carries real nozzle data is not overridden. - agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false); - CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos); - CHECK(last_payload.find("N/A") == std::string::npos); -} - -TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") { - std::string h, p; - REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p)); - CHECK(h == "192.168.1.9"); CHECK(p == "8280"); - REQUIRE(Probe::parse_lan_endpoint("http://host.local:9000/x", h, p)); - CHECK(h == "host.local"); CHECK(p == "9000"); - CHECK_FALSE(Probe::parse_lan_endpoint("", h, p)); -} - -TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaPrinterAgent]") { - const auto a = Probe::make_lan_client_id("dev-1"); - const auto b = Probe::make_lan_client_id("dev-1"); - CHECK(a == b); // drawn once per process - CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0); -} - -TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") { - Probe agent("/tmp"); - const int rc = agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false); - CHECK(rc == BAMBU_NETWORK_SUCCESS); - CHECK(agent.lan_connection_target() == "ws://10.255.255.1:8280/mqtt"); - CHECK(agent.get_user_selected_machine().empty()); // LAN path must not touch the cloud selection - agent.disconnect_printer(); -} - -TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") { - struct SeqProbe : OrcaPrinterAgent { - using OrcaPrinterAgent::OrcaPrinterAgent; - std::vector calls; - void emit_connect_sequence(const std::string& dev_id, - std::function /*sub*/, - std::function /*req*/) override { - OrcaPrinterAgent::emit_connect_sequence(dev_id, - [&](const std::string& id){ calls.push_back("sub:" + id); }, - [&](const std::string& body){ calls.push_back(body); }); - } - } probe("/tmp"); - probe.run_connect_sequence_for_test("dev-1"); - REQUIRE(probe.calls.size() == 5); - CHECK(probe.calls[0] == "sub:dev-1"); - CHECK(probe.calls[1].find("\"pushing\"") != std::string::npos); - CHECK(probe.calls[1].find("\"start\"") != std::string::npos); - CHECK(probe.calls[2].find("pushall") != std::string::npos); - CHECK(probe.calls[3].find("get_version") != std::string::npos); - CHECK(probe.calls[4].find("get_capabilities") != std::string::npos); - for (auto& c : probe.calls) - if (auto pos = c.find("sequence_id"); pos != std::string::npos) - CHECK(c.substr(pos).find("\"2") != std::string::npos); -} - -// Hidden: spawns the connect worker and attempts a real (failing) connect. -TEST_CASE("selecting a cloud printer configures the fleet socket", "[OrcaPrinterAgent][.integration]") { - auto cloud = std::make_shared("/tmp"); - cloud->set_api_base_url("api.example.com"); - OrcaPrinterAgent agent("/tmp"); - agent.set_cloud_agent(cloud); - - agent.set_user_selected_machine("printer-uuid-1"); - // The configure runs on the connect worker; poll rather than racing it. - std::string url; - for (int i = 0; i < 300; ++i) { - url = cloud->selected_printer_mqtt_url(); - if (!url.empty()) break; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - CHECK(url == "wss://api.example.com/api/v1/printers/mqtt"); - - agent.set_user_selected_machine(""); // selection changes do not tear down the fleet socket - CHECK(cloud->selected_printer_mqtt_url() == "wss://api.example.com/api/v1/printers/mqtt"); -} - -TEST_CASE("a stale-generation inbound message is dropped", "[OrcaPrinterAgent]") { - struct GenProbe : OrcaPrinterAgent { - using OrcaPrinterAgent::OrcaPrinterAgent; - using OrcaPrinterAgent::make_lan_message_handler; // expose for the test - }; - GenProbe agent("/tmp"); - int hits = 0; - agent.set_on_message_fn([&](std::string, std::string){ ++hits; }); - auto handler_gen1 = agent.make_lan_message_handler(/*generation=*/1); - // m_lan_generation starts at 0; two bumps -> 2, so the epoch-1 handler is stale. - agent.bump_lan_generation_for_test(); - agent.bump_lan_generation_for_test(); - handler_gen1("dev-1", "{}"); // late callback from gen 1 - CHECK(hits == 0); -} - -TEST_CASE("connect_server does not start an MQTT socket", "[OrcaCloud]") { - auto cloud = std::make_shared("/tmp"); - cloud->set_api_base_url("127.0.0.1:1"); // no session -> connect_server short-circuits before any probe - cloud->connect_server(); - REQUIRE(cloud->get_mqtt_connection() != nullptr); // created in the ctor - CHECK_FALSE(cloud->get_mqtt_connection()->is_running()); // never started - CHECK(cloud->selected_printer_mqtt_url().empty()); -} - -TEST_CASE("send_message* reject when there is no connection", "[OrcaPrinterAgent]") { - OrcaPrinterAgent agent("/tmp"); // no cloud agent, no LAN connection - CHECK(agent.send_message("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); - CHECK(agent.send_message_to_printer("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); - CHECK(agent.send_message("", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); // empty dev_id -} - -TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrinterAgent][.integration]") { - orca_mqtt_test::MockBroker broker; - OrcaPrinterAgent agent("/tmp"); - const auto ep = broker.host_port(); - agent.connect_printer("dev-1", ep.first + ":" + ep.second, "orcasonar", "code", false); - - for (int i = 0; i < 150 && broker.connect_count() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - REQUIRE(broker.connect_count() >= 1); - - CHECK(agent.send_message_to_printer("dev-1", R"({"print":{"command":"pause","sequence_id":"20007"}})", 0, 0) - == BAMBU_NETWORK_SUCCESS); - - // on_connected also publishes 4 requests; poll until "pause" specifically shows up. - bool saw_pause = false; - for (int i = 0; i < 150 && !saw_pause; ++i) { - for (const auto& r : broker.received_requests()) - if (r.find("pause") != std::string::npos) { saw_pause = true; break; } - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - } - CHECK(saw_pause); - agent.disconnect_printer(); -} - -TEST_CASE("destroying an agent mid-connect does not hang or crash", "[OrcaPrinterAgent]") { - for (int i = 0; i < 20; ++i) { - auto agent = std::make_unique("/tmp"); - agent->connect_printer("dev-1", "127.0.0.1:1", "orcasonar", "code", false); // nothing listening: instant ECONNREFUSED - agent.reset(); // ~OrcaPrinterAgent must stop the conn, join the thread, and not hang/crash - } - SUCCEED(); -}