From c33515914d3f1dc1ab7d46fb17dc19fbb7b342fe Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Wed, 7 Oct 2026 15:30:36 -0500 Subject: [PATCH] fix: Create Printer finds no system vendors in release builds (#16175) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- docs/HLSD/preset-cache.md | 11 +- src/libslic3r/PresetBundle.cpp | 112 ++++++++++-------- src/libslic3r/PresetCacheFormat.cpp | 42 +++++-- src/libslic3r/PresetCacheFormat.hpp | 5 + src/slic3r/GUI/CreatePresetsDialog.cpp | 50 +++++--- .../libslic3r/test_preset_bundle_loading.cpp | 51 ++++++++ 6 files changed, 190 insertions(+), 81 deletions(-) diff --git a/docs/HLSD/preset-cache.md b/docs/HLSD/preset-cache.md index aa39d522a0..ed7403f5ef 100644 --- a/docs/HLSD/preset-cache.md +++ b/docs/HLSD/preset-cache.md @@ -39,7 +39,7 @@ presets are never serialized — they have their own storage and their own lifec | Location | Contents on a shipped build | Role | |---|---|---| -| `resources/profiles/` | `.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; what installing copies from, and the only thing it is read for | +| `resources/profiles/` | `.opc` alone — the profile and its preset JSONs both pruned | What the app ships with and what installing copies from; read directly for vendors not installed | | `/system/` | `.opc` alone, or `.json` + `/` after an update | What the user has installed | | `/system/` (dev build) | `.json` + `/` + `.opc` written at runtime | A developer tree caches as it parses | | `/cache/wizard_profile_data.json` | The wizard's derived vendor catalog plus the stamps it was built from | Written and read by the setup wizard only; never shipped (see "The wizard's profile-data cache") | @@ -182,10 +182,11 @@ one startup. **A vendor is loaded from where it is installed and nowhere else.** For startup that is `/system/`; resources reaches the app by being *installed* into that -directory first, never by being loaded from. (The setup wizard is the one caller with -a different notion of "where": it also shows vendors the user has not installed, and -loads those from `resources/profiles` — see "The wizard's profile-data cache".) There -is one lookup tier and one parse source: +directory first, never by being loaded from. (The setup wizard and the Create Printer +dialog also offer vendors the user has not installed, and load those from +`resources/profiles`; see "The wizard's profile-data cache". The dialog's vendor-only and +filament-only scans read a cache only where it is the whole installation, and never write +one.) There is one lookup tier and one parse source: ``` load vendor V from /system: diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b1fad347f1..0b3d30d5a5 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2806,19 +2806,13 @@ std::pair PresetBundle::load_system_mod boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { - std::string vendor_file = dir_entry.path().string(); - if (Slic3r::is_json_file(vendor_file)) { - std::string vendor_name = dir_entry.path().filename().string(); - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - try { - // Load the config bundle, flatten it. - append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); - errors_cummulative += "\n"; - } + for (const std::string &vendor_name : vendor_names_in(dir)) { + try { + // Load the config bundle, flatten it. + append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; } } @@ -2841,35 +2835,33 @@ std::pair PresetBundle::load_system_fil PresetsConfigSubstitutions substitutions; std::string errors_cummulative; bool first = true; - for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { - std::string vendor_file = dir_entry.path().string(); - if (Slic3r::is_json_file(vendor_file)) { - std::string vendor_name = dir_entry.path().filename().string(); - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - try { - if (first) { - // Reset this PresetBundle and load the first vendor config. - append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); - first = false; - } else { - // Load the other vendor configs, merge them with this PresetBundle. - // Report duplicate profiles. - PresetBundle other; - append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); - std::vector duplicates = std::move(this->merge_presets({ &other }).front()); - if (!duplicates.empty()) { - errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; - for (size_t i = 0; i < duplicates.size(); ++i) { - if (i > 0) errors_cummulative += ", "; - errors_cummulative += duplicates[i]; - } + // The filament library loads first, so the filaments that inherit its bases resolve. + std::vector vendor_names; + for (const std::string &vendor_name : vendor_names_in(dir)) + vendor_names.insert(vendor_name == ORCA_FILAMENT_LIBRARY ? vendor_names.begin() : vendor_names.end(), vendor_name); + for (const std::string &vendor_name : vendor_names) { + try { + if (first) { + // Reset this PresetBundle and load the first vendor config. + append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); + first = false; + } else { + // Load the other vendor configs, merge them with this PresetBundle. + // Report duplicate profiles. + PresetBundle other; + append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule, this).first); + std::vector duplicates = std::move(this->merge_presets({ &other }).front()); + if (!duplicates.empty()) { + errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; + for (size_t i = 0; i < duplicates.size(); ++i) { + if (i > 0) errors_cummulative += ", "; + errors_cummulative += duplicates[i]; } } - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); - errors_cummulative += "\n"; } + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; } } @@ -7149,22 +7141,38 @@ PresetBundle::VendorRead PresetBundle::read_vendor(const std::string& dir, const // Reset this bundle, delete user profile files if SaveImported. this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); - // Orca: only a whole-vendor load has a cache — the vendor-only and filament-only - // scans want a slice of one. Validation reads the JSONs whatever is cached. - read.cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); - if (read.cacheable && ! validation_mode) { - // A vendor is loaded from where it is installed and nowhere else; resources - // reaches the app by being installed into `dir` first. The cache there is - // judged against the profile beside it — or, where the cache is the whole - // of the installation, against nothing, since nothing on disk can then be + // Orca: only a whole-vendor load writes a cache, and a vendor-only or filament-only + // scan reads one only where it is the whole installation, as in a shipped build. + // Validation reads the JSONs whatever is cached. + const bool vendor_only = flags.has(LoadConfigBundleAttribute::LoadVendorOnly); + const bool filament_only = flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + read.cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! filament_only; + const boost::filesystem::path dir_path(dir); + const boost::filesystem::path profile = dir_path / (vendor_name + ".json"); + const bool has_profile = boost::filesystem::exists(profile); + if (allow_cache && ! validation_mode && (read.cacheable || ((vendor_only || filament_only) && ! has_profile))) { + // The cache is judged against the profile beside it, or, where the cache is the + // whole of the installation, against nothing, since nothing on disk can then be // newer than it. That state is Semver::inf(), which no real profile carries. - const boost::filesystem::path dir_path(dir); - const boost::filesystem::path profile = dir_path / (vendor_name + ".json"); - const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) : Semver::inf(); + const Semver version = has_profile ? get_version_from_json(profile.string()) : Semver::inf(); read.cache_path = (dir_path / (vendor_name + ".opc")).string(); - read.from_cache = VendorCacheFile::load(read.cache_path, vendor_name, version, read.data); - if (read.from_cache) + if (vendor_only) { + VendorProfile vendor_profile; + if (VendorCacheFile::load_vendor_profile(read.cache_path, vendor_name, version, vendor_profile)) { + this->vendors.emplace(vendor_name, std::move(vendor_profile)); + read.vendor_only = true; + return read; + } + } else if (VendorCacheFile::load(read.cache_path, vendor_name, version, read.data)) { + read.from_cache = true; + if (filament_only) { + read.data.process_entries.clear(); + read.data.machine_entries.clear(); + // The stamp counts errors across the whole vendor. + read.data.parse_errors = 0; + } return read; + } } this->parse_vendor_json(read); return read; diff --git a/src/libslic3r/PresetCacheFormat.cpp b/src/libslic3r/PresetCacheFormat.cpp index 01e634a581..234981c1c5 100644 --- a/src/libslic3r/PresetCacheFormat.cpp +++ b/src/libslic3r/PresetCacheFormat.cpp @@ -471,9 +471,11 @@ bool VendorCacheFile::save(const std::string& path, const std::string& vendor_na } } -// static -bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name, - const Semver& expected_vendor_version, VendorCacheData& data) +// Reads a cache through its vendor profiles under the checks load() documents, then +// calls read_rest on the archive. +template +static bool read_cache_up_to_presets(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorMap& vendors, ReadRest&& read_rest) { std::string blob; if (! read_cache_blob(path, blob)) @@ -488,12 +490,9 @@ bool VendorCacheFile::load(const std::string& path, const std::string& expected_ return false; CacheDictionary dict; dict.load(ar); - ar(data.vendors); - load_entries(ar, data.process_entries, dict); - load_entries(ar, data.filament_entries, dict); - load_entries(ar, data.machine_entries, dict); - ar(data.parse_errors); - if (data.vendors.find(expected_vendor_name) == data.vendors.end()) + ar(vendors); + read_rest(ar, dict); + if (vendors.find(expected_vendor_name) == vendors.end()) throw std::runtime_error("vendor cache does not carry its own vendor profile"); return true; } catch (const std::exception& e) { @@ -502,6 +501,31 @@ bool VendorCacheFile::load(const std::string& path, const std::string& expected_ } } +// static +bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorCacheData& data) +{ + return read_cache_up_to_presets(path, expected_vendor_name, expected_vendor_version, data.vendors, + [&data](cereal::BinaryInputArchive& ar, const CacheDictionary& dict) { + load_entries(ar, data.process_entries, dict); + load_entries(ar, data.filament_entries, dict); + load_entries(ar, data.machine_entries, dict); + ar(data.parse_errors); + }); +} + +// static +bool VendorCacheFile::load_vendor_profile(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorProfile& vendor) +{ + VendorMap vendors; + if (! read_cache_up_to_presets(path, expected_vendor_name, expected_vendor_version, vendors, + [](cereal::BinaryInputArchive&, const CacheDictionary&) {})) + return false; + vendor = std::move(vendors.find(expected_vendor_name)->second); + return true; +} + // static std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name) { diff --git a/src/libslic3r/PresetCacheFormat.hpp b/src/libslic3r/PresetCacheFormat.hpp index 261e7ef796..f259f3cb4f 100644 --- a/src/libslic3r/PresetCacheFormat.hpp +++ b/src/libslic3r/PresetCacheFormat.hpp @@ -169,6 +169,11 @@ public: static bool load(const std::string& path, const std::string& expected_vendor_name, const Semver& expected_vendor_version, VendorCacheData& data); + // Read only the vendor's own profile, under the same checks as load(), + // without deserializing its presets. + static bool load_vendor_profile(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorProfile& vendor); + // Read the profile version a cache was stamped with, without deserializing // its presets. Empty if the file is unreadable, not a cache this build // understands, or not this vendor's. This is how an installed vendor's diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index c7c9cd2760..1ae4e3b81b 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -540,12 +540,9 @@ static std::string calculate_md5(const std::string &input) return md5; } -static std::string get_filament_id(std::string vendor_typr_serial) +// Loads every system filament and the user's own, which a new filament id must not collide with. +static void load_filament_id_sources(PresetBundle &temp_preset_bundle) { - std::unordered_map> filament_id_to_filament_name; - - // temp filament presets - PresetBundle temp_preset_bundle; temp_preset_bundle.load_system_filaments_json(Slic3r::ForwardCompatibilitySubstitutionRule::EnableSilent); std::string dir_user_presets = wxGetApp().app_config->get("preset_folder"); if (dir_user_presets.empty()) { @@ -553,6 +550,13 @@ static std::string get_filament_id(std::string vendor_typr_serial) } else { temp_preset_bundle.load_user_presets(dir_user_presets, ForwardCompatibilitySubstitutionRule::EnableSilent); } +} + +static std::string get_filament_id(std::string vendor_typr_serial, const PresetBundle &temp_preset_bundle) +{ + std::unordered_map> filament_id_to_filament_name; + + // temp filament presets const std::deque &filament_presets = temp_preset_bundle.filaments.get_presets(); for (const Preset &preset : filament_presets) { @@ -1193,7 +1197,9 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons() if (wxID_YES != dlg.ShowModal()) { return; } } - std::string user_filament_id = get_filament_id(filament_preset_name); + PresetBundle filament_id_sources; + load_filament_id_sources(filament_id_sources); + std::string user_filament_id = get_filament_id(filament_preset_name, filament_id_sources); const wxString &curr_create_type = curr_create_filament_type(); @@ -2259,13 +2265,19 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre } else { selected_vendor_id = m_printer_preset_vendor_selected.id; + // The vendor list is built from the bundled profiles, so a vendor the user never + // installed loads from them. + const boost::filesystem::path vendor_dir = is_vendor_installed(selected_vendor_id) ? + boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR : + boost::filesystem::path(Slic3r::resources_dir()) / PRESET_PROFILES_DIR; + try { // Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base // bundle so vendor filaments that inherit OFL bases resolve via the existing // cross-vendor inheritance path. // Orca: served from the vendor's preset cache where one covers it — a shipped // build carries that instead of the raw preset JSONs — and parsed otherwise. - temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(), + temp_preset_bundle.load_vendor_configs_from_json(vendor_dir.string(), selected_vendor_id, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, @@ -2873,13 +2885,21 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren return; } + // One load serves every clone below, since cloning assigns all filament ids before it saves a preset. + PresetBundle filament_id_sources; + if (!selected_filament_presets.empty()) + load_filament_id_sources(filament_id_sources); + auto create_filament_id = [&filament_id_sources](std::string vendor_typr_serial) { + return get_filament_id(vendor_typr_serial, filament_id_sources); + }; + std::vector successful_preset_names; if (curr_selected_preset_type == m_create_type.base_template) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " base template"; /****************************** clone filament preset ********************************/ std::vector failures; if (!selected_filament_presets.empty()) { - bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, get_filament_id, rewritten); + bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, create_filament_id, rewritten); if (!create_preset_result) { std::string message; for (const std::string &failure : failures) { message += "\t" + failure + "\n"; } @@ -2889,7 +2909,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren int res = dlg.ShowModal(); if (wxID_YES == res) { create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, - get_filament_id, true); + create_filament_id, true); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but filament has same preset, user cancel create the printer preset"; return; @@ -2905,7 +2925,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren if (!selected_process_presets.empty()) { generate_process_presets_data(selected_process_presets, printer_nozzle_name); bool create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, - get_filament_id, rewritten); + create_filament_id, rewritten); if (!create_preset_result) { std::string message; for (const std::string &failure : failures) { message += "\t" + failure + "\n"; } @@ -2914,7 +2934,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren wxYES | wxYES_DEFAULT | wxCENTRE); int res = dlg.ShowModal(); if (wxID_YES == res) { - create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, get_filament_id, true); + create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, create_filament_id, true); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but process has same preset, user cancel create the printer preset"; return; @@ -2926,7 +2946,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren /****************************** clone filament preset ********************************/ std::vector failures; if (!selected_filament_presets.empty()) { - bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, get_filament_id, rewritten); + bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, create_filament_id, rewritten); if (!create_preset_result) { std::string message; for (const std::string& failure : failures) { @@ -2937,7 +2957,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren wxYES | wxYES_DEFAULT | wxCENTRE); int res = dlg.ShowModal(); if (wxID_YES == res) { - create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, get_filament_id, true); + create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, create_filament_id, true); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but filament has same preset, user cancel create the printer preset"; return; @@ -2948,7 +2968,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren /****************************** clone process preset ********************************/ failures.clear(); if (!selected_process_presets.empty()) { - bool create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, get_filament_id, rewritten); + bool create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, create_filament_id, rewritten); if (!create_preset_result) { std::string message; for (const std::string& failure : failures) { @@ -2957,7 +2977,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren MessageDialog dlg(this, _L("Create process presets failed. As follows:\n") + from_u8(message) + _L("\nDo you want to rewrite it?"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE); int res = dlg.ShowModal(); if (wxID_YES == res) { - create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, get_filament_id, true); + create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, create_filament_id, true); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but filament has same preset, user cancel create the printer preset"; return; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index e2ff2a2322..a116a9021f 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -6086,6 +6086,57 @@ TEST_CASE("A vendor updated over the air resolves against the library installed } } +TEST_CASE("The vendor and filament scans read the bundled vendors that ship as their cache alone", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir temp_dir; + const fs::path profiles = temp_dir.path() / "resources" / PRESET_PROFILES_DIR; + ScopedResourcesDir scoped_resources(temp_dir.path() / "resources"); + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + fs::create_directories(profiles / lib / "filament"); + std::ofstream((profiles / (lib + ".json")).string()) + << R"({"version":"1.0.0","name":")" << lib << R"(",)" + << R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})"; + std::ofstream((profiles / lib / "filament" / "generic_pla.json").string()) + << R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false","filament_id":"GFL99"})"; + fs::create_directories(profiles / "Acme" / "machine"); + fs::create_directories(profiles / "Acme" / "filament"); + std::ofstream((profiles / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme",)" + << R"("machine_model_list":[{"name":"Acme One","sub_path":"machine/model.json"}],)" + << R"("machine_list":[{"name":"Acme Printer","sub_path":"machine/printer.json"}],)" + << R"("filament_list":[{"name":"Acme PLA","sub_path":"filament/pla.json"}]})"; + std::ofstream((profiles / "Acme" / "machine" / "model.json").string()) + << R"({"type":"machine_model","name":"Acme One","nozzle_diameter":"0.4"})"; + std::ofstream((profiles / "Acme" / "machine" / "printer.json").string()) + << R"({"type":"machine","name":"Acme Printer","from":"system","instantiation":"true","printer_model":"Acme One","printer_variant":"0.4"})"; + std::ofstream((profiles / "Acme" / "filament" / "pla.json").string()) + << R"({"type":"filament","name":"Acme PLA","from":"system","instantiation":"true","inherits":"Generic PLA","filament_id":"P0000001"})"; + + auto installed = [](const PresetCollection &presets) { + return std::count_if(presets.get_presets().begin(), presets.get_presets().end(), [](const Preset &preset) { return !preset.is_default; }); + }; + auto scan = [&installed] { + std::vector found; + PresetBundle models; + models.load_system_models_from_json(ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(installed(models.printers) + installed(models.prints) + installed(models.filaments) == 0); + for (const auto &[vendor_id, vendor] : models.vendors) + for (const VendorProfile::PrinterModel &model : vendor.models) + found.push_back(vendor_id + " model " + model.id); + PresetBundle filaments; + filaments.load_system_filaments_json(ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(installed(filaments.printers) + installed(filaments.prints) == 0); + for (const Preset &preset : filaments.filaments.get_presets()) + if (!preset.is_default) + found.push_back(preset.name + " " + preset.filament_id); + return found; + }; + const std::vector expected{"Acme model Acme One", "Acme PLA P0000001"}; + REQUIRE(scan() == expected); + reduce_vendors_to_caches(profiles, {lib, "Acme"}); + CHECK(scan() == expected); +} + namespace { // A default preset config for type, built the way PresetBundle builds its default presets.