diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 07d20cfb8b..94786fa34c 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -588,13 +588,15 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa return it->second.get(); // The library loads with no base of its own, so the tree a vendor inherits from - // is the same one that resolves the library's own presets. - const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY); - const PresetBundle *library = nullptr; - if (vendor_id != ORCA_FILAMENT_LIBRARY && - (boost::filesystem::is_regular_file(root_dir / (library_file + ".json")) || - (allow_cache && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"))))) { - library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache); + // is the same one that resolves the library's own presets. It is only a base, so + // it comes from its cache whenever that is all that is installed, even when the + // vendor itself is parsed (a vendor updated over the air). + const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY); + const bool library_json = boost::filesystem::is_regular_file(root_dir / (library_file + ".json")); + const bool library_cache_only = !library_json && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc")); + const PresetBundle *library = nullptr; + if (vendor_id != ORCA_FILAMENT_LIBRARY && (library_json || library_cache_only)) { + library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache || library_cache_only); if (library == nullptr) { error = "OrcaFilamentLibrary contains invalid presets"; return nullptr; @@ -2527,7 +2529,7 @@ void PresetBundle::clear_printer_hold_aliases() //BBS: add json related logic, load system presets from json std::pair PresetBundle::load_system_presets_from_json( - ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache) + ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches) { //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule; @@ -2547,14 +2549,14 @@ std::pair PresetBundle::load_system_pre // The vendors below are loaded whole and against each other — the filament // library first, then every other vendor with it as the base — so each parse // is complete enough to be worth caching. - m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode); + m_generate_vendor_caches = write_caches && (m_generate_vendor_caches || !validation_mode); // Sorted, so any duplicate-preset warning comes out in the same order on every run. std::vector vendors; for (const std::string& name : vendor_names_in(dir)) if (name == ORCA_FILAMENT_LIBRARY || !(validation_mode && !vendor_to_validate.empty() && name != vendor_to_validate)) vendors.push_back({ name, dir }); - auto result = this->load_vendors(vendors, compatibility_rule, allow_cache); + auto result = this->load_vendors(vendors, compatibility_rule, true); this->update_system_maps(); diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index e09ce9dcb5..4355e78168 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -839,7 +839,9 @@ private: //std::pair load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); //BBS: add json related logic - std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = true); + // Reads each vendor from its preset cache where one covers the profile, as every load does. + // write_caches = false keeps a read-only load from writing caches into the data directory. + std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches = true); // Update the multicolor information for filaments. void update_filament_multi_color(); // Update renamed_from and alias maps of system profiles. diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index addc33b468..ab1a3dd1a4 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -5788,6 +5788,131 @@ TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]") namespace { +// Writes each vendor's preset cache into dir, then deletes its profile JSONs: what a release build installs. +void reduce_vendors_to_caches(const fs::path &dir, const std::vector &vendor_ids) +{ + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + PresetBundle library; + if (fs::exists(dir / (lib + ".json"))) { + library.set_generate_vendor_caches(true); + library.load_vendor_configs_from_json(dir.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + } + for (const std::string &vendor_id : vendor_ids) { + if (vendor_id == lib) + continue; + PresetBundle writer; + writer.set_generate_vendor_caches(true); + writer.load_vendor_configs_from_json(dir.string(), vendor_id, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &library); + } + for (const std::string &vendor_id : vendor_ids) { + REQUIRE(fs::exists(dir / (vendor_id + ".opc"))); + fs::remove(dir / (vendor_id + ".json")); + fs::remove_all(dir / vendor_id); + } +} + +// The filament library with one abstract base filament, and an "Acme" vendor whose one filament inherits it. +void write_library_and_acme_filament(const fs::path &root) +{ + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + fs::create_directories(root / lib / "filament"); + std::ofstream((root / (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((root / lib / "filament" / "generic_pla.json").string()) + << R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false","filament_id":"GFL99","filament_cost":"27"})"; + fs::create_directories(root / "Acme" / "filament"); + std::ofstream((root / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","filament_list":[{"name":"Acme PLA","sub_path":"filament/pla.json"}]})"; + std::ofstream((root / "Acme" / "filament" / "pla.json").string()) + << R"({"type":"filament","name":"Acme PLA","from":"system","instantiation":"true","inherits":"Generic PLA"})"; +} + +} // namespace + +TEST_CASE("A read-only load resolves a user preset against vendors installed as their cache alone", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir temp_dir; + const fs::path data = temp_dir.path() / "data"; + const fs::path system = data / PRESET_SYSTEM_DIR; + ScopedDataDir scoped_data(data); + ScopedResourcesDir scoped_resources(temp_dir.path() / "resources"); + write_acme_printer_vendor(system, 33.); + reduce_vendors_to_caches(system, {"Acme"}); + + fs::create_directories(data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME); + std::ofstream((data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME / "My Acme.json").string()) + << R"({"type":"machine","name":"My Acme","from":"User","version":"2.3.0.0","inherits":"Acme Printer","printable_height":"123"})"; + + AppConfig app_config; + PresetBundle bundle; + std::string errors; + bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(), + &errors, true); + CHECK(errors.empty()); + const Preset *preset = bundle.printers.find_preset("My Acme"); + REQUIRE(preset != nullptr); + CHECK_THAT(preset->config.opt_float("printable_height"), Catch::Matchers::WithinAbs(123., 1e-6)); + CHECK_THAT(preset->config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6)); +} + +TEST_CASE("A read-only load writes no preset cache", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir temp_dir; + const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR; + ScopedDataDir scoped_data(temp_dir.path() / "data"); + ScopedResourcesDir scoped_resources(temp_dir.path() / "resources"); + write_acme_printer_vendor(system, 33.); + + AppConfig app_config; + PresetBundle bundle; + std::string errors; + bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(), + &errors, true); + CHECK(errors.empty()); + CHECK(bundle.printers.find_preset("Acme Printer") != nullptr); + CHECK_FALSE(fs::exists(system / "Acme.opc")); +} + +TEST_CASE("A vendor updated over the air resolves against the library installed as its cache alone", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir temp_dir; + const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR; + ScopedDataDir scoped_data(temp_dir.path() / "data"); + ScopedResourcesDir scoped_resources(temp_dir.path() / "resources"); + // System presets are found by name through the bundled profiles. + write_library_and_acme_filament(temp_dir.path() / "resources" / PRESET_PROFILES_DIR); + // The release install, then an update that brings Acme back as JSONs while the library stays a cache. + write_library_and_acme_filament(system); + reduce_vendors_to_caches(system, {PresetBundle::ORCA_FILAMENT_LIBRARY, "Acme"}); + write_library_and_acme_filament(temp_dir.path() / "update"); + fs::copy_file(temp_dir.path() / "update" / "Acme.json", system / "Acme.json"); + fs::create_directories(system / "Acme" / "filament"); + fs::copy_file(temp_dir.path() / "update" / "Acme" / "filament" / "pla.json", system / "Acme" / "filament" / "pla.json"); + + SECTION("by name") { + PresetBundle bundle; + DynamicPrintConfig config; + std::string error; + REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_FILAMENT, "Acme PLA", + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK_THAT(config.opt("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6)); + } + SECTION("by its source file") { + PresetBundle bundle; + DynamicPrintConfig config; + config.option(BBL_JSON_KEY_INHERITS, true)->value = "Generic PLA"; + std::string error; + REQUIRE(bundle.resolve_preset_config(config, Preset::TYPE_FILAMENT, (system / "Acme" / "filament" / "pla.json").string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK_THAT(config.opt("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6)); + } +} + +namespace { + // A default preset config for type, built the way PresetBundle builds its default presets. DynamicPrintConfig external_default_config(Preset::Type type) {