test(3mf): round-trip coverage for per-plate IMEX state

Validates that imex_parallel_mode and imex_head_filament_map survive a
full store_bbs_3mf → load_bbs_3mf cycle — the same silent-state-loss bug
class that produced the variant-vector truncation regression, applied to
IMEX plate state which rides the same XML metadata path.

- Positive round-trip: a plate with copy_mode + a non-trivial head
  filament map ("1:2,2:3") is saved and reloaded; both options land on
  the destination plate's config with the exact values preserved.
- Guard scope: a plate with mode="primary" and empty head-filament-map
  does NOT emit metadata (per the serializer's short-circuit), and the
  reload leaves both options absent from the destination config. If the
  serializer ever regressed to writing primary-mode plates, the load
  path would surface phantom "primary" strings on plates that shipped
  clean — this catches that.

Both scenarios call set_temporary_dir to point the BBS exporter's backup
scaffolding at a writable per-process temp directory (by default it
resolves under root at runtime, which fails for non-root test
processes).

All 27 assertions in 2 test cases pass under [3mf][IMEX].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-24 22:58:19 -04:00
co-authored by Claude Opus 4.7
parent 5434a5217c
commit cb15f35444
+135
View File
@@ -1,7 +1,9 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Format/STL.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem/operations.hpp>
@@ -133,6 +135,139 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
}
}
SCENARIO("BBS 3MF round-trips per-plate IMEX state (parallel mode + head filament map)", "[3mf][IMEX]") {
// Regression guard for the class of bug where per-plate state silently drops through
// save/load (the variant-truncation bug was the precipitating example; IMEX plate state
// rides the same XML metadata path and is equally vulnerable).
// BBS exporter scaffolds a backup dir under temporary_dir() for the project config file;
// point it at a writable location for the test process.
set_temporary_dir(boost::filesystem::temp_directory_path().string());
GIVEN("A Model with a single object on plate 0 and IMEX plate state set") {
Model src_model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
REQUIRE(load_stl(src_file.c_str(), &src_model));
src_model.add_default_instances();
DynamicPrintConfig src_config;
PlateDataPtrs src_plates;
auto *plate0 = new PlateData();
plate0->plate_index = 0;
plate0->config.set_key_value("imex_parallel_mode", new ConfigOptionString("copy_mode"));
plate0->config.set_key_value("imex_head_filament_map", new ConfigOptionString("1:2,2:3"));
src_plates.push_back(plate0);
WHEN("the model is saved to BBS 3MF and loaded back") {
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/imex_roundtrip.3mf";
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &src_model;
store_params.plate_data_list = src_plates;
store_params.config = &src_config;
REQUIRE(store_bbs_3mf(store_params));
Model dst_model;
DynamicPrintConfig dst_config;
PlateDataPtrs dst_plates;
std::vector<Preset*> dst_presets;
bool is_bbl = false;
Semver file_version;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model,
&dst_plates, &dst_presets, &is_bbl, &file_version);
boost::filesystem::remove(test_file);
THEN("load succeeds") {
REQUIRE(loaded);
}
THEN("the loaded plate list has the same number of plates") {
REQUIRE(dst_plates.size() == src_plates.size());
}
THEN("imex_parallel_mode round-trips with its original value") {
REQUIRE(dst_plates.size() >= 1);
auto *mode_opt = dst_plates[0]->config.option<ConfigOptionString>("imex_parallel_mode");
REQUIRE(mode_opt != nullptr);
REQUIRE(mode_opt->value == "copy_mode");
}
THEN("imex_head_filament_map round-trips with its original value") {
REQUIRE(dst_plates.size() >= 1);
auto *hfm_opt = dst_plates[0]->config.option<ConfigOptionString>("imex_head_filament_map");
REQUIRE(hfm_opt != nullptr);
REQUIRE(hfm_opt->value == "1:2,2:3");
}
release_PlateData_list(dst_plates);
}
release_PlateData_list(src_plates);
}
}
SCENARIO("BBS 3MF does not emit IMEX metadata when plate is in primary mode", "[3mf][IMEX]") {
// The serialization guard short-circuits when the mode is empty or "primary", so loading
// a plate that was saved in primary mode must not leave a stale imex_parallel_mode option
// on the plate's config. If this regressed, we'd see ghost "primary" strings appearing on
// plates that had no IMEX state at all.
set_temporary_dir(boost::filesystem::temp_directory_path().string());
GIVEN("A Model with plate 0 in primary mode and an empty head-filament map") {
Model src_model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
REQUIRE(load_stl(src_file.c_str(), &src_model));
src_model.add_default_instances();
DynamicPrintConfig src_config;
PlateDataPtrs src_plates;
auto *plate0 = new PlateData();
plate0->plate_index = 0;
plate0->config.set_key_value("imex_parallel_mode", new ConfigOptionString("primary"));
plate0->config.set_key_value("imex_head_filament_map", new ConfigOptionString(""));
src_plates.push_back(plate0);
WHEN("the model is saved to BBS 3MF and loaded back") {
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/imex_primary_roundtrip.3mf";
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &src_model;
store_params.plate_data_list = src_plates;
store_params.config = &src_config;
REQUIRE(store_bbs_3mf(store_params));
Model dst_model;
DynamicPrintConfig dst_config;
PlateDataPtrs dst_plates;
std::vector<Preset*> dst_presets;
bool is_bbl = false;
Semver file_version;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model,
&dst_plates, &dst_presets, &is_bbl, &file_version);
boost::filesystem::remove(test_file);
THEN("load succeeds") {
REQUIRE(loaded);
}
THEN("the loaded plate has no imex_parallel_mode option set (primary is not serialized)") {
REQUIRE(dst_plates.size() >= 1);
auto *mode_opt = dst_plates[0]->config.option<ConfigOptionString>("imex_parallel_mode");
REQUIRE(mode_opt == nullptr);
}
THEN("the loaded plate has no imex_head_filament_map option set (empty is not serialized)") {
REQUIRE(dst_plates.size() >= 1);
auto *hfm_opt = dst_plates[0]->config.option<ConfigOptionString>("imex_head_filament_map");
REQUIRE(hfm_opt == nullptr);
}
release_PlateData_list(dst_plates);
}
release_PlateData_list(src_plates);
}
}
SCENARIO("2D convex hull of sinking object", "[3mf][.]") {
GIVEN("model") {
// load a model