Compare commits

..
Author SHA1 Message Date
ExPikaPaka 771ed20d97 Add a CLI regression test for a project with empty settings
Runs --info over a copy of a shipped model whose Metadata/project_settings.config
has been rewritten to an empty object, so the test keeps covering the crash no
matter what settings the shipped models carry later.

Verified both ways: the test passes against this branch and fails with a
segmentation fault against a build without the guards.
2026-09-30 09:21:39 +02:00
ExPikaPaka 616f1e4336 Fix a crash on loading a 3MF with empty project settings
opt_float() dereferences what option<>() returns without checking it, and
option<>() is called with create = false. Three CLI sites read printable_height
that way, so a 3mf whose Metadata/project_settings.config holds an empty object
takes the CLI down with a null dereference. Both models shipped in
resources/handy_models are such files, so `--info` on either of them segfaults.

Guard the three reads the way the neighbouring reads of
extruder_clearance_height_to_rod and friends already are. All three target
variables are initialised to 0 and the consumer tests for > 0, so an absent
setting already had a defined meaning and nothing changes for a project that
carries the setting.
2026-09-30 09:17:43 +02:00
12 changed files with 151 additions and 540 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "OrcaFilamentLibrary", "name": "OrcaFilamentLibrary",
"version": "02.04.00.17", "version": "02.04.00.16",
"force_update": "0", "force_update": "0",
"description": "Orca Filament Library", "description": "Orca Filament Library",
"filament_list": [ "filament_list": [
@@ -6,7 +6,7 @@
"filament_id": "OFvgE0Zh", "filament_id": "OFvgE0Zh",
"instantiation": "false", "instantiation": "false",
"fan_cooling_layer_time": [ "fan_cooling_layer_time": [
"85" "81"
], ],
"fan_min_speed": [ "fan_min_speed": [
"50" "50"
+14 -50
View File
@@ -1913,7 +1913,10 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast<int>(old_printable_bbox.size().x()); old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y()); old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
} }
old_printable_height = (int)(config.opt_float("printable_height")); // A 3mf can carry an empty project_settings.config - the models in
// resources/handy_models do - and opt_float() dereferences without checking.
if (config.option<ConfigOptionFloat>("printable_height"))
old_printable_height = (int)(config.opt_float("printable_height"));
if (config.option<ConfigOptionFloat>("extruder_clearance_height_to_rod")) if (config.option<ConfigOptionFloat>("extruder_clearance_height_to_rod"))
old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod"); old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod");
@@ -2116,14 +2119,9 @@ int CLI::run(int argc, char **argv)
// One resolver for the whole run, so presets from the same vendor tree share its load. // One resolver for the whole run, so presets from the same vendor tree share its load.
std::unique_ptr<PresetBundle> system_preset_resolver; std::unique_ptr<PresetBundle> system_preset_resolver;
auto ensure_system_preset_resolver = [&system_preset_resolver]() -> PresetBundle & { auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
if (!system_preset_resolver) std::string &config_type, const std::string &config_from,
system_preset_resolver = std::make_unique<PresetBundle>(); bool probe_type, std::string &error) {
return *system_preset_resolver;
};
auto resolve_preset = [&ensure_cli_preset_bundle, &ensure_system_preset_resolver](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<ConfigOptionString>(BBL_JSON_KEY_INHERITS); const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
if (!probe_type && (inherits == nullptr || inherits->value.empty())) if (!probe_type && (inherits == nullptr || inherits->value.empty()))
return true; return true;
@@ -2131,7 +2129,9 @@ int CLI::run(int argc, char **argv)
PresetBundle *bundle = nullptr; PresetBundle *bundle = nullptr;
bool allow_source_manifest = false; bool allow_source_manifest = false;
if (config_from == "system") { if (config_from == "system") {
bundle = &ensure_system_preset_resolver(); if (!system_preset_resolver)
system_preset_resolver = std::make_unique<PresetBundle>();
bundle = system_preset_resolver.get();
allow_source_manifest = true; allow_source_manifest = true;
} else { } else {
bundle = ensure_cli_preset_bundle(error); bundle = ensure_cli_preset_bundle(error);
@@ -2592,7 +2592,8 @@ int CLI::run(int argc, char **argv)
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x()); orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y()); orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
} }
orig_printable_height = (int)(config.opt_float("printable_height")); if (config.option<ConfigOptionFloat>("printable_height"))
orig_printable_height = (int)(config.opt_float("printable_height"));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%") BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height; %__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0)) if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
@@ -3123,44 +3124,6 @@ int CLI::run(int argc, char **argv)
return 0; return 0;
}; };
// Load the project's printer and process settings as the GUI loads its presets: over the default preset,
// with every key the project does not list as changed, including keys saved before an option existed,
// taken from its current system preset.
auto load_project_preset = [this, &ensure_system_preset_resolver, &current_different_settings, filament_count](const std::string &system_name, Preset::Type type) {
if (system_name.empty())
return;
// Preset bookkeeping the CLI keeps in its own groups, e.g. inherits_group and print_compatible_printers.
static const std::set<std::string> bookkeeping_keys = {"inherits", "compatible_printers", "compatible_prints", "compatible_printers_condition",
"compatible_prints_condition", "print_settings_id", "printer_settings_id"};
const size_t index = type == Preset::TYPE_PRINTER ? filament_count + 1 : 0;
PresetBundle &resolver = ensure_system_preset_resolver();
DynamicPrintConfig system_config;
t_config_option_keys keys;
const DynamicPrintConfig config = Preset::load_external_config(type,
type == Preset::TYPE_PRINTER ? resolver.printers.default_preset_for(m_print_config).config : resolver.prints.default_preset().config,
m_print_config, PresetBundle::project_different_keys(index < current_different_settings.size() ? current_different_settings[index] : std::string()),
[&](const std::string &) -> DynamicPrintConfig * {
std::string error;
if (resolver.resolve_system_preset(system_config, type, system_name, config_substitution_rule, error))
return &system_config;
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system preset '%1%' not resolved (%2%); the project keeps its values") % system_name % error;
return nullptr;
}, &keys);
for (const std::string &key : keys) {
const ConfigOption *opt = config.option(key);
const ConfigOption *old = m_print_config.option(key);
if (bookkeeping_keys.count(key) != 0 || opt == nullptr || (old != nullptr && *old == *opt))
continue;
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% from '%2%': %3% -> %4%") % key % system_name % (old ? old->serialize() : std::string("(missing)")) % opt->serialize();
m_print_config.set_key_value(key, opt->clone());
}
};
// The --uptodate path refreshes the project from its own system configs.
if (new_printer_name.empty() && load_machine_config.empty())
load_project_preset(current_printer_system_name, Preset::TYPE_PRINTER);
if (new_process_name.empty() && load_process_config.empty())
load_project_preset(current_process_system_name, Preset::TYPE_PRINT);
std::vector<std::string>& different_settings = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values; std::vector<std::string>& different_settings = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
std::vector<std::string>& inherits_group = m_print_config.option<ConfigOptionStrings>("inherits_group", true)->values; std::vector<std::string>& inherits_group = m_print_config.option<ConfigOptionStrings>("inherits_group", true)->values;
inherits_group.resize(filament_count + 2, std::string()); inherits_group.resize(filament_count + 2, std::string());
@@ -4658,7 +4621,8 @@ int CLI::run(int argc, char **argv)
BoundingBoxf temp_printable_bbox(temp_printable_area); BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x()); printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y()); printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
printer_plate.printable_height = (int)(config.opt_float("printable_height")); if (config.option<ConfigOptionFloat>("printable_height"))
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
} }
if (temp_exclude_area.size() >= 4) { if (temp_exclude_area.size() >= 4) {
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x()); printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
+32 -50
View File
@@ -976,37 +976,6 @@ void Preset::get_extruder_names_and_keysets(Type type, std::string& extruder_id_
} }
} }
DynamicPrintConfig Preset::load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
const std::set<std::string> &different_settings_list,
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
t_config_option_keys *keys)
{
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
DynamicPrintConfig cfg(default_config);
// SoftFever: ignore print connection info from project
auto cfg_keys = cfg.keys();
cfg_keys.erase(std::remove_if(cfg_keys.begin(), cfg_keys.end(),
[](std::string &val) {
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
}),
cfg_keys.end());
cfg.apply_only(project_config, cfg_keys, true);
//add different settings check logic, replace the old system preset's default value with new system preset's default values
if (!different_settings_list.empty()) {
if (DynamicPrintConfig *base_config = find_base(Preset::inherits(cfg))) {
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
cfg.update_non_diff_values_to_base_config(*base_config, cfg_keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
}
if (keys)
*keys = std::move(cfg_keys);
return cfg;
}
bool Preset::has_lidar(PresetBundle *preset_bundle) bool Preset::has_lidar(PresetBundle *preset_bundle)
{ {
bool has_lidar = false; bool has_lidar = false;
@@ -2671,6 +2640,20 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
const Semver file_version, const Semver file_version,
const std::string filament_id) const std::string filament_id)
{ {
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
DynamicPrintConfig cfg(this->default_preset_for(combined_config).config);
// SoftFever: ignore print connection info from project
auto keys = cfg.keys();
keys.erase(std::remove_if(keys.begin(), keys.end(),
[](std::string &val) {
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
}),
keys.end());
cfg.apply_only(combined_config, keys, true);
std::string &inherits = Preset::inherits(cfg);
//add different settings check logic, replace the old system preset's default value with new system preset's default values
std::deque<Preset>::iterator it = this->find_preset_internal(original_name); std::deque<Preset>::iterator it = this->find_preset_internal(original_name);
bool found = it != m_presets.end() && it->name == original_name; bool found = it != m_presets.end() && it->name == original_name;
if (! found) { if (! found) {
@@ -2679,25 +2662,24 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
found = it != m_presets.end(); found = it != m_presets.end();
} }
t_config_option_keys keys; std::string extruder_id_name, extruder_variant_name;
DynamicPrintConfig cfg = Preset::load_external_config(m_type, this->default_preset_for(combined_config).config, combined_config, different_settings_list, std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
[this, &original_name, &path, found, it](const std::string &inherits) -> DynamicPrintConfig * { Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
if (!inherits.empty()) {
auto iter = this->find_preset_internal(inherits); if (!inherits.empty() && (different_settings_list.size() > 0)) {
if (iter == m_presets.end() || iter->name != inherits) auto iter = this->find_preset_internal(inherits);
iter = this->find_preset_renamed(inherits); if (iter == m_presets.end() || iter->name != inherits)
if (iter == m_presets.end()) iter = this->find_preset_renamed(inherits);
return nullptr; if (iter != m_presets.end()) {
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path; //std::vector<std::string> dirty_options = cfg.diff(iter->config);
return &iter->config; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
} cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
if (found && it->is_system) { }
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path; }
return &it->config; else if (found && it->is_system && (different_settings_list.size() > 0)) {
} BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
return nullptr; cfg.update_non_diff_values_to_base_config(it->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}, &keys); }
std::string &inherits = Preset::inherits(cfg);
//BBS: add config related logs //BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits;
-9
View File
@@ -403,15 +403,6 @@ public:
std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type
static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set<std::string>** p_key_set1, std::set<std::string>** p_key_set2); static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set<std::string>** p_key_set1, std::set<std::string>** p_key_set2);
// Config of a preset loaded from a project or config file: the project's values over the type's
// default preset config, without the print-host keys. When different_settings_list is not empty,
// every key not listed in it is then refreshed from the base system preset, which find_base returns
// for the project's "inherits" (nullptr when there is none), with the listed per-variant values
// mapped onto the base's extruder variants. keys, if given, receives the keys taken from the project.
static DynamicPrintConfig load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
const std::set<std::string> &different_settings_list,
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
t_config_option_keys *keys = nullptr);
std::string get_printer_id() const { return vendor ? vendor->id : ""; } std::string get_printer_id() const { return vendor ? vendor->id : ""; }
bool has_lidar(PresetBundle *preset_bundle); bool has_lidar(PresetBundle *preset_bundle);
+42 -58
View File
@@ -581,20 +581,18 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir, const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id, const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule, ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_cache) std::string &error)
{ {
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule, allow_cache); auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule);
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end()) if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
return it->second.get(); return it->second.get();
// The library loads with no base of its own, so the tree a vendor inherits from // 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. // 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;
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY && if (vendor_id != ORCA_FILAMENT_LIBRARY &&
(boost::filesystem::is_regular_file(root_dir / (library_file + ".json")) || boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".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);
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache);
if (library == nullptr) { if (library == nullptr) {
error = "OrcaFilamentLibrary contains invalid presets"; error = "OrcaFilamentLibrary contains invalid presets";
return nullptr; return nullptr;
@@ -603,7 +601,7 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa
auto bundle = std::make_unique<PresetBundle>(); auto bundle = std::make_unique<PresetBundle>();
bundle->m_preserve_vendor_source_paths = true; bundle->m_preserve_vendor_source_paths = true;
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, allow_cache); bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false);
if (bundle->error_count() != 0) { if (bundle->error_count() != 0) {
error = "Vendor bundle contains invalid presets"; error = "Vendor bundle contains invalid presets";
return nullptr; return nullptr;
@@ -646,43 +644,6 @@ bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset
return true; return true;
} }
bool PresetBundle::resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error)
{
const std::string vendor_id = find_preset_vendor(name, type);
if (vendor_id.empty()) {
error = "No vendor lists the preset";
return false;
}
// Release builds ship a vendor as its preset cache alone, without the profile JSONs.
auto installed = [&vendor_id](const fs::path &root) {
return fs::is_regular_file(root / (vendor_id + ".json")) || fs::is_regular_file(root / (vendor_id + ".opc"));
};
fs::path root_dir = fs::path(data_dir()) / PRESET_SYSTEM_DIR;
if (!installed(root_dir))
root_dir = fs::path(resources_dir()) / PRESET_PROFILES_DIR;
const bool cache_only = !fs::is_regular_file(root_dir / (vendor_id + ".json"));
try {
const PresetBundle *vendor = load_source_vendor(root_dir, vendor_id, compatibility_rule, error, cache_only);
if (vendor == nullptr)
return false;
const PresetCollection &collection = type == Preset::TYPE_PRINTER ? vendor->printers :
type == Preset::TYPE_PRINT ? vendor->prints : vendor->filaments;
const Preset *preset = collection.find_preset(name, false);
if (preset == nullptr) {
error = "Preset was not found in its vendor bundle";
return false;
}
config = preset->config;
} catch (const std::exception &ex) {
error = ex.what();
return false;
}
error.clear();
return true;
}
PresetBundle::PresetBundle(const PresetBundle &rhs) PresetBundle::PresetBundle(const PresetBundle &rhs)
{ {
*this = rhs; *this = rhs;
@@ -4704,15 +4665,6 @@ const std::set<std::string> ignore_settings_list ={
"print_settings_id", "filament_settings_id", "printer_settings_id" "print_settings_id", "filament_settings_id", "printer_settings_id"
}; };
std::set<std::string> PresetBundle::project_different_keys(const std::string &different_settings)
{
std::vector<std::string> keys;
Slic3r::unescape_strings_cstyle(different_settings, keys);
std::set<std::string> keys_set(keys.begin(), keys.end());
keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
return keys_set;
}
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const
{ {
DynamicPrintConfig out; DynamicPrintConfig out;
@@ -5420,7 +5372,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
if (!is_published) { if (!is_published) {
//BBS: add different settings logic //BBS: add different settings logic
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id");
std::set<std::string> print_different_keys_set = project_different_keys(different_values[0]); std::vector<std::string> print_different_keys_vector;
std::string print_different_settings = different_values[0];
Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector);
std::set<std::string> print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// print_different_keys_set.clear();
//}
//else
print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
if (!print_compatible_printers.empty()) { if (!print_compatible_printers.empty()) {
ConfigOptionStrings* compatible_printers = config.option<ConfigOptionStrings>("compatible_printers", true); ConfigOptionStrings* compatible_printers = config.option<ConfigOptionStrings>("compatible_printers", true);
compatible_printers->values = print_compatible_printers; compatible_printers->values = print_compatible_printers;
@@ -5431,7 +5391,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
//clear compatible printers //clear compatible printers
clear_compatible_printers(config); clear_compatible_printers(config);
std::set<std::string> printer_different_keys_set = project_different_keys(different_values[num_filaments + 1]); std::vector<std::string> printer_different_keys_vector;
std::string printer_different_settings = different_values[num_filaments + 1];
Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector);
std::set<std::string> printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// printer_different_keys_set.clear();
//}
//else
printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
//BBS: add config related logs //BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id"); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id");
load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string()); load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string());
@@ -5450,7 +5418,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
Preset *loaded = nullptr; Preset *loaded = nullptr;
//BBS: add different settings logic //BBS: add different settings logic
std::set<std::string> filament_different_keys_set = project_different_keys(different_values[1]); std::vector<std::string> filament_different_keys_vector;
std::string filament_different_settings = different_values[1];
Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector);
std::set<std::string> filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// filament_different_keys_set.clear();
//}
//else
filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
std::string filament_id = filament_ids[0]; std::string filament_id = filament_ids[0];
//BBS: add config related logs //BBS: add config related logs
@@ -5513,7 +5489,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
cfg.opt_string("inherits", true) = inherits_values[i + 1]; cfg.opt_string("inherits", true) = inherits_values[i + 1];
//BBS: add different settings logic //BBS: add different settings logic
std::set<std::string> filament_different_keys_set = project_different_keys(different_values[i+1]); std::vector<std::string> filament_different_keys_vector;
std::string filament_different_settings = different_values[i+1];
Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector);
std::set<std::string> filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// filament_different_keys_set.clear();
//}
//else
filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
std::string filament_id = filament_ids[i]; std::string filament_id = filament_ids[i];
+2 -10
View File
@@ -235,9 +235,6 @@ public:
// ORCA: utility function to find the vendor for a given preset name // ORCA: utility function to find the vendor for a given preset name
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type); static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
// Keys a project keeps when its presets are loaded: those listed in its escaped
// "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys.
static std::set<std::string> project_different_keys(const std::string &different_settings);
PresetBundle(); PresetBundle();
PresetBundle(const PresetBundle &rhs); PresetBundle(const PresetBundle &rhs);
@@ -277,10 +274,6 @@ public:
const std::string &source_file, const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule, ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_source_manifest = true); std::string &error, bool allow_source_manifest = true);
// Resolve a system preset by name. The vendor tree is read from data_dir()/system when installed
// there, as the GUI reads it, and from the bundled profiles otherwise.
bool resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error);
// Load selections (current print, current filaments, current printer) from config.ini // Load selections (current print, current filaments, current printer) from config.ini
// This is done just once on application start up. // This is done just once on application start up.
@@ -815,14 +808,13 @@ private:
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset // Vendor trees loaded by resolve_preset_config's manifest path, so every preset
// resolved through this bundle shares one load per source root and vendor. The // resolved through this bundle shares one load per source root and vendor. The
// filament library is one such tree, shared by every vendor under its root. // filament library is one such tree, shared by every vendor under its root.
// A tree read from its preset cache is kept apart: its presets carry no source file. std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule, bool>, std::unique_ptr<PresetBundle>>
m_source_vendor_bundles; m_source_vendor_bundles;
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir, const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id, const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule, ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_cache = false); std::string &error);
// Orca: validation only - flag any printer with two or more compatible // Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match). // filament presets sharing one filament_id (ambiguous AMS subtype match).
-1
View File
@@ -12595,7 +12595,6 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool."); new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool.");
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone."); new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate').");
} }
PrintStatisticsConfigDef::PrintStatisticsConfigDef() PrintStatisticsConfigDef::PrintStatisticsConfigDef()
+7 -8
View File
@@ -11,15 +11,14 @@ endif ()
add_test(NAME cli_strict_mode add_test(NAME cli_strict_mode
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}) COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
add_test(NAME cli_empty_project_config
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR})
set_tests_properties(cli_empty_project_config PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 300)
set_tests_properties(cli_strict_mode PROPERTIES set_tests_properties(cli_strict_mode PROPERTIES
LABELS "CLI;RequiresApp" LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77 SKIP_RETURN_CODE 77
TIMEOUT 900) TIMEOUT 900)
add_test(NAME cli_project_missing_keys
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_project_missing_keys.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
set_tests_properties(cli_project_missing_keys PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load.
#
# The CLI reads printable_height out of the project config with opt_float(), which dereferences
# what option<>() returns. With create = false that is null when the key is absent, so a project
# saved without settings used to take the CLI down with a segfault. Both models in
# resources/handy_models are such files.
#
# usage: test_cli_empty_project_config.sh <orca-slicer binary> <python3> <source dir>
set -u
BIN="${1:-}"
PY="${2:-python3}"
SRC="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/datadir"
# Rewrite the project settings to an empty object, so the test holds no matter what the shipped
# models carry later on.
cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf"
"$PY" - "$WORK/empty_config.3mf" <<'PYEOF'
import shutil, sys, zipfile
path = sys.argv[1]
entry = "Metadata/project_settings.config"
with zipfile.ZipFile(path) as src:
items = [(i, src.read(i.filename)) for i in src.infolist()]
with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst:
seen = False
for info, data in items:
if info.filename == entry:
data, seen = b"{\n}\n", True
dst.writestr(info, data)
if not seen:
dst.writestr(entry, b"{\n}\n")
shutil.move(path + ".new", path)
PYEOF
"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1
rc=$?
if [ $rc -ne 0 ]; then
echo "FAIL: --info on a project with empty settings exited $rc"
tail -20 "$WORK/info.txt"
exit 1
fi
grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; }
echo "PASS: a project with empty settings loads"
@@ -1,96 +0,0 @@
#!/usr/bin/env bash
# End-to-end check that the CLI loads a project's printer and process settings as the GUI does.
#
# The GUI takes every key a project does not list as changed from the project's current system preset:
# keys saved before an option existed, and keys holding an older system value. Keys the project lists
# in different_settings_to_system keep the project's value. A project is exported from the shipped
# Bambu Lab P1S presets; one printer key and one process key are removed, one printer key and one
# process key are changed without being listed, one key is changed and listed, and it is sliced again.
#
# usage: test_cli_project_missing_keys.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
set -u
BIN="${1:-}"
PY="${2:-python3}"
PROFILES="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-missing-keys.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
"$PY" - "$WORK/cube.stl" <<'EOF'
import sys
v = [(x, y, z) for z in (0, 10) for y in (0, 10) for x in (0, 10)]
with open(sys.argv[1], "w") as f:
f.write("solid cube\n")
for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])):
f.write("facet normal 0 0 0\nouter loop\n")
for p in tri:
f.write("vertex %g %g %g\n" % p)
f.write("endloop\nendfacet\n")
f.write("endsolid cube\n")
EOF
# slice <tag> <input> [option...]: slice into $WORK/<tag>/out.3mf with a fresh data directory.
slice() {
local out="$WORK/$1" input="$2"; shift 2
mkdir -p "$out"
timeout 300 "$BIN" --datadir "$out/datadir" "$@" --slice 0 --outputdir "$out" --export-3mf out.3mf "$input" \
> "$out/log" 2>&1 || { echo "FAIL: $1: orca-slicer exited $?"; tail -n 40 "$out/log"; exit 1; }
}
slice base "$WORK/cube.stl" \
--load-settings "$PROFILES/machine/Bambu Lab P1S 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
# The removed keys, with their option defaults from PrintConfig.cpp; stale keys changed without being
# listed as different, which must come back with the system value; and a listed key the project keeps.
"$PY" - "$WORK/base/out.3mf" "$WORK/old.3mf" <<'EOF' || exit $?
import json, sys, zipfile
src, dst = sys.argv[1], sys.argv[2]
missing = {"extruder_clearance_dist_to_rod": "40", "sparse_infill_density": "20%"}
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename == "Metadata/project_settings.config":
config = json.loads(data)
for key, default in missing.items():
if config[key] == default:
print("SKIP: %s is %s in the system preset, the option default, so the test cannot tell them apart" % (key, default))
sys.exit(77)
expected = {key: config.pop(key) for key in missing}
for key in ("top_shell_layers", "extruder_clearance_height_to_rod"):
expected[key] = config[key]
config[key] = str(int(float(config[key])) + 1)
expected["wall_loops"] = str(int(config["wall_loops"]) + 1)
config["wall_loops"] = expected["wall_loops"]
different = config["different_settings_to_system"]
different[0] = ";".join([k for k in different[0].split(";") if k] + ["wall_loops"])
data = json.dumps(config, indent=4)
zout.writestr(item, data)
with open(dst + ".expected.json", "w") as f:
json.dump(expected, f)
EOF
slice project "$WORK/old.3mf"
"$PY" - "$WORK/project/out.3mf" "$WORK/old.3mf.expected.json" <<'EOF'
import json, sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
config = json.loads(z.read("Metadata/project_settings.config"))
with open(sys.argv[2]) as f:
expected = json.load(f)
errors = ["%s is %r, want %r" % (key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
for e in errors:
print("FAIL: " + e)
sys.exit(1 if errors else 0)
EOF
status=$?
[ "$status" -eq 0 ] || { tail -n 40 "$WORK/project/log"; exit 1; }
echo "PASS"
@@ -5571,35 +5571,6 @@ struct ScopedDataDir
~ScopedDataDir() { set_data_dir(previous); } ~ScopedDataDir() { set_data_dir(previous); }
}; };
// resources_dir() is process-wide too; system preset lookups scan its profiles directory.
struct ScopedResourcesDir
{
std::string previous = resources_dir();
explicit ScopedResourcesDir(const fs::path &dir) { set_resources_dir(dir.string()); }
~ScopedResourcesDir() { set_resources_dir(previous); }
};
// An "Acme" vendor under root whose "Acme Printer" inherits extruder_clearance_dist_to_rod from an
// abstract base, with the printer in a nested sub_path so the name cannot be derived from the file.
void write_acme_printer_vendor(const fs::path &root, double dist_to_rod)
{
const fs::path machine_dir = root / "Acme" / "machine";
fs::create_directories(machine_dir / "nested");
std::ofstream((root / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme",)"
<< R"("machine_model_list":[{"name":"Acme One","sub_path":"machine/model.json"}],"machine_list":[)"
<< R"({"name":"fdm_acme_common","sub_path":"machine/base.json"},)"
<< R"({"name":"Acme Printer","sub_path":"machine/nested/printer.json"}]})";
std::ofstream((machine_dir / "model.json").string())
<< R"({"type":"machine_model","name":"Acme One","nozzle_diameter":"0.4"})";
std::ofstream((machine_dir / "base.json").string())
<< R"({"type":"machine","name":"fdm_acme_common","from":"system","instantiation":"false",)"
<< R"("extruder_clearance_dist_to_rod":")" << dist_to_rod << R"("})";
std::ofstream((machine_dir / "nested" / "printer.json").string())
<< R"({"type":"machine","name":"Acme Printer","from":"system","instantiation":"true","inherits":"fdm_acme_common",)"
<< R"("printer_model":"Acme One","printer_variant":"0.4"})";
}
std::string read_file(const fs::path &file) std::string read_file(const fs::path &file)
{ {
std::ifstream in(file.string(), std::ios::binary); std::ifstream in(file.string(), std::ios::binary);
@@ -5713,230 +5684,3 @@ TEST_CASE("A project saved with pressure advance per filament applies it to ever
check_double_vector(pla.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.95, 0.96 }); check_double_vector(pla.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.95, 0.96 });
check_double_vector(petg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.97 }); check_double_vector(petg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.97 });
} }
TEST_CASE("A system preset resolves by name from the bundled profiles", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK(error.empty());
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
}
TEST_CASE("A system preset resolves from the data directory copy of its vendor", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
write_acme_printer_vendor(temp_dir.path() / "data" / PRESET_SYSTEM_DIR, 35.);
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(35., 1e-6));
}
TEST_CASE("A system preset resolves from a vendor shipped as its preset cache alone", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
const fs::path profiles = temp_dir.path() / "resources" / "profiles";
write_acme_printer_vendor(profiles, 33.);
PresetBundle writer;
writer.set_generate_vendor_caches(true);
writer.load_vendor_configs_from_json(profiles.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
REQUIRE(fs::exists(profiles / "Acme.opc"));
// Release builds ship the cache and drop the profile JSONs, manifest included.
fs::remove(profiles / "Acme.json");
fs::remove_all(profiles / "Acme");
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
}
TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
CHECK_FALSE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Unknown Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_FALSE(error.empty());
}
namespace {
// A default preset config for type, built the way PresetBundle builds its default presets.
DynamicPrintConfig external_default_config(Preset::Type type)
{
DynamicPrintConfig config;
config.apply_only(static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()),
type == Preset::TYPE_PRINTER ? Preset::printer_options() : Preset::print_options());
Preset::inherits(config);
return config;
}
// A find_base callback that counts its calls and returns base.
auto base_finder(DynamicPrintConfig *base, int &calls)
{
return [base, &calls](const std::string &) {
++calls;
return base;
};
}
} // namespace
TEST_CASE("A key missing from a project takes the default preset value when there is no base", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
defaults.set("wall_loops", 7, true);
const DynamicPrintConfig project;
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
base_finder(nullptr, calls));
CHECK(calls == 1);
CHECK(config.opt_int("wall_loops") == 7);
}
TEST_CASE("A key missing from a project takes its base preset value", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
defaults.set("wall_loops", 7, true);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
const DynamicPrintConfig project;
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
base_finder(&base, calls));
CHECK(config.opt_int("wall_loops") == 5);
}
TEST_CASE("A project key listed as different keeps the project value", "[Preset][ExternalPreset]")
{
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
DynamicPrintConfig project;
project.set("wall_loops", 4, true);
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"wall_loops"},
base_finder(&base, calls));
CHECK(config.opt_int("wall_loops") == 4);
}
TEST_CASE("A project key not listed as different is refreshed to the base value", "[Preset][ExternalPreset]")
{
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
DynamicPrintConfig project;
project.set("wall_loops", 4, true);
project.set("inherits", "Base Process", true);
std::string inherits;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
[&base, &inherits](const std::string &name) {
inherits = name;
return &base;
});
CHECK(inherits == "Base Process");
CHECK(config.opt_int("wall_loops") == 5);
}
TEST_CASE("An empty different settings list keeps the project values without a base", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
defaults.set("top_shell_layers", 7, true);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
base.set("top_shell_layers", 9, true);
DynamicPrintConfig project;
project.set("wall_loops", 4, true);
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {}, base_finder(&base, calls));
CHECK(calls == 0);
CHECK(config.opt_int("wall_loops") == 4);
CHECK(config.opt_int("top_shell_layers") == 7);
}
TEST_CASE("Print-host keys are never taken from a project", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINTER);
defaults.set("print_host", "", true);
defaults.set("printhost_apikey", "", true);
DynamicPrintConfig project;
project.set("print_host", "http://project-host", true);
project.set("printhost_apikey", "project-key", true);
project.set("printer_notes", "project notes", true);
t_config_option_keys keys;
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINTER, defaults, project, {"printer_notes"},
base_finder(nullptr, calls), &keys);
CHECK(config.opt_string("print_host").empty());
CHECK(config.opt_string("printhost_apikey").empty());
CHECK(config.opt_string("printer_notes") == "project notes");
CHECK_FALSE(contains_key(keys, "print_host"));
CHECK_FALSE(contains_key(keys, "printhost_apikey"));
CHECK(contains_key(keys, "printer_notes"));
}
TEST_CASE("A per-variant project value maps onto its base preset's variant layout", "[Preset][ExternalPreset]")
{
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
DynamicPrintConfig base = defaults;
base.set_key_value("print_extruder_id", new ConfigOptionInts({1, 1}));
base.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
base.set_key_value("outer_wall_speed", new ConfigOptionFloats({200., 300.}));
base.set_key_value("inner_wall_speed", new ConfigOptionFloats({250., 350.}));
// A project saved before its printer had a High Flow variant.
DynamicPrintConfig project;
project.set_key_value("print_extruder_id", new ConfigOptionInts({1}));
project.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard"}));
project.set_key_value("outer_wall_speed", new ConfigOptionFloats({100.}));
project.set_key_value("inner_wall_speed", new ConfigOptionFloats({150.}));
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"outer_wall_speed"},
base_finder(&base, calls));
CHECK(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>{"Direct Drive Standard", "Direct Drive High Flow"});
// The listed key keeps the project's Standard value and takes High Flow from the base.
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 300.});
check_double_vector(config.option<ConfigOptionFloats>("inner_wall_speed")->values, {250., 350.});
}
TEST_CASE("A project's different settings always keep the preset bookkeeping keys", "[Preset][ExternalPreset]")
{
const std::set<std::string> keys = PresetBundle::project_different_keys(escape_strings_cstyle({"wall_loops", "top_shell_layers"}));
for (const char *key : {"wall_loops", "top_shell_layers", "inherits", "print_settings_id", "filament_settings_id", "printer_settings_id"})
CHECK(keys.count(key) == 1);
CHECK(PresetBundle::project_different_keys(std::string()).count("inherits") == 1);
}