Files
OrcaSlicer/tests/libslic3r/test_preset_bundle_loading.cpp
2026-07-01 21:55:19 +08:00

467 lines
21 KiB
C++

#include <catch2/catch_all.hpp>
#include <boost/filesystem.hpp>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/AppConfig.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<ConfigOptionString>("print_settings_id", true)->value = name;
config.option<ConfigOptionString>(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<ConfigOptionString>(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<ConfigOptionString>(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<std::string> 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<const PrintRegionConfig &>(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<std::string> 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 "<vendor> 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 "<vendor> 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");
}
namespace {
// A live reference to a preset's compatible_printers / compatible_prints list. Fetches the *stored*
// preset (real=true) so writes and reads hit the same object; creates the option if absent.
std::vector<std::string> &compatible_list(PresetCollection &coll, const std::string &preset_name, const char *field_key)
{
Preset *preset = coll.find_preset(preset_name, /*first_visible_if_not_found=*/false, /*real=*/true);
REQUIRE(preset != nullptr);
return preset->config.option<ConfigOptionStrings>(field_key, true)->values;
}
} // namespace
TEST_CASE("Renamed printer/process names are normalized into compatible lists on load", "[Preset][Rename]")
{
PresetBundle bundle;
// Current printer + process, each renamed from an older name.
add_inmemory_preset(bundle.printers, "New Printer");
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
add_inmemory_preset(bundle.prints, "New Process");
set_renamed_from(bundle.prints, "New Process", { "Old Process" });
// A user process still compatible with the OLD printer name.
add_inmemory_preset(bundle.prints, "My Process");
compatible_list(bundle.prints, "My Process", "compatible_printers") = { "Old Printer" };
// A user filament referencing the OLD printer AND OLD process names, plus an unknown printer.
add_inmemory_preset(bundle.filaments, "My Filament");
compatible_list(bundle.filaments, "My Filament", "compatible_printers") = { "Old Printer", "Unknown Printer" };
compatible_list(bundle.filaments, "My Filament", "compatible_prints") = { "Old Process" };
// Build the rename maps (done during system load in the real pipeline), then normalize.
AppConfig app_config;
bundle.load_installed_printers(app_config); // rebuilds every collection's rename map
bundle.normalize_compatible_presets();
// The stale printer name in a process' compatible_printers is rewritten to the current name.
CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector<std::string>{ "New Printer" });
// The stale process name in a filament's compatible_prints is rewritten (this field has no
// runtime rename fallback, so load-time normalization is the only fix).
CHECK(compatible_list(bundle.filaments, "My Filament", "compatible_prints") == std::vector<std::string>{ "New Process" });
// The renamed printer is rewritten while the unknown/deleted name is preserved as-is.
CHECK(compatible_list(bundle.filaments, "My Filament", "compatible_printers") ==
(std::vector<std::string>{ "New Printer", "Unknown Printer" }));
// Normalizing rewrites config in place without flagging the preset dirty.
CHECK_FALSE(bundle.prints.find_preset("My Process", false, true)->is_dirty);
// A system preset that already references the current name is left untouched (idempotent no-op).
bundle.normalize_compatible_presets();
CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector<std::string>{ "New Printer" });
}
TEST_CASE("Renamed names are normalized into a SYSTEM preset's compatible lists", "[Preset][Rename]")
{
PresetBundle bundle;
// Current printer + process, each renamed from an older name.
add_inmemory_preset(bundle.printers, "New Printer");
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
add_inmemory_preset(bundle.prints, "New Process");
set_renamed_from(bundle.prints, "New Process", { "Old Process" });
// A *system* (vendor) filament whose own compatible lists still reference the OLD names. A vendor
// profile can point at a sibling preset that was later renamed, so system presets must be
// normalized too (they are skipped by neither collection walk).
add_inmemory_preset(bundle.filaments, "System Filament").is_system = true;
compatible_list(bundle.filaments, "System Filament", "compatible_printers") = { "Old Printer" };
compatible_list(bundle.filaments, "System Filament", "compatible_prints") = { "Old Process" };
AppConfig app_config;
bundle.load_installed_printers(app_config); // build the rename maps
bundle.normalize_compatible_presets();
// The stale references in the system preset are rewritten to the current names.
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") ==
std::vector<std::string>{ "New Printer" });
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_prints") ==
std::vector<std::string>{ "New Process" });
// The rewrite does not flag the system preset dirty, and is idempotent.
CHECK_FALSE(bundle.filaments.find_preset("System Filament", false, true)->is_dirty);
bundle.normalize_compatible_presets();
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") ==
std::vector<std::string>{ "New Printer" });
}
TEST_CASE("compatible_prints on SLA materials resolves against sla_prints, not prints", "[Preset][Rename]")
{
PresetBundle bundle;
// A renamed SLA process, and a same-named FFF process that must NOT be picked up: resolving the
// SLA material's compatible_prints against `prints` would wrongly rewrite to "Wrong FFF Process".
add_inmemory_preset(bundle.sla_prints, "New SLA Process");
set_renamed_from(bundle.sla_prints, "New SLA Process", { "Old SLA Process" });
add_inmemory_preset(bundle.prints, "Wrong FFF Process");
set_renamed_from(bundle.prints, "Wrong FFF Process", { "Old SLA Process" });
add_inmemory_preset(bundle.sla_materials, "My SLA Material");
compatible_list(bundle.sla_materials, "My SLA Material", "compatible_prints") = { "Old SLA Process" };
AppConfig app_config;
bundle.load_installed_printers(app_config);
bundle.normalize_compatible_presets();
CHECK(compatible_list(bundle.sla_materials, "My SLA Material", "compatible_prints") ==
std::vector<std::string>{ "New SLA Process" });
}
TEST_CASE("Profile validator flags dangling and renamed preset references", "[Preset][Validate]")
{
PresetBundle bundle;
// Current printers: a real one, and a renamed one (its old name resolves via renamed_from).
add_inmemory_preset(bundle.printers, "Real Printer");
add_inmemory_preset(bundle.printers, "New Printer");
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
// A real process, referenced from a filament's compatible_prints.
add_inmemory_preset(bundle.prints, "Real Process").is_system = true;
// A fully valid system filament: references only current names.
add_inmemory_preset(bundle.filaments, "Good Filament").is_system = true;
compatible_list(bundle.filaments, "Good Filament", "compatible_printers") = { "Real Printer" };
compatible_list(bundle.filaments, "Good Filament", "compatible_prints") = { "Real Process" };
AppConfig app_config;
bundle.load_installed_printers(app_config); // build the rename maps
// With only valid references, the validator is clean.
CHECK_FALSE(bundle.check_preset_references());
SECTION("deleted compatible_printers is flagged") {
add_inmemory_preset(bundle.filaments, "Ghost Ref Filament").is_system = true;
compatible_list(bundle.filaments, "Ghost Ref Filament", "compatible_printers") = { "Ghost Printer" };
CHECK(bundle.check_preset_references());
}
SECTION("renamed compatible_printers (old name) is flagged") {
add_inmemory_preset(bundle.filaments, "Old Ref Filament").is_system = true;
compatible_list(bundle.filaments, "Old Ref Filament", "compatible_printers") = { "Old Printer" };
CHECK(bundle.check_preset_references());
}
SECTION("deleted compatible_prints is flagged") {
add_inmemory_preset(bundle.filaments, "Bad Process Ref").is_system = true;
compatible_list(bundle.filaments, "Bad Process Ref", "compatible_prints") = { "Ghost Process" };
CHECK(bundle.check_preset_references());
}
SECTION("deleted inherits parent is flagged") {
add_inmemory_preset(bundle.filaments, "Orphan Filament", "Ghost Parent").is_system = true;
CHECK(bundle.check_preset_references());
}
SECTION("non-system preset with a dangling reference is ignored") {
add_inmemory_preset(bundle.filaments, "User Filament"); // is_system stays false
compatible_list(bundle.filaments, "User Filament", "compatible_printers") = { "Ghost Printer" };
CHECK_FALSE(bundle.check_preset_references());
}
}