Merge branch 'main' into feat/generic-n-extruders

This commit is contained in:
Ian Chua committed 2026-10-15 16:04:30 +08:00
commit 46096416cd
473 files changed
+127462 -21603

No files matched your search

+5
View File
@@ -6,7 +6,12 @@ add_executable(${_TEST_NAME}_tests
test_creality_cfs_match.cpp
test_dev_mapping.cpp
test_dev_extder_system.cpp
test_device_access.cpp
test_moonraker_discovery.cpp
test_network_discovery.cpp
test_filament_bitmap_utils.cpp
test_colour_picker_field.cpp
test_filament_color_picker.cpp
test_device_progress.cpp
test_device_manager_integration.cpp
test_web_media_controller.cpp
+1 -1
View File
@@ -255,7 +255,7 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands"
const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts");
REQUIRE(first != nullptr);
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test",
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_migrate_filament_presets", "help_troubleshoot", "help_network_test",
"help_benchmark_3d_scene", "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki",
"open_youtube"}) {
const Slic3r::GUI::NativeCommand* c = find(key);
@@ -0,0 +1,47 @@
#include "slic3r/GUI/Field.hpp"
#include "libslic3r/Config.hpp"
#include <boost/any.hpp>
#include <catch2/catch_test_macros.hpp>
#include <string>
#include <wx/colour.h>
#include <wx/string.h>
using namespace Slic3r;
using namespace Slic3r::GUI;
namespace {
class TestColourPicker : public ColourPicker {
public:
TestColourPicker() : ColourPicker(ConfigOptionDef{}, "filament_colour") {}
std::string value() { return boost::any_cast<std::string>(get_value()); }
};
}
TEST_CASE("Undefined color fields remain distinct from opaque black", "[ColourPickerField]")
{
TestColourPicker field;
REQUIRE(field.value().empty());
field.set_value(std::string("#000000"));
REQUIRE(field.value() == "#000000");
field.set_value(std::string("invalid color"));
REQUIRE(field.value().empty());
field.set_value(boost::any(wxString("#123456")));
REQUIRE(field.value() == "#123456");
field.set_value(std::string());
REQUIRE(field.value().empty());
}
TEST_CASE("Programmatic color field assignment stays silent", "[ColourPickerField]")
{
TestColourPicker field;
int changes = 0;
field.m_on_change = [&](const std::string&, const boost::any&) { ++changes; };
field.set_value(std::string("#123456"), false);
field.set_value(boost::any(wxString("#654321")), true);
REQUIRE(changes == 0);
REQUIRE(field.value() == "#654321");
field.m_disable_change_event = true;
field.set_value(std::string("#FF0000"));
REQUIRE(field.value() == "#FF0000");
REQUIRE(changes == 0);
}
+128
View File
@@ -0,0 +1,128 @@
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <memory>
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
using namespace Slic3r;
TEST_CASE("Moonraker permits an empty access code", "[DeviceAccess]")
{
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
machine.set_access_code("", false);
REQUIRE(machine.has_access_right());
}
TEST_CASE("Moonraker logout revokes access until explicitly rebound", "[DeviceAccess]")
{
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
const std::string code = GENERATE("", "configured-key");
machine.set_access_code(code, false);
REQUIRE(machine.has_access_right());
machine.revoke_access();
REQUIRE(machine.get_access_code().empty());
REQUIRE_FALSE(machine.has_access_right());
machine.set_access_code(code, false);
REQUIRE(machine.has_access_right());
REQUIRE(machine.get_access_code() == code);
}
TEST_CASE("Revoked access prevents calls to the printer agent", "[DeviceAccess]")
{
class RecordingAgent : public MoonrakerPrinterAgent
{
public:
RecordingAgent() : MoonrakerPrinterAgent("") {}
int connections = 0;
int connect_printer(const PrinterConnectionParams&) override
{
++connections;
return 0;
}
int disconnect_printer() override { return 0; }
};
auto agent = std::make_shared<RecordingAgent>();
NetworkAgent network(nullptr, agent);
MachineObject machine(nullptr, &network, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = GENERATE(BBL_PRINTER_AGENT_ID, ORCA_PRINTER_AGENT_ID, MOONRAKER_PRINTER_AGENT_ID);
machine.set_access_code("configured-key", false);
REQUIRE(machine.connect() == 0);
REQUIRE(agent->connections == 1);
machine.revoke_access();
REQUIRE(machine.connect() == -1);
REQUIRE(agent->connections == 1);
machine.set_access_code("configured-key", false);
REQUIRE(machine.connect() == 0);
REQUIRE(agent->connections == 2);
}
TEST_CASE("Other printer agents require an access code", "[DeviceAccess]")
{
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = GENERATE(
BBL_PRINTER_AGENT_ID, ORCA_PRINTER_AGENT_ID, "qidi", "snapmaker", "crealityprint", "plugin", "unknown");
machine.set_access_code("", false);
REQUIRE_FALSE(machine.has_access_right());
machine.set_access_code("88888888", false);
REQUIRE(machine.has_access_right());
}
TEST_CASE("Rebinding Moonraker with an empty key saves the machine", "[DeviceAccess]")
{
AppConfig config;
DeviceManager manager(nullptr, false, &config);
MachineObject machine(&manager, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
machine.dev_connection_type = "lan";
machine.revoke_access();
machine.set_access_code("");
const auto& machines = config.get_local_machines();
REQUIRE(machines.count("test_dev") == 1);
REQUIRE(machines.at("test_dev").printer_agent_id == MOONRAKER_PRINTER_AGENT_ID);
REQUIRE(machines.at("test_dev").access_code.empty());
}
TEST_CASE("Moonraker discovery leaves another agent's saved machine alone", "[DeviceAccess]")
{
AppConfig config;
NetworkAgent network(nullptr, std::make_shared<MoonrakerPrinterAgent>(""));
DeviceManager manager(&network, false, &config);
const std::string owner = GENERATE(as<std::string>{}, ORCA_PRINTER_AGENT_ID, MOONRAKER_PRINTER_AGENT_ID);
BBLocalMachine saved;
saved.dev_id = "192.0.2.10:7125";
saved.dev_ip = saved.dev_id;
saved.printer_agent_id = owner;
config.update_local_machine(saved);
manager.on_machine_alive(R"({"dev_name":"Klipper","dev_id":"192.0.2.10:7125","dev_ip":"192.0.2.10:7125",
"dev_type":"","dev_signal":"0","connect_type":"lan","bind_state":"free"})");
REQUIRE((manager.get_local_machine(saved.dev_id) != nullptr) == (owner == MOONRAKER_PRINTER_AGENT_ID));
}
@@ -22,6 +22,13 @@
#include <cmath>
#include <wx/colour.h>
#ifdef __WXMSW__
#include <wx/bitmap.h>
#include <wx/image.h>
#include <cstdlib>
#include <utility>
#include <catch2/generators/catch_generators.hpp>
#endif
#include <wx/string.h>
#include "libslic3r/FilamentMixer.hpp"
@@ -260,3 +267,71 @@ TEST_CASE("sample_gradient_ramp returns nothing without a usable curve or step c
curve.points = {{0.0, kGradientMaxRatio, NAN, NAN}, {1.0, kGradientMinRatio, NAN, NAN}};
REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 0).empty());
}
// The headless GTK/macOS suite does not initialize GUI display/font stock objects.
// Exercise native bitmap pixels on Windows without adding that dependency elsewhere.
#ifdef __WXMSW__
TEST_CASE("Filament gradient pixels follow both endpoint directions without changing input", "[FilamentBitmapUtils]")
{
const bool reverse = GENERATE(false, true);
const std::vector<wxColour> colors = reverse ? std::vector<wxColour>{wxColour(0, 0, 255), wxColour(255, 0, 0)}
: std::vector<wxColour>{wxColour(255, 0, 0), wxColour(0, 0, 255)};
const auto original = colors;
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 16), true);
REQUIRE(bitmap.IsOk());
const wxImage image = bitmap.ConvertToImage();
REQUIRE(image.IsOk());
// Interior pixels avoid backend-specific rasterization at the gradient boundary.
for (const auto& sample : {std::pair<int, wxColour>{2, colors.front()}, {61, colors.back()}}) {
REQUIRE(std::abs(int(image.GetRed(sample.first, 8)) - int(sample.second.Red())) <= 16);
REQUIRE(std::abs(int(image.GetBlue(sample.first, 8)) - int(sample.second.Blue())) <= 16);
}
REQUIRE(colors == original);
}
TEST_CASE("Filament halves preserve the same caller color order as gradients", "[FilamentBitmapUtils]")
{
const bool reverse = GENERATE(false, true);
const std::vector<wxColour> colors = reverse ? std::vector<wxColour>{wxColour(0, 0, 255), wxColour(255, 0, 0)}
: std::vector<wxColour>{wxColour(255, 0, 0), wxColour(0, 0, 255)};
const auto original = colors;
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 16), false);
REQUIRE(bitmap.IsOk());
const wxImage image = bitmap.ConvertToImage();
REQUIRE(image.IsOk());
require_same_rgb(wxColour(image.GetRed(16, 8), image.GetGreen(16, 8), image.GetBlue(16, 8)), colors.front());
require_same_rgb(wxColour(image.GetRed(48, 8), image.GetGreen(48, 8), image.GetBlue(48, 8)), colors.back());
REQUIRE(colors == original);
}
TEST_CASE("Three color filament stripes retain caller order", "[FilamentBitmapUtils]")
{
const std::vector<wxColour> colors{wxColour(0, 0, 255), wxColour(0, 255, 0), wxColour(255, 0, 0)};
const auto original = colors;
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(72, 24), false);
REQUIRE(bitmap.IsOk());
const wxImage image = bitmap.ConvertToImage();
REQUIRE(image.IsOk());
for (int i = 0; i < 3; ++i) {
const int x = 12 + i * 24;
require_same_rgb(wxColour(image.GetRed(x, 12), image.GetGreen(x, 12), image.GetBlue(x, 12)), colors[i]);
}
REQUIRE(colors == original);
}
TEST_CASE("Four color filament quadrants retain caller order", "[FilamentBitmapUtils]")
{
const std::vector<wxColour> colors{wxColour(0, 0, 255), wxColour(0, 255, 0), wxColour(255, 0, 0), wxColour(255, 255, 0)};
const auto original = colors;
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 32), false);
REQUIRE(bitmap.IsOk());
const wxImage image = bitmap.ConvertToImage();
REQUIRE(image.IsOk());
for (int i = 0; i < 4; ++i) {
const int x = i % 2 ? 48 : 16, y = i / 2 ? 24 : 8;
require_same_rgb(wxColour(image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)), colors[i]);
}
REQUIRE(colors == original);
}
#endif // __WXMSW__
@@ -0,0 +1,179 @@
#include "slic3r/GUI/ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <algorithm>
#include <limits>
#include <stdexcept>
#include <optional>
#include <string>
#include <vector>
#include <cstddef>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
using namespace Slic3r;
using namespace Slic3r::GUI;
// One color more than an ordered gradient may hold.
static std::vector<std::string> too_many_gradient_colors()
{
return std::vector<std::string>(COLOR_PICKER_MAX_GRADIENT_COLORS + 1, "#FF0000");
}
TEST_CASE("Project gradient direction survives editing and RGB serialization", "[FilamentColorPicker]")
{
const std::vector<std::string> endpoints = {"#FF0000", "#0000FF"};
const FilamentColorPickerValue stored{endpoints, true};
const auto initial = filament_color_picker_selection(stored);
REQUIRE(initial);
REQUIRE(filament_color_picker_result(*initial).colors == endpoints);
REQUIRE_FALSE(filament_color_picker_changed(*initial, initial));
REQUIRE_FALSE(filament_color_picker_changed(*initial, std::nullopt));
const auto reverse = filament_color_picker_selection({{endpoints[1], endpoints[0]}, true});
REQUIRE(filament_color_picker_changed(*initial, reverse));
REQUIRE(filament_color_picker_result(*reverse).colors.front() == endpoints.back());
}
TEST_CASE("Unrepresentable project colors remain unchanged until a different selection is confirmed", "[FilamentColorPicker]")
{
const auto value = GENERATE(FilamentColorPickerValue{too_many_gradient_colors(), true},
FilamentColorPickerValue{{"#FF0000", "#0000FF"}, false});
REQUIRE_FALSE(filament_color_picker_selection(value));
const auto seed = filament_color_picker_initial(value);
REQUIRE_FALSE(filament_color_picker_changed(seed, seed));
REQUIRE_FALSE(filament_color_picker_changed(seed, std::nullopt));
const auto changed = filament_color_picker_selection({{"#FE0000"}, false});
REQUIRE(filament_color_picker_changed(seed, changed));
REQUIRE(filament_color_picker_result(*changed).colors == std::vector<std::string>{"#FE0000"});
}
TEST_CASE("Filament seeds preserve primary alpha when colors are invalid or absent", "[FilamentColorPicker]")
{
const FilamentColorPickerValue stale{{"invalid", "#00FF00"}, false, "#12345680"};
REQUIRE_FALSE(filament_color_picker_selection(stale));
REQUIRE(filament_color_picker_result(filament_color_picker_initial(stale)).colors == std::vector<std::string>{"#12345680"});
const auto value = filament_color_picker_selection({{"#12345680", "#ABCDEF01"}, true});
REQUIRE(value);
REQUIRE(filament_color_picker_result(*value).colors == std::vector<std::string>{"#12345680", "#ABCDEF01"});
const auto missing = filament_color_picker_initial({});
REQUIRE(filament_color_picker_result(missing).colors == std::vector<std::string>{"#000000"});
}
TEST_CASE("Filament project colors round trip every alpha boundary with compatible opaque spelling", "[FilamentColorPicker]")
{
const auto hex = GENERATE(std::string("#12345600"), std::string("#12345601"),
std::string("#12345680"), std::string("#123456FE"),
std::string("#123456FF"), std::string("#123456"));
const FilamentColorPickerValue stored{{hex}, false};
const auto selection = filament_color_picker_selection(stored);
REQUIRE(selection);
REQUIRE_FALSE(filament_color_picker_changed(*selection, selection));
REQUIRE_FALSE(filament_color_picker_changed(*selection, std::nullopt));
const auto result = filament_color_picker_result(*selection);
const std::string expected = hex == "#123456FF" ? "#123456" : hex;
REQUIRE(result.colors == std::vector<std::string>{expected});
ColorRGBA decoded;
REQUIRE(decode_color(result.colors.front(), decoded));
REQUIRE(filament_color_picker_result(ColorSelection{decoded}).colors == result.colors);
const auto reloaded = filament_color_picker_selection(result);
REQUIRE(reloaded);
REQUIRE(filament_color_picker_result(*reloaded).colors == result.colors);
}
TEST_CASE("Alpha only filament edits preserve ordered independent gradient endpoint alpha", "[FilamentColorPicker]")
{
const auto original = filament_color_picker_selection({{"#FF000000", "#0000FF80"}, true});
const auto edited = filament_color_picker_selection({{"#FF000001", "#0000FFFE"}, true});
REQUIRE(original);
REQUIRE(edited);
REQUIRE(filament_color_picker_changed(*original, edited));
const auto result = filament_color_picker_result(*edited);
REQUIRE(result.gradient);
REQUIRE(result.colors == std::vector<std::string>{"#FF000001", "#0000FFFE"});
const auto reloaded = filament_color_picker_selection(result);
REQUIRE(reloaded);
REQUIRE_FALSE(filament_color_picker_changed(*edited, reloaded));
const auto reversed = filament_color_picker_selection({{result.colors[1], result.colors[0]}, true});
REQUIRE(filament_color_picker_changed(*edited, reversed));
const auto solid = filament_color_picker_selection({{"#12345680"}, false});
const auto alpha_only = filament_color_picker_selection({{"#12345681"}, false});
REQUIRE(solid);
REQUIRE(alpha_only);
REQUIRE(filament_color_picker_changed(*solid, alpha_only));
REQUIRE(filament_color_picker_result(*alpha_only).colors == std::vector<std::string>{"#12345681"});
const auto mixed = filament_color_picker_selection({{"#FF0000FF", "#0000FF00"}, true});
REQUIRE(mixed);
REQUIRE(filament_color_picker_result(*mixed).colors == std::vector<std::string>{"#FF0000", "#0000FF00"});
}
TEST_CASE("Unrepresentable filament seeds retain first endpoint or valid primary alpha", "[FilamentColorPicker]")
{
auto too_many = too_many_gradient_colors();
too_many.front() = "#FF000000";
const auto value = GENERATE_COPY(FilamentColorPickerValue{too_many, true},
FilamentColorPickerValue{{"#FF000000", "#0000FF80"}, false},
FilamentColorPickerValue{{"invalid", "#FF0000"}, true, "#FF000000"});
REQUIRE_FALSE(filament_color_picker_selection(value));
const auto initial = filament_color_picker_initial(value);
REQUIRE(filament_color_picker_result(initial).colors == std::vector<std::string>{"#FF000000"});
REQUIRE_FALSE(filament_color_picker_changed(initial, initial));
REQUIRE_FALSE(filament_color_picker_changed(initial, std::nullopt));
const auto edited = filament_color_picker_selection({{"#FF000001"}, false});
REQUIRE(filament_color_picker_changed(initial, edited));
}
TEST_CASE("Ordered multi-color gradients round trip up to the color limit", "[FilamentColorPicker]")
{
const std::vector<std::string> all = {"#FF0000", "#FFFF0080", "#00FF00", "#00FFFF01", "#0000FF", "#FF00FF00", "#123456"};
for (std::size_t count = 2; count <= std::min(all.size(), COLOR_PICKER_MAX_GRADIENT_COLORS); ++count) {
const std::vector<std::string> colors(all.begin(), all.begin() + count);
const auto selection = filament_color_picker_selection({colors, true});
REQUIRE(selection);
const auto result = filament_color_picker_result(*selection);
REQUIRE(result.gradient);
REQUIRE(result.colors == colors);
REQUIRE_FALSE(filament_color_picker_changed(*selection, filament_color_picker_selection(result)));
std::vector<std::string> reordered = colors;
std::swap(reordered.front(), reordered.back());
REQUIRE(filament_color_picker_changed(*selection, filament_color_picker_selection({reordered, true})));
}
REQUIRE_FALSE(filament_color_picker_selection({too_many_gradient_colors(), true}));
}
TEST_CASE("Filament selections require complete hexadecimal colors and exact endpoint counts", "[FilamentColorPicker]")
{
const std::string hex = GENERATE("", "112233", "#ABC", "#1234567", "#123456789", "#GG2233", "#11223Z", "#112233G0", " #112233", "#112233 ");
REQUIRE_FALSE(filament_color_picker_selection({{hex}, false}));
REQUIRE_FALSE(filament_color_picker_selection({{"#112233", hex}, true}));
const auto malformed = GENERATE(FilamentColorPickerValue{{}, false},
FilamentColorPickerValue{{}, true},
FilamentColorPickerValue{{"#112233"}, true});
REQUIRE_FALSE(filament_color_picker_selection(malformed));
}
TEST_CASE("Every RGBA byte survives the public filament conversion round trip", "[FilamentColorPicker]")
{
constexpr char digits[] = "0123456789ABCDEF";
for (int byte = 0; byte < 256; ++byte) {
std::string hex = "#";
for (int channel = 0; channel < 4; ++channel) {
hex += digits[byte >> 4];
hex += digits[byte & 15];
}
const auto selection = filament_color_picker_selection({{hex}, false});
REQUIRE(selection);
const auto result = filament_color_picker_result(*selection);
REQUIRE(result.colors == std::vector<std::string>{byte == 255 ? hex.substr(0, 7) : hex});
REQUIRE_FALSE(filament_color_picker_changed(*selection, filament_color_picker_selection(result)));
}
}
TEST_CASE("Filament serialization rejects invalid native RGBA channels", "[FilamentColorPicker]")
{
const float invalid = GENERATE(-0.1f, 1.1f, std::numeric_limits<float>::infinity(), std::numeric_limits<float>::quiet_NaN());
const int channel = GENERATE(0, 1, 2, 3);
ColorRGBA color;
color[channel] = invalid;
REQUIRE_THROWS_AS(filament_color_picker_result(color), std::invalid_argument);
REQUIRE_THROWS_AS(filament_color_picker_result(ColorGradient{ColorRGBA::WHITE(), color}), std::invalid_argument);
}
@@ -0,0 +1,51 @@
#include <catch2/catch_test_macros.hpp>
#include <string>
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
using namespace Slic3r;
namespace {
class DiscoveryAgent : public MoonrakerPrinterAgent
{
public:
using MoonrakerPrinterAgent::MoonrakerPrinterAgent;
using MoonrakerPrinterAgent::device_info;
mutable unsigned fetch_count = 0;
protected:
bool fetch_device_info(const std::string&, const std::string&, MoonrakerDeviceInfo& info, std::string&) const override
{
++fetch_count;
info.dev_name = "Test printer";
return true;
}
};
} // namespace
TEST_CASE("Moonraker discovery waits for a complete device identity", "[MoonrakerDiscovery]")
{
DiscoveryAgent agent("");
unsigned announcements = 0;
nlohmann::json announcement;
agent.set_on_ssdp_msg_fn([&](std::string payload) {
++announcements;
announcement = nlohmann::json::parse(payload);
});
agent.start_discovery(true, false);
REQUIRE(announcements == 0);
REQUIRE(agent.fetch_count == 0);
agent.device_info.dev_id = "127.0.0.1:7125";
agent.device_info.dev_ip = agent.device_info.dev_id;
agent.device_info.base_url = "http://127.0.0.1:7125";
agent.start_discovery(true, false);
REQUIRE(announcements == 1);
REQUIRE(agent.fetch_count == 1);
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
REQUIRE(announcement.at("dev_name") == "Test printer");
}
@@ -0,0 +1,111 @@
#include <catch2/catch_test_macros.hpp>
#include <utility>
#include <memory>
#include <vector>
#include <functional>
#include "slic3r/Utils/bambu_networking.hpp"
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
using namespace Slic3r;
namespace {
class DiscoverySource : public MoonrakerPrinterAgent
{
public:
DiscoverySource() : MoonrakerPrinterAgent("") {}
OnMsgArrivedFn discovery_callback;
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{
discovery_callback = std::move(fn);
return 0;
}
};
} // namespace
TEST_CASE("Queued discovery cannot cross printer agent sessions", "[NetworkDiscovery]")
{
auto source = std::make_shared<DiscoverySource>();
NetworkAgent network(nullptr, source);
std::vector<std::function<void()>> pending;
std::vector<std::string> delivered;
network.set_queue_on_main_fn([&](std::function<void()> fn) { pending.push_back(std::move(fn)); });
network.set_on_ssdp_msg_fn([&](std::string payload) { delivered.push_back(std::move(payload)); });
auto outgoing_callback = source->discovery_callback;
outgoing_callback("unsaved-device");
outgoing_callback("saved-moonraker-device");
REQUIRE(delivered.empty());
REQUIRE(pending.size() == 2);
bool expect_active = true;
SECTION("Switch to another agent")
{
network.set_printer_agent(std::make_shared<DiscoverySource>());
}
SECTION("Clear the live agent")
{
network.set_printer_agent(nullptr);
expect_active = false;
}
SECTION("Switch away and back to the cached agent")
{
network.set_printer_agent(std::make_shared<DiscoverySource>());
network.set_printer_agent(source);
}
SECTION("Unregister discovery")
{
network.set_on_ssdp_msg_fn(nullptr);
expect_active = false;
}
SECTION("Replace the discovery listener")
{
network.set_on_ssdp_msg_fn([&](std::string payload) { delivered.push_back(std::move(payload)); });
}
for (auto& fn : pending)
fn();
pending.clear();
REQUIRE(delivered.empty());
outgoing_callback("late-outgoing-device");
REQUIRE(pending.empty());
REQUIRE(delivered.empty());
if (expect_active) {
auto active = std::dynamic_pointer_cast<DiscoverySource>(network.get_printer_agent());
REQUIRE(active);
REQUIRE(active->discovery_callback);
active->discovery_callback("active-device");
REQUIRE(delivered.empty());
REQUIRE(pending.size() == 1);
pending.front()();
REQUIRE(delivered == std::vector<std::string>{"active-device"});
}
}
TEST_CASE("Discovery callbacks expire when networking is destroyed", "[NetworkDiscovery]")
{
auto source = std::make_shared<DiscoverySource>();
std::vector<std::function<void()>> pending;
unsigned delivered = 0;
{
NetworkAgent network(nullptr, source);
network.set_queue_on_main_fn([&](std::function<void()> fn) { pending.push_back(std::move(fn)); });
network.set_on_ssdp_msg_fn([&](std::string) { ++delivered; });
source->discovery_callback("queued-device");
}
REQUIRE(pending.size() == 1);
pending.front()();
pending.clear();
source->discovery_callback("late-device");
REQUIRE(pending.empty());
REQUIRE(delivered == 0);
}
@@ -56,6 +56,14 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
REQUIRE(has_attr(host, "Model"));
REQUIRE(has_attr(host, "ModelObject"));
REQUIRE(has_attr(host, "Plater"));
REQUIRE(has_attr(host, "preset"));
py::object preset = host.attr("preset");
REQUIRE(has_attr(preset, "bundles"));
py::object bundles = preset.attr("bundles");
CHECK(has_attr(bundles, "list_ids"));
CHECK(has_attr(bundles, "reload_local"));
CHECK_FALSE(has_attr(host, "reload_local_bundle"));
CHECK_FALSE(has_attr(host, "list_bundle_ids"));
py::object preset_bundle_type = host.attr("PresetBundle");
CHECK(has_attr(preset_bundle_type, "prints"));
@@ -132,6 +140,7 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]")
{
py::object host = import_orca_module().attr("host");
py::object bundles = host.attr("preset").attr("bundles");
for (const char* function_name : { "preset_bundle", "plater", "model", "app_info", "selected_printer" }) {
CAPTURE(function_name);
@@ -143,6 +152,22 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
}
}
try {
bundles.attr("reload_local")("test-bundle");
FAIL("host.preset.bundles.reload_local unexpectedly succeeded without a wx application");
} catch (const py::error_already_set& error) {
CHECK(error.matches(PyExc_RuntimeError));
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
}
try {
bundles.attr("list_ids")();
FAIL("host.preset.bundles.list_ids unexpectedly succeeded without a wx application");
} catch (const py::error_already_set& error) {
CHECK(error.matches(PyExc_RuntimeError));
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
}
}
TEST_CASE("Plugin host API reports no GL details before the 3D view exists", "[PluginHost][Python]")