#include #include #include "libslic3r/PresetBundle.hpp" using namespace Slic3r; namespace { namespace fs = boost::filesystem; struct TempPresetDir { fs::path path; TempPresetDir() { path = fs::temp_directory_path() / fs::unique_path("orcaslicer-preset-%%%%-%%%%-%%%%"); fs::create_directories(path); } ~TempPresetDir() { boost::system::error_code ec; fs::remove_all(path, ec); } }; void write_print_preset(const DynamicPrintConfig &default_config, const fs::path &file, const std::string &name, const std::string &inherits = {}) { DynamicPrintConfig config(default_config); config.option("print_settings_id", true)->value = name; config.option(BBL_JSON_KEY_INHERITS, true)->value = inherits; fs::create_directories(file.parent_path()); config.save_to_json(file.string(), name, "User", "1.0.0"); } // Write a preset json carrying a name and an "inherits" value, using the given collection's // default config so it loads back into that collection. Works for any preset type. void write_preset_with_inherits(const DynamicPrintConfig &default_config, const fs::path &file, const std::string &name, const std::string &inherits) { DynamicPrintConfig config(default_config); config.option(BBL_JSON_KEY_INHERITS, true)->value = inherits; fs::create_directories(file.parent_path()); config.save_to_json(file.string(), name, "User", "1.0.0"); } // Add an in-memory preset (no file) with the given inherits value (empty => root preset). Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {}) { DynamicPrintConfig config(coll.default_preset().config); config.option(BBL_JSON_KEY_INHERITS, true)->value = inherits; return coll.load_preset(std::string(), name, config, /*select=*/false); } // Mark an already-loaded preset as renamed from one or more former names. void set_renamed_from(PresetCollection &coll, const std::string &preset_name, std::vector old_names) { for (auto it = coll.begin(); it != coll.end(); ++it) if (it->name == preset_name) it->renamed_from = std::move(old_names); } // A standalone print preset collection that exposes the protected rename-map builder, so a // renamed_from scenario can be set up without the full system-profile load pipeline. // (PresetCollection is non-copyable - it holds a mutex - so it is constructed directly with // the same type/keys/defaults PresetBundle uses for its print collection.) struct RenameTestCollection : public PresetCollection { RenameTestCollection() : PresetCollection(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) {} using PresetCollection::update_map_system_profile_renamed; }; } // namespace TEST_CASE("Preset identity is canonicalized from load path", "[Preset][Identity]") { TempPresetDir temp_dir; PresetBundle bundle; PresetsConfigSubstitutions substitutions; write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_PRINT_NAME / "User.json", "User"); write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_LOCAL_DIR / "bundle-1" / PRESET_PRINT_NAME / "LocalBundle.json", "LocalBundle"); write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_SUBSCRIBED_DIR / "remote-1" / PRESET_PRINT_NAME / "Subscribed.json", "Subscribed"); bundle.prints.load_presets(temp_dir.path.string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); bundle.prints.load_presets((temp_dir.path / PRESET_LOCAL_DIR / "bundle-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); bundle.prints.load_presets((temp_dir.path / PRESET_SUBSCRIBED_DIR / "remote-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); const Preset *root_user = bundle.prints.find_preset("User"); REQUIRE(root_user != nullptr); CHECK(root_user->name == "User"); CHECK_FALSE(root_user->is_from_bundle()); const Preset *local_bundle = bundle.prints.find_preset("_local/bundle-1/LocalBundle"); REQUIRE(local_bundle != nullptr); CHECK(local_bundle->name == "_local/bundle-1/LocalBundle"); CHECK(local_bundle->is_from_bundle()); const Preset *subscribed = bundle.prints.find_preset("_subscribed/remote-1/Subscribed"); REQUIRE(subscribed != nullptr); CHECK(subscribed->name == "_subscribed/remote-1/Subscribed"); CHECK(subscribed->is_from_bundle()); } TEST_CASE("Legacy bundle import without bundle metadata stays in the user preset directory", "[Preset][Identity]") { TempPresetDir temp_dir; PresetBundle bundle; PresetsConfigSubstitutions substitutions; std::vector result; int overwrite = 0; std::string file = (temp_dir.path / "legacy-bundle" / "Imported.json").string(); const fs::path user_root = temp_dir.path / "user"; write_print_preset(bundle.prints.default_preset().config, file, "Imported"); fs::create_directories(user_root); bundle.prints.update_user_presets_directory(user_root.string(), PRESET_PRINT_NAME); REQUIRE(bundle.import_json_presets( substitutions, file, [](std::string const &) { return 1; }, ForwardCompatibilitySubstitutionRule::Disable, overwrite, result)); const Preset *imported = bundle.prints.find_preset("Imported"); REQUIRE(imported != nullptr); CHECK(imported->name == "Imported"); CHECK(imported->bundle_id.empty()); CHECK_FALSE(imported->is_from_bundle()); // Detached user presets (no inherits) are saved in the "base" subfolder of the user preset root. CHECK(fs::equivalent(fs::path(imported->file).parent_path().parent_path(), user_root / PRESET_PRINT_NAME)); } TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundle]") { PresetBundle bundle; VendorProfile orca_vendor("ORCA"); VendorProfile::PrinterModel model; model.name = "Orca Test"; orca_vendor.models.emplace_back(model); bundle.vendors.emplace("ORCA", std::move(orca_vendor)); bundle.printers.get_edited_preset().config.erase("printer_model"); CHECK(bundle.get_current_vendor_type() == VendorType::Unknown); } TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][Bundle]") { PresetBundle bundle; DynamicPrintConfig& config = bundle.printers.get_edited_preset().config; config.erase("nozzle_diameter"); CHECK(bundle.get_printer_extruder_count() == 1); config.set_key_value("nozzle_diameter", new ConfigOptionFloats()); CHECK(bundle.get_printer_extruder_count() == 1); config.set_key_value("nozzle_diameter", new ConfigOptionFloats({ 0.4, 0.6 })); CHECK(bundle.get_printer_extruder_count() == 2); } TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]") { RenameTestCollection coll; // "New Process" is the current preset; it was renamed from "Old Process". add_inmemory_preset(coll, "New Process"); set_renamed_from(coll, "New Process", { "Old Process" }); coll.update_map_system_profile_renamed(); // The rename map knows the old name... const std::string *renamed = coll.get_preset_name_renamed("Old Process"); REQUIRE(renamed != nullptr); CHECK(*renamed == "New Process"); // ...and plain find_preset() now follows it (the core of this PR; previously this // resolution lived only in find_preset2 and a few call sites). const Preset *resolved = coll.find_preset("Old Process"); REQUIRE(resolved != nullptr); CHECK(resolved->name == "New Process"); // A genuinely unknown name still returns null (no spurious match). CHECK(coll.find_preset("Totally Unknown") == nullptr); // A child that still inherits the OLD name resolves through the runtime walker, // which uses plain find_preset(). Preset &child = add_inmemory_preset(coll, "Child Process", "Old Process"); const Preset *parent = coll.get_preset_parent(child); REQUIRE(parent != nullptr); CHECK(parent->name == "New Process"); } TEST_CASE("find_preset resolves a preset renamed more than once", "[Preset][Rename]") { RenameTestCollection coll; // "New Process" was renamed twice, so it carries both former names in renamed_from. add_inmemory_preset(coll, "New Process"); set_renamed_from(coll, "New Process", { "Original Process", "Old Process" }); coll.update_map_system_profile_renamed(); // Each historical name resolves to the current preset. for (const char *old_name : { "Original Process", "Old Process" }) { INFO("resolving old name: " << old_name); const std::string *renamed = coll.get_preset_name_renamed(old_name); REQUIRE(renamed != nullptr); CHECK(*renamed == "New Process"); const Preset *resolved = coll.find_preset(old_name); REQUIRE(resolved != nullptr); CHECK(resolved->name == "New Process"); } // A child inheriting either former name resolves through the runtime walker. Preset &child = add_inmemory_preset(coll, "Child Process", "Original Process"); REQUIRE(coll.get_preset_parent(child) != nullptr); CHECK(coll.get_preset_parent(child)->name == "New Process"); } TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the library", "[Preset][Rename]") { PresetBundle bundle; // The OrcaFilamentLibrary replacement that removed empty " Generic" profiles map to. add_inmemory_preset(bundle.filaments, "Generic PLA @System"); // Plain lookups do NOT fuzzy-match a removed vendor profile. CHECK(bundle.filaments.find_preset("Voron Generic PLA") == nullptr); CHECK(bundle.filaments.find_preset2("Voron Generic PLA", /*auto_match=*/false) == nullptr); // With auto_match, the removed "Voron Generic PLA" resolves to "Generic PLA @System". const Preset *matched = bundle.filaments.find_preset2("Voron Generic PLA", /*auto_match=*/true); REQUIRE(matched != nullptr); CHECK(matched->name == "Generic PLA @System"); // No library preset exists for an unrelated material => still no match. CHECK(bundle.filaments.find_preset2("BrandX Generic PETG", /*auto_match=*/true) == nullptr); } TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]") { TempPresetDir temp_dir; RenameTestCollection coll; // Current parent, renamed from "Old Process". add_inmemory_preset(coll, "New Process"); set_renamed_from(coll, "New Process", { "Old Process" }); coll.update_map_system_profile_renamed(); // A user preset on disk that still inherits the OLD name. write_preset_with_inherits(coll.default_preset().config, temp_dir.path / PRESET_PRINT_NAME / "Child.json", "Child", "Old Process"); PresetsConfigSubstitutions substitutions; coll.load_presets(temp_dir.path.string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); const Preset *child = coll.find_preset("Child"); REQUIRE(child != nullptr); // The dangling "Old Process" was rewritten to the resolved parent name at load time, // so the runtime walker (plain find_preset) can resolve the chain. CHECK(child->inherits() == "New Process"); REQUIRE(coll.get_preset_parent(*child) != nullptr); CHECK(coll.get_preset_parent(*child)->name == "New Process"); } TEST_CASE("Removed Generic parent is normalized into a loaded filament's inherits", "[Preset][Rename]") { TempPresetDir temp_dir; PresetBundle bundle; add_inmemory_preset(bundle.filaments, "Generic PLA @System"); // A user filament that still inherits a removed " Generic PLA" profile. write_preset_with_inherits(bundle.filaments.default_preset().config, temp_dir.path / PRESET_FILAMENT_NAME / "MyPLA.json", "MyPLA", "Voron Generic PLA"); PresetsConfigSubstitutions substitutions; bundle.filaments.load_presets(temp_dir.path.string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); const Preset *child = bundle.filaments.find_preset("MyPLA"); REQUIRE(child != nullptr); CHECK(child->inherits() == "Generic PLA @System"); REQUIRE(bundle.filaments.get_preset_parent(*child) != nullptr); CHECK(bundle.filaments.get_preset_parent(*child)->name == "Generic PLA @System"); }