CLI: let --export-settings - write the merged config JSON to stdout (#15698)

`--export-settings` already writes the merged config as JSON at the
right point in the CLI flow. Passing `-` now writes that same document
to stdout, so scripts can inspect the effective config without a temp
file. This replaces #14605.

- `ConfigBase::save_to_json` gains a stream overload. The file overload
serializes through it before opening the file, so the output format is
unchanged, and a config that cannot be serialized (invalid UTF-8) now
leaves the 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.
- To keep stdout pure JSON, `-` is rejected up front (stderr message,
`CLI_INVALID_PARAMS`, shell status 254) when combined with an action or
transform that can write to stdout or does real work: `--info`,
`--help`, `--orient`, slicing and exporting. Options that do nothing
without a slice (`--uptodate`, `--min-save`, `--pipe`, ...) are still
accepted.
- The one unconditional stdout write on a success path, "skip locked
instance" during arrange, now goes to the log.
- Every other value, including the default `output.json`, behaves as
before.

Tests in `tests/libslic3r/test_config.cpp`: the stream output equals the
file output and keeps the tab-indented format; invalid UTF-8 throws on
the strict path and is replaced when asked; a failed save leaves the
previous file intact.
This commit is contained in:
HanifKoh
2026-09-15 13:16:22 +08:00
committed by GitHub
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",