feat: Python Plugins

This commit is contained in:
Ian Chua
2026-07-02 17:49:36 +08:00
parent 395e070a0e
commit ecddf3d18f
183 changed files with 49955 additions and 2120 deletions

View File

@@ -14,6 +14,7 @@ add_executable(${_TEST_NAME}_tests
test_config.cpp
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_preset_diff.cpp
test_elephant_foot_compensation.cpp
test_geometry.cpp
test_placeholder_parser.cpp

View File

@@ -5,10 +5,14 @@
#include "libslic3r/LocalesUtils.hpp"
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/archives/binary.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp>
using namespace Slic3r;
SCENARIO("Generic config validation performs as expected.", "[Config]") {
@@ -401,3 +405,81 @@ SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vect
// }
// }
// }
TEST_CASE("save_to_json round-trips plugin capability references as strings", "[Config][plugins]") {
namespace fs = boost::filesystem;
const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca_plugins_%%%%-%%%%.json");
const std::vector<std::string> refs = {
"local_plugin;;post_process",
"cloud_plugin;550e8400-e29b-41d4-a716-446655440000;post_process"
};
std::unique_ptr<DynamicPrintConfig> config_ptr(
DynamicPrintConfig::new_from_defaults_keys({"post_process_plugin"}));
DynamicPrintConfig config = std::move(*config_ptr);
config.option<ConfigOptionStrings>("post_process_plugin", true)->values = refs;
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
nlohmann::json j;
{
boost::nowide::ifstream ifs(tmp.string());
ifs >> j;
}
REQUIRE(j["post_process_plugin"] == nlohmann::json(refs));
CHECK_FALSE(j.contains("plugins"));
DynamicPrintConfig reloaded = DynamicPrintConfig::full_print_config();
ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable);
std::map<std::string, std::string> key_values;
std::string reason;
REQUIRE(reloaded.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0);
CHECK(reason.empty());
CHECK(reloaded.option<ConfigOptionStrings>("post_process_plugin")->values == refs);
fs::remove(tmp);
}
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
const std::vector<std::string> refs = {
"master_plugin;;header-stamp",
"Sample Plugin;1f998ea9-0183-4cc5-957f-4eef659ba4e6;G-code Benchmark (.py)"
};
DynamicPrintConfig original = DynamicPrintConfig::full_print_config();
original.option<ConfigOptionStrings>("post_process_plugin", true)->values = refs;
std::map<std::string, std::string> serialized{
{"post_process_plugin", original.option<ConfigOptionStrings>("post_process_plugin")->serialize()}
};
CHECK(serialized["post_process_plugin"].find("\"master_plugin;;header-stamp\"") != std::string::npos);
DynamicPrintConfig reloaded = DynamicPrintConfig::full_print_config();
reloaded.load_string_map(serialized, ForwardCompatibilitySubstitutionRule::Disable);
CHECK(reloaded.option<ConfigOptionStrings>("post_process_plugin")->values == refs);
}
TEST_CASE("parse_capability_ref parses local and cloud references", "[Config][plugin]") {
const auto local = Slic3r::parse_capability_ref("local_plugin;;post_process");
REQUIRE(local.has_value());
CHECK(local->name == "local_plugin");
CHECK(local->capability_name == "post_process");
CHECK(local->uuid.empty());
const auto cloud = Slic3r::parse_capability_ref(
"cloud_plugin;550e8400-e29b-41d4-a716-446655440000;post_process");
REQUIRE(cloud.has_value());
CHECK(cloud->name == "cloud_plugin");
CHECK(cloud->capability_name == "post_process");
CHECK(cloud->uuid == "550e8400-e29b-41d4-a716-446655440000");
}
TEST_CASE("parse_capability_ref rejects malformed input", "[Config][plugin]") {
CHECK_FALSE(Slic3r::parse_capability_ref("").has_value());
CHECK_FALSE(Slic3r::parse_capability_ref("plugin").has_value());
CHECK_FALSE(Slic3r::parse_capability_ref("plugin;uuid").has_value());
CHECK_FALSE(Slic3r::parse_capability_ref(";;capability").has_value());
CHECK_FALSE(Slic3r::parse_capability_ref(";uuid;capability").has_value());
CHECK_FALSE(Slic3r::parse_capability_ref("plugin;;").has_value());
CHECK_FALSE(Slic3r::parse_capability_ref("plugin;uuid;").has_value());
}

View File

@@ -0,0 +1,35 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Preset.hpp"
#include "libslic3r/PrintConfig.hpp"
#include <algorithm>
using namespace Slic3r;
// Regression test for the python-plugin branch's intentional divergence from
// upstream in add_correct_opts_to_diff() (src/libslic3r/Preset.cpp): a vector
// option entry whose index is beyond the reference vector's length is reported
// dirty even when it duplicates an existing value. On main these duplicates
// were NOT flagged. See the comment on add_correct_opts_to_diff() in src/libslic3r/Preset.cpp.
TEST_CASE("deep_diff flags new vector entries that duplicate values[0]", "[PresetDiff][Config]")
{
// reference: single-extruder vector (one entry)
Preset reference(Preset::TYPE_PRINTER, "ref");
reference.config.set_key_value("nozzle_diameter", new ConfigOptionFloats{0.4});
// edited: a second extruder entry was added whose value duplicates the first
Preset edited(Preset::TYPE_PRINTER, "edited");
edited.config.set_key_value("nozzle_diameter", new ConfigOptionFloats{0.4, 0.4});
// deep_compare = true routes through deep_diff() -> add_correct_opts_to_diff()
std::vector<std::string> diff =
PresetCollection::dirty_options(&edited, &reference, /*deep_compare=*/true);
// The new index #1 is reported dirty even though 0.4 == values[0] (0.4).
REQUIRE(std::find(diff.begin(), diff.end(), "nozzle_diameter#1") != diff.end());
// Sanity: the unchanged existing index #0 is NOT reported, so the rule is
// specific to new indices rather than flagging the whole vector.
REQUIRE(std::find(diff.begin(), diff.end(), "nozzle_diameter#0") == diff.end());
}