#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetCacheFormat.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Utils.hpp" using namespace Slic3r; using Catch::Matchers::WithinAbs; namespace fs = boost::filesystem; namespace { struct TempDir { fs::path path; TempDir() { path = fs::temp_directory_path() / fs::unique_path("orca-cache-test-%%%%-%%%%"); fs::create_directories(path); } ~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); } }; std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id, const std::string& version = "1.0.0") { const fs::path p = dir / (vendor_id + ".json"); std::ofstream f(p.string()); f << R"({"version":")" << version << R"(","name":")" << vendor_id << R"("})"; return p.string(); } // One vendor profile with a single process preset beside it, as an install or an // update lays it down: /.json plus //process/standard.json. void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) { fs::create_directories(dir / vendor / "process"); std::ofstream((dir / (vendor + ".json")).string()) << R"({"version":")" << version << R"(","name":")" << vendor << R"(","process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}]})"; std::ofstream((dir / vendor / "process" / "standard.json").string()) << R"({"type":"process","name":"0.20mm Standard @)" << vendor << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; } // A vendor whose process list is `processes`, each a preset name and the text of // its sub-file, listed in that order. void write_process_vendor(const fs::path& dir, const std::string& vendor, const std::vector>& processes) { fs::create_directories(dir / vendor / "process"); std::ofstream index((dir / (vendor + ".json")).string()); index << R"({"version":"1.0.0","name":")" << vendor << R"(","process_list":[)"; for (size_t i = 0; i < processes.size(); ++ i) { const std::string sub_path = "process/p" + std::to_string(i) + ".json"; index << (i ? "," : "") << R"({"name":")" << processes[i].first << R"(","sub_path":")" << sub_path << R"("})"; std::ofstream((dir / vendor / sub_path).string()) << processes[i].second; } index << "]}"; } std::string process_json(const std::string& name, const std::string& extra = std::string()) { return R"({"type":"process","name":")" + name + R"(","from":"system",)" + extra + R"("layer_height":"0.2"})"; } // A small but complete vendor: one machine model, one process, a non-instantiated // base filament with an instantiated child that inherits it and includes a // dual-extruder template, a second standalone filament carrying explicit // metadata, and one machine preset with a rename that includes a G-code template // — so the equivalence test below sees every CachedPreset field populated. The // templates are listed before the presets that include them, as update-index // orders them, or after them when `templates_last` asks for the broken order. void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version, bool templates_last = false) { fs::create_directories(dir / vendor / "process"); fs::create_directories(dir / vendor / "filament"); fs::create_directories(dir / vendor / "machine"); const std::string filament_template = R"({"name":")" + vendor + R"( dual template","sub_path":"filament/template.json"})"; const std::string filament_presets = R"({"name":")" + vendor + R"( Base PLA","sub_path":"filament/base.json"},)" R"({"name":")" + vendor + R"( PLA @0.4","sub_path":"filament/pla.json"},)" R"({"name":")" + vendor + R"( Silk PLA @0.4","sub_path":"filament/silk.json"})"; const std::string machine_template = R"({"name":")" + vendor + R"( 0.4 template machine_start_gcode","sub_path":"machine/start.json"})"; const std::string machine_presets = R"({"name":")" + vendor + R"( 0.4 nozzle","sub_path":"machine/printer.json"})"; std::ofstream((dir / (vendor + ".json")).string()) << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" << R"("machine_model_list":[{"name":"Test Model","sub_path":"machine/model.json"}],)" << R"("process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}],)" << R"("filament_list":[)" << (templates_last ? filament_presets + "," + filament_template : filament_template + "," + filament_presets) << R"(],"machine_list":[)" << (templates_last ? machine_presets + "," + machine_template : machine_template + "," + machine_presets) << "]}"; std::ofstream((dir / vendor / "machine" / "model.json").string()) << R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})"; std::ofstream((dir / vendor / "process" / "standard.json").string()) << R"({"type":"process","name":"0.20mm Standard @)" << vendor << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; // The base sets two per-variant keys; the template restates one of them for // two variants and adds a third; the child restates the third. std::ofstream((dir / vendor / "filament" / "base.json").string()) << R"({"type":"filament","name":")" << vendor << R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42",)" << R"("activate_air_filtration":["1"],"filament_max_volumetric_speed":["12"]})"; std::ofstream((dir / vendor / "filament" / "template.json").string()) << R"({"type":"filament","name":")" << vendor << R"( dual template","from":"system","instantiation":"false",)" << R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)" << R"("filament_max_volumetric_speed":["20","22"],"filament_flush_temp":["0","0"]})"; std::ofstream((dir / vendor / "filament" / "pla.json").string()) << R"({"type":"filament","name":")" << vendor << R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)" << R"("setting_id":"GFSA04","description":"Test PLA description"})"; std::ofstream((dir / vendor / "filament" / "silk.json").string()) << R"({"type":"filament","name":")" << vendor << R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA",)" << R"("include":[")" << vendor << R"( dual template"],"filament_flush_temp":["5","5"]})"; // The template states two G-codes; the printer restates one of them. std::ofstream((dir / vendor / "machine" / "start.json").string()) << R"({"type":"machine","name":")" << vendor << R"( 0.4 template machine_start_gcode","from":"system",)" << R"("instantiation":"false","machine_start_gcode":"G28 ; template","machine_end_gcode":"M84 ; template"})"; std::ofstream((dir / vendor / "machine" / "printer.json").string()) << R"({"type":"machine","name":")" << vendor << R"( 0.4 nozzle","from":"system","instantiation":"true","printer_model":"Test Model","printer_variant":"0.4",)" << R"("include":[")" << vendor << R"( 0.4 template machine_start_gcode"],"machine_end_gcode":"M84 ; own",)" << R"("renamed_from":")" << vendor << R"( old 0.4 nozzle"})"; } // The filament library: one non-instantiated base filament other vendors inherit // from. `cost` lets a test bump the library and watch the change flow through. void write_lib_tree(const fs::path& dir, const std::string& version, const std::string& cost) { const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); fs::create_directories(dir / lib / "filament"); std::ofstream((dir / (lib + ".json")).string()) << R"({"version":")" << version << R"(","name":")" << lib << R"(",)" << R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})"; std::ofstream((dir / lib / "filament" / "generic_pla.json").string()) << R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false",)" << R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})"; } struct FixtureEntry { std::string name, sub_path; std::vector deps; // indices of the entries this one inherits or includes }; // Filaments with two levels of inheritance, a template to include, and `leaves` // presets that inherit either level, some including the template. Only the // subfiles are written; write_filament_list lists them. std::vector write_layered_filaments(const fs::path& dir, const std::string& vendor, int leaves) { fs::create_directories(dir / vendor / "filament"); std::vector entries { { vendor + " Base PLA", "filament/base.json", {} }, { vendor + " Mid PLA", "filament/mid.json", {0} }, { vendor + " dual template", "filament/template.json", {} }, }; std::ofstream((dir / vendor / "filament" / "base.json").string()) << R"({"type":"filament","name":")" << entries[0].name << R"(","from":"system","instantiation":"false",)" << R"("filament_id":"GFA_base","filament_cost":"42","filament_max_volumetric_speed":["12"]})"; std::ofstream((dir / vendor / "filament" / "mid.json").string()) << R"({"type":"filament","name":")" << entries[1].name << R"(","from":"system","instantiation":"false",)" << R"("inherits":")" << entries[0].name << R"(","filament_flow_ratio":"0.95"})"; std::ofstream((dir / vendor / "filament" / "template.json").string()) << R"({"type":"filament","name":")" << entries[2].name << R"(","from":"system","instantiation":"false",)" << R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)" << R"("filament_max_volumetric_speed":["20","22"]})"; for (int i = 0; i < leaves; ++ i) { const size_t parent = i % 2; const bool include = i % 3 == 0; entries.push_back({ vendor + " PLA " + std::to_string(i) + " @0.4", "filament/leaf" + std::to_string(i) + ".json", include ? std::vector{parent, 2} : std::vector{parent} }); std::ofstream((dir / vendor / entries.back().sub_path).string()) << R"({"type":"filament","name":")" << entries.back().name << R"(","from":"system","instantiation":"true",)" << R"("inherits":")" << entries[parent].name << R"(",)" << (include ? R"("include":[")" + entries[2].name + R"("],)" : std::string()) << R"("nozzle_temperature":[")" << (200 + i % 40) << R"("]})"; } return entries; } // The vendor profile for write_layered_filaments, listing its entries in `order`, // or as they were written when `order` is empty. void write_filament_list(const fs::path& dir, const std::string& vendor, const std::vector& entries, std::vector order = {}) { if (order.empty()) { order.resize(entries.size()); std::iota(order.begin(), order.end(), 0); } std::ofstream f((dir / (vendor + ".json")).string()); f << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"; for (size_t k = 0; k < order.size(); ++ k) f << (k ? "," : "") << R"({"name":")" << entries[order[k]].name << R"(","sub_path":")" << entries[order[k]].sub_path << R"("})"; f << "]}"; } // A random order of `entries` that lists each one after everything it depends on. std::vector shuffled_after_dependencies(const std::vector& entries, std::mt19937& rng) { std::vector order; std::vector listed(entries.size(), false); while (order.size() < entries.size()) { std::vector ready; for (size_t i = 0; i < entries.size(); ++ i) if (! listed[i] && std::all_of(entries[i].deps.begin(), entries[i].deps.end(), [&](size_t d) { return listed[d]; })) ready.push_back(i); const size_t next = ready[std::uniform_int_distribution(0, ready.size() - 1)(rng)]; listed[next] = true; order.push_back(next); } return order; } // A vendor whose one filament inherits the library's base and states nothing of // its own — everything it shows comes from the library it is resolved against. void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version) { fs::create_directories(dir / vendor / "filament"); std::ofstream((dir / (vendor + ".json")).string()) << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" << R"("filament_list":[{"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"}]})"; std::ofstream((dir / vendor / "filament" / "pla.json").string()) << R"({"type":"filament","name":")" << vendor << R"( PLA @0.4","from":"system","instantiation":"true","inherits":"Generic PLA"})"; } std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id) { const fs::path p = dir / (vendor_id + ".json"); std::ofstream f(p.string()); f << R"({"name":")" << vendor_id << R"("})"; return p.string(); } // Whole file as bytes, for the byte-identity comparisons below. std::string slurp(const fs::path& p) { std::string s; load_string_file(p, s); return s; } // Flip one byte of the body. The default lands in the stamps at the front, which // every reader checks; pass an offset past them to corrupt a file that still // answers VendorCacheFile::peek_version but cannot survive its CRC. void corrupt_blob_byte(const std::string& path, std::streamoff at = 30) { std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary); f.seekp(at); char b = 0; f.read(&b, 1); f.seekp(at); b ^= 0xFF; f.write(&b, 1); } // Overwrite `n` bytes at `payload_off` into the cache's payload (which starts at // file offset 20, behind the header) and recompute the header CRC, so the file // stays authentic and only the deserializer can object to its contents. void patch_payload_bytes(const std::string& path, size_t payload_off, const void* bytes, size_t n) { constexpr size_t header_size = 20; // magic(4) + version(4) + data_size(8) + crc32(4) std::ifstream in(path, std::ios::binary); std::vector data(std::istreambuf_iterator(in), {}); in.close(); REQUIRE(data.size() >= header_size + payload_off + n); std::memcpy(&data[header_size + payload_off], bytes, n); boost::crc_32_type crc; crc.process_bytes(&data[header_size], data.size() - header_size); const uint32_t new_crc = crc.checksum(); std::memcpy(&data[16], &new_crc, 4); std::ofstream out(path, std::ios::binary | std::ios::trunc); out.write(data.data(), static_cast(data.size())); } // Patch cache_version (the payload's first word) so the file passes the CRC // check but fails the cache_version check in VendorCacheFile::load. void patch_cache_version(const std::string& path, uint32_t wrong_version) { patch_payload_bytes(path, 0, &wrong_version, sizeof(wrong_version)); } // Truncates the cache's PAYLOAD (everything after the 20-byte header) by // `truncate_by` bytes and recomputes data_size/crc32 in the header, exactly // as the cache writer computes them, so the framing's size and CRC checks // still pass but cereal runs out of bytes partway through deserializing the // body — exercising VendorCacheFile::load's catch block instead of its early // (pre-body) rejection paths. void truncate_payload_and_fix_header(const std::string& path, size_t truncate_by) { constexpr size_t header_size = 20; // magic(4) + version(4) + data_size(8) + crc32(4) std::ifstream in(path, std::ios::binary); std::vector data(std::istreambuf_iterator(in), {}); in.close(); REQUIRE(data.size() > header_size + truncate_by); const size_t new_payload_size = data.size() - header_size - truncate_by; const uint64_t data_size_field = static_cast(new_payload_size); boost::crc_32_type crc; crc.process_bytes(&data[header_size], new_payload_size); const uint32_t crc_field = crc.checksum(); std::memcpy(&data[8], &data_size_field, sizeof(data_size_field)); // data_size offset std::memcpy(&data[16], &crc_field, sizeof(crc_field)); // crc32 offset std::ofstream out(path, std::ios::binary | std::ios::trunc); out.write(data.data(), static_cast(header_size + new_payload_size)); } // One vendor as a cache's VendorMap. It carries one printer model ("Test Model", // variant "0.4") so machine entries can pass install's model/variant validation. VendorMap one_vendor(const std::string& vendor_id, const std::string& name = "", Semver ver = Semver(1, 0, 0)) { VendorMap vendors; VendorProfile vp(vendor_id); vp.name = name.empty() ? vendor_id + " Corp" : name; vp.config_version = ver; VendorProfile::PrinterModel model; model.id = "Test Model"; model.variants.emplace_back(VendorProfile::PrinterVariant("0.4")); vp.models.push_back(model); vendors.emplace(vendor_id, vp); return vendors; } // Source-form entries as parse_subfile would emit them. The alias is derived by // install from the '@' in the name, exactly as it is for the JSON parse. CachedPreset filament_entry(const std::string& name, const std::string& filament_id = "GFA00", const std::string& inherits = "") { CachedPreset e; e.name = name; e.sub_path = "filament/" + name + ".json"; e.instantiation = "true"; e.filament_id = filament_id; e.inherits = inherits; return e; } CachedPreset printer_entry(const std::string& name) { CachedPreset e; e.name = name; e.sub_path = "machine/" + name + ".json"; e.instantiation = "true"; e.config_src.set_key_value("printer_model", new ConfigOptionString("Test Model")); e.config_src.set_key_value("printer_variant", new ConfigOptionString("0.4")); return e; } static bool save_one_vendor(const std::string& path, const VendorMap& vendors, const std::string& vendor, const std::string& vendor_version, const std::vector& filament_entries = {}, const std::vector& machine_entries = {}, const std::vector& process_entries = {}) { VendorCacheData data; data.vendors = vendors; data.process_entries = process_entries; data.filament_entries = filament_entries; data.machine_entries = machine_entries; return VendorCacheFile::save(path, vendor, vendor_version, data); } // resources_dir()/data_dir() are process-wide, so restore them however the test // leaves — including through a failed REQUIRE — to stay green under --order rand. struct ScopedDirs { std::string prev_data{data_dir()}, prev_rsrc{resources_dir()}; ScopedDirs(const fs::path& data, const fs::path& rsrc) { set_data_dir(data.string()); set_resources_dir(rsrc.string()); } ~ScopedDirs() { set_data_dir(prev_data); set_resources_dir(prev_rsrc); } }; // A data dir and a resources dir, both pointed at by the process-wide accessors, // with the two directories a vendor is installed into and shipped from already // created. What every install- and load-order test needs before it starts. struct InstallDirs { TempDir data, rsrc; fs::path system = data.path / PRESET_SYSTEM_DIR; fs::path profiles = rsrc.path / "profiles"; ScopedDirs scoped { data.path, rsrc.path }; InstallDirs() { fs::create_directories(system); fs::create_directories(profiles); } }; // Helper: filter a collection by vendor_id. std::vector presets_for(const PresetCollection& coll, const std::string& vendor_id) { std::vector out; for (const Preset& p : coll()) if (p.is_system && p.vendor && p.vendor->id == vendor_id) out.push_back(&p); return out; } } // namespace namespace Slic3r { inline bool operator==(const VendorProfile::PrinterVariant& a, const VendorProfile::PrinterVariant& b) { return a.name == b.name; } inline bool operator==(const VendorProfile::PrinterModel& a, const VendorProfile::PrinterModel& b) { return a.id == b.id && a.name == b.name && a.model_id == b.model_id && a.technology == b.technology && a.family == b.family && a.variants == b.variants && a.default_materials == b.default_materials && a.not_support_bed_types == b.not_support_bed_types && a.bed_model == b.bed_model && a.bed_texture == b.bed_texture && a.image_bed_type == b.image_bed_type && a.bottom_texture_end_name == b.bottom_texture_end_name && a.use_double_extruder_default_texture == b.use_double_extruder_default_texture && a.bottom_texture_rect == b.bottom_texture_rect && a.bottom_texture_rect_longer == b.bottom_texture_rect_longer && a.middle_texture_rect == b.middle_texture_rect && a.hotend_model == b.hotend_model; } } // namespace Slic3r static bool vendor_deep_equal(const VendorProfile& a, const VendorProfile& b) { return a.name == b.name && a.id == b.id && a.config_version == b.config_version && a.config_update_url == b.config_update_url && a.changelog_url == b.changelog_url && a.models == b.models && a.default_filaments == b.default_filaments && a.default_sla_materials == b.default_sla_materials; } static bool preset_deep_equal(const Preset& a, const Preset& b) { return a.type == b.type && a.is_default == b.is_default && a.is_external == b.is_external && a.is_system == b.is_system && a.is_visible == b.is_visible && a.is_dirty == b.is_dirty && a.is_compatible == b.is_compatible && a.is_project_embedded == b.is_project_embedded && a.name == b.name && a.file == b.file && a.loaded == b.loaded && a.config.equals(b.config) && a.alias == b.alias && a.renamed_from == b.renamed_from && a.m_excluded_from == b.m_excluded_from && a.m_from_orca_filament_lib == b.m_from_orca_filament_lib && a.bundle_id == b.bundle_id && a.version == b.version && a.ini_str == b.ini_str && a.setting_id == b.setting_id && a.filament_id == b.filament_id && a.user_id == b.user_id && a.base_id == b.base_id && a.sync_info == b.sync_info && a.description == b.description && a.updated_time == b.updated_time && a.key_values == b.key_values; } TEST_CASE("a saved cache loads back with names, aliases and filament ids intact", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {filament_entry(vid + " PLA @0.4", "GFL_acme_pla")}, {printer_entry(vid + " Printer 0.4")})); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); REQUIRE(out.vendors.count(vid) == 1); auto fi = presets_for(out.filaments, vid); auto pr = presets_for(out.printers, vid); REQUIRE(fi.size() == 1); CHECK(fi[0]->name == vid + " PLA @0.4"); CHECK(fi[0]->alias == "Acme PLA"); CHECK(fi[0]->filament_id == "GFL_acme_pla"); REQUIRE(pr.size() == 1); CHECK(pr[0]->name == vid + " Printer 0.4"); } TEST_CASE("loading a missing cache file returns false", "[VendorCache]") { TempDir tmp; PresetBundle out; REQUIRE(!out.load_vendor_cache((tmp.path / "nonexistent.opc").string(), "Acme", Semver("1.0.0"))); } TEST_CASE("a cache with a corrupted byte is rejected by the CRC check", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {filament_entry(vid + " PLA")})); corrupt_blob_byte(cache.string()); PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); } TEST_CASE("two vendors produce two independent cache files", "[VendorCache]") { TempDir tmp; const fs::path cacheA = tmp.path / "vendorA.opc"; const fs::path cacheB = tmp.path / "vendorB.opc"; REQUIRE(save_one_vendor(cacheA.string(), one_vendor("VendorA"), "VendorA", "1.0.0", {filament_entry("VendorA PLA")})); REQUIRE(save_one_vendor(cacheB.string(), one_vendor("VendorB"), "VendorB", "1.0.0", {filament_entry("VendorB PLA")})); // Corrupt only vendor B's file; vendor A's must be unaffected. corrupt_blob_byte(cacheB.string()); PresetBundle outA; REQUIRE(outA.load_vendor_cache(cacheA.string(), "VendorA", Semver("1.0.0"))); REQUIRE(outA.vendors.count("VendorA") == 1); REQUIRE(presets_for(outA.filaments, "VendorA").size() == 1); PresetBundle outB; REQUIRE(!outB.load_vendor_cache(cacheB.string(), "VendorB", Semver("1.0.0"))); REQUIRE(outB.vendors.empty()); } TEST_CASE("vendor profile fields survive a cache round-trip", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; VendorMap vendors; VendorProfile vp(vid); vp.name = "Acme Corporation"; vp.config_version = Semver(2, 5, 1); VendorProfile::PrinterModel model; model.id = "AcmePro"; model.name = "Acme Pro"; VendorProfile::PrinterVariant v0_4; v0_4.name = "0.4"; model.variants.push_back(v0_4); vp.models.push_back(model); vendors.emplace(vid, vp); REQUIRE(save_one_vendor(cache.string(), vendors, vid, "2.5.1")); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("2.5.1"))); REQUIRE(out.vendors.count(vid) == 1); const VendorProfile& gvp = out.vendors.at(vid); REQUIRE(vendor_deep_equal(gvp, vendors.at(vid))); // Spot-check the fields the old test asserted directly, so a // vendor_deep_equal regression still points at what actually broke. CHECK(gvp.id == vid); CHECK(gvp.name == "Acme Corporation"); REQUIRE(gvp.models.size() == 1); CHECK(gvp.models[0].id == "AcmePro"); CHECK(gvp.models[0].name == "Acme Pro"); REQUIRE(gvp.models[0].variants.size() == 1); CHECK(gvp.models[0].variants[0].name == "0.4"); } TEST_CASE("config option values survive a cache round-trip", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; auto entry = filament_entry(vid + " PETG @0.4"); entry.config_src.set_key_value("filament_type", new ConfigOptionStrings({"PETG"})); REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {entry})); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); auto fi = presets_for(out.filaments, vid); REQUIRE(fi.size() == 1); const auto* ft = fi[0]->config.option("filament_type"); REQUIRE(ft != nullptr); REQUIRE(ft->values.size() >= 1); CHECK(ft->values[0] == "PETG"); } TEST_CASE("multiple presets in one collection all round-trip", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; const std::vector fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"}; const std::vector pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"}; std::vector filament_entries, machine_entries; for (const auto& n : fi_names) filament_entries.push_back(filament_entry(n)); for (const auto& n : pr_names) machine_entries.push_back(printer_entry(n)); REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", filament_entries, machine_entries)); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); auto fi = presets_for(out.filaments, vid); auto pr = presets_for(out.printers, vid); REQUIRE(fi.size() == 3); REQUIRE(pr.size() == 2); std::set fi_got, pr_got; for (const auto* p : fi) fi_got.insert(p->name); for (const auto* p : pr) pr_got.insert(p->name); for (const auto& n : fi_names) CHECK(fi_got.count(n) == 1); for (const auto& n : pr_names) CHECK(pr_got.count(n) == 1); } TEST_CASE("a truncated cache file is rejected", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "truncated.opc"; { std::ofstream f(cache.string(), std::ios::binary); const char data[] = {0x4F, 0x52, 0x43}; f.write(data, sizeof(data)); } PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); } TEST_CASE("a cache with the wrong magic number is rejected", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {filament_entry(vid + " PLA")})); { std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary); const uint32_t bad = 0xDEADBEEFu; f.write(reinterpret_cast(&bad), sizeof(bad)); } PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); } TEST_CASE("a vendor with no presets saves and loads cleanly", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor(vid, "Acme Corporation"), vid, "1.0.0")); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); REQUIRE(out.vendors.count(vid) == 1); CHECK(out.vendors.at(vid).id == vid); CHECK(out.vendors.at(vid).name == "Acme Corporation"); CHECK(presets_for(out.filaments, vid).empty()); CHECK(presets_for(out.printers, vid).empty()); CHECK(presets_for(out.prints, vid).empty()); } TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); write_full_vendor_tree(user, "Acme", "1.0.0"); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); PresetBundle from_json; from_json.set_generate_vendor_caches(true); from_json.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(fs::exists(user / "Acme.opc")); // Take the preset JSONs away: were the cache rejected, the load below would // have nothing to parse — so its success proves the cache answered. fs::remove_all(user / "Acme"); PresetBundle from_cache; from_cache.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); // Both paths run the same install code over the same entries, so everything // observable must come out identical — the vendor profile and every preset, // field by field. REQUIRE(from_cache.vendors.count("Acme") == 1); REQUIRE(vendor_deep_equal(from_cache.vendors.at("Acme"), from_json.vendors.at("Acme"))); const std::pair colls[] = { {&from_json.prints, &from_cache.prints}, {&from_json.filaments, &from_cache.filaments}, {&from_json.printers, &from_cache.printers}, }; for (const auto& [jc, cc] : colls) { auto a = presets_for(*jc, "Acme"); auto b = presets_for(*cc, "Acme"); REQUIRE(a.size() == b.size()); REQUIRE(!a.empty()); for (size_t i = 0; i < a.size(); ++i) { CHECK(a[i]->name == b[i]->name); CHECK(preset_deep_equal(*a[i], *b[i])); } } // Pin the explicit metadata against symmetric loss: dropping a field from // visit_entry (PresetCacheFormat.cpp) keeps the two bundles equal to each // other, but not to the fixture. const Preset* pla = from_cache.filaments.find_preset("Acme PLA @0.4", false); REQUIRE(pla != nullptr); CHECK(pla->setting_id == "GFSA04"); CHECK(pla->description == "Test PLA description"); const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false); REQUIRE(silk != nullptr); CHECK(silk->filament_id == "GFA_base"); // inherited from the non-instantiated base const auto* cost = silk->config.option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false); REQUIRE(pr != nullptr); CHECK(pr->renamed_from == std::vector{"Acme old 0.4 nozzle"}); CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include } TEST_CASE("a wide vendor loads from its cache exactly as it loads from JSON", "[VendorCache]") { InstallDirs dirs; constexpr int leaves = 400; write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", leaves)); PresetBundle from_json; from_json.set_generate_vendor_caches(true); from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(fs::exists(dirs.system / "Acme.opc")); fs::remove_all(dirs.system / "Acme"); PresetBundle from_cache; from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); auto a = presets_for(from_json.filaments, "Acme"); auto b = presets_for(from_cache.filaments, "Acme"); REQUIRE(a.size() == size_t(leaves)); REQUIRE(b.size() == a.size()); for (size_t i = 0; i < a.size(); ++ i) { CHECK(a[i]->name == b[i]->name); CHECK(preset_deep_equal(*a[i], *b[i])); } CHECK(from_cache.error_count() == from_json.error_count()); // The two-level chain resolved, so the leaf has the middle profile's flow ratio // and the base profile's filament id, neither of which it states. const Preset* leaf = from_cache.filaments.find_preset("Acme PLA 7 @0.4", false); REQUIRE(leaf != nullptr); CHECK(leaf->filament_id == "GFA_base"); const auto* flow = leaf->config.option("filament_flow_ratio"); REQUIRE(flow != nullptr); CHECK_THAT(flow->values.front(), WithinAbs(0.95, 1e-9)); } TEST_CASE("presets install the same in any order that lists each after what it depends on", "[VendorCache]") { InstallDirs dirs; const std::vector entries = write_layered_filaments(dirs.system, "Acme", 150); auto load = [&](const std::vector& order) { write_filament_list(dirs.system, "Acme", entries, order); auto bundle = std::make_unique(); bundle->load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); return bundle; }; const auto reference = load({}); CHECK(reference->error_count() == 0); const auto expected = presets_for(reference->filaments, "Acme"); REQUIRE(expected.size() == 150); // Every seed checks against one reference load, which GENERATE would repeat per seed. for (unsigned seed = 0; seed < 8; ++ seed) { CAPTURE(seed); std::mt19937 rng(seed); const auto shuffled = load(shuffled_after_dependencies(entries, rng)); CHECK(shuffled->error_count() == 0); const auto actual = presets_for(shuffled->filaments, "Acme"); REQUIRE(actual.size() == expected.size()); for (size_t i = 0; i < actual.size(); ++ i) CHECK(preset_deep_equal(*actual[i], *expected[i])); } } TEST_CASE("a key misplaced into a vendor preset is reported and removed", "[VendorCache]") { InstallDirs dirs; write_process_vendor(dirs.system, "Acme", { { "Acme template", R"({"type":"process","name":"Acme template","from":"system","instantiation":"false","wall_loops":"5","filament_density":"1.2"})" }, { "Acme base", R"({"type":"process","name":"Acme base","from":"system","instantiation":"false","filament_cost":"5"})" }, { "0.20mm Standard @Acme", process_json("0.20mm Standard @Acme", R"("instantiation":"true","inherits":"Acme base","include":["Acme template"],"nozzle_temperature":["210"],)") } }); PresetBundle bundle; bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(bundle.error_count() == 3); const Preset* preset = bundle.prints.find_preset("0.20mm Standard @Acme", false); REQUIRE(preset != nullptr); CHECK_FALSE(preset->config.has("filament_cost")); CHECK_FALSE(preset->config.has("filament_density")); CHECK_FALSE(preset->config.has("nozzle_temperature")); CHECK(preset->config.opt_int("wall_loops") == 5); } TEST_CASE("each vendor loads from the directory it is listed with", "[VendorCache]") { InstallDirs dirs; write_vendor_tree(dirs.system, "Acme", "1.0.0"); write_vendor_tree(dirs.profiles, "Zeta", "2.0.0"); PresetBundle bundle; bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.profiles } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false); CHECK(bundle.vendors.count("Acme") == 1); CHECK(bundle.vendors.count("Zeta") == 1); CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr); } TEST_CASE("a vendor that fails to load is left out, reported, and the others still load", "[VendorCache]") { InstallDirs dirs; write_vendor_tree(dirs.system, "Acme", "1.0.0"); write_process_vendor(dirs.system, "Broken", { { "Broken A", "{not-json" } }); PresetBundle bundle; std::vector failed; const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false, nullptr, &failed).second; CHECK(bundle.vendors.count("Acme") == 1); CHECK(bundle.vendors.count("Broken") == 0); CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); CHECK(errors.find("Broken") != std::string::npos); CHECK(failed == std::vector{ "Broken" }); } TEST_CASE("a filament library that fails partway is reported, and vendors inheriting from it are left out", "[VendorCache]") { InstallDirs dirs; const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); write_lib_tree(dirs.system, "1.0.0", "20"); std::ofstream((dirs.system / (lib + ".json")).string()) << R"({"version":"1.0.0","name":")" << lib << R"(","filament_list":[)" << R"({"name":"Generic PLA","sub_path":"filament/generic_pla.json"},)" << R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json"}]})"; std::ofstream((dirs.system / lib / "filament" / "generic_petg.json").string()) << "{not-json"; write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0"); write_vendor_tree(dirs.system, "Zeta", "1.0.0"); PresetBundle bundle; std::vector failed; const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { lib, dirs.system }, { "Zeta", dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false, nullptr, &failed).second; CHECK(failed == std::vector{ lib, "Acme" }); CHECK(errors.find("generic_petg.json") != std::string::npos); CHECK(bundle.vendors.count(lib) == 1); CHECK(bundle.vendors.count("Acme") == 0); CHECK(bundle.filaments.find_preset("Acme PLA @0.4", false) == nullptr); CHECK(bundle.vendors.count("Zeta") == 1); CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr); } TEST_CASE("a vendor whose profile cannot be read is left out, reported, and the others still load", "[VendorCache]") { InstallDirs dirs; write_vendor_tree(dirs.system, "Acme", "1.0.0"); std::ofstream((dirs.system / "Broken.json").string()) << "{not-json"; PresetBundle bundle; const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false).second; CHECK(bundle.vendors.count("Acme") == 1); CHECK(bundle.vendors.count("Broken") == 0); CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); CHECK(errors.find("Broken.json") != std::string::npos); } TEST_CASE("a canceled vendor load starts no vendor", "[VendorCache]") { InstallDirs dirs; write_vendor_tree(dirs.system, "Acme", "1.0.0"); write_vendor_tree(dirs.system, "Zeta", "1.0.0"); const std::atomic cancel { true }; PresetBundle bundle; bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false, &cancel); CHECK(bundle.vendors.empty()); CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) == nullptr); } TEST_CASE("a preset two vendors both define is kept from the first listed and reported under the others", "[VendorCache]") { InstallDirs dirs; for (const std::string vendor : { "Acme", "Mira", "Zeta" }) write_process_vendor(dirs.system, vendor, { { "Shared", process_json("Shared", R"("instantiation":"true",)") }, { "Own @" + vendor, process_json("Own @" + vendor, R"("instantiation":"true",)") } }); PresetBundle bundle; const std::string errors = bundle.load_vendors({ { "Mira", dirs.system }, { "Acme", dirs.system }, { "Zeta", dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false).second; const Preset* shared = bundle.prints.find_preset("Shared", false); REQUIRE(shared != nullptr); REQUIRE(shared->vendor != nullptr); CHECK(shared->vendor->id == "Mira"); CHECK(errors.find("vendor Mira") == std::string::npos); CHECK(errors.find("Found duplicated settings in vendor Acme's json file lists: Shared") != std::string::npos); CHECK(errors.find("Found duplicated settings in vendor Zeta's json file lists: Shared") != std::string::npos); CHECK(bundle.error_count() == 2); for (const std::string vendor : { "Acme", "Mira", "Zeta" }) { const Preset* own = bundle.prints.find_preset("Own @" + vendor, false); REQUIRE(own != nullptr); CHECK(own->vendor == &bundle.vendors.at(vendor)); } } TEST_CASE("filaments merged from several vendors come out generic first, then by name", "[VendorCache]") { InstallDirs dirs; auto write_filaments = [&](const std::string& vendor, const std::vector& names) { fs::create_directories(dirs.system / vendor / "filament"); std::ofstream index((dirs.system / (vendor + ".json")).string()); index << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"; for (size_t i = 0; i < names.size(); ++ i) { const std::string sub_path = "filament/f" + std::to_string(i) + ".json"; index << (i ? "," : "") << R"({"name":")" << names[i] << R"(","sub_path":")" << sub_path << R"("})"; std::ofstream((dirs.system / vendor / sub_path).string()) << R"({"type":"filament","name":")" << names[i] << R"(","from":"system","instantiation":"true",)" << R"("filament_id":"GF)" << vendor << i << R"("})"; } index << "]}"; }; write_filaments("Zeta", { "Zeta PLA @0.4", "Generic PETG @Zeta" }); write_filaments("Acme", { "Acme PLA @0.4", "Generic PLA @Acme" }); PresetBundle bundle; bundle.load_vendors({ { "Zeta", dirs.system }, { "Acme", dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false); std::vector names; for (const Preset& preset : bundle.filaments.get_presets()) if (! preset.is_default) names.push_back(preset.name); CHECK(names == std::vector{ "Generic PETG @Zeta", "Generic PLA @Acme", "Acme PLA @0.4", "Zeta PLA @0.4" }); for (const std::string& name : names) CHECK(bundle.filaments.find_preset(name, false) != nullptr); } TEST_CASE("a vendor read while the filament library loads resolves against it, from JSON and from its cache", "[VendorCache]") { InstallDirs dirs; const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); write_lib_tree(dirs.system, "1.0.0", "20"); write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0"); auto load = [&] { auto bundle = std::make_unique(); bundle->set_generate_vendor_caches(true); bundle->load_vendors({ { "Acme", dirs.system }, { lib, dirs.system } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true); return bundle; }; const auto from_json = load(); REQUIRE(fs::exists(dirs.system / "Acme.opc")); fs::remove_all(dirs.system / "Acme"); const auto from_cache = load(); for (const PresetBundle* bundle : { from_json.get(), from_cache.get() }) { const Preset* pla = bundle->filaments.find_preset("Acme PLA @0.4", false); REQUIRE(pla != nullptr); CHECK(pla->filament_id == "GFL99"); CHECK_THAT(pla->config.option("filament_cost")->values.front(), WithinAbs(20., 1e-9)); CHECK(bundle->error_count() == 0); } } TEST_CASE("repeated cache loads of one vendor produce the same presets", "[VendorCache]") { InstallDirs dirs; write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", 400)); PresetBundle seed; seed.set_generate_vendor_caches(true); seed.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(fs::exists(dirs.system / "Acme.opc")); fs::remove_all(dirs.system / "Acme"); std::vector loads(3); for (PresetBundle& bundle : loads) bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); auto first = presets_for(loads[0].filaments, "Acme"); REQUIRE(!first.empty()); for (size_t run = 1; run < loads.size(); ++ run) { auto again = presets_for(loads[run].filaments, "Acme"); REQUIRE(again.size() == first.size()); for (size_t i = 0; i < first.size(); ++ i) { CHECK(first[i]->name == again[i]->name); CHECK(preset_deep_equal(*first[i], *again[i])); } CHECK(loads[run].error_count() == loads[0].error_count()); } } TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); // One process preset without the required "instantiation" key — a parse-phase // error the load survives, so it must reach the cache's parse_errors stamp. fs::create_directories(user / "Acme" / "process"); std::ofstream((user / "Acme.json").string()) << R"({"version":"1.0.0","name":"Acme","process_list":[{"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})"; std::ofstream((user / "Acme" / "process" / "standard.json").string()) << R"({"type":"process","name":"0.20mm Standard @Acme","from":"system","layer_height":"0.2"})"; ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); PresetBundle from_json; from_json.set_generate_vendor_caches(true); from_json.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(fs::exists(user / "Acme.opc")); CHECK(from_json.error_count() > 0); fs::remove_all(user / "Acme"); PresetBundle from_cache; from_cache.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(from_cache.error_count() == from_json.error_count()); CHECK(presets_for(from_cache.prints, "Acme").size() == 1); } TEST_CASE("a non-instantiated base in a regular vendor's cache resolves its children and stays out of the library maps", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "vendor.opc"; // Entry order is the resolution order: the base must install (into the local // config maps) before the child that inherits it. auto base = filament_entry("Acme Base PLA", "GFA_base"); base.instantiation = "false"; base.config_src.set_key_value("filament_cost", new ConfigOptionFloats({42.})); auto child = filament_entry("Acme Silk PLA @0.4", "", "Acme Base PLA"); REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0", {base, child})); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); auto fi = presets_for(out.filaments, "Acme"); REQUIRE(fi.size() == 1); // the base never becomes a preset CHECK(fi[0]->name == "Acme Silk PLA @0.4"); CHECK(fi[0]->filament_id == "GFA_base"); const auto* cost = fi[0]->config.option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); // Only the filament library's bases persist as the cross-vendor inheritance // maps; a regular vendor's stay local to its own load. CHECK(out.m_config_maps.empty()); CHECK(out.m_filament_id_maps.empty()); } TEST_CASE("a cache with the wrong cache version is rejected", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {filament_entry(vid + " PLA")})); patch_cache_version(cache.string(), 0xFFFFFFFFu); PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); } TEST_CASE("a cache truncated mid-blob is rejected", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {filament_entry(vid + " PLA")})); { std::ifstream in(cache.string(), std::ios::binary); std::vector buf(30); // 20-byte header + 10 bytes of blob in.read(buf.data(), 30); in.close(); std::ofstream out(cache.string(), std::ios::binary | std::ios::trunc); out.write(buf.data(), 30); } PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); } TEST_CASE("printer model bed texture fields survive a cache round-trip", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path cache = tmp.path / "vendor.opc"; VendorMap vendors = one_vendor(vid); VendorProfile::PrinterModel model; model.id = "N1"; model.name = "Neat One"; model.bottom_texture_rect_longer = "5,5,50,10"; vendors.at(vid).models.push_back(model); REQUIRE(save_one_vendor(cache.string(), vendors, vid, "1.0.0")); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); REQUIRE(out.vendors.at(vid).models.size() == 2); REQUIRE(vendor_deep_equal(out.vendors.at(vid), vendors.at(vid))); CHECK(out.vendors.at(vid).models[1].bottom_texture_rect_longer == "5,5,50,10"); } TEST_CASE("a cache older than the vendor profile on disk is rejected", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.1"))); } TEST_CASE("a cache newer than the vendor profile on disk is used", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.2.0", {filament_entry("Acme PLA")})); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); CHECK(presets_for(out.filaments, "Acme").size() == 1); } TEST_CASE("a vendor cache outlives a filament library update and resolves against the new library", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); write_lib_tree(user, "1.0.0", "20"); write_vendor_with_lib_filament(user, "Acme", "1.0.0"); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); // First launch: the library parses first, then the vendor against it, and // both caches are written. PresetBundle base1; base1.set_generate_vendor_caches(true); base1.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); PresetBundle acme1; acme1.set_generate_vendor_caches(true); acme1.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, &base1); REQUIRE(fs::exists(user / "Acme.opc")); { auto fi = presets_for(acme1.filaments, "Acme"); REQUIRE(fi.size() == 1); const auto* cost = fi[0]->config.option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(20., 1e-9)); } // An update delivers a new library only; the vendor stays as it was. write_lib_tree(user, "2.0.0", "30"); PresetBundle base2; base2.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); // Take the vendor's preset JSONs away: were its cache rejected, the load // below would have nothing to parse — so its success proves the cache // survived the library bump. fs::remove_all(user / "Acme"); PresetBundle acme2; auto [substitutions, presets_loaded] = acme2.load_vendor_configs_from_json( user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, &base2); CHECK(presets_loaded == 1); auto fi = presets_for(acme2.filaments, "Acme"); REQUIRE(fi.size() == 1); // The cache holds only the vendor's own diff; the library values come from // the library loaded now, not the one in effect when the cache was written. const auto* cost = fi[0]->config.option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); CHECK(fi[0]->filament_id == "GFL99"); } TEST_CASE("a vendor installed as its cache alone still loads after a library update", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); write_lib_tree(user, "1.0.0", "20"); write_vendor_with_lib_filament(user, "Acme", "1.0.0"); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); // Generate the vendor's cache, then strip the vendor to the cache alone — // the shape of a packaged install, which ships each vendor as its .opc and // nothing else. PresetBundle base1; base1.set_generate_vendor_caches(true); base1.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); PresetBundle acme1; acme1.set_generate_vendor_caches(true); acme1.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, &base1); fs::remove(user / "Acme.json"); fs::remove_all(user / "Acme"); // An OTA update then delivers a new library only. With no JSONs anywhere to // fall back on, the vendor must keep loading from its cache. write_lib_tree(user, "2.0.0", "30"); PresetBundle base2; base2.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); PresetBundle acme2; auto [substitutions, presets_loaded] = acme2.load_vendor_configs_from_json( user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, &base2); CHECK(presets_loaded == 1); REQUIRE(acme2.vendors.count("Acme") == 1); auto fi = presets_for(acme2.filaments, "Acme"); REQUIRE(fi.size() == 1); const auto* cost = fi[0]->config.option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); } TEST_CASE("a cache install stopped by a canceled task group throws instead of rejecting the cache", "[VendorCache]") { TempDir tmp; const std::string cache = (tmp.path / "Acme.opc").string(); REQUIRE(save_one_vendor(cache, one_vendor("Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4")})); PresetBundle bundle; bool threw = false; tbb::task_group_context context; tbb::parallel_for(tbb::blocked_range(0, 1), [&](const tbb::blocked_range&) { context.cancel_group_execution(); try { bundle.load_vendor_cache(cache, "Acme", Semver(1, 0, 0)); } catch (const std::runtime_error&) { threw = true; } }, context); CHECK(threw); } TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); write_vendor_tree(user, "Acme", "1.0.0"); // A cache claiming the installed version, but whose entry inherits a preset // no loaded library provides. REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4", "GFA00", "No Such Base")})); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); // Directly: the load fails and leaves the bundle clean. PresetBundle direct; REQUIRE(!direct.load_vendor_cache((user / "Acme.opc").string(), "Acme", Semver("1.0.0"))); CHECK(direct.vendors.empty()); // Through the vendor load: the JSONs answer instead, as if no cache existed. PresetBundle out; auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(presets_loaded == 1); CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's // Through load_vendors, which reads the cache before installing it. PresetBundle several; several.load_vendors({ { "Acme", user } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true); CHECK(several.vendors.at("Acme").name == "Acme"); CHECK(several.prints.find_preset("0.20mm Standard @Acme", false) != nullptr); } TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); PresetBundle out; // An unversioned vendor profile has no version to compare against. REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver::invalid())); // And a cache carrying no version of its own cannot cover a profile that has one. REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "")); REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); REQUIRE(out.vendors.empty()); } TEST_CASE("a versionless profile beside a cache keeps the cache from being served", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4")})); // The profile beside the cache parses to no usable version, which can no // more judge the cache's staleness than it could be cached itself. write_versionless_vendor_json(user, "Acme"); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); PresetBundle out; auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); // Nothing came from the cache: the versionless profile was parsed instead, // and it carries no presets. CHECK(presets_loaded == 0); } TEST_CASE("a vendor's cache is its whole installation", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources"; const fs::path data = tmp.path / "data"; fs::create_directories(rsrc / "profiles" / "Acme" / "machine"); write_vendor_json(rsrc / "profiles", "Acme"); std::ofstream((rsrc / "profiles" / "Acme" / "machine" / "printer.json").string()) << "{}"; REQUIRE(save_one_vendor((rsrc / "profiles" / "Acme.opc").string(), one_vendor("Acme"), "Acme", "1.0.0")); ScopedDirs dirs(data, rsrc); REQUIRE(install_vendor_bundles_from_resources({"Acme"})); // The cache carries the presets, the vendor profile and the version they were // built at, so it is installed on its own. CHECK(fs::exists(data / "system" / "Acme.opc")); CHECK(!fs::exists(data / "system" / "Acme.json")); CHECK(!fs::exists(data / "system" / "Acme")); CHECK(is_vendor_installed("Acme")); CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); // A vendor with no cache is installed as its profile and preset JSONs instead, // parsing them being the only way left to load it — and the cache the previous // install left behind has to go, or it would shadow the profile just installed. fs::remove(rsrc / "profiles" / "Acme.opc"); REQUIRE(install_vendor_bundles_from_resources({"Acme"})); CHECK(!fs::exists(data / "system" / "Acme.opc")); CHECK(fs::exists(data / "system" / "Acme" / "machine" / "printer.json")); CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); // Installing the cache again takes the profile and its preset JSONs back out. REQUIRE(save_one_vendor((rsrc / "profiles" / "Acme.opc").string(), one_vendor("Acme"), "Acme", "1.0.0")); REQUIRE(install_vendor_bundles_from_resources({"Acme"})); CHECK(fs::exists(data / "system" / "Acme.opc")); CHECK(!fs::exists(data / "system" / "Acme.json")); CHECK(!fs::exists(data / "system" / "Acme")); } TEST_CASE("a vendor shipped as a cache alone is installed and loaded from it", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); // A packaged build: every vendor is its cache, with no profile of any kind // beside it — not even the filament library's. const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); REQUIRE(save_one_vendor((rsrc / (lib + ".opc")).string(), one_vendor(lib, "Shipped Library"), lib, "1.0.0")); REQUIRE(save_one_vendor((rsrc / "Acme.opc").string(), one_vendor("Acme", "Shipped Acme"), "Acme", "1.0.0")); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); // The version the build ships the vendor at comes from the cache, there being // no profile to read it from. CHECK(resource_vendor_version("Acme") == Semver(1, 0, 0)); // Resources reaches the app by being installed, never by being loaded from. REQUIRE(install_vendor_bundles_from_resources({lib, "Acme"})); CHECK(fs::exists(user / "Acme.opc")); CHECK(!fs::exists(user / "Acme.json")); CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); PresetBundle after; after.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(after.vendors.at("Acme").name == "Shipped Acme"); } TEST_CASE("a vendor with a profile in the data dir is parsed there and cached there, whatever resources ships", "[VendorCache]") { // The reported regression: a valid resources/profiles/.opc answered // first, so the JSON in system/ was never parsed and system/.opc was // never written. Main reads system/ and nothing else. InstallDirs dirs; write_vendor_tree(dirs.system, "Shadow", "1.0.0"); // A cache in resources at the very same version — under the old two-tier // lookup this was accepted and the parse skipped. REQUIRE(save_one_vendor((dirs.profiles / "Shadow.opc").string(), one_vendor("Shadow"), "Shadow", "1.0.0")); PresetBundle bundle; bundle.set_generate_vendor_caches(true); REQUIRE(bundle.load_vendor_configs_from_json(dirs.system.string(), "Shadow", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent).second > 0); // Parsed from system/, and its cache written back beside the profile. CHECK(fs::exists(dirs.system / "Shadow.opc")); CHECK(presets_for(bundle.prints, "Shadow").size() == 1); } TEST_CASE("a vendor with nothing installed is not loaded from resources", "[VendorCache]") { // Resources reaches the app by being installed into system/ first. A vendor // that is not installed is not loaded, however completely resources ships it. InstallDirs dirs; write_vendor_tree(dirs.profiles, "Absent", "1.0.0"); REQUIRE(save_one_vendor((dirs.profiles / "Absent.opc").string(), one_vendor("Absent"), "Absent", "1.0.0")); PresetBundle bundle; REQUIRE_THROWS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Absent", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent)); CHECK(presets_for(bundle.prints, "Absent").empty()); } TEST_CASE("a cache installed with no profile beside it is used whatever its version", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); write_vendor_json(rsrc, "Acme"); // Installed at an older version than the one now shipped in resources. Nothing // sits beside it claiming to be newer, so the cache is what the vendor is. REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Installed Acme"), "Acme", "0.9.0")); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == "0.9.0"); CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Other").empty()); CHECK(installed_vendor_version("Acme") == Semver(0, 9, 0)); // Loading the vendor takes the installed cache, not the newer shipped profile. PresetBundle out; out.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(out.vendors.at("Acme").name == "Installed Acme"); } TEST_CASE("a vendor whose cache covers it is loaded without parsing any JSON", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4")}, {printer_entry("Acme Printer 0.4")})); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); // The cache is the whole installation — no profile, no preset JSONs — and the // caller asks for the vendor exactly as it would for a JSON install. PresetBundle out; auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); CHECK(substitutions.empty()); CHECK(presets_loaded == 2); CHECK(out.vendors.at("Acme").name == "Cached Acme"); // Nothing was written back: the presets never came from a parse. CHECK(!fs::exists(user / "Acme.json")); } TEST_CASE("a vendor whose cache is stale falls back to parsing its JSONs", "[VendorCache]") { TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); // An update installed the vendor at 2.0.0; the cache next to it was built from // the profile before that, so it no longer covers what is on disk. write_vendor_tree(user, "Acme", "2.0.0"); REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0")); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); PresetBundle out; auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(presets_loaded == 1); CHECK(out.vendors.at("Acme").config_version == Semver(2, 0, 0)); // A one-off parse like this one leaves the stale cache alone: only a bundle // told its parses are complete writes one. CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == "1.0.0"); PresetBundle caching; caching.set_generate_vendor_caches(true); caching.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == get_version_from_json((user / "Acme.json").string()).to_string()); } TEST_CASE("a cache with a mismatched vendor name is rejected", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor("VendorA"), "VendorA", "1.0.0")); PresetBundle out; REQUIRE(!out.load_vendor_cache(cache.string(), "VendorB", Semver("1.0.0"))); } TEST_CASE("a cache is rejected against an unparsable version", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "vendor.opc"; REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); PresetBundle out; // A profile version that does not parse comes out of get_version_from_json // as zero, which cannot be judged any more than Semver::invalid() can. REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver())); REQUIRE(out.vendors.empty()); // rejection happens before the body is touched } TEST_CASE("the filament library's inheritance maps are rebuilt on cache load", "[VendorCache]") { // m_config_maps/m_filament_id_maps are the inheritance base other vendors // resolve against. The cache no longer stores them: they are rebuilt by // installing the library's entries — including the non-instantiated bases, // which exist for exactly this and never become presets. TempDir tmp; const fs::path cache = tmp.path / "lib.opc"; const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); auto base = filament_entry("Generic PLA", "GFL99"); base.instantiation = "false"; base.config_src.set_key_value("filament_cost", new ConfigOptionFloats({20.})); REQUIRE(save_one_vendor(cache.string(), one_vendor(lib), lib, "1.0.0", {base})); PresetBundle out; REQUIRE(out.load_vendor_cache(cache.string(), lib, Semver("1.0.0"))); REQUIRE(out.m_config_maps.count("Generic PLA") == 1); const auto* cost = out.m_config_maps.at("Generic PLA").option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(20., 1e-9)); CHECK(out.m_filament_id_maps.at("Generic PLA") == "GFL99"); CHECK(presets_for(out.filaments, lib).empty()); // not instantiated, not a preset } TEST_CASE("the same fixture parsed twice serializes byte-identically", "[VendorCache]") { // Shipped caches must be reproducible: the same profiles must produce the // same bytes on every machine that generates them. TempDir tmp; const fs::path rsrc = tmp.path / "resources" / "profiles"; const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; fs::create_directories(rsrc); fs::create_directories(user); write_full_vendor_tree(user, "Acme", "1.0.0"); ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); PresetBundle first; first.set_generate_vendor_caches(true); first.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(fs::exists(user / "Acme.opc")); const std::string bytes1 = slurp(user / "Acme.opc"); fs::remove(user / "Acme.opc"); PresetBundle second; second.set_generate_vendor_caches(true); second.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(slurp(user / "Acme.opc") == bytes1); } TEST_CASE("a cache that fails mid-body deserialization is rejected and leaves the bundle clean", "[VendorCache]") { TempDir tmp; const std::string vid = "Acme"; const fs::path valid_cache = tmp.path / "valid.opc"; const fs::path corrupt_cache = tmp.path / "corrupt.opc"; REQUIRE(save_one_vendor(valid_cache.string(), one_vendor(vid), vid, "1.0.0", {filament_entry(vid + " PLA @0.4")}, {printer_entry(vid + " Printer 0.4")})); // Truncate the tail (machine entries + parse_errors, per VendorCacheFile::save's // field order) so the header's size/CRC still validate but cereal runs out of // bytes partway through the body. Grow the cut if a given size ever stops // throwing (e.g. after an unrelated field-order change to the cache format). size_t truncate_by = 40; bool throws = false; for (; truncate_by <= 200; truncate_by += 8) { fs::copy_file(valid_cache, corrupt_cache, fs::copy_option::overwrite_if_exists); truncate_payload_and_fix_header(corrupt_cache.string(), truncate_by); PresetBundle probe_bundle; if (!probe_bundle.load_vendor_cache(corrupt_cache.string(), vid, Semver("1.0.0"))) { throws = true; break; } } REQUIRE(throws); PresetBundle out; REQUIRE(!out.load_vendor_cache(corrupt_cache.string(), vid, Semver("1.0.0"))); // The catch block put the bundle back the way a failed parse would leave it. CHECK(out.vendors.empty()); CHECK(out.m_config_maps.empty()); CHECK(presets_for(out.filaments, vid).empty()); // The recovery must leave a bundle a caller can still load a good cache into. REQUIRE(out.load_vendor_cache(valid_cache.string(), vid, Semver("1.0.0"))); CHECK(out.vendors.count(vid) == 1); CHECK(presets_for(out.filaments, vid).size() == 1); } TEST_CASE("a cache rejected mid-body leaves the error count where it found it", "[VendorCache]") { InstallDirs dirs; // A vendor whose root profile counts a parse error, so the bundle carries a // non-zero tally into the load below. Without one there is nothing for a // rejected cache to zero, and nothing to underflow. std::ofstream((dirs.system / "Noisy.json").string()) << R"({"version":"1.0.0","name":"Noisy","process_list":"not a list"})"; write_vendor_tree(dirs.system, "Counted", "1.0.0"); // A cache that passes every stamp and then dies in the entries. REQUIRE(save_one_vendor((dirs.system / "Counted.opc").string(), one_vendor("Counted"), "Counted", "1.0.0", {filament_entry("Counted PLA @0.4")})); truncate_payload_and_fix_header((dirs.system / "Counted.opc").string(), 8); PresetBundle bundle; bundle.set_generate_vendor_caches(true); bundle.load_vendor_configs_from_json(dirs.system.string(), "Noisy", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(bundle.error_count() > 0); // The same bundle: the cache is tried, fails mid-body, and the parse that // follows must be measured against the tally the cache found rather than // against zero. REQUIRE(bundle.load_vendor_configs_from_json(dirs.system.string(), "Counted", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent).second > 0); // The rewritten cache must carry the parse's own error count, not an // underflowed one. Reload it and check the bundle does not inherit a // nonsensical tally. PresetBundle reloaded; REQUIRE(reloaded.load_vendor_cache((dirs.system / "Counted.opc").string(), "Counted", Semver(1, 0, 0))); CHECK(reloaded.error_count() == 0); } TEST_CASE("a preset is traced to its vendor in a build that ships caches alone", "[VendorCache]") { InstallDirs dirs; std::vector filaments { filament_entry("Cached PLA @0.4") }; std::vector printers { printer_entry("Cached 0.4 nozzle") }; REQUIRE(save_one_vendor((dirs.profiles / "Cached.opc").string(), one_vendor("Cached"), "Cached", "1.0.0", filaments, printers)); CHECK(PresetBundle::find_preset_vendor("Cached PLA @0.4", Preset::TYPE_FILAMENT) == "Cached"); CHECK(PresetBundle::find_preset_vendor("Cached 0.4 nozzle", Preset::TYPE_PRINTER) == "Cached"); CHECK(PresetBundle::find_preset_vendor("Nobody's PLA", Preset::TYPE_FILAMENT).empty()); } TEST_CASE("a bundle that cannot be installed does not drop the others", "[VendorCache]") { InstallDirs dirs; write_vendor_tree(dirs.profiles, "Good", "1.0.0"); // An empty name sorts first out of a std::map, and a name resources does not // carry can appear anywhere. Neither may cost the batch the vendors it can // install. CHECK_FALSE(install_vendor_bundles_from_resources({"", "Absent", "Good"})); CHECK(fs::exists(dirs.system / "Good.json")); } TEST_CASE("a cache that arrives unusable leaves the profile fallback in place", "[VendorCache]") { InstallDirs dirs; write_vendor_tree(dirs.profiles, "Torn", "1.0.0"); const std::string cache = (dirs.profiles / "Torn.opc").string(); REQUIRE(save_one_vendor(cache, one_vendor("Torn"), "Torn", "1.0.0")); // Past the stamps at the front, so the 1 KB peek that chooses the cache form // still succeeds — only the CRC, which decides whether it can be served, // catches this. corrupt_blob_byte(cache, std::streamoff(fs::file_size(cache)) - 4); REQUIRE(VendorCacheFile::peek_version(cache, "Torn") == "1.0.0"); CHECK(install_vendor_bundles_from_resources({"Torn"})); CHECK(fs::exists(dirs.system / "Torn.json")); CHECK_FALSE(fs::exists(dirs.system / "Torn.opc")); } TEST_CASE("a vendor installed as an unreadable cache alone counts as not installed", "[VendorCache]") { InstallDirs dirs; const std::string cache = (dirs.system / "Broken.opc").string(); REQUIRE(save_one_vendor(cache, one_vendor("Broken"), "Broken", "1.0.0")); REQUIRE(is_vendor_installed("Broken")); // A cache this build cannot serve is not an installation: there is no // profile beside it and, since the single-tier load, nowhere else to load // the vendor from. corrupt_blob_byte(cache); CHECK_FALSE(is_vendor_installed("Broken")); CHECK_FALSE(installed_vendor_version("Broken").valid()); } TEST_CASE("a stale profile beside a newer cache does not hide the cache's version", "[VendorCache]") { InstallDirs dirs; write_vendor_json(dirs.system, "Both", "1.0.0"); REQUIRE(save_one_vendor((dirs.system / "Both.opc").string(), one_vendor("Both"), "Both", "2.0.0")); // The cache covers the profile, so the cache is what a load serves — and // 2.0.0 is the version installed, not the 1.0.0 the profile still claims. CHECK(installed_vendor_version("Both") == Semver(2, 0, 0)); } TEST_CASE("a profile newer than the cache beside it is the installed version", "[VendorCache]") { InstallDirs dirs; write_vendor_json(dirs.system, "Both", "3.0.0"); REQUIRE(save_one_vendor((dirs.system / "Both.opc").string(), one_vendor("Both"), "Both", "2.0.0")); // The cache no longer covers the profile, so the profile is parsed — and // its version is the one in force. CHECK(installed_vendor_version("Both") == Semver(3, 0, 0)); } TEST_CASE("a header claiming more body than the file holds is rejected", "[VendorCache]") { TempDir tmp; const std::string cache = (tmp.path / "Bounded.opc").string(); REQUIRE(save_one_vendor(cache, one_vendor("Bounded"), "Bounded", "1.0.0")); // Claim a body far larger than the file. Nothing may be allocated on the // strength of that number. { std::fstream f(cache, std::ios::in | std::ios::out | std::ios::binary); const uint64_t huge = 400ull * 1024ull * 1024ull; f.seekp(8); f.write(reinterpret_cast(&huge), sizeof(huge)); } PresetBundle bundle; REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0))); } TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]") { TempDir tmp; const std::string cache = (tmp.path / "Durable.opc").string(); REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0")); const std::string before = slurp(cache); // A directory where the temp file wants to go: the write cannot complete, // and must not have destroyed what was already there to find that out. const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp"); fs::create_directories(blocker); REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0")); CHECK(slurp(cache) == before); fs::remove_all(blocker); } TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]") { // The regression the fingerprint used to prevent by refusing the file // outright: nothing in the payload depends on serialization_key_ordinal, so // a build that inserted an option ahead of these reads them back correctly. TempDir tmp; const std::string cache = (tmp.path / "Ordinal.opc").string(); auto e = filament_entry("Ordinal PLA @0.4"); e.config_src.set_key_value("filament_cost", new ConfigOptionFloats({42.})); e.config_src.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); REQUIRE(save_one_vendor(cache, one_vendor("Ordinal"), "Ordinal", "1.0.0", {e})); PresetBundle bundle; REQUIRE(bundle.load_vendor_cache(cache, "Ordinal", Semver(1, 0, 0))); const auto filaments = presets_for(bundle.filaments, "Ordinal"); REQUIRE(filaments.size() == 1); const auto* cost = filaments.front()->config.option("filament_cost"); REQUIRE(cost != nullptr); CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); CHECK(filaments.front()->config.option("filament_type")->values.front() == "PLA"); } // ---- CacheDictionary and the name-keyed config payload ------------------- namespace { // Round-trip one config through the dictionary payload, optionally mutating the // dictionary between write and read to stand in for another build's schema. DynamicPrintConfig roundtrip_config(const DynamicPrintConfig& in, const std::function& mutate_blob = {}) { CacheDictionary wdict; wdict.collect(in); std::ostringstream os(std::ios::binary); { cereal::BinaryOutputArchive ar(os); wdict.save(ar); save_config(ar, in, wdict); } std::string blob = os.str(); if (mutate_blob) mutate_blob(blob); std::istringstream is(blob, std::ios::binary); cereal::BinaryInputArchive ar(is); CacheDictionary rdict; rdict.load(ar); DynamicPrintConfig out; load_config(ar, out, rdict); return out; } } // namespace TEST_CASE("a config round-trips through the cache dictionary", "[VendorCache]") { DynamicPrintConfig in; in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); in.set_key_value("printer_model", new ConfigOptionString("Test Model")); in.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.6})); in.set_key_value("spiral_mode", new ConfigOptionBool(true)); const DynamicPrintConfig out = roundtrip_config(in); CHECK_THAT(out.opt_float("layer_height"), WithinAbs(0.28, 1e-9)); CHECK(out.opt_string("printer_model") == "Test Model"); REQUIRE(out.option("nozzle_diameter") != nullptr); CHECK(out.option("nozzle_diameter")->values.size() == 2); CHECK(out.opt_bool("spiral_mode") == true); } TEST_CASE("an enum option round-trips by name, not by index", "[VendorCache]") { // top_surface_pattern is a coEnum; its stored int is an index into an enum // whose order is not a wire contract. Assert on the NAME, so a reordering // of the enum in PrintConfig.cpp cannot make this test pass by accident. const ConfigOptionDef* def = print_config_def.get("top_surface_pattern"); REQUIRE(def != nullptr); REQUIRE(def->type == coEnum); REQUIRE(def->enum_keys_map != nullptr); const int monotonic = def->enum_keys_map->at("monotonic"); DynamicPrintConfig in; in.set_key_value("top_surface_pattern", new ConfigOptionEnumGeneric(def->enum_keys_map, monotonic)); const DynamicPrintConfig out = roundtrip_config(in); REQUIRE(out.option("top_surface_pattern") != nullptr); CHECK(out.opt_enum("top_surface_pattern") == InfillPattern(monotonic)); CHECK(out.option("top_surface_pattern")->serialize() == "monotonic"); } TEST_CASE("a nullable vector enum round-trips by name, nil included", "[VendorCache]") { // coEnums carries a vector of ints and, unlike coEnum, its ConfigOptionType // does not fit in a byte - a truncated type in the dictionary would make a // reader take this for a scalar enum and run off the end of the stream. // nozzle_type is also nullable, and nil is an int no enum_keys_map names, // so this covers the dictionary's unnamed-value escape hatch too. const ConfigOptionDef* def = print_config_def.get("nozzle_type"); REQUIRE(def != nullptr); REQUIRE(def->type == coEnums); REQUIRE(def->nullable); REQUIRE(def->enum_keys_map != nullptr); const int brass = def->enum_keys_map->at("brass"); const int nil = ConfigOptionInts::nil_value(); DynamicPrintConfig in; auto* opt = new ConfigOptionEnumsGenericNullable(def->enum_keys_map); opt->values = { brass, nil }; in.set_key_value("nozzle_type", opt); in.set_key_value("printer_model", new ConfigOptionString("Test Model")); const DynamicPrintConfig out = roundtrip_config(in); const auto* got = out.option("nozzle_type"); REQUIRE(got != nullptr); CHECK(got->values == std::vector{brass, nil}); CHECK(out.opt_string("printer_model") == "Test Model"); } TEST_CASE("an option the build no longer knows is dropped, and the rest still load", "[VendorCache]") { DynamicPrintConfig in; in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); in.set_key_value("printer_model", new ConfigOptionString("Test Model")); // Rename the key in the dictionary the reader sees: "layer_height" becomes // "layer_heighX", a key no build defines. Same length, so the blob's // offsets are untouched - this is exactly what a removed or renamed option // looks like to a reader. const DynamicPrintConfig out = roundtrip_config(in, [](std::string& blob) { const size_t at = blob.find("layer_height"); REQUIRE(at != std::string::npos); blob[at + 11] = 'X'; }); CHECK(out.option("layer_height") == nullptr); CHECK(out.opt_string("printer_model") == "Test Model"); } TEST_CASE("an option whose type changed is dropped, and the rest still load", "[VendorCache]") { // A payload from a build where layer_height was a coString. This one has it // as a coFloat, so nothing can be done with the value - but the dictionary // says how it was written, so its bytes are still consumed and printer_model // behind it still lands. Hand-written rather than round-tripped: only a // dictionary this build did not produce can disagree with it. std::ostringstream os(std::ios::binary); { cereal::BinaryOutputArchive ar(os); const std::vector keys { "layer_height", "printer_model" }; const std::vector types { uint16_t(coString), uint16_t(coString) }; const std::vector enums { std::string() }; // the ENUM_UNNAMED slot ar(keys, types, enums); ar(uint32_t(2)); ar(uint16_t(0)); ar(ConfigOptionString("0.28")); ar(uint16_t(1)); ar(ConfigOptionString("Test Model")); } std::istringstream is(os.str(), std::ios::binary); cereal::BinaryInputArchive ar(is); CacheDictionary rdict; rdict.load(ar); DynamicPrintConfig out; REQUIRE_NOTHROW(load_config(ar, out, rdict)); CHECK(out.option("layer_height") == nullptr); CHECK(out.opt_string("printer_model") == "Test Model"); } TEST_CASE("skip_config consumes a config without building one", "[VendorCache]") { DynamicPrintConfig in; in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); in.set_key_value("printer_model", new ConfigOptionString("Test Model")); CacheDictionary wdict; wdict.collect(in); std::ostringstream os(std::ios::binary); { cereal::BinaryOutputArchive ar(os); wdict.save(ar); save_config(ar, in, wdict); ar(std::string("sentinel")); // must still be reachable after the skip } std::istringstream is(os.str(), std::ios::binary); cereal::BinaryInputArchive ar(is); CacheDictionary rdict; rdict.load(ar); skip_config(ar, rdict); std::string sentinel; ar(sentinel); CHECK(sentinel == "sentinel"); } TEST_CASE("a dictionary index past the end of the table is refused", "[VendorCache]") { DynamicPrintConfig in; in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); CacheDictionary wdict; wdict.collect(in); std::ostringstream os(std::ios::binary); { cereal::BinaryOutputArchive ar(os); wdict.save(ar); save_config(ar, in, wdict); } std::string blob = os.str(); // The payload's tail is the option count (uint32), the key index (uint16) // and the double. Point the key index somewhere the table does not go. const uint16_t bad = 0xFFFE; std::memcpy(&blob[blob.size() - sizeof(double) - sizeof(uint16_t)], &bad, sizeof(bad)); std::istringstream is(blob, std::ios::binary); cereal::BinaryInputArchive ar(is); CacheDictionary rdict; rdict.load(ar); DynamicPrintConfig out; REQUIRE_THROWS(load_config(ar, out, rdict)); } TEST_CASE("a stamp string with an absurd length is rejected, not allocated", "[VendorCache]") { // The stamps are read from whatever .opc a directory holds, and a // string resize to a garbage 64-bit length does not fail as a catchable // bad_alloc — it takes the app down through the out-of-memory handler. A // CRC-valid body opening with the right cache version but foreign framing // where the name's length word sits must be refused before anything is // allocated. TempDir tmp; const std::string cache = (tmp.path / "Evil.opc").string(); REQUIRE(save_one_vendor(cache, one_vendor("Evil"), "Evil", "1.0.0")); // The vendor name's length word sits right behind the payload's version // word; make it claim a ~9-exabyte name. const uint64_t huge = 0x7FFFFFFFFFFFFFFFull; patch_payload_bytes(cache, sizeof(uint32_t), &huge, sizeof(huge)); PresetBundle out; REQUIRE(! out.load_vendor_cache(cache, "Evil", Semver::inf())); CHECK(out.vendors.empty()); CHECK(VendorCacheFile::peek_version(cache, "Evil").empty()); } TEST_CASE("an included template's keys land on the preset, between the parent's and its own", "[VendorCache]") { InstallDirs dirs; write_full_vendor_tree(dirs.system, "Acme", "1.0.0"); PresetBundle bundle; bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(bundle.error_count() == 0); const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false); REQUIRE(pr != nullptr); CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // from the include CHECK(pr->config.opt_string("machine_end_gcode") == "M84 ; own"); // the preset's own key wins CHECK(presets_for(bundle.printers, "Acme").size() == 1); // the template is no preset const Preset* silk = bundle.filaments.find_preset("Acme Silk PLA @0.4", false); REQUIRE(silk != nullptr); const auto* speed = silk->config.option("filament_max_volumetric_speed"); REQUIRE(speed != nullptr); CHECK(speed->values == std::vector{20., 22.}); // the include wins over the parent const auto* flush = silk->config.option("filament_flush_temp"); REQUIRE(flush != nullptr); CHECK(flush->values == std::vector{5, 5}); // the preset's own key wins // A per-variant key the template never mentions keeps the parent's value. // The loader pads every base to its variant count; an include taken from the // padded copy would carry the padded default [0,0] over the parent's 1. const auto* air = silk->config.option("activate_air_filtration"); REQUIRE(air != nullptr); CHECK(air->values == std::vector{1, 1}); CHECK(presets_for(bundle.filaments, "Acme").size() == 2); // the template is no preset } TEST_CASE("an include listed after the preset that names it is an error, and the preset loads without it", "[VendorCache]") { InstallDirs dirs; write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true); PresetBundle bundle; bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); // One error per unresolved include: the printer's and the filament's. CHECK(bundle.error_count() == 2); const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false); REQUIRE(pr != nullptr); CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template"); const Preset* silk = bundle.filaments.find_preset("Acme Silk PLA @0.4", false); REQUIRE(silk != nullptr); CHECK(silk->config.option("filament_max_volumetric_speed")->values == std::vector{12.}); } TEST_CASE("an include listed after the preset that names it is missing from the cache load too", "[VendorCache]") { InstallDirs dirs; write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true); PresetBundle from_json; from_json.set_generate_vendor_caches(true); from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); REQUIRE(fs::exists(dirs.system / "Acme.opc")); fs::remove_all(dirs.system / "Acme"); PresetBundle from_cache; from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(from_cache.error_count() == 2); const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false); REQUIRE(pr != nullptr); CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template"); const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false); REQUIRE(silk != nullptr); const auto& speed = silk->config.option("filament_max_volumetric_speed")->values; REQUIRE(speed.size() == 1); CHECK_THAT(speed.front(), WithinAbs(12., 1e-9)); } TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]") { InstallDirs dirs; write_full_vendor_tree(dirs.system, "Acme", "1.0.0"); const fs::path start = dirs.system / "Acme" / "machine" / "start.json"; SECTION("named as G-code") { std::ofstream(start.string()) << R"({"type":"machine","name":"Acme 0.4 template machine_start_gcode","from":"system",)" << R"("machine_start_gcode":"G28 ; template"})"; } SECTION("not named, so included by its name in the vendor index") { std::ofstream(start.string()) << R"({"type":"machine","from":"system","machine_start_gcode":"G28 ; template"})"; } PresetBundle bundle; bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); CHECK(bundle.error_count() == 0); const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false); REQUIRE(pr != nullptr); CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); CHECK(presets_for(bundle.printers, "Acme").size() == 1); } TEST_CASE("a sub-file that fails to parse leaves the ones listed before it installed", "[VendorCache]") { InstallDirs dirs; write_process_vendor(dirs.system, "Acme", { { "Acme A", process_json("Acme A", R"("instantiation":"true",)") }, { "Acme B", "{not-json" }, { "Acme C", process_json("Acme C", R"("instantiation":"true",)") } }); PresetBundle bundle; CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent), ConfigurationError); CHECK(bundle.prints.find_preset("Acme A", false) != nullptr); CHECK(bundle.prints.find_preset("Acme C", false) == nullptr); } TEST_CASE("a load that fails to install partway counts no errors of the sub-files after it", "[VendorCache]") { InstallDirs dirs; // B names a parent nobody defines. C states no instantiation, an error of its own. write_process_vendor(dirs.system, "Acme", { { "Acme A", process_json("Acme A", R"("instantiation":"true",)") }, { "Acme B", process_json("Acme B", R"("instantiation":"true","inherits":"Nobody",)") }, { "Acme C", process_json("Acme C") } }); PresetBundle bundle; CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent), ConfigurationError); // B's missing parent and B's failed install. CHECK(bundle.error_count() == 2); CHECK(bundle.prints.find_preset("Acme A", false) != nullptr); }