mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-15 23:12:08 +00:00
Rvert from per-verndor to single cache file
Replace N per-vendor .cache files with a single system_presets.cache that holds all vendors and presets in one serialized blob. Cache load is now all-or-nothing: on hit all vendors are applied from the bundle (sub-second); on miss all vendors are parsed from JSON and a fresh bundle is written to the user cache dir. Invalidation is driven by bundle_key - a sorted concatenation of all vendor JSON version strings. Any vendor update invalidates the whole cache and triggers re-parse on next launch. Guide wizard (WebGuideDialog) loads the bundled cache into a plain PresetBundle instead of a separate VendorGuideData struct, removing the duplicate data model. generate_system_cache simplified from a per-vendor loop to a single save_system_presets_cache() call producing one output file.
This commit is contained in:
@@ -45,12 +45,9 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
|
||||
set_logging_level(log_level);
|
||||
// In validation_mode, load_system_presets_from_json uses data_dir() directly
|
||||
// (no /system/ suffix), so point data_dir at the profiles directory.
|
||||
set_data_dir(profiles_path);
|
||||
set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string());
|
||||
|
||||
// load_presets creates user preset dirs under data_dir().
|
||||
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
|
||||
if (!fs::exists(user_dir))
|
||||
fs::create_directories(user_dir);
|
||||
@@ -71,56 +68,21 @@ int main(int argc, char* argv[])
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Collect all vendor names from JSON files in the profiles directory.
|
||||
std::vector<std::string> vendor_names;
|
||||
for (const auto& e : fs::directory_iterator(profiles_path)) {
|
||||
if (e.path().extension() == ".json")
|
||||
vendor_names.push_back(e.path().stem().string());
|
||||
const std::string output_path =
|
||||
(fs::path(profiles_path) / "system_presets.cache").make_preferred().string();
|
||||
|
||||
std::cout << "Saving single-bundle cache to: " << output_path << "\n";
|
||||
|
||||
const auto stats = preset_bundle->save_system_presets_cache(profiles_path, output_path);
|
||||
|
||||
if (!stats.ok) {
|
||||
std::cerr << "ERROR: verification failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sort: PresetBundle::ORCA_FILAMENT_LIBRARY first, rest alphabetical.
|
||||
std::sort(vendor_names.begin(), vendor_names.end(),
|
||||
[](const std::string& a, const std::string& b) {
|
||||
if (a == PresetBundle::ORCA_FILAMENT_LIBRARY) return true;
|
||||
if (b == PresetBundle::ORCA_FILAMENT_LIBRARY) return false;
|
||||
return a < b;
|
||||
});
|
||||
|
||||
size_t total_print = 0, total_filament = 0, total_printer = 0;
|
||||
int saved = 0, failed = 0;
|
||||
|
||||
for (const auto& vendor_name : vendor_names) {
|
||||
try {
|
||||
const std::string json_path = (fs::path(profiles_path) / (vendor_name + ".json")).string();
|
||||
const std::string cache_path = (fs::path(profiles_path) / (vendor_name + ".cache")).make_preferred().string();
|
||||
const bool is_orca_lib = (vendor_name == PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
|
||||
const auto stats = preset_bundle->save_bundled_vendor_cache(vendor_name, json_path, is_orca_lib, cache_path);
|
||||
|
||||
if (!stats.ok) {
|
||||
std::cerr << "ERROR: " << vendor_name << ": verification failed\n";
|
||||
++failed;
|
||||
} else {
|
||||
std::cout << " [ok] " << vendor_name << ".cache"
|
||||
<< " (" << stats.print_presets << " print, "
|
||||
<< stats.filament_presets << " filament, "
|
||||
<< stats.printer_presets << " printer)\n";
|
||||
total_print += stats.print_presets;
|
||||
total_filament += stats.filament_presets;
|
||||
total_printer += stats.printer_presets;
|
||||
++saved;
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << "ERROR: " << vendor_name << ": " << ex.what() << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\nDone: " << saved << " cache(s) written";
|
||||
if (failed) std::cout << ", " << failed << " FAILED";
|
||||
std::cout << "\n"
|
||||
<< " Total print presets: " << total_print << "\n"
|
||||
<< " Total filament presets: " << total_filament << "\n"
|
||||
<< " Total printer presets: " << total_printer << "\n";
|
||||
return failed ? 1 : 0;
|
||||
std::cout << "[ok] system_presets.cache\n"
|
||||
<< " Total print presets: " << stats.print_presets << "\n"
|
||||
<< " Total filament presets: " << stats.filament_presets << "\n"
|
||||
<< " Total printer presets: " << stats.printer_presets << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2220,71 +2220,26 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
|
||||
if (validation_mode)
|
||||
dir = (boost::filesystem::path(data_dir())).make_preferred();
|
||||
|
||||
// Per-vendor binary cache: try user cache, then bundled cache, then JSON parse.
|
||||
// Vendors that hit a cache are applied immediately; misses go through JSON parsing below.
|
||||
std::set<std::string> cache_miss_vendors;
|
||||
std::map<std::string, std::string> vendor_json_versions; // vendor_id → version string on disk
|
||||
|
||||
// Single-bundle cache: try user cache, then bundled cache, then JSON parse.
|
||||
bool loaded_from_cache = false;
|
||||
if (!validation_mode) {
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
this->reset(false);
|
||||
bool all_from_cache = true;
|
||||
|
||||
// Collect all vendor names from the system directory.
|
||||
std::vector<std::string> all_vendor_names;
|
||||
try {
|
||||
for (const auto& e : boost::filesystem::directory_iterator(dir)) {
|
||||
if (Slic3r::is_json_file(e.path().string()))
|
||||
all_vendor_names.push_back(e.path().stem().string());
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: cannot scan system dir: " << ex.what();
|
||||
}
|
||||
|
||||
// Load ORCA_FILAMENT_LIBRARY first (other vendors' filaments inherit from it).
|
||||
// Then load remaining vendors from per-vendor caches.
|
||||
auto try_vendor_cache = [&](const std::string& vendor_name) -> bool {
|
||||
const std::string json_path = (dir / (vendor_name + ".json")).string();
|
||||
const std::string ver_str = get_vendor_cache_key(json_path);
|
||||
vendor_json_versions[vendor_name] = ver_str;
|
||||
return try_load_and_apply_vendor_cache(vendor_name, ver_str);
|
||||
};
|
||||
|
||||
// Sort: ORCA_FILAMENT_LIBRARY goes first, rest alphabetical.
|
||||
std::sort(all_vendor_names.begin(), all_vendor_names.end(),
|
||||
[](const std::string& a, const std::string& b) {
|
||||
if (a == ORCA_FILAMENT_LIBRARY) return true;
|
||||
if (b == ORCA_FILAMENT_LIBRARY) return false;
|
||||
return a < b;
|
||||
});
|
||||
|
||||
for (const auto& name : all_vendor_names) {
|
||||
if (!try_vendor_cache(name)) {
|
||||
cache_miss_vendors.insert(name);
|
||||
all_from_cache = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (all_from_cache && !all_vendor_names.empty()) {
|
||||
const std::string expected_key = compute_system_presets_cache_key(dir.string());
|
||||
if (try_load_system_presets_from_cache(expected_key)) {
|
||||
update_system_maps();
|
||||
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from per-vendor cache in " << ms << " ms";
|
||||
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from single-bundle cache in " << ms << " ms";
|
||||
return {PresetsConfigSubstitutions{}, ""};
|
||||
}
|
||||
if (!cache_miss_vendors.empty())
|
||||
BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << cache_miss_vendors.size()
|
||||
<< " vendor(s) need JSON parse: "
|
||||
<< [&]{ std::string s; for (auto& v : cache_miss_vendors) s += v + " "; return s; }();
|
||||
loaded_from_cache = false; // cache miss — fall through to JSON parse
|
||||
}
|
||||
|
||||
const auto json_load_t0 = std::chrono::steady_clock::now();
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
std::string errors_cummulative;
|
||||
std::set<std::string> errored_vendors; // vendors whose JSON parse failed — skip their cache save
|
||||
// first = true means no vendor has been loaded yet (from cache or JSON).
|
||||
// false = at least one vendor was applied from the per-vendor cache above.
|
||||
bool first = validation_mode || cache_miss_vendors.size() == vendor_json_versions.size();
|
||||
bool first = true;
|
||||
std::vector<std::string> vendor_names;
|
||||
// store all vendor names in vendor_names
|
||||
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
|
||||
@@ -2306,9 +2261,6 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
|
||||
std::vector<std::string> other_vendors;
|
||||
other_vendors.reserve(vendor_names.size());
|
||||
for (auto& vn : vendor_names) {
|
||||
// Skip vendors already loaded from the per-vendor cache.
|
||||
if (!validation_mode && !cache_miss_vendors.count(vn))
|
||||
continue;
|
||||
if (vn == ORCA_FILAMENT_LIBRARY)
|
||||
orca_lib_vendor = vn;
|
||||
else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate))
|
||||
@@ -2397,22 +2349,19 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
|
||||
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from JSON in " << json_ms << " ms";
|
||||
}
|
||||
|
||||
// Save per-vendor binary caches for vendors that parsed successfully.
|
||||
// Vendors whose JSON produced a parse error are skipped individually so one
|
||||
// bad vendor does not block caching of all others.
|
||||
if (!validation_mode && !cache_miss_vendors.empty()) {
|
||||
// Save single-bundle cache after successful JSON parse.
|
||||
if (!validation_mode && errored_vendors.empty()) {
|
||||
const auto save_t0 = std::chrono::steady_clock::now();
|
||||
for (const auto& vendor_name : cache_miss_vendors) {
|
||||
if (errored_vendors.count(vendor_name))
|
||||
continue;
|
||||
const bool is_orca_lib = (vendor_name == ORCA_FILAMENT_LIBRARY);
|
||||
const std::string ver_str = vendor_json_versions.count(vendor_name)
|
||||
? vendor_json_versions.at(vendor_name) : "";
|
||||
write_vendor_cache(vendor_cache_user_path(vendor_name), vendor_name, ver_str, is_orca_lib);
|
||||
}
|
||||
const auto stats = save_system_presets_cache(dir.string(), user_system_presets_cache_path());
|
||||
const auto save_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - save_t0).count();
|
||||
BOOST_LOG_TRIVIAL(info) << "PresetBundle: per-vendor caches saved in " << save_ms << " ms";
|
||||
if (stats.ok)
|
||||
BOOST_LOG_TRIVIAL(info) << "PresetBundle: single-bundle cache saved in " << save_ms << " ms"
|
||||
<< " (print=" << stats.print_presets
|
||||
<< " filament=" << stats.filament_presets
|
||||
<< " printer=" << stats.printer_presets << ")";
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: single-bundle cache save failed";
|
||||
}
|
||||
|
||||
//BBS: add config related logs
|
||||
@@ -5904,10 +5853,7 @@ bool BundleMetadata::save_to_json(const std::string& path) const
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ---- VendorCache implementation (PresetBundle methods) ------------------
|
||||
// The VendorCacheHeader struct holds only metadata. Preset collections are
|
||||
// serialized directly from/into PresetBundle's own prints/filaments/printers/…
|
||||
// to avoid ever duplicating those vectors in memory.
|
||||
// ---- System presets single-bundle cache implementation ------------------
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -5921,23 +5867,77 @@ struct CacheFileHeader {
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
|
||||
|
||||
// Presets for one vendor, grouped so vendor pointers can be re-wired on load.
|
||||
struct VendorPresetGroup {
|
||||
std::vector<Preset> prints, filaments, printers, sla_prints, sla_materials;
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) { ar(prints, filaments, printers, sla_prints, sla_materials); }
|
||||
};
|
||||
|
||||
struct SystemPresetsCache {
|
||||
static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
|
||||
static constexpr uint32_t CACHE_VERSION = 1;
|
||||
|
||||
uint32_t cache_version = CACHE_VERSION;
|
||||
uint32_t config_options_count = 0;
|
||||
std::string bundle_key;
|
||||
|
||||
VendorMap vendors;
|
||||
std::map<std::string, VendorPresetGroup> preset_groups;
|
||||
std::map<std::string, DynamicPrintConfig> config_maps;
|
||||
std::map<std::string, std::string> filament_id_maps;
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar)
|
||||
{
|
||||
ar(cache_version, config_options_count, bundle_key,
|
||||
vendors, preset_groups, config_maps, filament_id_maps);
|
||||
}
|
||||
|
||||
bool is_valid(const std::string& expected_key) const
|
||||
{
|
||||
return cache_version == CACHE_VERSION
|
||||
&& config_options_count == static_cast<uint32_t>(print_config_def.options.size())
|
||||
&& bundle_key == expected_key;
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// static
|
||||
std::string PresetBundle::vendor_cache_user_path(const std::string& vendor_id)
|
||||
std::string PresetBundle::bundled_system_presets_cache_path()
|
||||
{
|
||||
return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / (vendor_id + ".cache"))
|
||||
return (boost::filesystem::path(resources_dir()) / "profiles" / "system_presets.cache")
|
||||
.make_preferred().string();
|
||||
}
|
||||
|
||||
// static
|
||||
std::string PresetBundle::vendor_cache_bundled_path(const std::string& vendor_id)
|
||||
std::string PresetBundle::user_system_presets_cache_path()
|
||||
{
|
||||
return (boost::filesystem::path(resources_dir()) / "profiles" / (vendor_id + ".cache"))
|
||||
return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / "system_presets.cache")
|
||||
.make_preferred().string();
|
||||
}
|
||||
|
||||
// static — raw file I/O + magic/version/CRC check, shared by all cache readers.
|
||||
// static
|
||||
std::string PresetBundle::compute_system_presets_cache_key(const std::string& system_dir)
|
||||
{
|
||||
// Sorted map so key is stable regardless of directory iteration order.
|
||||
std::map<std::string, std::string> keys;
|
||||
try {
|
||||
for (const auto& e : boost::filesystem::directory_iterator(system_dir)) {
|
||||
if (Slic3r::is_json_file(e.path().string()))
|
||||
keys[e.path().stem().string()] = get_vendor_cache_key(e.path().string());
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot scan " << system_dir << ": " << ex.what();
|
||||
}
|
||||
std::string combined;
|
||||
for (const auto& [name, key] : keys)
|
||||
combined += name + ":" + key + ";";
|
||||
return combined;
|
||||
}
|
||||
|
||||
// static
|
||||
bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blob)
|
||||
{
|
||||
try {
|
||||
@@ -5947,7 +5947,7 @@ bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blo
|
||||
CacheFileHeader fhdr;
|
||||
if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)))
|
||||
return false;
|
||||
if (fhdr.magic != VendorCacheHeader::CACHE_MAGIC || fhdr.version != VendorCacheHeader::CACHE_VERSION)
|
||||
if (fhdr.magic != SystemPresetsCache::CACHE_MAGIC)
|
||||
return false;
|
||||
if (fhdr.data_size == 0 || fhdr.data_size > 512u * 1024u * 1024u)
|
||||
return false;
|
||||
@@ -5957,197 +5957,177 @@ bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blo
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(out_blob.data(), out_blob.size());
|
||||
if (crc.checksum() != fhdr.crc32) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCache: CRC mismatch: " << path;
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: CRC mismatch: " << path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCache: read failed (" << path << "): " << e.what();
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: read failed (" << path << "): " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize PresetBundle's preset collections for vendor_id directly to file.
|
||||
// Uses cereal's size-tag protocol so the binary layout matches what a
|
||||
// std::vector<Preset> archive would produce — no intermediate vector copy.
|
||||
void PresetBundle::write_vendor_cache(const std::string& path,
|
||||
const std::string& vendor_id,
|
||||
const std::string& json_ver,
|
||||
bool capture_filament_maps) const
|
||||
// static
|
||||
void PresetBundle::write_cache_blob(const std::string& path, const std::string& blob)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
{
|
||||
cereal::BinaryOutputArchive ar(oss);
|
||||
|
||||
VendorCacheHeader hdr;
|
||||
hdr.cache_version = VendorCacheHeader::CACHE_VERSION;
|
||||
hdr.config_options_count = static_cast<uint32_t>(print_config_def.options.size());
|
||||
hdr.vendor_json_version = json_ver;
|
||||
auto vp_it = vendors.find(vendor_id);
|
||||
if (vp_it != vendors.end())
|
||||
hdr.profile = vp_it->second;
|
||||
ar(hdr);
|
||||
|
||||
// Write each collection filtered to vendor_id without copying into a temp vector.
|
||||
auto write_col = [&](const PresetCollection& coll) {
|
||||
std::vector<const Preset*> ptrs;
|
||||
for (const Preset& p : coll())
|
||||
if (p.is_system && p.vendor && p.vendor->id == vendor_id)
|
||||
ptrs.push_back(&p);
|
||||
ar(cereal::make_size_tag(static_cast<cereal::size_type>(ptrs.size())));
|
||||
for (const Preset* p : ptrs)
|
||||
ar(*p);
|
||||
};
|
||||
write_col(prints);
|
||||
write_col(filaments);
|
||||
write_col(printers);
|
||||
write_col(sla_prints);
|
||||
write_col(sla_materials);
|
||||
|
||||
if (capture_filament_maps) {
|
||||
ar(m_config_maps, m_filament_id_maps);
|
||||
} else {
|
||||
const std::map<std::string, DynamicPrintConfig> empty_cm;
|
||||
const std::map<std::string, std::string> empty_fi;
|
||||
ar(empty_cm, empty_fi);
|
||||
}
|
||||
}
|
||||
const std::string blob = oss.str();
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(blob.data(), blob.size());
|
||||
try {
|
||||
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
||||
boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCache: cannot open for writing: " << path;
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot open for writing: " << path;
|
||||
return;
|
||||
}
|
||||
CacheFileHeader fhdr;
|
||||
fhdr.magic = VendorCacheHeader::CACHE_MAGIC;
|
||||
fhdr.version = VendorCacheHeader::CACHE_VERSION;
|
||||
fhdr.magic = SystemPresetsCache::CACHE_MAGIC;
|
||||
fhdr.version = SystemPresetsCache::CACHE_VERSION;
|
||||
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
||||
fhdr.crc32 = crc.checksum();
|
||||
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
||||
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCache: write failed (" << path << "): " << e.what();
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: write failed (" << path << "): " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
// Read a cache file and — if valid — apply its presets directly into PresetBundle's
|
||||
// collections. Deserializes all 5 collections into temp vectors first so that
|
||||
// PresetBundle is not modified if deserialization fails partway through.
|
||||
bool PresetBundle::load_and_apply_vendor_cache(const std::string& path, const std::string& json_ver)
|
||||
// Apply one VendorPresetGroup from a loaded cache into this PresetBundle's collections.
|
||||
static void apply_vendor_preset_group(VendorPresetGroup& grp,
|
||||
const VendorProfile* vp,
|
||||
PresetCollection& prints_coll,
|
||||
PresetCollection& filaments_coll,
|
||||
PrinterPresetCollection& printers_coll,
|
||||
PresetCollection& sla_prints_coll,
|
||||
PresetCollection& sla_materials_coll)
|
||||
{
|
||||
std::string blob;
|
||||
if (!read_cache_blob(path, blob))
|
||||
return false;
|
||||
try {
|
||||
std::istringstream iss(blob);
|
||||
cereal::BinaryInputArchive ar(iss);
|
||||
|
||||
VendorCacheHeader hdr;
|
||||
ar(hdr);
|
||||
if (!hdr.is_valid(json_ver))
|
||||
return false;
|
||||
|
||||
// Deserialize all collections before touching PresetBundle (rollback safety).
|
||||
std::vector<Preset> col_print, col_filament, col_printer, col_sla_print, col_sla_material;
|
||||
std::map<std::string, DynamicPrintConfig> config_maps;
|
||||
std::map<std::string, std::string> filament_id_maps;
|
||||
ar(col_print, col_filament, col_printer, col_sla_print, col_sla_material);
|
||||
ar(config_maps, filament_id_maps);
|
||||
|
||||
// Full deserialization succeeded — apply to PresetBundle.
|
||||
vendors.emplace(hdr.profile.id, hdr.profile);
|
||||
const VendorProfile* vp = &vendors.at(hdr.profile.id);
|
||||
|
||||
auto apply_col = [&](std::vector<Preset>& cached, PresetCollection& coll, bool is_filaments) {
|
||||
for (Preset& cp : cached) {
|
||||
DynamicPrintConfig config = cp.config;
|
||||
Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), /*select=*/false, cp.version);
|
||||
p.is_system = true;
|
||||
p.is_visible = cp.is_visible;
|
||||
p.alias = cp.alias;
|
||||
p.renamed_from = cp.renamed_from;
|
||||
p.filament_id = cp.filament_id;
|
||||
p.setting_id = cp.setting_id;
|
||||
p.description = cp.description;
|
||||
p.m_from_orca_filament_lib = cp.m_from_orca_filament_lib;
|
||||
p.m_excluded_from = cp.m_excluded_from;
|
||||
p.vendor = vp;
|
||||
if (is_filaments)
|
||||
coll.set_printer_hold_alias(p.alias, p);
|
||||
}
|
||||
};
|
||||
apply_col(col_print, prints, false);
|
||||
apply_col(col_filament, filaments, true);
|
||||
apply_col(col_printer, printers, false);
|
||||
apply_col(col_sla_print, sla_prints, false);
|
||||
apply_col(col_sla_material, sla_materials, false);
|
||||
|
||||
if (!config_maps.empty()) {
|
||||
m_config_maps = std::move(config_maps);
|
||||
m_filament_id_maps = std::move(filament_id_maps);
|
||||
auto apply = [&](std::vector<Preset>& cached, PresetCollection& coll, bool is_filaments) {
|
||||
for (Preset& cp : cached) {
|
||||
DynamicPrintConfig config = cp.config;
|
||||
Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), false, cp.version);
|
||||
p.is_system = true;
|
||||
p.is_visible = cp.is_visible;
|
||||
p.alias = cp.alias;
|
||||
p.renamed_from = cp.renamed_from;
|
||||
p.filament_id = cp.filament_id;
|
||||
p.setting_id = cp.setting_id;
|
||||
p.description = cp.description;
|
||||
p.m_from_orca_filament_lib = cp.m_from_orca_filament_lib;
|
||||
p.m_excluded_from = cp.m_excluded_from;
|
||||
p.vendor = vp;
|
||||
if (is_filaments)
|
||||
coll.set_printer_hold_alias(p.alias, p);
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCache: load failed (" << path << "): " << e.what();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
apply(grp.prints, prints_coll, false);
|
||||
apply(grp.filaments, filaments_coll, true);
|
||||
apply(grp.printers, printers_coll, false);
|
||||
apply(grp.sla_prints, sla_prints_coll, false);
|
||||
apply(grp.sla_materials, sla_materials_coll, false);
|
||||
}
|
||||
|
||||
bool PresetBundle::try_load_and_apply_vendor_cache(const std::string& vendor_id,
|
||||
const std::string& json_ver)
|
||||
bool PresetBundle::try_load_system_presets_from_cache(const std::string& expected_key)
|
||||
{
|
||||
if (load_and_apply_vendor_cache(vendor_cache_user_path(vendor_id), json_ver))
|
||||
auto try_path = [&](const std::string& path) -> bool {
|
||||
std::string blob;
|
||||
if (!read_cache_blob(path, blob))
|
||||
return false;
|
||||
try {
|
||||
std::istringstream iss(blob);
|
||||
cereal::BinaryInputArchive ar(iss);
|
||||
SystemPresetsCache cache;
|
||||
ar(cache);
|
||||
if (!cache.is_valid(expected_key))
|
||||
return false;
|
||||
|
||||
this->reset(false);
|
||||
vendors = std::move(cache.vendors);
|
||||
|
||||
for (auto& [vendor_id, grp] : cache.preset_groups) {
|
||||
auto it = vendors.find(vendor_id);
|
||||
if (it == vendors.end()) continue;
|
||||
apply_vendor_preset_group(grp, &it->second,
|
||||
prints, filaments, printers,
|
||||
sla_prints, sla_materials);
|
||||
}
|
||||
m_config_maps = std::move(cache.config_maps);
|
||||
m_filament_id_maps = std::move(cache.filament_id_maps);
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: apply failed (" << path << "): " << e.what();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (try_path(user_system_presets_cache_path()))
|
||||
return true;
|
||||
if (load_and_apply_vendor_cache(vendor_cache_bundled_path(vendor_id), json_ver)) {
|
||||
// Promote bundled cache to user cache so future loads skip this path.
|
||||
write_vendor_cache(vendor_cache_user_path(vendor_id), vendor_id, json_ver,
|
||||
vendor_id == ORCA_FILAMENT_LIBRARY);
|
||||
if (try_path(bundled_system_presets_cache_path())) {
|
||||
// Promote bundled cache to user path so future loads skip JSON parse.
|
||||
const std::string user_path = user_system_presets_cache_path();
|
||||
const std::string bundled_path = bundled_system_presets_cache_path();
|
||||
try {
|
||||
boost::filesystem::create_directories(
|
||||
boost::filesystem::path(user_path).parent_path());
|
||||
boost::filesystem::copy_file(bundled_path, user_path,
|
||||
boost::filesystem::copy_options::overwrite_existing);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: promote failed: " << e.what();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PresetBundle::VendorCacheStats PresetBundle::save_bundled_vendor_cache(
|
||||
const std::string& vendor_id, const std::string& json_path,
|
||||
bool capture_filament_maps, const std::string& output_path) const
|
||||
PresetBundle::SaveCacheResult PresetBundle::save_system_presets_cache(
|
||||
const std::string& profiles_dir, const std::string& output_path) const
|
||||
{
|
||||
const std::string json_ver = get_vendor_cache_key(json_path);
|
||||
write_vendor_cache(output_path, vendor_id, json_ver, capture_filament_maps);
|
||||
SystemPresetsCache cache;
|
||||
cache.config_options_count = static_cast<uint32_t>(print_config_def.options.size());
|
||||
cache.bundle_key = compute_system_presets_cache_key(profiles_dir);
|
||||
cache.vendors = vendors;
|
||||
cache.config_maps = m_config_maps;
|
||||
cache.filament_id_maps = m_filament_id_maps;
|
||||
|
||||
VendorCacheStats stats;
|
||||
auto count_col = [&](const PresetCollection& coll) -> size_t {
|
||||
size_t n = 0;
|
||||
auto fill = [&](const PresetCollection& coll,
|
||||
std::vector<Preset> VendorPresetGroup::*field) {
|
||||
for (const Preset& p : coll())
|
||||
if (p.is_system && p.vendor && p.vendor->id == vendor_id) ++n;
|
||||
return n;
|
||||
if (p.is_system && p.vendor)
|
||||
(cache.preset_groups[p.vendor->id].*field).push_back(p);
|
||||
};
|
||||
stats.print_presets = count_col(prints);
|
||||
stats.filament_presets = count_col(filaments);
|
||||
stats.printer_presets = count_col(printers);
|
||||
fill(prints, &VendorPresetGroup::prints);
|
||||
fill(filaments, &VendorPresetGroup::filaments);
|
||||
fill(sla_prints, &VendorPresetGroup::sla_prints);
|
||||
fill(sla_materials, &VendorPresetGroup::sla_materials);
|
||||
// PrinterPresetCollection is a PresetCollection subclass; iterate directly.
|
||||
for (const Preset& p : printers())
|
||||
if (p.is_system && p.vendor)
|
||||
cache.preset_groups[p.vendor->id].printers.push_back(p);
|
||||
|
||||
// Verify: read back header and check validity.
|
||||
if (std::string blob; read_cache_blob(output_path, blob)) {
|
||||
std::ostringstream oss;
|
||||
{ cereal::BinaryOutputArchive ar(oss); ar(cache); }
|
||||
const std::string blob = oss.str();
|
||||
write_cache_blob(output_path, blob);
|
||||
|
||||
SaveCacheResult result;
|
||||
// Verify: read back and check validity.
|
||||
if (std::string vblob; read_cache_blob(output_path, vblob)) {
|
||||
try {
|
||||
std::istringstream iss(blob);
|
||||
std::istringstream iss(vblob);
|
||||
cereal::BinaryInputArchive ar(iss);
|
||||
VendorCacheHeader hdr; ar(hdr);
|
||||
stats.ok = hdr.is_valid(json_ver);
|
||||
SystemPresetsCache v; ar(v);
|
||||
result.ok = v.is_valid(cache.bundle_key);
|
||||
} catch (...) {}
|
||||
}
|
||||
return stats;
|
||||
for (const auto& [vid, grp] : cache.preset_groups) {
|
||||
result.print_presets += grp.prints.size();
|
||||
result.filament_presets += grp.filaments.size();
|
||||
result.printer_presets += grp.printers.size();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// static
|
||||
bool PresetBundle::load_vendor_cache_for_guide(const std::string& cache_path,
|
||||
const std::string& json_ver,
|
||||
VendorProfile& out_profile,
|
||||
std::vector<Preset>& out_printer_presets,
|
||||
std::vector<Preset>& out_filament_presets,
|
||||
std::vector<Preset>& out_print_presets)
|
||||
bool PresetBundle::load_system_presets_cache_for_guide(const std::string& cache_path,
|
||||
PresetBundle& out_bundle)
|
||||
{
|
||||
std::string blob;
|
||||
if (!read_cache_blob(cache_path, blob))
|
||||
@@ -6155,19 +6135,25 @@ bool PresetBundle::load_vendor_cache_for_guide(const std::string& cache_path,
|
||||
try {
|
||||
std::istringstream iss(blob);
|
||||
cereal::BinaryInputArchive ar(iss);
|
||||
VendorCacheHeader hdr; ar(hdr);
|
||||
if (!hdr.is_valid(json_ver))
|
||||
SystemPresetsCache cache;
|
||||
ar(cache);
|
||||
if (cache.cache_version != SystemPresetsCache::CACHE_VERSION)
|
||||
return false;
|
||||
out_profile = std::move(hdr.profile);
|
||||
// Must read in serialization order: print, filament, printer, sla_print, sla_material.
|
||||
std::vector<Preset> col_sla_print, col_sla_material;
|
||||
std::map<std::string, DynamicPrintConfig> cm;
|
||||
std::map<std::string, std::string> fi;
|
||||
ar(out_print_presets, out_filament_presets, out_printer_presets, col_sla_print, col_sla_material);
|
||||
ar(cm, fi);
|
||||
return true;
|
||||
|
||||
out_bundle.vendors = std::move(cache.vendors);
|
||||
for (auto& [vendor_id, grp] : cache.preset_groups) {
|
||||
auto it = out_bundle.vendors.find(vendor_id);
|
||||
if (it == out_bundle.vendors.end()) continue;
|
||||
apply_vendor_preset_group(grp, &it->second,
|
||||
out_bundle.prints,
|
||||
out_bundle.filaments,
|
||||
out_bundle.printers,
|
||||
out_bundle.sla_prints,
|
||||
out_bundle.sla_materials);
|
||||
}
|
||||
return !out_bundle.vendors.empty();
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCache: guide load failed (" << cache_path << "): " << e.what();
|
||||
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: guide load failed (" << cache_path << "): " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,38 +152,31 @@ struct PresetBundleMetadata
|
||||
class PresetBundle
|
||||
{
|
||||
public:
|
||||
// ---- Per-vendor binary preset cache ------------------------------------
|
||||
// One file per vendor:
|
||||
// Bundled (CI-generated): resources/profiles/<vendor_id>.cache
|
||||
// User (runtime): data_dir/system/<vendor_id>.cache
|
||||
// ---- System presets single-bundle cache --------------------------------
|
||||
// All vendor profiles + all system presets in one file.
|
||||
// Bundled (CI): resources/profiles/system_presets.cache
|
||||
// User runtime: data_dir/system/system_presets.cache
|
||||
|
||||
struct VendorCacheStats {
|
||||
struct SaveCacheResult {
|
||||
bool ok = false;
|
||||
size_t print_presets = 0;
|
||||
size_t filament_presets = 0;
|
||||
size_t printer_presets = 0;
|
||||
};
|
||||
|
||||
static std::string vendor_cache_user_path(const std::string& vendor_id);
|
||||
static std::string vendor_cache_bundled_path(const std::string& vendor_id);
|
||||
static std::string bundled_system_presets_cache_path();
|
||||
static std::string user_system_presets_cache_path();
|
||||
|
||||
// Capture the given vendor from the loaded bundle, write to output_path,
|
||||
// and verify the result. Used by generate_system_cache and tests.
|
||||
// json_path is the vendor's .json profile file; its version/mtime is computed internally.
|
||||
VendorCacheStats save_bundled_vendor_cache(const std::string& vendor_id,
|
||||
const std::string& json_path,
|
||||
bool capture_filament_maps,
|
||||
const std::string& output_path) const;
|
||||
// Capture the currently-loaded PresetBundle and write a single-bundle cache
|
||||
// to output_path. profiles_dir contains the vendor JSON files (for the key).
|
||||
// Used by generate_system_cache and tests.
|
||||
SaveCacheResult save_system_presets_cache(const std::string& profiles_dir,
|
||||
const std::string& output_path) const;
|
||||
|
||||
// Load a vendor cache file and extract the data needed by the guide wizard.
|
||||
// json_ver must match the cache's stored version key (use get_vendor_cache_key()).
|
||||
// Returns false if the file is missing, stale, or corrupt.
|
||||
static bool load_vendor_cache_for_guide(const std::string& cache_path,
|
||||
const std::string& json_ver,
|
||||
VendorProfile& out_profile,
|
||||
std::vector<Preset>& out_printer_presets,
|
||||
std::vector<Preset>& out_filament_presets,
|
||||
std::vector<Preset>& out_print_presets);
|
||||
// Load a single-bundle cache into out_bundle for the guide wizard.
|
||||
// Returns false if the cache is missing, stale, or corrupt.
|
||||
static bool load_system_presets_cache_for_guide(const std::string& cache_path,
|
||||
PresetBundle& out_bundle);
|
||||
|
||||
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
|
||||
Preset &in_print_preset,
|
||||
@@ -527,42 +520,14 @@ public:
|
||||
bool check_preset_references() const;
|
||||
|
||||
private:
|
||||
// ---- VendorCache — header-only metadata struct --------------------------
|
||||
// Preset collections are NOT stored here; they are serialized directly from
|
||||
// PresetBundle's own prints/filaments/printers/… on save, and applied directly
|
||||
// into those collections on load. This avoids duplicating the preset vectors.
|
||||
struct VendorCacheHeader {
|
||||
static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
|
||||
static constexpr uint32_t CACHE_VERSION = 8;
|
||||
|
||||
uint32_t cache_version = CACHE_VERSION;
|
||||
uint32_t config_options_count = 0;
|
||||
std::string vendor_json_version;
|
||||
VendorProfile profile;
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar)
|
||||
{
|
||||
ar(cache_version, config_options_count, vendor_json_version, profile);
|
||||
}
|
||||
|
||||
bool is_valid(const std::string& current_vendor_json_version) const
|
||||
{
|
||||
return cache_version == CACHE_VERSION
|
||||
&& config_options_count == static_cast<uint32_t>(print_config_def.options.size())
|
||||
&& vendor_json_version == current_vendor_json_version;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true and applies the cache if a valid file exists for vendor_id.
|
||||
bool try_load_and_apply_vendor_cache(const std::string& vendor_id, const std::string& json_ver);
|
||||
// Read a cache file, verify it, and apply its presets directly into PresetBundle's collections.
|
||||
bool load_and_apply_vendor_cache(const std::string& path, const std::string& json_ver);
|
||||
// Write PresetBundle's presets for vendor_id directly to a cache file — no intermediate copy.
|
||||
void write_vendor_cache(const std::string& path, const std::string& vendor_id,
|
||||
const std::string& json_ver, bool capture_filament_maps) const;
|
||||
// Read raw blob bytes (file I/O + magic/CRC check) shared by all cache readers.
|
||||
// Compute combined invalidation key from all vendor JSON files in system_dir.
|
||||
static std::string compute_system_presets_cache_key(const std::string& system_dir);
|
||||
// Try user cache then bundled cache; apply all presets on hit.
|
||||
bool try_load_system_presets_from_cache(const std::string& expected_key);
|
||||
// Read raw cache blob: verify magic, size, CRC.
|
||||
static bool read_cache_blob(const std::string& path, std::string& out_blob);
|
||||
// Write a cache blob with the standard 20-byte file header.
|
||||
static void write_cache_blob(const std::string& path, const std::string& blob);
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
|
||||
@@ -1397,42 +1397,21 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
|
||||
}
|
||||
|
||||
// Builds guide profile JSON from the per-vendor bundled caches
|
||||
// (resources/profiles/<vendor>.cache, generated by CI).
|
||||
// (resources/profiles/system_presets.cache, generated by CI).
|
||||
// This avoids the 90-second LoadProfileFamily fallback on first launch.
|
||||
bool GuideFrame::BuildProfileDataFromBundledCache()
|
||||
{
|
||||
// Enumerate per-vendor .cache files in resources/profiles/.
|
||||
std::vector<boost::filesystem::path> cache_files;
|
||||
try {
|
||||
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) {
|
||||
if (e.path().extension() == ".cache")
|
||||
cache_files.push_back(e.path());
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache: cannot scan " << rsrc_vendor_dir << ": " << ex.what();
|
||||
return false;
|
||||
}
|
||||
if (cache_files.empty())
|
||||
const std::string cache_path = PresetBundle::bundled_system_presets_cache_path();
|
||||
if (!boost::filesystem::exists(cache_path))
|
||||
return false;
|
||||
|
||||
try {
|
||||
for (const auto& cache_path : cache_files) {
|
||||
const std::string vendor_id = cache_path.stem().string();
|
||||
// Validate against the version in the corresponding vendor JSON.
|
||||
const boost::filesystem::path json_path = rsrc_vendor_dir / (vendor_id + ".json");
|
||||
const std::string ver_str = get_vendor_cache_key(json_path.string());
|
||||
PresetBundle bundle;
|
||||
if (!PresetBundle::load_system_presets_cache_for_guide(cache_path, bundle))
|
||||
return false;
|
||||
|
||||
VendorProfile vc_profile;
|
||||
std::vector<Preset> vc_printer_presets;
|
||||
std::vector<Preset> vc_filament_presets;
|
||||
std::vector<Preset> vc_print_presets;
|
||||
if (!PresetBundle::load_vendor_cache_for_guide(cache_path.string(), ver_str,
|
||||
vc_profile, vc_printer_presets,
|
||||
vc_filament_presets, vc_print_presets))
|
||||
continue;
|
||||
|
||||
// Models from this vendor's cached profile
|
||||
for (const auto& cm : vc_profile.models) {
|
||||
for (const auto& [vendor_id, vp] : bundle.vendors) {
|
||||
for (const auto& cm : vp.models) {
|
||||
std::string nozzle_str;
|
||||
for (const auto& v : cm.variants) {
|
||||
if (!nozzle_str.empty()) nozzle_str += ";";
|
||||
@@ -1444,7 +1423,7 @@ bool GuideFrame::BuildProfileDataFromBundledCache()
|
||||
materials_str += m;
|
||||
}
|
||||
boost::filesystem::path cover_path =
|
||||
(boost::filesystem::path(resources_dir()) / "profiles" / vc_profile.id / (cm.id + "_cover.png"))
|
||||
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (cm.id + "_cover.png"))
|
||||
.make_preferred();
|
||||
if (!boost::filesystem::exists(cover_path))
|
||||
cover_path =
|
||||
@@ -1454,7 +1433,7 @@ bool GuideFrame::BuildProfileDataFromBundledCache()
|
||||
json entry;
|
||||
entry["model"] = cm.id;
|
||||
entry["name"] = cm.name;
|
||||
entry["vendor"] = vc_profile.id;
|
||||
entry["vendor"] = vp.id;
|
||||
entry["nozzle_diameter"] = nozzle_str;
|
||||
entry["materials"] = materials_str;
|
||||
entry["cover"] = cover_path.string();
|
||||
@@ -1462,60 +1441,59 @@ bool GuideFrame::BuildProfileDataFromBundledCache()
|
||||
entry["sub_path"] = "";
|
||||
m_ProfileJson["model"].push_back(entry);
|
||||
}
|
||||
|
||||
// Machines from cached printer presets
|
||||
for (const auto& cp : vc_printer_presets) {
|
||||
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
|
||||
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
|
||||
if (!pm || pm->value.empty() || !pv) continue;
|
||||
|
||||
json mach;
|
||||
mach["model"] = pm->value;
|
||||
mach["nozzle"] = pv->value;
|
||||
m_ProfileJson["machine"][cp.name] = mach;
|
||||
}
|
||||
|
||||
// Filaments from cached filament presets
|
||||
for (const auto& cp : vc_filament_presets) {
|
||||
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
|
||||
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
|
||||
const auto* compat = cp.config.option<ConfigOptionStrings>("compatible_printers");
|
||||
|
||||
std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : "";
|
||||
std::string type = (ft && !ft->values.empty()) ? ft->values[0] : "";
|
||||
|
||||
std::string model_list;
|
||||
if (compat) {
|
||||
for (const std::string& pname : compat->values) {
|
||||
if (m_ProfileJson["machine"].contains(pname)) {
|
||||
std::string m = m_ProfileJson["machine"][pname]["model"];
|
||||
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
|
||||
model_list += "[" + m + "++" + n + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json ff;
|
||||
ff["name"] = cp.name;
|
||||
ff["sub_path"] = cp.file;
|
||||
ff["vendor"] = vendor;
|
||||
ff["type"] = type;
|
||||
ff["models"] = model_list;
|
||||
ff["selected"] = 0;
|
||||
m_ProfileJson["filament"][cp.name] = ff;
|
||||
}
|
||||
|
||||
// Process from cached print presets
|
||||
for (const auto& cp : vc_print_presets) {
|
||||
if (!cp.is_visible) continue;
|
||||
json entry;
|
||||
entry["name"] = cp.name;
|
||||
entry["sub_path"] = cp.file;
|
||||
m_ProfileJson["process"].push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled per-vendor caches ("
|
||||
for (const Preset& cp : bundle.printers()) {
|
||||
if (!cp.is_system || !cp.vendor) continue;
|
||||
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
|
||||
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
|
||||
if (!pm || pm->value.empty() || !pv) continue;
|
||||
|
||||
json mach;
|
||||
mach["model"] = pm->value;
|
||||
mach["nozzle"] = pv->value;
|
||||
m_ProfileJson["machine"][cp.name] = mach;
|
||||
}
|
||||
|
||||
for (const Preset& cp : bundle.filaments()) {
|
||||
if (!cp.is_system || !cp.vendor) continue;
|
||||
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
|
||||
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
|
||||
const auto* compat = cp.config.option<ConfigOptionStrings>("compatible_printers");
|
||||
|
||||
std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : "";
|
||||
std::string type = (ft && !ft->values.empty()) ? ft->values[0] : "";
|
||||
|
||||
std::string model_list;
|
||||
if (compat) {
|
||||
for (const std::string& pname : compat->values) {
|
||||
if (m_ProfileJson["machine"].contains(pname)) {
|
||||
std::string m = m_ProfileJson["machine"][pname]["model"];
|
||||
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
|
||||
model_list += "[" + m + "++" + n + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json ff;
|
||||
ff["name"] = cp.name;
|
||||
ff["sub_path"] = cp.file;
|
||||
ff["vendor"] = vendor;
|
||||
ff["type"] = type;
|
||||
ff["models"] = model_list;
|
||||
ff["selected"] = 0;
|
||||
m_ProfileJson["filament"][cp.name] = ff;
|
||||
}
|
||||
|
||||
for (const Preset& cp : bundle.prints()) {
|
||||
if (!cp.is_system || !cp.vendor || !cp.is_visible) continue;
|
||||
json entry;
|
||||
entry["name"] = cp.name;
|
||||
entry["sub_path"] = cp.file;
|
||||
m_ProfileJson["process"].push_back(entry);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from single-bundle cache ("
|
||||
<< m_ProfileJson["model"].size() << " models, "
|
||||
<< m_ProfileJson["machine"].size() << " machines, "
|
||||
<< m_ProfileJson["filament"].size() << " filaments)";
|
||||
|
||||
@@ -23,8 +23,6 @@ struct TempDir {
|
||||
~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); }
|
||||
};
|
||||
|
||||
// Write a minimal vendor JSON with a version field so get_vendor_cache_key() returns
|
||||
// a deterministic Semver string rather than an mtime-based key.
|
||||
std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id,
|
||||
const std::string& version = "1.0.0")
|
||||
{
|
||||
@@ -34,8 +32,6 @@ std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id,
|
||||
return p.string();
|
||||
}
|
||||
|
||||
// Write a vendor JSON without a "version" field — get_vendor_cache_key() will return
|
||||
// "mtime:<timestamp>" instead of a Semver string.
|
||||
std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id)
|
||||
{
|
||||
const fs::path p = dir / (vendor_id + ".json");
|
||||
@@ -44,33 +40,39 @@ std::string write_versionless_vendor_json(const fs::path& dir, const std::string
|
||||
return p.string();
|
||||
}
|
||||
|
||||
// Binary-patch the config_options_count field in a cache file (blob offset 4) and
|
||||
// recompute the CRC so the file passes the magic/size/CRC checks but fails is_valid().
|
||||
// File layout: [20-byte CacheFileHeader][blob]; in blob: uint32 cache_version @ 0, uint32 options_count @ 4.
|
||||
void patch_cache_options_count(const std::string& path, uint32_t wrong_count)
|
||||
void corrupt_blob_byte(const std::string& path)
|
||||
{
|
||||
std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
|
||||
f.seekp(30);
|
||||
char b = 0; f.read(&b, 1);
|
||||
f.seekp(30);
|
||||
b ^= 0xFF;
|
||||
f.write(&b, 1);
|
||||
}
|
||||
|
||||
// Patch cache_version (blob[0..3]) and recompute CRC so the file passes
|
||||
// the CRC check but fails the cache_version check in load_system_presets_cache_for_guide.
|
||||
void patch_cache_version(const std::string& path, uint32_t wrong_version)
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::vector<char> data(std::istreambuf_iterator<char>(in), {});
|
||||
in.close();
|
||||
if (data.size() < 28) return; // 20-byte header + 4 (cache_version) + 4 (options_count)
|
||||
|
||||
std::memcpy(&data[24], &wrong_count, 4); // file[24..27] = blob[4..7] = options_count
|
||||
|
||||
// Recompute CRC over the modified blob (everything after the 20-byte file header).
|
||||
if (data.size() < 24) return;
|
||||
std::memcpy(&data[20], &wrong_version, 4);
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(&data[20], data.size() - 20);
|
||||
const uint32_t new_crc = crc.checksum();
|
||||
std::memcpy(&data[16], &new_crc, 4); // file[16..19] = crc32 field in CacheFileHeader
|
||||
|
||||
std::memcpy(&data[16], &new_crc, 4);
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
||||
}
|
||||
|
||||
void add_vendor(PresetBundle& bundle, const std::string& vendor_id)
|
||||
void add_vendor(PresetBundle& bundle, const std::string& vendor_id,
|
||||
const std::string& name = "", Semver ver = Semver(1, 0, 0))
|
||||
{
|
||||
VendorProfile vp(vendor_id);
|
||||
vp.name = vendor_id + " Corp";
|
||||
vp.config_version = Semver(1, 0, 0);
|
||||
vp.name = name.empty() ? vendor_id + " Corp" : name;
|
||||
vp.config_version = ver;
|
||||
bundle.vendors.emplace(vendor_id, vp);
|
||||
}
|
||||
|
||||
@@ -83,14 +85,31 @@ Preset& add_system_preset(PresetCollection& coll, const std::string& name,
|
||||
return p;
|
||||
}
|
||||
|
||||
PresetBundle::SaveCacheResult save_one_vendor(PresetBundle& src,
|
||||
const fs::path& profiles_dir,
|
||||
const fs::path& cache_path)
|
||||
{
|
||||
return src.save_system_presets_cache(profiles_dir.string(), cache_path.string());
|
||||
}
|
||||
|
||||
// Helper: filter a collection by vendor_id.
|
||||
std::vector<const Preset*> presets_for(const PresetCollection& coll, const std::string& vendor_id)
|
||||
{
|
||||
std::vector<const Preset*> out;
|
||||
for (const Preset& p : coll())
|
||||
if (p.is_system && p.vendor && p.vendor->id == vendor_id)
|
||||
out.push_back(&p);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("VendorCache: save and load via guide API", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: save and load via guide API", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
@@ -99,91 +118,63 @@ TEST_CASE("VendorCache: save and load via guide API", "[VendorCache]")
|
||||
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp);
|
||||
fp.alias = "Acme PLA";
|
||||
fp.filament_id = "GFL_acme_pla";
|
||||
|
||||
add_system_preset(src.printers, vid + " Printer 0.4", vp);
|
||||
|
||||
const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
const auto stats = save_one_vendor(src, tmp.path, cache);
|
||||
REQUIRE(stats.ok);
|
||||
CHECK(stats.filament_presets == 1);
|
||||
CHECK(stats.printer_presets == 1);
|
||||
CHECK(stats.print_presets == 0);
|
||||
|
||||
VendorProfile out_profile;
|
||||
std::vector<Preset> out_printers, out_filaments, out_prints;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path),
|
||||
out_profile, out_printers, out_filaments, out_prints));
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
REQUIRE(out.vendors.count(vid) == 1);
|
||||
|
||||
CHECK(out_profile.id == vid);
|
||||
REQUIRE(out_filaments.size() == 1);
|
||||
CHECK(out_filaments[0].name == vid + " PLA @0.4");
|
||||
CHECK(out_filaments[0].alias == "Acme PLA");
|
||||
CHECK(out_filaments[0].filament_id == "GFL_acme_pla");
|
||||
REQUIRE(out_printers.size() == 1);
|
||||
CHECK(out_printers[0].name == vid + " Printer 0.4");
|
||||
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("VendorCache: stale json_ver is rejected", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid, "1.0.0");
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(cache.string(), "2.0.0", p, pr, fi, pp));
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: missing file returns false", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: missing file returns false", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
(tmp.path / "nonexistent.cache").string(), "1.0.0", p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(
|
||||
(tmp.path / "nonexistent.cache").string(), out));
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: corrupt data is rejected by CRC check", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: corrupt data rejected by CRC check", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
corrupt_blob_byte(cache.string());
|
||||
|
||||
// Flip a byte in the data region (after the 20-byte CacheFileHeader).
|
||||
{
|
||||
std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary);
|
||||
f.seekp(30);
|
||||
char b; f.read(&b, 1);
|
||||
f.seekp(30);
|
||||
b ^= 0xFF;
|
||||
f.write(&b, 1);
|
||||
}
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: multiple vendors do not bleed into each other", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: multiple vendors in one bundle are isolated", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid1 = "VendorA";
|
||||
const std::string vid2 = "VendorB";
|
||||
write_vendor_json(tmp.path, vid1);
|
||||
write_vendor_json(tmp.path, vid2);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
const std::string json1 = write_vendor_json(tmp.path, vid1);
|
||||
const std::string json2 = write_vendor_json(tmp.path, vid2);
|
||||
|
||||
for (const auto& vid : {vid1, vid2}) {
|
||||
add_vendor(src, vid);
|
||||
const VendorProfile* vp = &src.vendors.at(vid);
|
||||
@@ -191,27 +182,29 @@ TEST_CASE("VendorCache: multiple vendors do not bleed into each other", "[Vendor
|
||||
add_system_preset(src.printers, vid + " Printer", vp);
|
||||
}
|
||||
|
||||
src.save_bundled_vendor_cache(vid1, json1, false, (tmp.path / (vid1 + ".cache")).string());
|
||||
src.save_bundled_vendor_cache(vid2, json2, false, (tmp.path / (vid2 + ".cache")).string());
|
||||
REQUIRE(save_one_vendor(src, tmp.path, cache).ok);
|
||||
|
||||
for (const auto& [vid, jpath] : std::vector<std::pair<std::string, std::string>>{{vid1, json1}, {vid2, json2}}) {
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
(tmp.path / (vid + ".cache")).string(), get_vendor_cache_key(jpath),
|
||||
p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
REQUIRE(out.vendors.count(vid1) == 1);
|
||||
REQUIRE(out.vendors.count(vid2) == 1);
|
||||
|
||||
for (const auto& vid : {vid1, vid2}) {
|
||||
auto fi = presets_for(out.filaments, vid);
|
||||
auto pr = presets_for(out.printers, vid);
|
||||
REQUIRE(fi.size() == 1);
|
||||
CHECK(fi[0].name == vid + " PLA");
|
||||
CHECK(fi[0]->name == vid + " PLA");
|
||||
REQUIRE(pr.size() == 1);
|
||||
CHECK(pr[0].name == vid + " Printer");
|
||||
CHECK(pr[0]->name == vid + " Printer");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: vendor profile fields are preserved", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: vendor profile fields are preserved", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid, "2.5.1");
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid, "2.5.1");
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
VendorProfile vp(vid);
|
||||
@@ -224,55 +217,51 @@ TEST_CASE("VendorCache: vendor profile fields are preserved", "[VendorCache]")
|
||||
model.variants.push_back(v0_4);
|
||||
vp.models.push_back(model);
|
||||
src.vendors.emplace(vid, vp);
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
|
||||
VendorProfile out_p; std::vector<Preset> pr, fi, pp;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), out_p, pr, fi, pp));
|
||||
|
||||
CHECK(out_p.id == vid);
|
||||
CHECK(out_p.name == "Acme Corporation");
|
||||
REQUIRE(out_p.models.size() == 1);
|
||||
CHECK(out_p.models[0].id == "AcmePro");
|
||||
CHECK(out_p.models[0].name == "Acme Pro");
|
||||
REQUIRE(out_p.models[0].variants.size() == 1);
|
||||
CHECK(out_p.models[0].variants[0].name == "0.4");
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
REQUIRE(out.vendors.count(vid) == 1);
|
||||
const VendorProfile& gvp = out.vendors.at(vid);
|
||||
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("VendorCache: config option values are preserved", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: config option values are preserved", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
const VendorProfile* vp = &src.vendors.at(vid);
|
||||
|
||||
Preset& fp = add_system_preset(src.filaments, vid + " PETG @0.4", vp);
|
||||
Preset& fp = add_system_preset(src.filaments, vid + " PETG @0.4", &src.vendors.at(vid));
|
||||
fp.config.set_key_value("filament_type", new ConfigOptionStrings({"PETG"}));
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
|
||||
auto fi = presets_for(out.filaments, vid);
|
||||
REQUIRE(fi.size() == 1);
|
||||
const auto* ft = fi[0].config.option<ConfigOptionStrings>("filament_type");
|
||||
const auto* ft = fi[0]->config.option<ConfigOptionStrings>("filament_type");
|
||||
REQUIRE(ft != nullptr);
|
||||
REQUIRE(ft->values.size() >= 1);
|
||||
CHECK(ft->values[0] == "PETG");
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: multiple presets per collection round-trip", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: multiple presets per collection round-trip", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
@@ -280,75 +269,70 @@ TEST_CASE("VendorCache: multiple presets per collection round-trip", "[VendorCac
|
||||
|
||||
const std::vector<std::string> fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"};
|
||||
const std::vector<std::string> pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"};
|
||||
|
||||
for (const auto& n : fi_names) add_system_preset(src.filaments, n, vp);
|
||||
for (const auto& n : pr_names) add_system_preset(src.printers, n, vp);
|
||||
|
||||
const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
const auto stats = save_one_vendor(src, tmp.path, cache);
|
||||
REQUIRE(stats.ok);
|
||||
CHECK(stats.filament_presets == 3);
|
||||
CHECK(stats.printer_presets == 2);
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
|
||||
auto fi = presets_for(out.filaments, vid);
|
||||
auto pr = presets_for(out.printers, vid);
|
||||
REQUIRE(fi.size() == 3);
|
||||
REQUIRE(pr.size() == 2);
|
||||
|
||||
std::set<std::string> fi_got, pr_got;
|
||||
for (const auto& x : fi) fi_got.insert(x.name);
|
||||
for (const auto& x : pr) pr_got.insert(x.name);
|
||||
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("VendorCache: truncated file is rejected", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: truncated file is rejected", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const fs::path cache = tmp.path / "truncated.cache";
|
||||
|
||||
// 3 bytes — shorter than the 20-byte CacheFileHeader
|
||||
{
|
||||
std::ofstream f(cache.string(), std::ios::binary);
|
||||
const char data[] = {0x4F, 0x52, 0x43};
|
||||
f.write(data, sizeof(data));
|
||||
}
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), "1.0.0", p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: wrong magic is rejected", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: wrong magic is rejected", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
|
||||
// Overwrite first 4 bytes (magic field) with a wrong value; rest of file stays valid.
|
||||
{
|
||||
std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary);
|
||||
const uint32_t bad = 0xDEADBEEFu;
|
||||
f.write(reinterpret_cast<const char*>(&bad), sizeof(bad));
|
||||
}
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
}
|
||||
|
||||
TEST_CASE("VendorCache: vendor with no presets saves and loads cleanly", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: vendor with no presets saves and loads cleanly", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
VendorProfile vp(vid);
|
||||
@@ -356,144 +340,91 @@ TEST_CASE("VendorCache: vendor with no presets saves and loads cleanly", "[Vendo
|
||||
vp.config_version = Semver(1, 0, 0);
|
||||
src.vendors.emplace(vid, vp);
|
||||
|
||||
const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
const auto stats = save_one_vendor(src, tmp.path, cache);
|
||||
REQUIRE(stats.ok);
|
||||
CHECK(stats.print_presets == 0);
|
||||
CHECK(stats.filament_presets == 0);
|
||||
CHECK(stats.printer_presets == 0);
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
CHECK(p.id == vid);
|
||||
CHECK(p.name == "Acme Corporation");
|
||||
CHECK(fi.empty());
|
||||
CHECK(pr.empty());
|
||||
CHECK(pp.empty());
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
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("VendorCache: all Preset metadata fields are preserved", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: all Preset metadata fields are preserved", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
const VendorProfile* vp = &src.vendors.at(vid);
|
||||
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", &src.vendors.at(vid));
|
||||
fp.setting_id = "sid-test-001";
|
||||
fp.description = "A test filament preset";
|
||||
fp.bundle_id = "bundle-xyz";
|
||||
fp.user_id = "user-abc";
|
||||
fp.base_id = "base-123";
|
||||
fp.sync_info = "update";
|
||||
fp.updated_time = 1700000000LL;
|
||||
fp.key_values = {{"color", "red"}, {"diameter", "1.75"}};
|
||||
fp.ini_str = "[filament]\nnozzle_temperature = 230\n";
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
|
||||
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp);
|
||||
fp.setting_id = "sid-test-001";
|
||||
fp.description = "A test filament preset";
|
||||
fp.bundle_id = "bundle-xyz";
|
||||
fp.user_id = "user-abc";
|
||||
fp.base_id = "base-123";
|
||||
fp.sync_info = "update";
|
||||
fp.updated_time = 1700000000LL;
|
||||
fp.key_values = {{"color", "red"}, {"diameter", "1.75"}};
|
||||
fp.ini_str = "[filament]\nnozzle_temperature = 230\n";
|
||||
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
|
||||
auto fi = presets_for(out.filaments, vid);
|
||||
REQUIRE(fi.size() == 1);
|
||||
const Preset& out = fi[0];
|
||||
CHECK(out.setting_id == "sid-test-001");
|
||||
CHECK(out.description == "A test filament preset");
|
||||
CHECK(out.bundle_id == "bundle-xyz");
|
||||
CHECK(out.user_id == "user-abc");
|
||||
CHECK(out.base_id == "base-123");
|
||||
CHECK(out.sync_info == "update");
|
||||
CHECK(out.updated_time == 1700000000LL);
|
||||
REQUIRE(out.key_values.count("color") == 1);
|
||||
CHECK(out.key_values.at("color") == "red");
|
||||
REQUIRE(out.key_values.count("diameter") == 1);
|
||||
CHECK(out.key_values.at("diameter") == "1.75");
|
||||
CHECK(out.ini_str == "[filament]\nnozzle_temperature = 230\n");
|
||||
CHECK(fi[0]->setting_id == "sid-test-001");
|
||||
CHECK(fi[0]->description == "A test filament preset");
|
||||
CHECK(fi[0]->bundle_id == "bundle-xyz");
|
||||
CHECK(fi[0]->user_id == "user-abc");
|
||||
CHECK(fi[0]->base_id == "base-123");
|
||||
CHECK(fi[0]->sync_info == "update");
|
||||
CHECK(fi[0]->updated_time == 1700000000LL);
|
||||
REQUIRE(fi[0]->key_values.count("color") == 1);
|
||||
CHECK(fi[0]->key_values.at("color") == "red");
|
||||
REQUIRE(fi[0]->key_values.count("diameter") == 1);
|
||||
CHECK(fi[0]->key_values.at("diameter") == "1.75");
|
||||
CHECK(fi[0]->ini_str == "[filament]\nnozzle_temperature = 230\n");
|
||||
}
|
||||
|
||||
// TC09 — versionless JSON uses mtime as the cache key; same mtime → cache valid.
|
||||
// TC10 — after the JSON file is touched (mtime changes), the old key no longer matches.
|
||||
TEST_CASE("VendorCache: versionless vendor uses mtime key", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: wrong cache_version is rejected", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_versionless_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
patch_cache_version(cache.string(), 0xFFFFFFFFu);
|
||||
|
||||
const std::string key_before = get_vendor_cache_key(json_path);
|
||||
REQUIRE_FALSE(key_before.empty());
|
||||
// Must be mtime-based, not a Semver.
|
||||
REQUIRE(key_before.substr(0, 6) == "mtime:");
|
||||
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
|
||||
SECTION("same mtime → cache is valid") {
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
CHECK(fi.size() == 1);
|
||||
}
|
||||
|
||||
SECTION("touched file changes mtime → cache is invalid") {
|
||||
// Advance mtime by 2 seconds so the key definitely differs.
|
||||
const std::time_t old_mtime = fs::last_write_time(json_path);
|
||||
fs::last_write_time(json_path, old_mtime + 2);
|
||||
|
||||
const std::string key_after = get_vendor_cache_key(json_path);
|
||||
REQUIRE(key_after != key_before);
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), key_after, p, pr, fi, pp));
|
||||
}
|
||||
PresetBundle out;
|
||||
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
}
|
||||
|
||||
// TC07 — config_options_count in the cache header must equal the live print_config_def
|
||||
// option count. A patched value (1) is rejected by is_valid() even if the CRC is correct.
|
||||
TEST_CASE("VendorCache: config_options_count mismatch is rejected", "[VendorCache]")
|
||||
TEST_CASE("SystemPresetsCache: mid-blob truncation is rejected", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
const std::string vid = "Acme";
|
||||
write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
save_one_vendor(src, tmp.path, cache);
|
||||
|
||||
// Patch config_options_count to 1 and fix CRC — file is structurally valid but semantically stale.
|
||||
patch_cache_options_count(cache.string(), 1u);
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
}
|
||||
|
||||
// TC12 variant — header is intact but the blob is cut short; the blob read fails.
|
||||
TEST_CASE("VendorCache: mid-blob truncation is rejected", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
const std::string json_path = write_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / (vid + ".cache");
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
|
||||
|
||||
// Keep the full 20-byte CacheFileHeader intact; truncate the blob to 10 bytes.
|
||||
// data_size in the header still says the full blob length, so the read will fail.
|
||||
{
|
||||
std::ifstream in(cache.string(), std::ios::binary);
|
||||
std::vector<char> buf(30); // 20-byte header + 10 bytes of blob
|
||||
@@ -503,7 +434,25 @@ TEST_CASE("VendorCache: mid-blob truncation is rejected", "[VendorCache]")
|
||||
out.write(buf.data(), 30);
|
||||
}
|
||||
|
||||
VendorProfile p; std::vector<Preset> pr, fi, pp;
|
||||
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
|
||||
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
|
||||
PresetBundle out;
|
||||
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
}
|
||||
|
||||
TEST_CASE("SystemPresetsCache: versionless vendor uses mtime key in bundle", "[VendorCache]")
|
||||
{
|
||||
TempDir tmp;
|
||||
const std::string vid = "Acme";
|
||||
write_versionless_vendor_json(tmp.path, vid);
|
||||
const fs::path cache = tmp.path / "system_presets.cache";
|
||||
|
||||
PresetBundle src;
|
||||
add_vendor(src, vid);
|
||||
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
|
||||
REQUIRE(save_one_vendor(src, tmp.path, cache).ok);
|
||||
|
||||
PresetBundle out;
|
||||
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
|
||||
auto fi = presets_for(out.filaments, vid);
|
||||
REQUIRE(fi.size() == 1);
|
||||
CHECK(fi[0]->name == vid + " PLA");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user