CLI: let --export-settings - write the merged config to stdout

--export-settings already writes the merged config as JSON at the right
point in the CLI flow. Passing - writes the same document to stdout.

- ConfigBase::save_to_json gains a stream overload. The file overload
  serializes through it before opening the file, so the format is
  unchanged and a config that cannot be serialized leaves an existing
  file untouched instead of truncating it.
- On stdout, invalid UTF-8 in string values is written as U+FFFD instead
  of ending the process with an uncaught type_error; files keep the
  strict behaviour.
- - is rejected up front when combined with an action or transform that
  can write to stdout or does real work, so stdout carries only the
  JSON.
- The unconditional "skip locked instance" stdout write during arrange
  now goes to the log.
- Tests in tests/libslic3r/test_config.cpp.
This commit is contained in:
packerlschupfer
2026-09-14 19:35:29 +02:00
parent 70247ad298
commit 31eb8a2bd1
5 changed files with 99 additions and 9 deletions
+55
View File
@@ -15,6 +15,8 @@
#include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp>
#include <sstream>
using namespace Slic3r;
SCENARIO("Generic config validation performs as expected.", "[Config]") {
@@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
}
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("wall_loops", new ConfigOptionInt(3));
config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" }));
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5"));
ScopedTemporaryFile tmp(".json");
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
std::string file_contents;
{
boost::nowide::ifstream ifs(tmp.string());
file_contents.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// The file format: one tab per nesting level and a trailing newline.
REQUIRE_FALSE(file_contents.empty());
CHECK(file_contents.rfind("{\n\t\"", 0) == 0);
CHECK(file_contents.back() == '\n');
std::ostringstream strict, replaced;
config.save_to_json(strict, "test_preset", "User", "1.0.0.0");
config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true);
CHECK(strict.str() == file_contents);
CHECK(replaced.str() == file_contents);
CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5");
}
TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
std::ostringstream strict, replaced;
CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true));
CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD");
}
TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
ScopedTemporaryFile tmp(".json");
{
boost::nowide::ofstream ofs(tmp.string());
ofs << "previous";
}
CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
boost::nowide::ifstream ifs(tmp.string());
const std::string contents((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
CHECK(contents == "previous");
}
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
const std::vector<std::string> refs = {
"master_plugin;;header-stamp",