mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-02 13:20:59 +00:00
perf: load presets in parallel, cutting preset load time by over 60% (#15943)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
co-authored by
Rodrigo Faselli
parent
2769b12ce7
commit
f5679ad343
+45
-19
@@ -28,8 +28,8 @@ Per-vendor granularity is what makes the system practical:
|
||||
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
|
||||
vendors keep theirs — even when the bumped vendor is the shared Orca filament
|
||||
library everyone else inherits from.
|
||||
- The setup wizard, which loads vendors one at a time, gets the same speedup as
|
||||
startup without a second code path.
|
||||
- The setup wizard loads its vendors through the same routine as startup, so it
|
||||
gets the same speedup without a second code path.
|
||||
- A vendor with no cache, or a broken one, costs only that vendor a parse.
|
||||
|
||||
A cache holds *system* presets only. User presets, project settings and modified
|
||||
@@ -162,15 +162,20 @@ cache nothing can invalidate is worse than no cache.
|
||||
|
||||
Vendors load in a fixed order, because filament inheritance crosses exactly one
|
||||
boundary: any vendor's filament may inherit from the shared Orca filament library,
|
||||
and nothing else reaches across vendors — an `include` is always vendor-local. The
|
||||
library therefore goes first, alone; every other vendor follows in parallel, resolving
|
||||
against it; and the results are merged in a stable order:
|
||||
and nothing else reaches across vendors — an `include` is always vendor-local. Only
|
||||
installing a vendor's presets crosses it; reading the vendor, from its cache or its
|
||||
JSONs, needs nothing from the library. So every other vendor is read while the library
|
||||
loads, each is installed against it as soon as both are done, and the results are
|
||||
merged in a stable order:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"]
|
||||
lib["1 · OrcaFilamentLibrary loaded;<br/>meanwhile every other vendor read<br/>from its cache or its JSONs"] --> par["2 · every other vendor installed<br/>in parallel, each into its own bundle,<br/>filaments resolving against the library"] --> merge["3 · bundles merged into one,<br/>in one pass per collection,<br/>in stable vendor order"]
|
||||
```
|
||||
|
||||
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
|
||||
which hand it the vendors to load and the directory each is installed in.
|
||||
|
||||
Whether a vendor comes from its cache or from a parse changes nothing in that
|
||||
order — both produce the same bundle, so cached and parsed vendors mix freely in
|
||||
one startup.
|
||||
@@ -211,11 +216,12 @@ shipped cache answered first, so the profile in `<data_dir>/system/` was never p
|
||||
and its cache was never written back.
|
||||
|
||||
Serving from a cache is not a memory-image restore. The entries are deserialized and
|
||||
then installed one by one — inheritance resolved against the presets installed before
|
||||
them and the currently loaded filament library, includes layered in, configs flattened
|
||||
onto the collection defaults, validated and registered — by the same function the JSON
|
||||
path calls straight after parsing a sub-file. An `include` layers what the included
|
||||
base states, between the parent and the preset's own keys: the base's diff against the
|
||||
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
|
||||
to once it has parsed the sub-files: inheritance resolved against the presets installed
|
||||
before them and the currently loaded filament library, includes layered in, configs
|
||||
flattened onto the collection defaults, validated and registered. An `include` layers
|
||||
what the included base states, between the parent and the preset's own keys: the base's
|
||||
diff against the
|
||||
default, taken when the base itself was installed and before the per-variant padding
|
||||
`inherits` sees, so only what a template sets reaches the presets including it. The two
|
||||
paths share everything below the parse, which is what makes a cache-loaded bundle
|
||||
@@ -223,6 +229,16 @@ indistinguishable from a JSON-loaded one by construction rather than by test cov
|
||||
Installation also rebuilds each preset's file path from the local data directory, so a
|
||||
shipped cache never carries the generating machine's paths.
|
||||
|
||||
Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only
|
||||
what is registered under the names it inherits and includes, and `commit_vendor_preset`
|
||||
registers it, the only step that writes anything shared. Entries resolve across threads
|
||||
in runs and commit in the order the vendor lists them. A run ends before an entry that
|
||||
inherits or includes one already in it, since that one's commit registers what the
|
||||
entry resolves against, so no entry in a run reads what another in it registers. An
|
||||
entry's parse messages are held until it commits. The bundle, the log's parse and
|
||||
install messages and the error count therefore come out as parsing and installing one
|
||||
entry at a time would leave them, whatever the listing order.
|
||||
|
||||
App upgrades work because a cache normally survives one. Only a deliberate
|
||||
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
|
||||
install time rather than at load: a vendor whose cache this build cannot read counts
|
||||
@@ -254,17 +270,20 @@ the wizard caches the *derived JSON*, not another form of the inputs:
|
||||
open, the wizard computes the current stamps (one version peek per vendor) and, when
|
||||
they match, serves the catalog from the file — no bundle built, no preset installed.
|
||||
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
|
||||
per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
|
||||
skipping the rebuild entirely wins.
|
||||
per-vendor caches costs over a second of preset installation whatever feeds it, so
|
||||
only skipping the rebuild entirely wins.
|
||||
|
||||
Any change to the set — a vendor added, removed or updated, or its cache-only
|
||||
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
|
||||
the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
|
||||
they cover) and writes the catalog back. Selections, region and per-open decorations
|
||||
are applied downstream of the cache either way, so a served catalog is
|
||||
indistinguishable from a rebuilt one. Nothing ships this file and the updater never
|
||||
touches it; it is a locally written artifact, re-derived whenever stale, written
|
||||
through a temp file and rename so half a cache is never readable.
|
||||
the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
|
||||
startup uses (per-vendor caches serving where they cover), and writes the catalog
|
||||
back. When a vendor fails to load, the filament library included, that open falls
|
||||
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
|
||||
and writes nothing. Selections, region and per-open decorations are applied
|
||||
downstream of the cache either way, so a served catalog is indistinguishable from a
|
||||
rebuilt one. Nothing ships this file and the updater never touches it; it is a
|
||||
locally written artifact, re-derived whenever stale, written through a temp file and
|
||||
rename so half a cache is never readable.
|
||||
|
||||
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
|
||||
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
|
||||
@@ -378,6 +397,13 @@ enumerates only `*.json` will find no vendors at all in a packaged build.
|
||||
the `CachedPreset` field list — written and read by `visit_entry` in
|
||||
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
|
||||
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
|
||||
- **Adding a kind of reference between presets**, as `inherits` and `include` are:
|
||||
parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped),
|
||||
have `install_vendor_entries` end a run before an entry that names one already in it
|
||||
and retain what the names point at, look them up only in `resolve_vendor_preset`, and
|
||||
register what they point at only in `commit_vendor_preset`. The listing-order test in
|
||||
`test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses
|
||||
it.
|
||||
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
|
||||
65535 options and one cache at most 65535 distinct enum value names.
|
||||
`CacheDictionary::save` throws past that, which surfaces when CI generates the
|
||||
|
||||
@@ -355,6 +355,7 @@ set(lisbslic3r_sources
|
||||
Optimize/Optimizer.hpp
|
||||
Orient.cpp
|
||||
Orient.hpp
|
||||
ParallelResolve.hpp
|
||||
ParameterUtils.cpp
|
||||
ParameterUtils.hpp
|
||||
pchheader.cpp
|
||||
|
||||
+26
-13
@@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
|
||||
return std::move(substitutions_ctxt.substitutions);
|
||||
}
|
||||
|
||||
// Case-insensitive compare of a JSON key against a fixed ASCII one, without
|
||||
// boost::iequals, whose std::locale() takes a lock the whole process shares in the
|
||||
// MSVC runtime.
|
||||
static bool ascii_iequals(const std::string &key, const char *literal)
|
||||
{
|
||||
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
|
||||
size_t i = 0;
|
||||
for (; i < key.size() && literal[i] != '\0'; ++ i)
|
||||
if (lower(key[i]) != lower(literal[i]))
|
||||
return false;
|
||||
return i == key.size() && literal[i] == '\0';
|
||||
}
|
||||
|
||||
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
|
||||
{
|
||||
json j;
|
||||
@@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
||||
}
|
||||
//parse the json elements
|
||||
for (auto it = j.begin(); it != j.end(); it++) {
|
||||
if (boost::iequals(it.key(),BBL_JSON_KEY_VERSION)) {
|
||||
if (ascii_iequals(it.key(), BBL_JSON_KEY_VERSION)) {
|
||||
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
||||
//skip it
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
||||
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
|
||||
if (it.value() == "project_settings")
|
||||
is_project_settings = true;
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_URL)) {
|
||||
key_values.emplace(BBL_JSON_KEY_URL, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
||||
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
||||
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
||||
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
||||
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
||||
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
|
||||
}
|
||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
||||
else if (ascii_iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
||||
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
|
||||
}
|
||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
||||
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
||||
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
|
||||
}
|
||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
||||
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
||||
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
|
||||
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
||||
} else if (ascii_iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
||||
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
|
||||
} else {
|
||||
t_config_option_key opt_key = it.key();
|
||||
|
||||
@@ -13,8 +13,19 @@
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
// How many setters this thread holds, so the ones nested in another can skip
|
||||
// setlocale, which takes a lock the whole process shares on Windows.
|
||||
static thread_local int s_numeric_locale_depth = 0;
|
||||
|
||||
CNumericLocalesSetter::CNumericLocalesSetter()
|
||||
{
|
||||
// Nested in another setter on this thread, whose "C" the separator check
|
||||
// confirms is still set.
|
||||
if (s_numeric_locale_depth > 0 && is_decimal_separator_point()) {
|
||||
m_nested = true;
|
||||
++ s_numeric_locale_depth;
|
||||
return;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
|
||||
@@ -29,12 +40,17 @@ CNumericLocalesSetter::CNumericLocalesSetter()
|
||||
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
|
||||
uselocale(m_new_locale);
|
||||
#endif
|
||||
// Counted last, since the destructor does not run for a constructor that throws.
|
||||
++ s_numeric_locale_depth;
|
||||
}
|
||||
|
||||
|
||||
|
||||
CNumericLocalesSetter::~CNumericLocalesSetter()
|
||||
{
|
||||
-- s_numeric_locale_depth;
|
||||
if (m_nested)
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
|
||||
#else
|
||||
|
||||
@@ -19,8 +19,13 @@ class CNumericLocalesSetter {
|
||||
public:
|
||||
CNumericLocalesSetter();
|
||||
~CNumericLocalesSetter();
|
||||
// A copy would restore the locale twice, and count down once more than up.
|
||||
CNumericLocalesSetter(const CNumericLocalesSetter&) = delete;
|
||||
CNumericLocalesSetter& operator=(const CNumericLocalesSetter&) = delete;
|
||||
|
||||
private:
|
||||
// Inside another setter on this thread, which does the setting and restoring.
|
||||
bool m_nested { false };
|
||||
#ifdef _WIN32
|
||||
std::string m_orig_numeric_locale;
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/task_arena.h>
|
||||
#include <tbb/task_group.h>
|
||||
|
||||
#include "Exception.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// A batch of resolved presets, each a whole config, is what resolve_then_commit adds
|
||||
// to peak memory, so it stays far below a vendor's preset count and above any core count.
|
||||
inline constexpr size_t resolve_batch_size = 64;
|
||||
|
||||
// Resolve `count` items that do not depend on each other and install them one at
|
||||
// a time.
|
||||
//
|
||||
// `resolve(i)` runs on any thread and must read only, since the items are
|
||||
// resolved side by side. `commit(i, resolved)` is called for every item in index
|
||||
// order, on the calling thread, and is where shared state is written.
|
||||
//
|
||||
// The items are worked through in batches, so what is resolved and held at once
|
||||
// does not grow with `count`. An exception from either callable propagates after
|
||||
// the batches before it have been committed. A cancellation of the caller's task
|
||||
// group, which stops a batch partway without an exception, throws RuntimeError
|
||||
// before that batch commits.
|
||||
//
|
||||
// `ChunkSetup`, when given, is constructed once for each piece of a batch TBB hands
|
||||
// out, for per-thread state a resolve would otherwise set up per item, such as the
|
||||
// C numeric locale, whose setting takes a lock the whole process shares.
|
||||
template<class ChunkSetup = std::monostate, class Resolve, class Commit>
|
||||
void resolve_then_commit(size_t count, Resolve resolve, Commit commit)
|
||||
{
|
||||
using Resolved = std::invoke_result_t<Resolve&, size_t>;
|
||||
|
||||
std::vector<Resolved> resolved(std::min(count, resolve_batch_size));
|
||||
for (size_t first = 0; first < count; first += resolve_batch_size) {
|
||||
const size_t last = std::min(first + resolve_batch_size, count);
|
||||
// Isolated, so a thread waiting on the batch runs none of the caller's other
|
||||
// tasks before finishing it.
|
||||
tbb::this_task_arena::isolate([&] {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(first, last),
|
||||
[&](const tbb::blocked_range<size_t>& range) {
|
||||
ChunkSetup setup;
|
||||
(void) setup;
|
||||
for (size_t i = range.begin(); i < range.end(); ++ i)
|
||||
resolved[i - first] = resolve(i);
|
||||
});
|
||||
});
|
||||
if (tbb::is_current_task_group_canceling())
|
||||
throw RuntimeError("resolve_then_commit: canceled before the batch was resolved");
|
||||
for (size_t i = first; i < last; ++ i)
|
||||
commit(i, std::move(resolved[i - first]));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
+236
-155
@@ -5,6 +5,8 @@
|
||||
#include "Preset.hpp"
|
||||
#include "PresetBundle.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "ParallelResolve.hpp"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -506,10 +508,11 @@ void Preset::normalize(DynamicPrintConfig &config)
|
||||
handle_legacy_sla(config);
|
||||
}
|
||||
|
||||
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config)
|
||||
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||
const DynamicPrintConfig *added)
|
||||
{
|
||||
std::string incorrect_keys;
|
||||
for (const std::string &key : config.keys())
|
||||
for (const std::string &key : (added != nullptr ? *added : config).keys())
|
||||
if (! default_config.has(key)) {
|
||||
if (incorrect_keys.empty())
|
||||
incorrect_keys = key;
|
||||
@@ -621,7 +624,6 @@ void Preset::load_info(const std::string& file)
|
||||
}
|
||||
else if (v.first.compare("base_id") == 0) {
|
||||
this->base_id = v.second.get_value<std::string>();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id;
|
||||
if (this->base_id.compare("null") == 0)
|
||||
this->base_id.clear();
|
||||
}
|
||||
@@ -1669,6 +1671,169 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
|
||||
return get_preset_canonical_name(parsed.bare, origin);
|
||||
}
|
||||
|
||||
PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
|
||||
const boost::filesystem::path &file, const std::string &canonical_name,
|
||||
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||
const std::string &extruder_id_name, const std::string &extruder_variant_name,
|
||||
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const
|
||||
{
|
||||
UserPresetLoad out;
|
||||
out.preset = Preset(m_type, canonical_name, false);
|
||||
Preset &preset = out.preset;
|
||||
preset.bundle_id = load_origin.bundle_id;
|
||||
preset.file = file.string();
|
||||
// Load the preset file, apply preset values on top of defaults.
|
||||
try {
|
||||
fs::path idx_path(preset.file);
|
||||
idx_path.replace_extension(".info");
|
||||
if (fs::exists(idx_path)) {
|
||||
out.info_file = idx_path.string();
|
||||
preset.load_info(out.info_file);
|
||||
}
|
||||
DynamicPrintConfig config;
|
||||
//BBS: change to json format
|
||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
||||
if (! config_substitutions.empty())
|
||||
out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
||||
if (!reason.empty()) {
|
||||
out.discard_file = true;
|
||||
out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str());
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||
boost::optional<Semver> version = Semver::parse(version_str);
|
||||
if (!version) return out;
|
||||
preset.version = *version;
|
||||
|
||||
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
||||
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
||||
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
||||
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
||||
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
||||
|
||||
//Orca: find and use the inherit config as the base
|
||||
const Preset* inherit_preset = nullptr;
|
||||
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
||||
|
||||
// check inherits_config
|
||||
if (inherits_config) {
|
||||
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
||||
std::string inherits_value = option_str->value;
|
||||
// Orca: try to find if the parent preset has been renamed
|
||||
inherit_preset = this->find_preset2(inherits_value);
|
||||
Preset::normalize_inherits(config, inherit_preset);
|
||||
}
|
||||
const Preset& default_preset = this->default_preset_for(config);
|
||||
if (inherit_preset) {
|
||||
preset.config = inherit_preset->config;
|
||||
preset.filament_id = inherit_preset->filament_id;
|
||||
extend_default_config_length(config, false, {});
|
||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
else {
|
||||
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
||||
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
||||
out.errors.push_back((boost::format("can not find parent %1% for config %2%!") % inherits_config2->value % preset.file).str());
|
||||
return out;
|
||||
}
|
||||
// We support custom root preset now
|
||||
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
||||
preset.config = default_preset.config;
|
||||
preset.config.apply(std::move(config));
|
||||
extend_default_config_length(preset.config, true, default_preset.config);
|
||||
}
|
||||
|
||||
Preset::normalize(preset.config);
|
||||
// Report configuration fields, which are misplaced into a wrong group.
|
||||
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
||||
if (!incorrect_keys.empty())
|
||||
out.errors.push_back("Error in a preset file: The preset \"" + preset.file +
|
||||
"\" contains the following incorrect keys: " + incorrect_keys + ", which were removed");
|
||||
|
||||
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
||||
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
||||
if (compatible_printers && compatible_printers->values.empty()) {
|
||||
size_t at_pos = canonical_name.find('@');
|
||||
if (at_pos != std::string::npos && at_pos + 1 < canonical_name.length()) {
|
||||
compatible_printers->values.push_back(canonical_name.substr(at_pos + 1));
|
||||
out.save_compatible_printers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preset.loaded = true;
|
||||
out.complete = true;
|
||||
} catch (const std::ifstream::failure &err) {
|
||||
out.discard_file = true;
|
||||
out.errors.push_back((boost::format("The user-config cannot be loaded: %1%. Reason: %2%") % preset.file % err.what()).str());
|
||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
||||
} catch (const std::runtime_error &err) {
|
||||
out.discard_file = true;
|
||||
out.errors.push_back((boost::format("Failed loading the user-config file: %1%. Reason: %2%") % preset.file % err.what()).str());
|
||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
||||
}
|
||||
out.install = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
void PresetCollection::commit_user_preset(UserPresetLoad &&loaded, std::deque<Preset> &presets_loaded,
|
||||
PresetsConfigSubstitutions &substitutions,
|
||||
const std::function<void(Preset&)> &preset_loaded_fn,
|
||||
bool read_only)
|
||||
{
|
||||
Preset &preset = loaded.preset;
|
||||
if (! loaded.info_file.empty())
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << loaded.info_file << " and base_id: " << preset.base_id;
|
||||
append(substitutions, std::move(loaded.substitutions));
|
||||
for (const std::string &error : loaded.errors) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << error;
|
||||
}
|
||||
if (loaded.discard_file && !read_only) {
|
||||
fs::path file_path(loaded.preset.file);
|
||||
if (fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
}
|
||||
if (!loaded.install)
|
||||
return;
|
||||
|
||||
if (loaded.complete) {
|
||||
if (loaded.save_compatible_printers) {
|
||||
// A filesystem error from the rewrite is counted, and the preset still loads.
|
||||
try {
|
||||
if (!read_only)
|
||||
preset.save(nullptr);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
|
||||
} catch (const std::runtime_error &err) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what();
|
||||
}
|
||||
}
|
||||
//BBS: add some workaround for previous incorrect settings
|
||||
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
||||
preset.setting_id.clear();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << preset.name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
||||
// add alias for custom filament preset
|
||||
set_custom_preset_alias(preset);
|
||||
}
|
||||
|
||||
if (preset_loaded_fn != nullptr)
|
||||
preset_loaded_fn(preset);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
||||
presets_loaded.emplace_back(std::move(preset));
|
||||
}
|
||||
|
||||
// Load all presets found in dir_path.
|
||||
// Throws an exception on error.
|
||||
void PresetCollection::load_presets(
|
||||
@@ -1706,6 +1871,8 @@ void PresetCollection::load_presets(
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
|
||||
struct UserPresetFile { fs::path path; std::string canonical_name; };
|
||||
std::vector<UserPresetFile> files;
|
||||
//BBS: change to json format
|
||||
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
||||
{
|
||||
@@ -1721,149 +1888,26 @@ void PresetCollection::load_presets(
|
||||
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
|
||||
continue;
|
||||
}
|
||||
files.push_back({ dir_entry.path(), std::move(canonical_name) });
|
||||
}
|
||||
}
|
||||
|
||||
resolve_then_commit<CNumericLocalesSetter>(files.size(),
|
||||
[&](size_t i) {
|
||||
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
|
||||
extruder_id_name, extruder_variant_name, key_set1, key_set2);
|
||||
},
|
||||
[&](size_t, UserPresetLoad &&loaded) {
|
||||
// Committing can remove an unreadable preset's file, and a filesystem error
|
||||
// there is reported without stopping the rest of the directory.
|
||||
try {
|
||||
Preset preset(m_type, canonical_name, false);
|
||||
preset.bundle_id = resolved_origin.bundle_id;
|
||||
preset.file = dir_entry.path().string();
|
||||
// Load the preset file, apply preset values on top of defaults.
|
||||
try {
|
||||
fs::path idx_path(preset.file);
|
||||
idx_path.replace_extension(".info");
|
||||
if (fs::exists(idx_path)) {
|
||||
preset.load_info(idx_path.string());
|
||||
}
|
||||
DynamicPrintConfig config;
|
||||
//BBS: change to json format
|
||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
||||
if (! config_substitutions.empty())
|
||||
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
||||
if (!reason.empty()) {
|
||||
fs::path file_path(preset.file);
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
|
||||
++m_errors;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||
boost::optional<Semver> version = Semver::parse(version_str);
|
||||
if (!version) continue;
|
||||
preset.version = *version;
|
||||
|
||||
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
||||
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
||||
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
||||
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
||||
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
||||
|
||||
//Orca: find and use the inherit config as the base
|
||||
Preset* inherit_preset = nullptr;
|
||||
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
||||
|
||||
// check inherits_config
|
||||
if (inherits_config) {
|
||||
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
||||
std::string inherits_value = option_str->value;
|
||||
// Orca: try to find if the parent preset has been renamed
|
||||
inherit_preset = this->find_preset2(inherits_value);
|
||||
Preset::normalize_inherits(config, inherit_preset);
|
||||
} else {
|
||||
;
|
||||
}
|
||||
const Preset& default_preset = this->default_preset_for(config);
|
||||
if (inherit_preset) {
|
||||
preset.config = inherit_preset->config;
|
||||
preset.filament_id = inherit_preset->filament_id;
|
||||
extend_default_config_length(config, false, {});
|
||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
else {
|
||||
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
||||
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file;
|
||||
++m_errors;
|
||||
continue;
|
||||
}
|
||||
// We support custom root preset now
|
||||
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
||||
preset.config = default_preset.config;
|
||||
preset.config.apply(std::move(config));
|
||||
extend_default_config_length(preset.config, true, default_preset.config);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
||||
|
||||
Preset::normalize(preset.config);
|
||||
// Report configuration fields, which are misplaced into a wrong group.
|
||||
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
||||
if (!incorrect_keys.empty()) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "Error in a preset file: The preset \"" << preset.file
|
||||
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
|
||||
}
|
||||
|
||||
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
||||
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
||||
if (compatible_printers && compatible_printers->values.empty()) {
|
||||
size_t at_pos = name.find('@');
|
||||
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
|
||||
compatible_printers->values.push_back(name.substr(at_pos + 1));
|
||||
if (!read_only)
|
||||
preset.save(nullptr);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preset.loaded = true;
|
||||
//BBS: add some workaround for previous incorrect settings
|
||||
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
||||
preset.setting_id.clear();
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
||||
// add alias for custom filament preset
|
||||
set_custom_preset_alias(preset);
|
||||
} catch (const std::ifstream::failure &err) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
|
||||
fs::path file_path(preset.file);
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
||||
} catch (const std::runtime_error &err) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
|
||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
||||
fs::path file_path(preset.file);
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
file_path.replace_extension(".info");
|
||||
if (!read_only && fs::exists(file_path))
|
||||
fs::remove(file_path);
|
||||
}
|
||||
|
||||
if (preset_loaded_fn != nullptr)
|
||||
preset_loaded_fn(preset);
|
||||
|
||||
presets_loaded.emplace_back(preset);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
||||
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, read_only);
|
||||
} catch (const std::runtime_error &err) {
|
||||
errors_cummulative += err.what();
|
||||
errors_cummulative += "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (presets_loaded.size() > 0)
|
||||
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
|
||||
sort_presets();
|
||||
@@ -2827,6 +2871,18 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
||||
return std::make_pair(&preset, false);
|
||||
}
|
||||
|
||||
Preset& PresetCollection::append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config)
|
||||
{
|
||||
lock();
|
||||
Preset &preset = m_presets.emplace_back(m_type, name, false);
|
||||
preset.file = std::move(path);
|
||||
preset.config = std::move(config);
|
||||
preset.loaded = true;
|
||||
preset.is_dirty = false;
|
||||
unlock();
|
||||
return preset;
|
||||
}
|
||||
|
||||
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
|
||||
{
|
||||
lock();
|
||||
@@ -3846,28 +3902,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
|
||||
std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
|
||||
{
|
||||
std::vector<std::string> duplicates;
|
||||
for (Preset &preset : other.m_presets) {
|
||||
if (preset.is_default || preset.is_external)
|
||||
continue;
|
||||
Preset key(m_type, preset.name);
|
||||
auto it = (m_type == Preset::TYPE_FILAMENT)
|
||||
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less)
|
||||
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
|
||||
if (it == m_presets.end() || it->name != preset.name) {
|
||||
auto less = [this](const Preset &a, const Preset &b) {
|
||||
return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
|
||||
};
|
||||
struct Incoming { Preset *preset; size_t source; };
|
||||
auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
|
||||
// Each of `others` is sorted, so its presets form one sorted run.
|
||||
std::vector<Incoming> incoming;
|
||||
std::vector<size_t> run_ends { 0 };
|
||||
for (size_t source = 0; source < others.size(); ++ source) {
|
||||
for (Preset &preset : others[source]->m_presets)
|
||||
if (! preset.is_default && ! preset.is_external)
|
||||
incoming.push_back({ &preset, source });
|
||||
assert(std::is_sorted(incoming.begin() + run_ends.back(), incoming.end(), incoming_less));
|
||||
run_ends.push_back(incoming.size());
|
||||
}
|
||||
// Merged pairwise and stably, so equal names stay in the order of `others`.
|
||||
const size_t runs = others.size();
|
||||
for (size_t width = 1; width < runs; width *= 2)
|
||||
for (size_t i = 0; i + width < runs; i += 2 * width)
|
||||
std::inplace_merge(incoming.begin() + run_ends[i], incoming.begin() + run_ends[i + width],
|
||||
incoming.begin() + run_ends[std::min(i + 2 * width, runs)], incoming_less);
|
||||
|
||||
std::vector<std::vector<std::string>> duplicates(others.size());
|
||||
std::deque<Preset> merged;
|
||||
auto own = m_presets.begin() + m_num_default_presets;
|
||||
std::move(m_presets.begin(), own, std::back_inserter(merged));
|
||||
// On equal names this collection's preset is kept, else the earliest of `others`,
|
||||
// and each repeat is listed under the collection it came from.
|
||||
for (auto next = incoming.begin(); own != m_presets.end() || next != incoming.end();) {
|
||||
if (next == incoming.end() || (own != m_presets.end() && ! less(*next->preset, *own)))
|
||||
merged.emplace_back(std::move(*own ++));
|
||||
else {
|
||||
Preset &preset = *(next ++)->preset;
|
||||
if (preset.vendor != nullptr) {
|
||||
// Re-assign a pointer to the vendor structure in the new PresetBundle.
|
||||
auto it = new_vendors.find(preset.vendor->id);
|
||||
assert(it != new_vendors.end());
|
||||
preset.vendor = &it->second;
|
||||
}
|
||||
m_presets.emplace(it, std::move(preset));
|
||||
} else
|
||||
duplicates.emplace_back(std::move(preset.name));
|
||||
merged.emplace_back(std::move(preset));
|
||||
}
|
||||
for (; next != incoming.end() && next->preset->name == merged.back().name; ++ next)
|
||||
duplicates[next->source].emplace_back(next->preset->name);
|
||||
}
|
||||
m_presets = std::move(merged);
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
|
||||
@@ -445,7 +445,10 @@ public:
|
||||
static std::string remove_suffix_modified(const std::string& name);
|
||||
static void normalize(DynamicPrintConfig &config);
|
||||
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
|
||||
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config);
|
||||
// `added`, when given, is the diff applied over a copy of default_config, and only
|
||||
// its keys are checked, since no other key can be missing from default_config.
|
||||
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||
const DynamicPrintConfig *added = nullptr);
|
||||
|
||||
// BBS: move constructor to public
|
||||
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
||||
@@ -801,6 +804,8 @@ public:
|
||||
// Return number of presets including the "- default -" preset.
|
||||
size_t size() const { return m_presets.size(); }
|
||||
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
|
||||
// How many presets this collection refused or repaired while loading.
|
||||
int error_count() const { return m_errors; }
|
||||
|
||||
// For Print / Filament presets, disable those, which are not compatible with the printer.
|
||||
template<typename PreferedCondition>
|
||||
@@ -874,8 +879,10 @@ protected:
|
||||
// This is a temporary state, which shall be fixed immediately by the following step.
|
||||
bool select_preset_by_name_strict(const std::string &name);
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors);
|
||||
// Move the presets of `others` into this collection in one pass. A name this
|
||||
// collection or an earlier one of `others` already has is left out, and reported
|
||||
// in the list of the collection that repeats it.
|
||||
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors);
|
||||
|
||||
// Update m_map_alias_to_profile_name from loaded system profiles.
|
||||
void update_map_alias_to_profile_name();
|
||||
@@ -890,6 +897,43 @@ protected:
|
||||
void set_custom_preset_alias(Preset &preset);
|
||||
|
||||
private:
|
||||
// One preset file read and flattened against the presets already in this
|
||||
// collection, before anything the collection shares has been touched.
|
||||
struct UserPresetLoad
|
||||
{
|
||||
Preset preset;
|
||||
// Joins the collection. A file that threw partway still joins it, without
|
||||
// the steps that did not run.
|
||||
bool install { false };
|
||||
// The whole of the load ran, so the preset is ready to be aliased.
|
||||
bool complete { false };
|
||||
// A filament preset that named no compatible printer and was given one from
|
||||
// its name, which commit writes back to its file.
|
||||
bool save_compatible_printers { false };
|
||||
// Unreadable, so commit removes it and its .info file.
|
||||
bool discard_file { false };
|
||||
// The .info file read beside the preset, which commit logs.
|
||||
std::string info_file;
|
||||
// Counted and logged by commit, in the order the directory listed the files.
|
||||
std::vector<std::string> errors;
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
};
|
||||
|
||||
// Read and flatten one preset file. It reads only, and resolves against the presets
|
||||
// loaded before this pass, never another file of the same pass, so the files of a
|
||||
// pass are independent of each other.
|
||||
UserPresetLoad resolve_user_preset(const boost::filesystem::path &file, const std::string &canonical_name,
|
||||
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||
const std::string &extruder_id_name, const std::string &extruder_variant_name,
|
||||
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const;
|
||||
|
||||
// Install one resolved preset. The collection, its alias maps, the error count
|
||||
// and the preset files on disk are touched here and only here.
|
||||
void commit_user_preset(UserPresetLoad &&loaded, std::deque<Preset> &presets_loaded,
|
||||
PresetsConfigSubstitutions &substitutions,
|
||||
const std::function<void(Preset&)> &preset_loaded_fn,
|
||||
bool read_only);
|
||||
|
||||
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
||||
|
||||
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
||||
@@ -901,6 +945,10 @@ private:
|
||||
return a.name < b.name;
|
||||
}
|
||||
|
||||
// Append a preset without keeping the collection sorted, for a caller installing
|
||||
// many at once; find_preset() is unusable until sort_presets() runs.
|
||||
Preset& append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config);
|
||||
|
||||
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
|
||||
void sort_presets() {
|
||||
if (m_type == Preset::TYPE_FILAMENT)
|
||||
|
||||
+558
-389
File diff suppressed because it is too large
Load Diff
+172
-27
@@ -15,6 +15,7 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -622,36 +623,180 @@ public:
|
||||
// default_filament_profile must resolve to a system filament.
|
||||
bool check_printer_default_materials() const;
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
||||
// One vendor to load, and the directory it is installed in.
|
||||
struct VendorSource
|
||||
{
|
||||
std::string name;
|
||||
boost::filesystem::path dir;
|
||||
};
|
||||
|
||||
// Load `vendors` into this bundle, the Orca filament library directly and every
|
||||
// other vendor in parallel into a bundle of its own that inherits from it, merged
|
||||
// in the order given. A vendor that cannot be loaded has its error added to the
|
||||
// returned text, or thrown in validation mode, and its name to `failed`; it is
|
||||
// left out, except for the library, which keeps what it installed before the
|
||||
// failure. Once `cancel` is set, no further vendor starts loading.
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_vendors(const std::vector<VendorSource>& vendors,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache,
|
||||
const std::atomic<bool>* cancel = nullptr, std::vector<std::string>* failed = nullptr);
|
||||
|
||||
private:
|
||||
// Load one vendor from the preset cache installed in `dir`, judged against
|
||||
// the vendor profile there. False, with this bundle left clean, when there
|
||||
// is no usable cache and the vendor has to be parsed. This is how
|
||||
// load_vendor_configs_from_json reads a cache.
|
||||
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
|
||||
// Move the presets and vendor profiles of `others` into this bundle, in one pass
|
||||
// over each collection. A preset whose name this bundle or an earlier one of
|
||||
// `others` already has is left out and listed under the bundle that repeats it.
|
||||
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
|
||||
|
||||
// Load one source-form preset entry into this bundle: resolve `inherits`
|
||||
// and `include`, flatten, validate and register the preset. Returns the
|
||||
// reason loading failed, empty on success. See the definition for the
|
||||
// sharing contract between the JSON parse and the cache load.
|
||||
// retain_configs / retain_includes, when non-null, name the only presets
|
||||
// registered into config_maps / include_maps (a config copy each). The
|
||||
// cache load passes the names its entries inherit / include — the only
|
||||
// ones ever looked up again; the JSON parse retains all, not knowing what
|
||||
// later subfiles name.
|
||||
std::string load_vendor_preset(const CachedPreset& entry,
|
||||
const std::string& path, const std::string& vendor_name,
|
||||
const PresetBundle* base_bundle,
|
||||
LoadConfigBundleAttributes flags,
|
||||
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
|
||||
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
|
||||
std::map<std::string, std::string>& filament_id_maps,
|
||||
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
|
||||
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
|
||||
// What parsing one entry's JSON sub-file reported. Its errors and warnings are
|
||||
// logged when the entry installs, so they come out in listing order with the
|
||||
// entry's install errors, as parsing and installing one entry at a time leaves them.
|
||||
struct EntryParse
|
||||
{
|
||||
ConfigSubstitutions substitutions;
|
||||
// Counted in the bundle's error count.
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
};
|
||||
|
||||
// An EntryParse for each entry of the VendorCacheData list of the same name.
|
||||
struct VendorParse
|
||||
{
|
||||
std::vector<EntryParse> process_entries;
|
||||
std::vector<EntryParse> filament_entries;
|
||||
std::vector<EntryParse> machine_entries;
|
||||
};
|
||||
|
||||
// One vendor read from its cache or its JSONs by read_vendor, for
|
||||
// install_vendor_read to install.
|
||||
struct VendorRead
|
||||
{
|
||||
std::string dir;
|
||||
std::string vendor_name;
|
||||
LoadConfigBundleAttributes flags;
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule;
|
||||
// The errors this bundle had counted before the read, which the cache stamp leaves out.
|
||||
int errors_at_entry { 0 };
|
||||
// A whole-vendor load, which can read a cache and write one.
|
||||
bool cacheable { false };
|
||||
// Read from the cache at cache_path, which install can still reject.
|
||||
bool from_cache { false };
|
||||
std::string cache_path;
|
||||
// Only the vendor profile was asked for.
|
||||
bool vendor_only { false };
|
||||
VendorCacheData data;
|
||||
// What each entry's JSON parse reported, and whether and with which version
|
||||
// the cache is written once the entries install.
|
||||
VendorParse parsed;
|
||||
bool will_cache { false };
|
||||
std::string version;
|
||||
// The sub-file the parse stopped at, its kind, why, and the errors it reported.
|
||||
std::string reason;
|
||||
std::string failed_subfile;
|
||||
const char* failed_kind { nullptr };
|
||||
std::vector<std::string> failed_errors;
|
||||
};
|
||||
|
||||
// Read a vendor into this bundle's vendor profiles and `data`, from its cache
|
||||
// when one covers it, else from its JSONs; nothing is installed. Throws
|
||||
// ConfigurationError when the vendor's own JSON cannot be parsed.
|
||||
VendorRead read_vendor(const std::string& dir, const std::string& vendor_name, LoadConfigBundleAttributes flags,
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache);
|
||||
|
||||
// Parse the vendor's JSONs into `read`, up to the first sub-file that fails.
|
||||
void parse_vendor_json(VendorRead& read);
|
||||
|
||||
// Install what read_vendor read, against base_bundle's filament library. A cache
|
||||
// that cannot be installed is replaced by a parse of the JSONs. Throws
|
||||
// ConfigurationError at the first entry that cannot be installed, or after
|
||||
// installing the entries before a sub-file that could not be parsed.
|
||||
std::pair<PresetsConfigSubstitutions, size_t> install_vendor_read(VendorRead&& read, const PresetBundle* base_bundle);
|
||||
|
||||
// Install a cache's entries. False, with this bundle left clean, when one of them
|
||||
// cannot be installed.
|
||||
bool install_vendor_cache(const std::string& cache_path, const std::string& vendor_name, VendorCacheData&& data,
|
||||
const PresetBundle* base_bundle);
|
||||
|
||||
// Log and count errors reported by a resolve or a parse.
|
||||
void log_errors(const std::vector<std::string>& errors);
|
||||
|
||||
// The state of installing one collection of one vendor, which
|
||||
// resolve_vendor_preset reads through a const reference and only
|
||||
// commit_vendor_preset writes.
|
||||
struct VendorInstall
|
||||
{
|
||||
// The directory holding <vendor_name>/, whose sub-paths the entries name.
|
||||
std::string path;
|
||||
std::string vendor_name;
|
||||
const VendorProfile* vendor_profile;
|
||||
const PresetBundle* base_bundle;
|
||||
LoadConfigBundleAttributes flags;
|
||||
PresetCollection* presets;
|
||||
// The Orca filament library, which keeps every config for other vendors to
|
||||
// resolve against.
|
||||
bool is_from_lib;
|
||||
PresetsConfigSubstitutions* substitutions;
|
||||
// The names some entry inherits / includes, the only ones whose configs /
|
||||
// include diffs are looked up again.
|
||||
std::set<std::string> inherited;
|
||||
std::set<std::string> included;
|
||||
std::map<std::string, DynamicPrintConfig> config_maps;
|
||||
std::map<std::string, DynamicPrintConfig> include_maps;
|
||||
std::map<std::string, std::string> filament_id_maps;
|
||||
std::unordered_set<std::string> installed_names;
|
||||
size_t count { 0 };
|
||||
};
|
||||
|
||||
// Install a vendor's source-form entries, parsed from its JSON or read from its
|
||||
// cache: processes, then filaments, then printers. Both loads go through here,
|
||||
// so a cache-loaded bundle cannot come out different from a JSON-loaded one.
|
||||
// `parsed` is given for entries parsed just now. `complete` says the entries
|
||||
// are the vendor's whole lists; only then are the filament library's configs
|
||||
// and filament ids left in m_config_maps and m_filament_id_maps. Returns the
|
||||
// number of presets installed, and throws ConfigurationError at the first
|
||||
// entry that cannot be installed.
|
||||
size_t install_vendor(const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle,
|
||||
LoadConfigBundleAttributes flags, const VendorCacheData& entries,
|
||||
VendorParse* parsed, bool complete, PresetsConfigSubstitutions& substitutions);
|
||||
|
||||
// Install one collection's entries in the order they are listed.
|
||||
void install_vendor_entries(VendorInstall& install, const std::vector<CachedPreset>& entries,
|
||||
std::vector<EntryParse>* parsed);
|
||||
|
||||
// One entry flattened against the preset it inherits, before anything this
|
||||
// bundle shares has been touched.
|
||||
struct PresetInstall
|
||||
{
|
||||
DynamicPrintConfig config;
|
||||
std::string file_path;
|
||||
// Empty when the preset is its own alias.
|
||||
std::string alias;
|
||||
std::string filament_id;
|
||||
std::vector<std::string> renamed_from;
|
||||
// Reported by commit, so resolving entries together leaves the log and
|
||||
// the error count as one entry at a time produces them.
|
||||
std::vector<std::string> errors;
|
||||
// What a base states for the presets that include it, when it is retained.
|
||||
std::optional<DynamicPrintConfig> included;
|
||||
// The config kept for the entries that inherit this one, or for other
|
||||
// vendors when this is the filament library.
|
||||
std::optional<DynamicPrintConfig> retained;
|
||||
// Not instantiated, so it contributes a config and no preset.
|
||||
bool config_only { false };
|
||||
// Non-empty when the entry is rejected, and says why.
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
// Flatten one entry against the config it inherits, from this collection's
|
||||
// config_maps or base_bundle's filament library, with the include diffs it
|
||||
// names layered in. It looks up nothing but the names the entry inherits and
|
||||
// includes, and writes nothing.
|
||||
PresetInstall resolve_vendor_preset(const CachedPreset& entry, const VendorInstall& install) const;
|
||||
|
||||
// Install a resolved entry. The collections, the maps in `install` and the
|
||||
// error count are touched here and only here, one entry at a time.
|
||||
// Presets are appended, so a repeated name is caught with `installed_names`,
|
||||
// and the collection is sorted once every entry is in.
|
||||
std::string commit_vendor_preset(const CachedPreset& entry, PresetInstall&& resolved,
|
||||
ConfigSubstitutions&& substitutions, VendorInstall& install);
|
||||
|
||||
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
||||
void clear_printer_hold_aliases();
|
||||
|
||||
@@ -109,7 +109,7 @@ void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
|
||||
|
||||
// One preset as its JSON subfile states it: the config diff, the names of the
|
||||
// preset it inherits and the presets it includes, and the parse metadata —
|
||||
// everything the parse phase of load_vendor_configs_from_json extracts and
|
||||
// everything PresetBundle::parse_vendor_json extracts and
|
||||
// nothing it derives. Inheritance and includes are resolved when the entry is
|
||||
// installed, against whatever filament library is loaded then, so a cache
|
||||
// carries no other vendor's values and no other vendor's update can make it
|
||||
|
||||
@@ -1405,23 +1405,26 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
||||
// is served from the shipped profiles. Each is stamped by name and
|
||||
// version alone: a profile change requires a version bump, so those two
|
||||
// determine content wherever the vendor's copy sits.
|
||||
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
|
||||
std::vector<VendorSource> ordered;
|
||||
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
|
||||
std::vector<PresetBundle::VendorSource> ordered;
|
||||
json stamps = json::array();
|
||||
auto add_vendor = [&ordered, &stamps](const std::string& name, const boost::filesystem::path& dir) {
|
||||
// The version a load from `dir` would serve: the profile's where one
|
||||
// exists (a cache is only served while it covers the profile beside
|
||||
// it), the cache's own stamp where the cache is the whole vendor.
|
||||
// A profile without a version (blacklist.json) carries no presets
|
||||
// and is passed over.
|
||||
const boost::filesystem::path profile = dir / (name + ".json");
|
||||
std::string version;
|
||||
if (boost::filesystem::exists(profile)) {
|
||||
const Semver v = get_version_from_json(profile.string());
|
||||
if (v.valid())
|
||||
ordered.push_back({name, dir, v.to_string()});
|
||||
if (! v.valid())
|
||||
return;
|
||||
version = v.to_string();
|
||||
} else {
|
||||
ordered.push_back({name, dir,
|
||||
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
|
||||
version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name);
|
||||
}
|
||||
ordered.push_back({name, dir});
|
||||
stamps.push_back({name, version});
|
||||
};
|
||||
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
||||
@@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
||||
add_vendor(name, dir);
|
||||
if (ordered.empty())
|
||||
return false;
|
||||
json stamps = json::array();
|
||||
for (const VendorSource& v : ordered)
|
||||
stamps.push_back({v.name, v.version});
|
||||
|
||||
// What this function derives is a pure function of that stamped set, so
|
||||
// the derived JSON is cached whole: a fresh cache makes an open one
|
||||
@@ -1461,26 +1461,18 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
||||
}
|
||||
|
||||
// Each vendor comes from its preset cache where one covers it, which is
|
||||
// what makes this worth doing instead of the scan below; loading into a
|
||||
// bundle per vendor keeps the install order the startup path has.
|
||||
PresetBundle bundle;
|
||||
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
|
||||
const boost::filesystem::path& dir, const PresetBundle* base) {
|
||||
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
|
||||
};
|
||||
for (const VendorSource& v : ordered) {
|
||||
if (*m_cancel_token)
|
||||
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
|
||||
if (v.name == filament_library) {
|
||||
load_vendor(bundle, v.name, v.dir, nullptr);
|
||||
} else {
|
||||
PresetBundle tmp;
|
||||
load_vendor(tmp, v.name, v.dir, &bundle);
|
||||
bundle.merge_presets(std::move(tmp));
|
||||
}
|
||||
}
|
||||
if (bundle.vendors.empty())
|
||||
// what makes this worth doing instead of the scan below.
|
||||
PresetBundle bundle;
|
||||
std::vector<std::string> failed;
|
||||
const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||
/*allow_cache=*/true, m_cancel_token.get(), &failed).second;
|
||||
if (*m_cancel_token || bundle.vendors.empty())
|
||||
return false;
|
||||
if (! errors.empty())
|
||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
|
||||
// A vendor that failed to load sends this open to the scan below, which lists
|
||||
// what it can read of every vendor.
|
||||
if (! failed.empty())
|
||||
return false;
|
||||
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
||||
return false;
|
||||
|
||||
@@ -16,8 +16,10 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_clipper_utils.cpp
|
||||
test_config.cpp
|
||||
test_config_variant_expansion.cpp
|
||||
test_locales_utils.cpp
|
||||
test_drc.cpp
|
||||
test_toolordering_nozzle_group.cpp
|
||||
test_parallel_resolve.cpp
|
||||
test_preset_bundle_loading.cpp
|
||||
test_preset_setting_id.cpp
|
||||
test_preset_diff.cpp
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <clocale>
|
||||
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// Switches this thread's numeric locale to one whose decimal separator is a comma,
|
||||
// when the system has one installed.
|
||||
struct CommaNumericLocale
|
||||
{
|
||||
#ifdef _WIN32
|
||||
bool apply()
|
||||
{
|
||||
for (const char* name : { "de-DE", "German_Germany.1252" })
|
||||
if (std::setlocale(LC_NUMERIC, name) != nullptr)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
locale_t locale { (locale_t) 0 };
|
||||
bool apply()
|
||||
{
|
||||
for (const char* name : { "de_DE.UTF-8", "de_DE.utf8", "de_DE" })
|
||||
if ((locale = newlocale(LC_NUMERIC_MASK, name, (locale_t) 0)) != (locale_t) 0) {
|
||||
uselocale(locale);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Freed once the setters around apply() have put the thread's own locale back.
|
||||
~CommaNumericLocale()
|
||||
{
|
||||
if (locale != (locale_t) 0)
|
||||
freelocale(locale);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("a setter nested in another leaves the C locale in place for the outer one", "[LocalesUtils]")
|
||||
{
|
||||
CNumericLocalesSetter outer;
|
||||
{
|
||||
CNumericLocalesSetter inner;
|
||||
CHECK(is_decimal_separator_point());
|
||||
}
|
||||
CHECK(is_decimal_separator_point());
|
||||
}
|
||||
|
||||
TEST_CASE("a setter nested in another sets C again when the locale changed between them", "[LocalesUtils]")
|
||||
{
|
||||
CommaNumericLocale comma;
|
||||
{
|
||||
CNumericLocalesSetter outer;
|
||||
if (! comma.apply())
|
||||
SKIP("no locale with a comma decimal separator is installed");
|
||||
REQUIRE_FALSE(is_decimal_separator_point());
|
||||
{
|
||||
CNumericLocalesSetter inner;
|
||||
CHECK(is_decimal_separator_point());
|
||||
}
|
||||
CHECK_FALSE(is_decimal_separator_point());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/task_group.h>
|
||||
|
||||
#include "libslic3r/ParallelResolve.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
thread_local int t_live_setups = 0;
|
||||
|
||||
// Counts how many are alive on the constructing thread.
|
||||
struct CountingSetup
|
||||
{
|
||||
CountingSetup() { ++ t_live_setups; }
|
||||
~CountingSetup() { -- t_live_setups; }
|
||||
};
|
||||
|
||||
std::vector<size_t> first_indices(size_t count)
|
||||
{
|
||||
std::vector<size_t> indices(count);
|
||||
std::iota(indices.begin(), indices.end(), size_t(0));
|
||||
return indices;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("every item resolves once and commits in index order on the calling thread", "[ParallelResolve]")
|
||||
{
|
||||
const size_t count = 200;
|
||||
std::vector<int> resolves(count, 0);
|
||||
std::vector<size_t> committed, values;
|
||||
std::vector<bool> on_caller;
|
||||
const std::thread::id caller = std::this_thread::get_id();
|
||||
resolve_then_commit(count,
|
||||
[&](size_t i) { ++ resolves[i]; return i * 3; },
|
||||
[&](size_t i, size_t resolved) {
|
||||
committed.push_back(i);
|
||||
values.push_back(resolved);
|
||||
on_caller.push_back(std::this_thread::get_id() == caller);
|
||||
});
|
||||
|
||||
CHECK(committed == first_indices(count));
|
||||
for (size_t i = 0; i < count; ++ i) {
|
||||
CHECK(resolves[i] == 1);
|
||||
CHECK(values[i] == i * 3);
|
||||
CHECK(on_caller[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("every item resolves inside one chunk setup", "[ParallelResolve]")
|
||||
{
|
||||
const size_t count = 200;
|
||||
std::vector<int> live(count, 0);
|
||||
resolve_then_commit<CountingSetup>(count,
|
||||
[&](size_t i) { live[i] = t_live_setups; return 0; },
|
||||
[](size_t, int) {});
|
||||
|
||||
for (size_t i = 0; i < count; ++ i)
|
||||
CHECK(live[i] == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("an exception from resolve leaves the batches before it committed", "[ParallelResolve]")
|
||||
{
|
||||
const size_t count = 200, fails_at = 150;
|
||||
REQUIRE(fails_at >= resolve_batch_size);
|
||||
std::vector<size_t> committed;
|
||||
CHECK_THROWS_AS(resolve_then_commit(count,
|
||||
[&](size_t i) {
|
||||
if (i == fails_at)
|
||||
throw std::runtime_error("resolve failed");
|
||||
return i;
|
||||
},
|
||||
[&](size_t i, size_t) { committed.push_back(i); }),
|
||||
std::runtime_error);
|
||||
|
||||
CHECK(committed == first_indices(fails_at / resolve_batch_size * resolve_batch_size));
|
||||
}
|
||||
|
||||
TEST_CASE("an exception from commit stops at the item that threw", "[ParallelResolve]")
|
||||
{
|
||||
const size_t count = 200, fails_at = 90;
|
||||
std::vector<size_t> committed;
|
||||
CHECK_THROWS_AS(resolve_then_commit(count,
|
||||
[](size_t i) { return i; },
|
||||
[&](size_t i, size_t) {
|
||||
if (i == fails_at)
|
||||
throw std::runtime_error("commit failed");
|
||||
committed.push_back(i);
|
||||
}),
|
||||
std::runtime_error);
|
||||
|
||||
CHECK(committed == first_indices(fails_at));
|
||||
}
|
||||
|
||||
TEST_CASE("a canceled task group stops before committing an item it did not resolve", "[ParallelResolve]")
|
||||
{
|
||||
std::vector<size_t> committed;
|
||||
bool threw = false;
|
||||
tbb::task_group_context context;
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, 1), [&](const tbb::blocked_range<size_t>&) {
|
||||
context.cancel_group_execution();
|
||||
try {
|
||||
resolve_then_commit(200,
|
||||
[](size_t i) { return i + 1; },
|
||||
[&](size_t, size_t resolved) { committed.push_back(resolved); });
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
}, context);
|
||||
|
||||
CHECK(threw);
|
||||
CHECK(committed.empty());
|
||||
}
|
||||
@@ -59,6 +59,16 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const
|
||||
config.save_to_json(file.string(), name, "User", "1.0.0");
|
||||
}
|
||||
|
||||
// A user preset file stating nothing but the preset it inherits, so every value it
|
||||
// ends up with came from resolving that parent.
|
||||
void write_minimal_child(const fs::path &file, const std::string &name, const std::string &inherits)
|
||||
{
|
||||
fs::create_directories(file.parent_path());
|
||||
std::ofstream(file.string())
|
||||
<< R"({"type":"process","name":")" << name << R"(","from":"User","version":"1.0.0","inherits":")"
|
||||
<< inherits << R"("})";
|
||||
}
|
||||
|
||||
// Add an in-memory preset (no file) with the given inherits value (empty => root preset).
|
||||
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
|
||||
{
|
||||
@@ -261,6 +271,162 @@ TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundl
|
||||
CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast<int>(expected_bed_type)));
|
||||
}
|
||||
|
||||
TEST_CASE("A directory of user presets loads with each one resolved against its parent", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
RenameTestCollection coll;
|
||||
|
||||
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
|
||||
parent.is_system = true;
|
||||
|
||||
constexpr int children = 400;
|
||||
for (int i = 0; i < children; ++ i)
|
||||
write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"),
|
||||
"Child " + std::to_string(i), "Parent Process");
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::Disable);
|
||||
|
||||
CHECK(coll.size() == size_t(children) + 2); // the children, the default preset and the parent
|
||||
CHECK(coll.error_count() == 0);
|
||||
for (int i = 0; i < children; ++ i) {
|
||||
const Preset *child = coll.find_preset("Child " + std::to_string(i));
|
||||
REQUIRE(child != nullptr);
|
||||
CHECK(child->inherits() == "Parent Process");
|
||||
CHECK(child->alias == "Child " + std::to_string(i));
|
||||
CHECK(child->loaded);
|
||||
REQUIRE(child->config.option<ConfigOptionFloat>("layer_height") != nullptr);
|
||||
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
|
||||
auto seed_directory = [&]() {
|
||||
for (int i = 0; i < 200; ++ i)
|
||||
write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"),
|
||||
"Child " + std::to_string(i), "Parent Process");
|
||||
};
|
||||
|
||||
std::vector<std::vector<std::string>> names_per_run;
|
||||
for (int run = 0; run < 3; ++ run) {
|
||||
RenameTestCollection coll;
|
||||
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||
parent.is_system = true;
|
||||
if (run == 0)
|
||||
seed_directory();
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::Disable);
|
||||
|
||||
std::vector<std::string> names;
|
||||
for (auto it = coll.begin(); it != coll.end(); ++ it)
|
||||
names.push_back(it->name + "|" + it->alias + "|" + it->inherits());
|
||||
names_per_run.push_back(std::move(names));
|
||||
}
|
||||
|
||||
REQUIRE(names_per_run[0].size() > 200);
|
||||
CHECK(names_per_run[1] == names_per_run[0]);
|
||||
CHECK(names_per_run[2] == names_per_run[0]);
|
||||
}
|
||||
|
||||
TEST_CASE("An unreadable user preset is counted and removed while the rest still load", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
RenameTestCollection coll;
|
||||
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||
|
||||
for (int i = 0; i < 20; ++ i)
|
||||
write_preset_with_inherits(coll.default_preset().config, dir / ("Good " + std::to_string(i) + ".json"),
|
||||
"Good " + std::to_string(i), std::string());
|
||||
fs::create_directories(dir);
|
||||
std::ofstream((dir / "Broken.json").string()) << "{not-json";
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
|
||||
CHECK(coll.error_count() == 1);
|
||||
CHECK(coll.find_preset("Broken") == nullptr);
|
||||
CHECK_FALSE(fs::exists(dir / "Broken.json"));
|
||||
for (int i = 0; i < 20; ++ i)
|
||||
CHECK(coll.find_preset("Good " + std::to_string(i)) != nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("A user filament naming no compatible printer gets the one after its @, in memory and on disk", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
PresetBundle bundle;
|
||||
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
|
||||
REQUIRE(bundle.filaments.default_preset().config.option<ConfigOptionStrings>("compatible_printers")->values.empty());
|
||||
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
|
||||
const std::vector<std::string> expected { "Test Printer" };
|
||||
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
|
||||
REQUIRE(preset != nullptr);
|
||||
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == expected);
|
||||
|
||||
DynamicPrintConfig saved;
|
||||
std::map<std::string, std::string> key_values;
|
||||
std::string reason;
|
||||
saved.load_from_json(file.string(), ForwardCompatibilitySubstitutionRule::EnableSilent, key_values, reason);
|
||||
REQUIRE(reason.empty());
|
||||
REQUIRE(saved.option<ConfigOptionStrings>("compatible_printers") != nullptr);
|
||||
CHECK(saved.option<ConfigOptionStrings>("compatible_printers")->values == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("A user preset's setting id equal to its base id is dropped in memory, not in the .info written back", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
PresetBundle bundle;
|
||||
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
|
||||
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
|
||||
fs::path info = file;
|
||||
info.replace_extension(".info");
|
||||
std::ofstream(info.string()) << "sync_info = \nuser_id = \nsetting_id = PFUS1\nbase_id = PFUS1\nupdated_time = 0\n";
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
|
||||
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
|
||||
REQUIRE(preset != nullptr);
|
||||
CHECK(preset->setting_id.empty());
|
||||
CHECK(preset->base_id == "PFUS1");
|
||||
Preset reloaded(Preset::TYPE_FILAMENT, "My PLA @Test Printer");
|
||||
reloaded.load_info(info.string());
|
||||
CHECK(reloaded.setting_id == "PFUS1");
|
||||
}
|
||||
|
||||
TEST_CASE("A user preset that is not loaded still reports its substituted values", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
RenameTestCollection coll;
|
||||
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||
fs::create_directories(dir);
|
||||
std::ofstream((dir / "Orphan.json").string())
|
||||
<< R"({"type":"process","name":"Orphan","from":"User","version":"1.0.0","inherits":"No Such Parent",)"
|
||||
<< R"("wall_generator":"no_such_generator"})";
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::Enable);
|
||||
|
||||
CHECK(coll.find_preset("Orphan") == nullptr);
|
||||
CHECK(coll.error_count() == 1);
|
||||
REQUIRE(substitutions.size() == 1);
|
||||
CHECK(substitutions.front().preset_name == "Orphan");
|
||||
}
|
||||
|
||||
TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]")
|
||||
{
|
||||
RenameTestCollection coll;
|
||||
|
||||
@@ -3,12 +3,21 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/crc.hpp>
|
||||
#include <cereal/archives/binary.hpp>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/task_group.h>
|
||||
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PresetCacheFormat.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
@@ -52,6 +61,27 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std
|
||||
<< 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<std::pair<std::string, std::string>>& 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
|
||||
@@ -128,6 +158,80 @@ void write_lib_tree(const fs::path& dir, const std::string& version, const std::
|
||||
<< R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})";
|
||||
}
|
||||
|
||||
struct FixtureEntry {
|
||||
std::string name, sub_path;
|
||||
std::vector<size_t> 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<FixtureEntry> write_layered_filaments(const fs::path& dir, const std::string& vendor, int leaves)
|
||||
{
|
||||
fs::create_directories(dir / vendor / "filament");
|
||||
std::vector<FixtureEntry> 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<size_t>{parent, 2} : std::vector<size_t>{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<FixtureEntry>& entries,
|
||||
std::vector<size_t> 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<size_t> shuffled_after_dependencies(const std::vector<FixtureEntry>& entries, std::mt19937& rng)
|
||||
{
|
||||
std::vector<size_t> order;
|
||||
std::vector<bool> listed(entries.size(), false);
|
||||
while (order.size() < entries.size()) {
|
||||
std::vector<size_t> 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<size_t>(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)
|
||||
@@ -624,6 +728,299 @@ TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "
|
||||
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<ConfigOptionFloats>("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<FixtureEntry> entries = write_layered_filaments(dirs.system, "Acme", 150);
|
||||
auto load = [&](const std::vector<size_t>& order) {
|
||||
write_filament_list(dirs.system, "Acme", entries, order);
|
||||
auto bundle = std::make_unique<PresetBundle>();
|
||||
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<std::string> 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<std::string>{ "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<std::string> 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<std::string>{ 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<bool> 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<std::string>& 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<std::string> names;
|
||||
for (const Preset& preset : bundle.filaments.get_presets())
|
||||
if (! preset.is_default)
|
||||
names.push_back(preset.name);
|
||||
CHECK(names == std::vector<std::string>{ "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<PresetBundle>();
|
||||
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<ConfigOptionFloats>("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<PresetBundle> 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;
|
||||
@@ -866,6 +1263,27 @@ TEST_CASE("a vendor installed as its cache alone still loads after a library upd
|
||||
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<size_t>(0, 1), [&](const tbb::blocked_range<size_t>&) {
|
||||
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;
|
||||
@@ -893,6 +1311,12 @@ TEST_CASE("a cache entry whose parent is missing falls back to the vendor's 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]")
|
||||
@@ -1691,6 +2115,31 @@ TEST_CASE("an include listed after the preset that names it is an error, and the
|
||||
CHECK(silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>{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<ConfigOptionFloats>("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;
|
||||
@@ -1712,3 +2161,35 @@ TEST_CASE("a G-code template that states no instantiation is included, not loade
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user