From bcb4f17d9ae5dd9c807550c92163389dcba66f28 Mon Sep 17 00:00:00 2001 From: Maximilian Ghazanfar <95306766+mazzanfar@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:12:55 -0400 Subject: [PATCH] fix(cli): resolve inherited presets through vendor manifests (#15438) * fix(cli): resolve inherited presets through vendor manifests * fix(cli): resolve typeless inherited presets Probe the configured preset collections when a preset JSON omits its type. Reject missing, cross-type, and duplicate identities instead of silently selecting a candidate. * fix(cli): allow missing app config during preset resolution * fix(cli): tolerate malformed app config during preset resolution --- src/OrcaSlicer.cpp | 82 +++- src/libslic3r/AppConfig.cpp | 5 + src/libslic3r/AppConfig.hpp | 4 +- src/libslic3r/Preset.cpp | 22 +- src/libslic3r/Preset.hpp | 2 +- src/libslic3r/PresetBundle.cpp | 221 ++++++++-- src/libslic3r/PresetBundle.hpp | 31 +- .../libslic3r/test_preset_bundle_loading.cpp | 395 ++++++++++++++++++ 8 files changed, 718 insertions(+), 44 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 0a770e0d8a..31f39921f4 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1974,7 +1974,79 @@ int CLI::run(int argc, char **argv) } } - auto load_config_file = [](const std::string& file, DynamicPrintConfig& config, std::string& config_type, + std::unique_ptr cli_preset_bundle; + auto ensure_cli_preset_bundle = [&cli_preset_bundle, config_substitution_rule](std::string &error) -> PresetBundle * { + if (cli_preset_bundle) + return cli_preset_bundle.get(); + try { + AppConfig app_config; + const std::string app_config_error = app_config.load_if_exists(); + if (!app_config_error.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Ignoring invalid app config during CLI preset resolution: " << app_config_error; + app_config.reset(); + } + + auto bundle = std::make_unique(); + std::string load_error; + bundle->load_presets(app_config, config_substitution_rule, + PresetBundle::PresetPreferences(), &load_error, true); + if (!load_error.empty()) { + error = "Failed to load presets for inheritance resolution: " + load_error; + return nullptr; + } + cli_preset_bundle = std::move(bundle); + return cli_preset_bundle.get(); + } catch (const std::exception &ex) { + error = ex.what(); + return nullptr; + } + }; + + auto resolve_preset = [&ensure_cli_preset_bundle, config_substitution_rule](const std::string &file, DynamicPrintConfig &config, + std::string &config_type, const std::string &config_from, + bool probe_type, std::string &error) { + const auto *inherits = config.option(BBL_JSON_KEY_INHERITS); + if (!probe_type && (inherits == nullptr || inherits->value.empty())) + return true; + + std::unique_ptr source_bundle; + PresetBundle *bundle = nullptr; + bool allow_source_manifest = false; + if (config_from == "system") { + source_bundle = std::make_unique(); + bundle = source_bundle.get(); + allow_source_manifest = true; + } else { + bundle = ensure_cli_preset_bundle(error); + if (bundle == nullptr) + return false; + } + + if (probe_type) { + Preset::Type preset_type; + if (!bundle->resolve_preset_config_type(config, preset_type, file, config_substitution_rule, + error, allow_source_manifest)) + return false; + config_type = Preset::get_type_string(preset_type); + return true; + } + + Preset::Type preset_type; + if (config_type == "process") + preset_type = Preset::TYPE_PRINT; + else if (config_type == "filament") + preset_type = Preset::TYPE_FILAMENT; + else if (config_type == "machine") + preset_type = Preset::TYPE_PRINTER; + else { + error = "Unsupported preset type: " + config_type; + return false; + } + return bundle->resolve_preset_config(config, preset_type, file, config_substitution_rule, + error, allow_source_manifest); + }; + + auto load_config_file = [config_substitution_rule, &resolve_preset](const std::string& file, DynamicPrintConfig& config, std::string& config_type, std::string& config_name, std::string& filament_id, std::string& config_from) { if (! boost::filesystem::exists(file)) { boost::nowide::cerr << __FUNCTION__<< ": can not find setting file: " << file << std::endl; @@ -2003,9 +2075,15 @@ int CLI::run(int argc, char **argv) } auto type_iter = key_values.find(BBL_JSON_KEY_TYPE); - if (type_iter != key_values.end()) { + const bool probe_type = type_iter == key_values.end(); + if (!probe_type) config_type = type_iter->second; + + if (!resolve_preset(file, config, config_type, config_from, probe_type, reason)) { + boost::nowide::cerr << __FUNCTION__ << boost::format(": can not resolve preset %1%: %2%") % file % reason << std::endl; + return CLI_CONFIG_FILE_ERROR; } + if (config_type == "machine") { //config.set("printer_settings_id", config_name, true); //printer_inherits = config.option("inherits", true)->value; diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 29eb96aa5b..d26296cd69 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -1823,4 +1823,9 @@ bool AppConfig::exists() return boost::filesystem::exists(config_path()); } +std::string AppConfig::load_if_exists() +{ + return boost::filesystem::exists(loading_path()) ? load() : std::string(); +} + }; // namespace Slic3r diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 0a278f4f1f..c799502993 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -113,8 +113,10 @@ public: void set_defaults(); // Load the slic3r.ini from a user profile directory (or a datadir, if configured). - // return error string or empty strinf + // Return an error string, or an empty string on success. std::string load(); + // Treat a missing config as default state; otherwise load it normally. + std::string load_if_exists(); // Store the slic3r.ini into a user profile directory (or a datadir, if configured). void save(); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index de17a678ce..279ad49d92 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1653,7 +1653,7 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con void PresetCollection::load_presets( const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule, - std::function preset_loaded_fn, const PresetOrigin &load_origin) + std::function preset_loaded_fn, const PresetOrigin &load_origin, bool read_only) { // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, // see https://github.com/prusa3d/PrusaSlicer/issues/732 @@ -1662,7 +1662,7 @@ void PresetCollection::load_presets( // Load custom roots first if (fs::exists(dir / "base")) { - load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin); + load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin, read_only); } //BBS: add config related logs @@ -1670,7 +1670,8 @@ void PresetCollection::load_presets( //BBS do not parse folder if not exists m_dir_path = dir.string(); if (!fs::exists(dir)) { - fs::create_directory(dir); + if (!read_only) + fs::create_directory(dir); return; } @@ -1720,10 +1721,10 @@ void PresetCollection::load_presets( substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) }); if (!reason.empty()) { fs::path file_path(preset.file); - if (fs::exists(file_path)) + if (!read_only && fs::exists(file_path)) fs::remove(file_path); file_path.replace_extension(".info"); - if (fs::exists(file_path)) + if (!read_only && fs::exists(file_path)) fs::remove(file_path); BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file; ++m_errors; @@ -1794,7 +1795,8 @@ void PresetCollection::load_presets( size_t at_pos = name.find('@'); if (at_pos != std::string::npos && at_pos + 1 < name.length()) { compatible_printers->values.push_back(name.substr(at_pos + 1)); - preset.save(nullptr); + if (!read_only) + preset.save(nullptr); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name; } } @@ -1812,10 +1814,10 @@ void PresetCollection::load_presets( ++m_errors; BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what(); fs::path file_path(preset.file); - if (fs::exists(file_path)) + if (!read_only && fs::exists(file_path)) fs::remove(file_path); file_path.replace_extension(".info"); - if (fs::exists(file_path)) + if (!read_only && fs::exists(file_path)) fs::remove(file_path); //throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what()); } catch (const std::runtime_error &err) { @@ -1823,10 +1825,10 @@ void PresetCollection::load_presets( BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what(); //throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what()); fs::path file_path(preset.file); - if (fs::exists(file_path)) + if (!read_only && fs::exists(file_path)) fs::remove(file_path); file_path.replace_extension(".info"); - if (fs::exists(file_path)) + if (!read_only && fs::exists(file_path)) fs::remove(file_path); } diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index e0728d7fa2..6a7871d07d 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -558,7 +558,7 @@ public: void add_default_preset(const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name); // Load ini files of the particular type from the provided directory path. - void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, std::function preset_loaded_fn = nullptr, const PresetOrigin &load_origin = PresetOrigin()); + void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, std::function preset_loaded_fn = nullptr, const PresetOrigin &load_origin = PresetOrigin(), bool read_only = false); //BBS: update user presets directory void update_user_presets_directory(const std::string& dir_path, const std::string& type); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 29bccf0966..6d4e77837a 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -453,6 +453,158 @@ PresetBundle::PresetBundle() this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options); } +bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Type type, + const std::string &source_file, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error, bool allow_source_manifest) +{ + if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) + compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; + else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) + compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; + + auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * { + switch (preset_type) { + case Preset::TYPE_PRINT: return &bundle.prints; + case Preset::TYPE_FILAMENT: return &bundle.filaments; + case Preset::TYPE_PRINTER: return &bundle.printers; + default: return nullptr; + } + }; + + PresetCollection *collection = collection_for_type(*this, type); + if (collection == nullptr) { + error = "Unsupported preset type"; + return false; + } + + const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal(); + auto find_loaded = [&](PresetBundle &bundle) -> const Preset * { + PresetCollection *loaded_collection = collection_for_type(bundle, type); + const Preset *resolved = nullptr; + for (const Preset &preset : loaded_collection->get_presets()) { + if (preset.file.empty()) + continue; + + boost::system::error_code ec; + const bool same_file = boost::filesystem::equivalent(source_path, boost::filesystem::path(preset.file), ec); + if (ec || !same_file) + continue; + if (resolved != nullptr) { + error = "Preset identity is ambiguous"; + return nullptr; + } + resolved = &preset; + } + return resolved; + }; + + if (const Preset *resolved = find_loaded(*this)) { + config = resolved->config; + error.clear(); + return true; + } + if (error == "Preset identity is ambiguous") + return false; + if (!allow_source_manifest) { + error = "Preset was not found in the loaded bundle"; + return false; + } + + // A manifest-backed source file can be resolved without requiring the vendor + // to have been copied into data_dir()/system. Find the nearest ancestor whose + // sibling manifest names it, then let the canonical vendor loader flatten the + // complete tree (including nested sub_path entries and library inheritance). + for (boost::filesystem::path vendor_dir = source_path.parent_path(); !vendor_dir.empty(); vendor_dir = vendor_dir.parent_path()) { + const std::string vendor_id = vendor_dir.filename().string(); + if (vendor_id.empty()) + continue; + const boost::filesystem::path root_dir = vendor_dir.parent_path(); + const boost::filesystem::path manifest = root_dir / (vendor_id + ".json"); + if (!boost::filesystem::is_regular_file(manifest)) + continue; + const boost::filesystem::path manifest_relative = source_path.lexically_relative(vendor_dir); + if (manifest_relative.empty() || *manifest_relative.begin() == "..") + continue; + + try { + PresetBundle library_bundle; + const PresetBundle *base_bundle = nullptr; + if (vendor_id != ORCA_FILAMENT_LIBRARY && + boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) { + library_bundle.m_preserve_vendor_source_paths = true; + library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem, + compatibility_rule, nullptr, false); + if (library_bundle.error_count() != 0) { + error = "OrcaFilamentLibrary contains invalid presets"; + return false; + } + base_bundle = &library_bundle; + } + + PresetBundle source_bundle; + source_bundle.m_preserve_vendor_source_paths = true; + source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, + compatibility_rule, base_bundle, false); + if (source_bundle.error_count() != 0) { + error = "Vendor bundle contains invalid presets"; + return false; + } + + const Preset *resolved = find_loaded(source_bundle); + if (resolved == nullptr) { + if (error.empty()) + error = "Source file is not an instantiated preset in its vendor manifest"; + return false; + } + config = resolved->config; + error.clear(); + return true; + } catch (const std::exception &ex) { + error = ex.what(); + return false; + } + } + + error = "Preset was not found in the loaded bundle"; + return false; +} + +bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, + const std::string &source_file, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error, bool allow_source_manifest) +{ + std::optional> resolved; + for (Preset::Type candidate_type : types_list(ptFFF)) { + DynamicPrintConfig candidate_config(config); + std::string candidate_error; + if (!resolve_preset_config(candidate_config, candidate_type, source_file, compatibility_rule, + candidate_error, allow_source_manifest)) { + if (candidate_error == "Preset identity is ambiguous") { + error = std::move(candidate_error); + return false; + } + continue; + } + if (resolved) { + error = "Preset type is ambiguous"; + return false; + } + resolved.emplace(candidate_type, std::move(candidate_config)); + } + + if (!resolved) { + error = "Preset type could not be resolved"; + return false; + } + + type = resolved->first; + config = std::move(resolved->second); + error.clear(); + return true; +} + PresetBundle::PresetBundle(const PresetBundle &rhs) { *this = rhs; @@ -574,7 +726,8 @@ void PresetBundle::copy_files(const std::string& from) } PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, - const PresetPreferences& preferred_selection/* = PresetPreferences()*/) + const PresetPreferences& preferred_selection/* = PresetPreferences()*/, + std::string *errors, bool read_only) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; @@ -585,16 +738,20 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward const auto startup_t0 = std::chrono::steady_clock::now(); //BBS: change system config to json - std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); + std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule, !read_only); + if (errors != nullptr) + *errors = errors_cummulative; // BBS load preset from user's folder, load system default if // BBS: change directories by design std::string dir_user_presets = config.get("preset_folder"); if (dir_user_presets.empty()) { - load_user_presets(DEFAULT_USER_FOLDER_NAME, substitution_rule); + load_user_presets(DEFAULT_USER_FOLDER_NAME, substitution_rule, read_only); } else { - load_user_presets(dir_user_presets, substitution_rule); + load_user_presets(dir_user_presets, substitution_rule, read_only); } + if (errors != nullptr && errors->empty() && m_errors != 0) + *errors = "Preset loading reported " + std::to_string(m_errors) + " error(s)"; // Rewrite renamed compatible_printers / compatible_prints references before selection. Skipped // in validation mode so the profile validator (has_errors -> check_preset_references) sees the @@ -1010,18 +1167,26 @@ std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_n return out; } -PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule) +PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule, bool read_only) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry and user is: " << user; PresetsConfigSubstitutions substitutions; std::string errors_cummulative; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); - if (!fs::exists(user_folder)) fs::create_directory(user_folder); + if (!fs::exists(user_folder)) { + if (read_only) + return substitutions; + fs::create_directory(user_folder); + } std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user; fs::path folder(user_folder / user); - if (!fs::exists(folder)) fs::create_directory(folder); + if (!fs::exists(folder)) { + if (read_only) + return substitutions; + fs::create_directory(folder); + } bundles.WriteLock(); bundles.m_bundles.clear(); @@ -1049,13 +1214,13 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.print_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only); this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only); this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only); metadata.bundle_type = BundleType::Local; metadata.path = metadata_file.string(); @@ -1085,13 +1250,13 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.print_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only); this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only); this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only); metadata.bundle_type = BundleType::Subscribed; metadata.path = metadata_file.string(); @@ -1110,17 +1275,20 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For const auto json_t0 = std::chrono::steady_clock::now(); try { std::string sel = prints.get_selected_preset().name; - this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); + this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule, + nullptr, PresetOrigin(), read_only); prints.select_preset_by_name(sel, false); } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } try { std::string sel = filaments.get_selected_preset().name; - this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); + this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule, + nullptr, PresetOrigin(), read_only); filaments.select_preset_by_name(sel, false); } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } try { std::string sel = printers.get_selected_preset().name; - this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); + this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule, + nullptr, PresetOrigin(), read_only); printers.select_preset_by_name(sel, false); } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); @@ -2266,7 +2434,8 @@ 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) +std::pair PresetBundle::load_system_presets_from_json( + ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache) { //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule; @@ -2288,7 +2457,7 @@ 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 = m_generate_vendor_caches || ! validation_mode; + m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode); PresetsConfigSubstitutions substitutions; std::string errors_cummulative; @@ -2318,7 +2487,8 @@ std::pair PresetBundle::load_system_pre // state into this load. this->clear_printer_hold_aliases(); this->m_errors = 0; - append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); + append(substitutions, this->load_vendor_configs_from_json( + dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule, nullptr, allow_cache).first); first = false; } catch (const std::runtime_error &err) { if (validation_mode) @@ -2343,7 +2513,7 @@ std::pair PresetBundle::load_system_pre bundle->set_generate_vendor_caches(m_generate_vendor_caches); try { auto result = bundle->load_vendor_configs_from_json( - dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this); + dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this, allow_cache); parallel_substitutions[i] = std::move(result.first); parallel_bundles[i] = std::move(bundle); } catch (const std::runtime_error &err) { @@ -5280,9 +5450,11 @@ std::string PresetBundle::load_vendor_preset( return reason; } - auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / entry.sub_path).make_preferred(); - if(validation_mode) + auto file_path = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / vendor_name / entry.sub_path).make_preferred(); + if (validation_mode) file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred(); + if (m_preserve_vendor_source_paths) + file_path = (boost::filesystem::path(path) / vendor_name / entry.sub_path).make_preferred(); // Load the preset into the list of presets, save it to disk. Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); @@ -5348,7 +5520,8 @@ std::string PresetBundle::load_vendor_preset( //BBS: Load a config bundle file from json std::pair PresetBundle::load_vendor_configs_from_json( - const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, + ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle, bool allow_cache) { // Enable substitutions for user config bundle, throw an exception when loading a system profile. ConfigSubstitutionContext substitution_context { compatibility_rule }; @@ -5366,7 +5539,7 @@ std::pair PresetBundle::load_vendor_configs_ // 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. const boost::filesystem::path dir_path(dir); - const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + const bool cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { size_t presets_loaded = 0; for (const PresetCollection* coll : std::initializer_list{ diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index b927c5ee6f..07e5444906 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -230,7 +230,22 @@ public: // Load selections (current print, current filaments, current printer) from config.ini // select preferred presets, if any exist PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule, - const PresetPreferences& preferred_selection = PresetPreferences()); + const PresetPreferences& preferred_selection = PresetPreferences(), + std::string *errors = nullptr, bool read_only = false); + + // Resolve an explicitly named source file through a canonical flattened + // preset. Exact loaded-file identity is preferred; otherwise a manifest- + // backed vendor tree is loaded from that source root without using caches. + bool resolve_preset_config(DynamicPrintConfig &config, Preset::Type type, + const std::string &source_file, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error, bool allow_source_manifest = true); + // Resolve a source file whose JSON omits `type`. Succeeds only when exactly + // one FFF preset collection owns the file and returns that collection's type. + bool resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, + const std::string &source_file, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error, bool allow_source_manifest = true); // Load selections (current print, current filaments, current printer) from config.ini // This is done just once on application start up. @@ -238,7 +253,7 @@ public: void load_selections(AppConfig &config, const PresetPreferences& preferred_selection = PresetPreferences()); // BBS Load user presets - PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule); + PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule, bool read_only = false); PresetsConfigSubstitutions load_user_presets(AppConfig &config, std::map>& my_presets, ForwardCompatibilitySubstitutionRule rule); // Orca: Import subscribed bundle presets (load and save to disk in one operation), handles one bundle at a time PresetsConfigSubstitutions update_subscribed_presets(AppConfig& config, @@ -481,10 +496,13 @@ public: //Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance // Orca: `dir` is where the vendor is looked for — its own directory, whether or // not the profile JSONs are still there. A whole-vendor load comes from the - // vendor's preset cache whenever one covers the profile on disk, and is parsed - // from the JSONs in `dir` only when none does. Nothing here reads resources. + // vendor's preset cache whenever one covers the profile on disk and allow_cache + // is true, and is parsed from the JSONs in `dir` otherwise. Nothing here reads + // resources implicitly. std::pair load_vendor_configs_from_json( - const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, + ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr, + bool allow_cache = true); // Export a config bundle file containing all the presets and the names of the active presets. //void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); @@ -606,6 +624,7 @@ private: // Whether to (re)write a per-vendor cache after a JSON parse. bool m_generate_vendor_caches { false }; + bool m_preserve_vendor_source_paths { false }; // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). @@ -613,7 +632,7 @@ private: //std::pair load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); //BBS: add json related logic - std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); + std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = 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 59aae221b5..26cbf387ec 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -554,6 +554,401 @@ struct LibraryFilamentTestCollection : public PresetCollection } // namespace +TEST_CASE("Missing app config is accepted as default CLI state", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + AppConfig app_config; + app_config.set_loading_path((dir.path() / "missing.conf").string()); + CHECK(app_config.load_if_exists().empty()); +} + +TEST_CASE("Read-only user preset loading does not create or delete files", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + PresetBundle bundle; + PresetsConfigSubstitutions substitutions; + + const fs::path missing_root = dir.path() / "missing-user"; + bundle.prints.load_presets(missing_root.string(), PRESET_PRINT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::EnableSilent, nullptr, + PresetOrigin(), true); + CHECK_FALSE(fs::exists(missing_root / PRESET_PRINT_NAME)); + + const fs::path malformed = dir.path() / "existing-user" / PRESET_PRINT_NAME / "malformed.json"; + fs::create_directories(malformed.parent_path()); + std::ofstream(malformed.string()) << "{not-json"; + bundle.prints.load_presets((dir.path() / "existing-user").string(), PRESET_PRINT_NAME, substitutions, + ForwardCompatibilitySubstitutionRule::EnableSilent, nullptr, + PresetOrigin(), true); + CHECK(fs::exists(malformed)); +} + +TEST_CASE("Typeless preset resolution probes loaded FFF collections", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path source_file = dir.path() / "typeless-process.json"; + std::ofstream(source_file.string()) << R"({"name":"Typeless Process","from":"User"})"; + + PresetBundle bundle; + Preset &process = add_inmemory_preset(bundle.prints, "Typeless Process"); + process.file = source_file.string(); + process.config.option("travel_speed", true)->values = {321.0}; + + DynamicPrintConfig raw; + Preset::Type resolved_type = Preset::TYPE_INVALID; + std::string error; + REQUIRE(bundle.resolve_preset_config_type(raw, resolved_type, source_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error, false)); + CHECK(error.empty()); + CHECK(resolved_type == Preset::TYPE_PRINT); + REQUIRE(raw.option("travel_speed")->values.size() == 1); + CHECK_THAT(raw.option("travel_speed")->values.front(), Catch::Matchers::WithinAbs(321.0, 1e-6)); +} + +TEST_CASE("Typeless preset resolution preserves duplicate identity ambiguity", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path source_file = dir.path() / "duplicate-process.json"; + std::ofstream(source_file.string()) << "{}"; + + PresetBundle bundle; + add_inmemory_preset(bundle.prints, "First Process Identity").file = source_file.string(); + add_inmemory_preset(bundle.prints, "Second Process Identity").file = source_file.string(); + + DynamicPrintConfig raw; + Preset::Type resolved_type = Preset::TYPE_INVALID; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config_type(raw, resolved_type, source_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error, false)); + CHECK(error == "Preset identity is ambiguous"); + CHECK(resolved_type == Preset::TYPE_INVALID); +} + +TEST_CASE("Typeless preset resolution rejects cross-type ambiguity", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path source_file = dir.path() / "ambiguous.json"; + std::ofstream(source_file.string()) << "{}"; + + PresetBundle bundle; + add_inmemory_preset(bundle.prints, "Process Identity").file = source_file.string(); + add_inmemory_preset(bundle.filaments, "Filament Identity").file = source_file.string(); + + DynamicPrintConfig raw; + Preset::Type resolved_type = Preset::TYPE_INVALID; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config_type(raw, resolved_type, source_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error, false)); + CHECK(error == "Preset type is ambiguous"); + CHECK(resolved_type == Preset::TYPE_INVALID); +} + +TEST_CASE("Typeless preset resolution rejects a missing type candidate", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path source_file = dir.path() / "unknown.json"; + std::ofstream(source_file.string()) << "{}"; + + PresetBundle bundle; + DynamicPrintConfig raw; + Preset::Type resolved_type = Preset::TYPE_INVALID; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config_type(raw, resolved_type, source_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error, false)); + CHECK(error == "Preset type could not be resolved"); + CHECK(resolved_type == Preset::TYPE_INVALID); +} + +TEST_CASE("Exact file resolution rejects multiple preset identities", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path source_file = dir.path() / "duplicate.json"; + std::ofstream(source_file.string()) << "{}"; + + PresetBundle bundle; + Preset &first = add_inmemory_preset(bundle.prints, "First Identity"); + first.file = source_file.string(); + Preset &second = add_inmemory_preset(bundle.prints, "Second Identity"); + second.file = source_file.string(); + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "Parent"; + + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, source_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error, false)); + CHECK(error == "Preset identity is ambiguous"); +} + +TEST_CASE("System preset resolution returns the canonical vendor configuration", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir source_dir; + PresetBundle bundle; + + VendorProfile vendor("VendorB"); + vendor.name = "Vendor B"; + auto [vendor_it, inserted] = bundle.vendors.emplace(vendor.id, std::move(vendor)); + REQUIRE(inserted); + + Preset &resolved = add_inmemory_preset(bundle.prints, "Vendor B Process", "fdm_process_common"); + resolved.is_system = true; + resolved.vendor = &vendor_it->second; + resolved.file = (source_dir.path() / "vendor-b-process.json").string(); + std::ofstream(resolved.file) << "{}"; + resolved.config.option("travel_speed", true)->values = {321.0}; + resolved.config.option("wall_loops", true)->value = 2; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + raw.option("wall_loops", true)->value = 5; + + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, resolved.file, + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(error.empty()); + REQUIRE(raw.option("travel_speed")->values.size() == 1); + CHECK_THAT(raw.option("travel_speed")->values.front(), Catch::Matchers::WithinAbs(321.0, 1e-6)); + CHECK(raw.option("wall_loops")->value == 2); +} + +TEST_CASE("Manifest-backed preset resolution loads the source vendor tree", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path vendor_dir = dir.path() / "Acme"; + const fs::path child_file = vendor_dir / "process" / "nested" / "child.json"; + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"fdm_process_common","sub_path":"process/base.json"},)" + << R"({"name":"Acme Process","sub_path":"process/nested/child.json"}]})"; + fs::create_directories(child_file.parent_path()); + std::ofstream((vendor_dir / "process" / "base.json").string()) + << R"({"type":"process","name":"fdm_process_common","from":"system",)" + << R"("instantiation":"false","travel_speed":["321"]})"; + std::ofstream(child_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","inherits":"fdm_process_common","wall_loops":"5"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + raw.option("wall_loops", true)->value = 5; + + PresetBundle bundle; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(error.empty()); + REQUIRE(raw.option("travel_speed")->values.size() == 1); + CHECK_THAT(raw.option("travel_speed")->values.front(), Catch::Matchers::WithinAbs(321.0, 1e-6)); + CHECK(raw.option("wall_loops")->value == 5); +} + +TEST_CASE("Manifest-backed resolution is scoped to the explicit source root", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + auto write_vendor = [&](const std::string &root_name, double travel_speed) { + const fs::path root = dir.path() / root_name; + const fs::path child_file = root / "Acme" / "process" / "child.json"; + fs::create_directories(child_file.parent_path()); + std::ofstream((root / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"fdm_process_common","sub_path":"process/base.json"},)" + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + std::ofstream((root / "Acme" / "process" / "base.json").string()) + << R"({"type":"process","name":"fdm_process_common","from":"system",)" + << R"("instantiation":"false","travel_speed":[")" << travel_speed << R"("]})"; + std::ofstream(child_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","inherits":"fdm_process_common"})"; + return child_file; + }; + + const fs::path source_a = write_vendor("root-a", 111.0); + const fs::path source_b = write_vendor("root-b", 222.0); + REQUIRE(fs::exists(source_a)); + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "synthetic-parent-marker"; + + PresetBundle bundle; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, source_b.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + REQUIRE(raw.option("travel_speed")->values.size() == 1); + CHECK_THAT(raw.option("travel_speed")->values.front(), Catch::Matchers::WithinAbs(222.0, 1e-6)); +} + +TEST_CASE("Exact-only resolution rejects an unconfigured manifest-backed file", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path source_file = dir.path() / "Acme" / "process" / "child.json"; + fs::create_directories(source_file.parent_path()); + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + std::ofstream(source_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "Some Parent"; + + PresetBundle bundle; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, source_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error, false)); + CHECK(error == "Preset was not found in the loaded bundle"); +} + +TEST_CASE("Vendor filament resolution uses the shared Orca library base", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path library_dir = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY; + const fs::path vendor_dir = dir.path() / "Acme"; + const fs::path child_file = vendor_dir / "filament" / "nested" / "petg.json"; + + std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string()) + << R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)" + << R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"}]})"; + fs::create_directories(library_dir / "filament"); + std::ofstream((library_dir / "filament" / "pet.json").string()) + << R"({"type":"filament","name":"fdm_filament_pet","from":"system",)" + << R"("filament_id":"GFL99","instantiation":"false",)" + << R"("filament_type":["PETG"],"filament_density":["1.27"]})"; + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","filament_list":[)" + << R"({"name":"Acme PETG","sub_path":"filament/nested/petg.json","filament_id":"GFA00"}]})"; + fs::create_directories(child_file.parent_path()); + std::ofstream(child_file.string()) + << R"({"type":"filament","name":"Acme PETG","from":"system",)" + << R"("filament_id":"GFA00","instantiation":"true","inherits":"fdm_filament_pet"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet"; + + PresetBundle bundle; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, child_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(error.empty()); + CHECK(raw.opt_string("filament_type", 0u) == "PETG"); + REQUIRE(raw.option("filament_density")->values.size() == 1); + CHECK_THAT(raw.option("filament_density")->values.front(), Catch::Matchers::WithinAbs(1.27, 1e-6)); +} + +TEST_CASE("Manifest-backed resolution rejects a missing parent", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path child_file = dir.path() / "Acme" / "process" / "child.json"; + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + fs::create_directories(child_file.parent_path()); + std::ofstream(child_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","inherits":"Missing Parent","layer_height":"0.2"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "Missing Parent"; + + PresetBundle bundle; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK_FALSE(error.empty()); +} + +TEST_CASE("Manifest-backed resolution rejects a vendor load with malformed entries", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path child_file = dir.path() / "Acme" / "process" / "child.json"; + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[123,)" + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + fs::create_directories(child_file.parent_path()); + std::ofstream(child_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + + PresetBundle bundle; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK_FALSE(error.empty()); +} + +TEST_CASE("Manifest-backed resolution rejects files absent from the vendor manifest", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path listed_file = dir.path() / "Acme" / "process" / "listed.json"; + const fs::path unlisted_file = dir.path() / "Acme" / "process" / "unlisted.json"; + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"Listed Process","sub_path":"process/listed.json"}]})"; + fs::create_directories(listed_file.parent_path()); + std::ofstream(listed_file.string()) + << R"({"type":"process","name":"Listed Process","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + std::ofstream(unlisted_file.string()) + << R"({"type":"process","name":"Unlisted Process","from":"system",)" + << R"("instantiation":"true","inherits":"fdm_process_common"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + + PresetBundle bundle; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, unlisted_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(error == "Source file is not an instantiated preset in its vendor manifest"); +} + +TEST_CASE("Manifest-backed resolution rejects a mismatched preset type", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path process_file = dir.path() / "Acme" / "process" / "child.json"; + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + fs::create_directories(process_file.parent_path()); + std::ofstream(process_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_common"; + + PresetBundle bundle; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, process_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(error == "Source file is not an instantiated preset in its vendor manifest"); +} + +TEST_CASE("Resolution terminates when no vendor manifest exists", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path detached_file = dir.path() / "detached.json"; + std::ofstream(detached_file.string()) << "{}"; + + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "Missing Parent"; + + PresetBundle bundle; + std::string error; + CHECK_FALSE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, detached_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(error == "Preset was not found in the loaded bundle"); +} + // Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic // library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible // with that printer and the plater combo box lists the shared alias twice.