diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 33afab065..9d24d8cb7 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -425,7 +425,30 @@ std::string ConfigBase::opt_serialize(const t_config_option_key &opt_key) const return opt->serialize(); } -bool ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) +void ConfigBase::set(const std::string &opt_key, int value, bool create) +{ + ConfigOption *opt = this->option_throw(opt_key, create); + switch (opt->type()) { + case coInt: static_cast(opt)->value = value; break; + case coFloat: static_cast(opt)->value = value; break; + case coFloatOrPercent: static_cast(opt)->value = value; static_cast(opt)->percent = false; break; + case coString: static_cast(opt)->value = std::to_string(value); break; + default: throw BadOptionTypeException("Configbase::set() - conversion from int not possible"); + } +} + +void ConfigBase::set(const std::string &opt_key, double value, bool create) +{ + ConfigOption *opt = this->option_throw(opt_key, create); + switch (opt->type()) { + case coFloat: static_cast(opt)->value = value; break; + case coFloatOrPercent: static_cast(opt)->value = value; static_cast(opt)->percent = false; break; + case coString: static_cast(opt)->value = std::to_string(value); break; + default: throw BadOptionTypeException("Configbase::set() - conversion from float not possible"); + } +} + +bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) { t_config_option_key opt_key = opt_key_src; std::string value = value_src; @@ -438,12 +461,16 @@ bool ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const s return this->set_deserialize_raw(opt_key, value, append); } -bool ConfigBase::set_deserialize(std::initializer_list items) +void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, bool append) +{ + if (! this->set_deserialize_nothrow(opt_key_src, value_src, append)) + throw BadOptionTypeException("ConfigBase::set_deserialize() failed"); +} + +void ConfigBase::set_deserialize(std::initializer_list items) { - bool deserialized = true; for (const SetDeserializeItem &item : items) - deserialized &= this->set_deserialize(item.opt_key, item.opt_value, item.append); - return deserialized; + this->set_deserialize(item.opt_key, item.opt_value, item.append); } bool ConfigBase::set_deserialize_raw(const t_config_option_key &opt_key_src, const std::string &value, bool append) @@ -831,7 +858,7 @@ bool DynamicConfig::read_cli(int argc, char** argv, t_config_option_keys* extra, static_cast(opt_base)->value = value; } else { // Any scalar value of a type different from Bool and String. - if (! this->set_deserialize(opt_key, value, false)) { + if (! this->set_deserialize_nothrow(opt_key, value, false)) { boost::nowide::cerr << "Invalid value supplied for --" << token.c_str() << std::endl; return false; } diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 1557eb5ab..c49dd134e 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -52,6 +52,16 @@ public: std::runtime_error(std::string("No definition exception: ") + opt_key) {} }; +/// Indicate that an unsupported accessor was called on a config option. +class BadOptionTypeException : public std::runtime_error +{ +public: + BadOptionTypeException() : + std::runtime_error("Bad option type exception") {} + BadOptionTypeException(const char* message) : + std::runtime_error(message) {} +}; + // Type of a configuration value. enum ConfigOptionType { coVectorType = 0x4000, @@ -117,10 +127,10 @@ public: virtual ConfigOption* clone() const = 0; // Set a value from a ConfigOption. The two options should be compatible. virtual void set(const ConfigOption *option) = 0; - virtual int getInt() const { throw std::runtime_error("Calling ConfigOption::getInt on a non-int ConfigOption"); } - virtual double getFloat() const { throw std::runtime_error("Calling ConfigOption::getFloat on a non-float ConfigOption"); } - virtual bool getBool() const { throw std::runtime_error("Calling ConfigOption::getBool on a non-boolean ConfigOption"); } - virtual void setInt(int /* val */) { throw std::runtime_error("Calling ConfigOption::setInt on a non-int ConfigOption"); } + virtual int getInt() const { throw BadOptionTypeException("Calling ConfigOption::getInt on a non-int ConfigOption"); } + virtual double getFloat() const { throw BadOptionTypeException("Calling ConfigOption::getFloat on a non-float ConfigOption"); } + virtual bool getBool() const { throw BadOptionTypeException("Calling ConfigOption::getBool on a non-boolean ConfigOption"); } + virtual void setInt(int /* val */) { throw BadOptionTypeException("Calling ConfigOption::setInt on a non-int ConfigOption"); } virtual bool operator==(const ConfigOption &rhs) const = 0; bool operator!=(const ConfigOption &rhs) const { return ! (*this == rhs); } bool is_scalar() const { return (int(this->type()) & int(coVectorType)) == 0; } @@ -1513,32 +1523,48 @@ protected: public: // Non-virtual methods: bool has(const t_config_option_key &opt_key) const { return this->option(opt_key) != nullptr; } + const ConfigOption* option(const t_config_option_key &opt_key) const { return const_cast(this)->option(opt_key, false); } + ConfigOption* option(const t_config_option_key &opt_key, bool create = false) { return this->optptr(opt_key, create); } + template TYPE* option(const t_config_option_key &opt_key, bool create = false) { ConfigOption *opt = this->optptr(opt_key, create); return (opt == nullptr || opt->type() != TYPE::static_type()) ? nullptr : static_cast(opt); } + template const TYPE* option(const t_config_option_key &opt_key) const { return const_cast(this)->option(opt_key, false); } - template - TYPE* option_throw(const t_config_option_key &opt_key, bool create = false) + + ConfigOption* option_throw(const t_config_option_key &opt_key, bool create = false) { ConfigOption *opt = this->optptr(opt_key, create); if (opt == nullptr) throw UnknownOptionException(opt_key); + return opt; + } + + const ConfigOption* option_throw(const t_config_option_key &opt_key) const + { return const_cast(this)->option_throw(opt_key, false); } + + template + TYPE* option_throw(const t_config_option_key &opt_key, bool create = false) + { + ConfigOption *opt = this->option_throw(opt_key, create); if (opt->type() != TYPE::static_type()) - throw std::runtime_error("Conversion to a wrong type"); + throw BadOptionTypeException("Conversion to a wrong type"); return static_cast(opt); } + template const TYPE* option_throw(const t_config_option_key &opt_key) const { return const_cast(this)->option_throw(opt_key, false); } + // Apply all keys of other ConfigBase defined by this->def() to this ConfigBase. // An UnknownOptionException is thrown in case some option keys of other are not defined by this->def(), // or this ConfigBase is of a StaticConfig type and it does not support some of the keys, and ignore_nonexistent is not set. @@ -1551,9 +1577,25 @@ public: t_config_option_keys diff(const ConfigBase &other) const; t_config_option_keys equal(const ConfigBase &other) const; std::string opt_serialize(const t_config_option_key &opt_key) const; + + // Set a value. Convert numeric types using a C style implicit conversion / promotion model. + // Throw if option is not avaiable and create is not enabled, + // or if the conversion is not possible. + // Conversion to string is always possible. + void set(const std::string &opt_key, bool value, bool create = false) + { this->option_throw(opt_key, create)->value = value; } + void set(const std::string &opt_key, int value, bool create = false); + void set(const std::string &opt_key, double value, bool create = false); + void set(const std::string &opt_key, const char *value, bool create = false) + { this->option_throw(opt_key, create)->value = value; } + void set(const std::string &opt_key, const std::string &value, bool create = false) + { this->option_throw(opt_key, create)->value = value; } + // Set a configuration value from a string, it will call an overridable handle_legacy() // to resolve renamed and removed configuration keys. - bool set_deserialize(const t_config_option_key &opt_key, const std::string &str, bool append = false); + bool set_deserialize_nothrow(const t_config_option_key &opt_key_src, const std::string &value_src, bool append = false); + // May throw BadOptionTypeException() if the operation fails. + void set_deserialize(const t_config_option_key &opt_key, const std::string &str, bool append = false); struct SetDeserializeItem { SetDeserializeItem(const char *opt_key, const char *opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {} SetDeserializeItem(const std::string &opt_key, const std::string &opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {} @@ -1567,7 +1609,8 @@ public: SetDeserializeItem(const std::string &opt_key, const double value, bool append = false) : opt_key(opt_key), opt_value(std::to_string(value)), append(append) {} std::string opt_key; std::string opt_value; bool append = false; }; - bool set_deserialize(std::initializer_list items); + // May throw BadOptionTypeException() if the operation fails. + void set_deserialize(std::initializer_list items); double get_abs_value(const t_config_option_key &opt_key) const; double get_abs_value(const t_config_option_key &opt_key, double ratio_over) const; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 59d7b7036..33105bff3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -433,6 +433,7 @@ void PrintConfigDef::init_fff_params() "If left zero, default extrusion width will be used if set, otherwise 1.125 x nozzle diameter will be used. " "If expressed as percentage (for example 200%), it will be computed over layer height."); def->sidetext = L("mm or %"); + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); @@ -541,6 +542,7 @@ void PrintConfigDef::init_fff_params() "(see the tooltips for perimeter extrusion width, infill extrusion width etc). " "If expressed as percentage (for example: 230%), it will be computed over layer height."); def->sidetext = L("mm or %"); + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); @@ -863,6 +865,7 @@ void PrintConfigDef::init_fff_params() "If set to zero, it will use the default extrusion width."); def->sidetext = L("mm or %"); def->ratio_over = "first_layer_height"; + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(200, true)); @@ -994,6 +997,7 @@ void PrintConfigDef::init_fff_params() "You may want to use fatter extrudates to speed up the infill and make your parts stronger. " "If expressed as percentage (for example 90%) it will be computed over layer height."); def->sidetext = L("mm or %"); + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); @@ -1406,6 +1410,7 @@ void PrintConfigDef::init_fff_params() "If expressed as percentage (for example 200%) it will be computed over layer height."); def->sidetext = L("mm or %"); def->aliases = { "perimeters_extrusion_width" }; + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); @@ -1743,6 +1748,7 @@ void PrintConfigDef::init_fff_params() "If left zero, default extrusion width will be used if set, otherwise 1.125 x nozzle diameter will be used. " "If expressed as percentage (for example 90%) it will be computed over layer height."); def->sidetext = L("mm or %"); + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); @@ -1917,6 +1923,7 @@ void PrintConfigDef::init_fff_params() "If left zero, default extrusion width will be used if set, otherwise nozzle diameter will be used. " "If expressed as percentage (for example 90%) it will be computed over layer height."); def->sidetext = L("mm or %"); + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); @@ -2076,6 +2083,7 @@ void PrintConfigDef::init_fff_params() "If left zero, default extrusion width will be used if set, otherwise nozzle diameter will be used. " "If expressed as percentage (for example 90%) it will be computed over layer height."); def->sidetext = L("mm or %"); + def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloatOrPercent(0, false)); diff --git a/tests/data/test_config/new_from_ini.ini b/tests/data/test_config/new_from_ini.ini new file mode 100644 index 000000000..33d65f590 --- /dev/null +++ b/tests/data/test_config/new_from_ini.ini @@ -0,0 +1 @@ +filament_colour = #ABCD diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 61d4667b7..e6f04d282 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -2,6 +2,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp test_3mf.cpp + test_config.cpp test_geometry.cpp test_polygon.cpp ) diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp new file mode 100644 index 000000000..85055aa65 --- /dev/null +++ b/tests/libslic3r/test_config.cpp @@ -0,0 +1,203 @@ +#include + +#include "libslic3r/PrintConfig.hpp" + +using namespace Slic3r; + +SCENARIO("Generic config validation performs as expected.", "[Config]") { + GIVEN("A config generated from default options") { + Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); + WHEN( "perimeter_extrusion_width is set to 250%, a valid value") { + config.set_deserialize("perimeter_extrusion_width", "250%"); + THEN( "The config is read as valid.") { + REQUIRE(config.validate().empty()); + } + } + WHEN( "perimeter_extrusion_width is set to -10, an invalid value") { + config.set("perimeter_extrusion_width", -10); + THEN( "Validate returns error") { + REQUIRE(! config.validate().empty()); + } + } + + WHEN( "perimeters is set to -10, an invalid value") { + config.set("perimeters", -10); + THEN( "Validate returns error") { + REQUIRE(! config.validate().empty()); + } + } + } +} + +SCENARIO("Config accessor functions perform as expected.", "[Config]") { + GIVEN("A config generated from default options") { + Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); + WHEN("A boolean option is set to a boolean value") { + REQUIRE_NOTHROW(config.set("gcode_comments", true)); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("gcode_comments")->getBool() == true); + } + } + WHEN("A boolean option is set to a string value representing a 0 or 1") { + CHECK_NOTHROW(config.set_deserialize("gcode_comments", "1")); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("gcode_comments")->getBool() == true); + } + } + WHEN("A boolean option is set to a string value representing something other than 0 or 1") { + THEN("A BadOptionTypeException exception is thrown.") { + REQUIRE_THROWS_AS(config.set("gcode_comments", "Z"), BadOptionTypeException); + } + AND_THEN("Value is unchanged.") { + REQUIRE(config.opt("gcode_comments")->getBool() == false); + } + } + WHEN("A boolean option is set to an int value") { + THEN("A BadOptionTypeException exception is thrown.") { + REQUIRE_THROWS_AS(config.set("gcode_comments", 1), BadOptionTypeException); + } + } + WHEN("A numeric option is set from serialized string") { + config.set_deserialize("bed_temperature", "100"); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("bed_temperature")->get_at(0) == 100); + } + } +#if 0 + //FIXME better design accessors for vector elements. + WHEN("An integer-based option is set through the integer interface") { + config.set("bed_temperature", 100); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("bed_temperature")->get_at(0) == 100); + } + } +#endif + WHEN("An floating-point option is set through the integer interface") { + config.set("perimeter_speed", 10); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("perimeter_speed")->getFloat() == 10.0); + } + } + WHEN("A floating-point option is set through the double interface") { + config.set("perimeter_speed", 5.5); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("perimeter_speed")->getFloat() == 5.5); + } + } + WHEN("An integer-based option is set through the double interface") { + THEN("A BadOptionTypeException exception is thrown.") { + REQUIRE_THROWS_AS(config.set("bed_temperature", 5.5), BadOptionTypeException); + } + } + WHEN("A numeric option is set to a non-numeric value.") { + THEN("A BadOptionTypeException exception is thown.") { + REQUIRE_THROWS_AS(config.set_deserialize("perimeter_speed", "zzzz"), BadOptionTypeException); + } + THEN("The value does not change.") { + REQUIRE(config.opt("perimeter_speed")->getFloat() == 60.0); + } + } + WHEN("A string option is set through the string interface") { + config.set("printhost_apikey", "100"); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("printhost_apikey")->value == "100"); + } + } + WHEN("A string option is set through the integer interface") { + config.set("printhost_apikey", 100); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("printhost_apikey")->value == "100"); + } + } + WHEN("A string option is set through the double interface") { + config.set("printhost_apikey", 100.5); + THEN("The underlying value is set correctly.") { + REQUIRE(config.opt("printhost_apikey")->value == std::to_string(100.5)); + } + } + WHEN("A float or percent is set as a percent through the string interface.") { + config.set_deserialize("first_layer_extrusion_width", "100%"); + THEN("Value and percent flag are 100/true") { + auto tmp = config.opt("first_layer_extrusion_width"); + REQUIRE(tmp->percent == true); + REQUIRE(tmp->value == 100); + } + } + WHEN("A float or percent is set as a float through the string interface.") { + config.set_deserialize("first_layer_extrusion_width", "100"); + THEN("Value and percent flag are 100/false") { + auto tmp = config.opt("first_layer_extrusion_width"); + REQUIRE(tmp->percent == false); + REQUIRE(tmp->value == 100); + } + } + WHEN("A float or percent is set as a float through the int interface.") { + config.set("first_layer_extrusion_width", 100); + THEN("Value and percent flag are 100/false") { + auto tmp = config.opt("first_layer_extrusion_width"); + REQUIRE(tmp->percent == false); + REQUIRE(tmp->value == 100); + } + } + WHEN("A float or percent is set as a float through the double interface.") { + config.set("first_layer_extrusion_width", 100.5); + THEN("Value and percent flag are 100.5/false") { + auto tmp = config.opt("first_layer_extrusion_width"); + REQUIRE(tmp->percent == false); + REQUIRE(tmp->value == 100.5); + } + } + WHEN("An invalid option is requested during set.") { + THEN("A BadOptionTypeException exception is thrown.") { + REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", 1), UnknownOptionException); + REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", 1.0), UnknownOptionException); + REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", "1"), UnknownOptionException); + REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", true), UnknownOptionException); + } + } + + WHEN("An invalid option is requested during get.") { + THEN("A UnknownOptionException exception is thrown.") { + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + } + } + WHEN("An invalid option is requested during opt.") { + THEN("A UnknownOptionException exception is thrown.") { + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + REQUIRE_THROWS_AS(config.option_throw("deadbeef_invalid_option", false), UnknownOptionException); + } + } + + WHEN("getX called on an unset option.") { + THEN("The default is returned.") { + REQUIRE(config.opt_float("layer_height") == 0.3); + REQUIRE(config.opt_int("raft_layers") == 0); + REQUIRE(config.opt_bool("support_material") == false); + } + } + + WHEN("getFloat called on an option that has been set.") { + config.set("layer_height", 0.5); + THEN("The set value is returned.") { + REQUIRE(config.opt_float("layer_height") == 0.5); + } + } + } +} + +SCENARIO("Config ini load/save interface", "[Config]") { + WHEN("new_from_ini is called") { + Slic3r::DynamicPrintConfig config; + std::string path = std::string(TEST_DATA_DIR) + "/test_config/new_from_ini.ini"; + config.load_from_ini(path); + THEN("Config object contains ini file options.") { + REQUIRE(config.option_throw("filament_colour", false)->values.size() == 1); + REQUIRE(config.option_throw("filament_colour", false)->values.front() == "#ABCD"); + } + } +}