add profile validator checks

This commit is contained in:
SoftFever
2026-07-01 21:55:19 +08:00
parent 57297d5ab1
commit f516f47c8e
5 changed files with 211 additions and 18 deletions

View File

@@ -57,6 +57,20 @@ jobs:
set +e set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log ./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v BBL -f 2>&1 | tee ${{ runner.temp }}/validate_filament_subtypes.log
exit ${PIPESTATUS[0]} exit ${PIPESTATUS[0]}
# Flag inherits/compatible_printers/compatible_prints references that point at a deleted or
# renamed preset. Opt-in per vendor for now (via -r); enabled for BBL and Qidi until other
# vendors' profiles are cleaned up. Runs before the custom-preset injection below.
- name: validate preset references for BBL and Qidi profiles
id: validate_preset_references
continue-on-error: true
run: |
set +e
rc=0
for v in BBL Qidi; do
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 -v "$v" -r 2>&1 | tee -a ${{ runner.temp }}/validate_preset_references.log
[ ${PIPESTATUS[0]} -ne 0 ] && rc=1
done
exit $rc
- name: validate custom presets - name: validate custom presets
id: validate_custom id: validate_custom
@@ -76,7 +90,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact - name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_preset_references.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
{ {
# Marker matched by check_profiles_comment.yml to delete prior comments. # Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -111,6 +125,15 @@ jobs:
echo "" echo ""
fi fi
if [ "${{ steps.validate_preset_references.outcome }}" = "failure" ]; then
echo "### BBL/Qidi Preset Reference Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_preset_references.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then
echo "### Custom Preset Validation Failed" echo "### Custom Preset Validation Failed"
echo "" echo ""
@@ -133,7 +156,7 @@ jobs:
retention-days: 1 retention-days: 1
- name: Fail if any check failed - name: Fail if any check failed
if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_preset_references.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
echo "One or more profile checks failed. See above for details." echo "One or more profile checks failed. See above for details."
exit 1 exit 1

View File

@@ -96,6 +96,7 @@ int main(int argc, char* argv[])
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified") ("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test") ("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.") ("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
("check_preset_references,r", po::bool_switch()->default_value(false), "Also flag presets whose inherits/compatible_printers/compatible_prints reference a deleted or renamed preset. Off unless this flag is present.")
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs."); ("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
// clang-format on // clang-format on
@@ -120,6 +121,7 @@ int main(int argc, char* argv[])
int log_level = vm["log_level"].as<int>(); int log_level = vm["log_level"].as<int>();
bool generate_user_preset = vm["generate_presets"].as<bool>(); bool generate_user_preset = vm["generate_presets"].as<bool>();
bool check_filament_subtypes = vm["check_filament_subtypes"].as<bool>(); bool check_filament_subtypes = vm["check_filament_subtypes"].as<bool>();
bool check_preset_references = vm["check_preset_references"].as<bool>();
// check if path is valid, and return error if not // check if path is valid, and return error if not
if (!fs::exists(path) || !fs::is_directory(path)) { if (!fs::exists(path) || !fs::is_directory(path)) {
@@ -166,7 +168,7 @@ int main(int argc, char* argv[])
return 0; return 0;
} }
if (preset_bundle->has_errors(check_filament_subtypes)) { if (preset_bundle->has_errors(check_filament_subtypes, check_preset_references)) {
std::cout << "Validation failed" << std::endl; std::cout << "Validation failed" << std::endl;
return 1; return 1;
} }

View File

@@ -532,8 +532,16 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward
load_user_presets(dir_user_presets, substitution_rule); load_user_presets(dir_user_presets, substitution_rule);
} }
// Rewrite renamed compatible_printers / compatible_prints references before selection. // Rewrite renamed compatible_printers / compatible_prints references before selection. Skipped
this->normalize_compatible_presets(); // in validation mode so the profile validator (has_errors -> check_preset_references) sees the
// raw vendor-JSON references instead of the silently-repaired ones.
if (!validation_mode)
this->normalize_compatible_presets();
// Rewrite renamed compatible_printers / compatible_prints references before selection. Skipped
// in validation mode so the profile validator (has_errors -> check_preset_references) sees the
// raw vendor-JSON references instead of the silently-repaired ones.
if (!validation_mode)
this->normalize_compatible_presets();
this->update_multi_material_filament_presets(); this->update_multi_material_filament_presets();
this->update_compatible(PresetSelectCompatibleType::Never); this->update_compatible(PresetSelectCompatibleType::Never);
@@ -5278,21 +5286,20 @@ static void normalize_compatible_field(Preset &preset, const char *field_key, Pr
void PresetBundle::normalize_compatible_presets() void PresetBundle::normalize_compatible_presets()
{ {
// compatible_printers references a printer preset; compatible_prints (filaments / SLA materials) // compatible_printers references a printer preset; compatible_prints (filaments / SLA materials)
// references a process preset. Skip system presets: they are the targets of renames, not holders // references a process preset. System presets are normalized too: a vendor profile can itself
// of stale references, and their vendor data must not be mutated. (begin()/end() skip defaults.) // reference a sibling preset by a name that was later renamed, and the rewrite is in-memory only
auto normalize = [](PresetCollection &holders, PresetCollection &printers, PresetCollection *processes) { // (system presets are never persisted back to vendor JSON). (begin()/end() skip defaults.)
auto normalize = [this](PresetCollection &holders, PresetCollection *processes) {
for (Preset &p : holders) { for (Preset &p : holders) {
if (p.is_system) normalize_compatible_field(p, "compatible_printers", this->printers);
continue;
normalize_compatible_field(p, "compatible_printers", printers);
if (processes != nullptr) if (processes != nullptr)
normalize_compatible_field(p, "compatible_prints", *processes); normalize_compatible_field(p, "compatible_prints", *processes);
} }
}; };
normalize(this->prints, this->printers, nullptr); normalize(this->prints, nullptr);
normalize(this->filaments, this->printers, &this->prints); normalize(this->filaments, &this->prints);
normalize(this->sla_prints, this->printers, nullptr); normalize(this->sla_prints, nullptr);
normalize(this->sla_materials, this->printers, &this->sla_prints); normalize(this->sla_materials, &this->sla_prints);
} }
void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible) void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible)
@@ -5526,7 +5533,8 @@ void PresetBundle::set_default_suppressed(bool default_suppressed)
printers.set_default_suppressed(default_suppressed); printers.set_default_suppressed(default_suppressed);
} }
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes, bool check_references) const
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes, bool check_references) const
{ {
if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0) if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0)
return true; return true;
@@ -5549,6 +5557,12 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
if (check_duplicate_filament_subtypes && this->check_duplicate_filament_subtypes()) if (check_duplicate_filament_subtypes && this->check_duplicate_filament_subtypes())
has_errors = true; has_errors = true;
if (check_references && this->check_preset_references())
has_errors = true;
if (check_references && this->check_preset_references())
has_errors = true;
return has_errors; return has_errors;
} }
@@ -5579,6 +5593,68 @@ static std::string preset_file_uri(const std::string &file)
return uri; return uri;
} }
// Orca: validator-only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a name that no longer resolves. Uses find_preset (exact match, then
// the renamed_from map - no fuzzy find_preset2, no alias resolution), so:
// nullptr -> the referenced preset was deleted/renamed away (name is dangling),
// resolved != name -> the reference uses an old name that renamed_from maps to a current one.
// Both should be fixed at the source rather than relying on load-time normalization - which is why
// normalize_compatible_presets() is skipped in validation mode (see load_presets), so this sees the
// raw vendor-JSON references. Safe under a single-vendor run (-v) too: inherits / compatible_printers
// / compatible_prints only name same-vendor or OrcaFilamentLibrary presets (both loaded), so a
// reference that does not resolve is genuinely dangling rather than an unloaded cross-vendor preset.
bool PresetBundle::check_preset_references() const
{
bool found = false;
// Resolve one reference (an inherits parent or a compatible_* entry) against its target
// collection and log if it is dangling (unknown) or uses a renamed preset's old name.
auto report_ref = [&](const Preset &p, const std::string &name, const PresetCollection &target,
const char *verb, const char *noun) {
const Preset *resolved = target.find_preset(name, false);
if (resolved == nullptr) {
found = true;
BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " unknown " << noun << " \"" << name << "\":\n"
<< preset_file_uri(p.file);
} else if (resolved->name != name) {
found = true;
BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " renamed " << noun << " \"" << name
<< "\" (now \"" << resolved->name << "\"):\n" << preset_file_uri(p.file);
}
};
auto check_list = [&](const Preset &p, const char *key, const PresetCollection &target) {
const auto *opt = p.config.option<ConfigOptionStrings>(key);
if (opt == nullptr)
return;
for (const std::string &name : opt->values)
if (!name.empty())
report_ref(p, name, target, "references", key);
};
auto check_collection = [&](const PresetCollection &holders, const PresetCollection *processes) {
for (const Preset &p : holders) {
if (!p.is_system)
continue;
if (const std::string &inh = p.inherits(); !inh.empty())
report_ref(p, inh, holders, "inherits", "parent");
check_list(p, "compatible_printers", this->printers);
if (processes != nullptr)
check_list(p, "compatible_prints", *processes);
}
};
// Printers carry no compatible_printers/compatible_prints (those name a printer, so a printer
// holding them makes no sense); check_list is a no-op for them, so only their inherits is checked.
check_collection(this->printers, nullptr);
check_collection(this->prints, nullptr);
check_collection(this->filaments, &this->prints);
check_collection(this->sla_prints, nullptr);
check_collection(this->sla_materials, &this->sla_prints);
return found;
}
// Orca: a filament is matched from the AMS by (filament_id + printer compatibility). // Orca: a filament is matched from the AMS by (filament_id + printer compatibility).
// For any one printer, at most one instantiated filament preset with a given // For any one printer, at most one instantiated filament preset with a given
// filament_id may be compatible - otherwise the AMS match is ambiguous and the // filament_id may be compatible - otherwise the AMS match is ambiguous and the

View File

@@ -485,8 +485,13 @@ public:
return { Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL }; return { Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL };
} }
// Orca: for validation only. The duplicate filament subtype check is opt-in for now // Orca: for validation only. The duplicate filament subtype and preset-reference checks are
bool has_errors(bool check_duplicate_filament_subtypes = false) const; // opt-in for now (enabled per-vendor by the profile-check CI as vendors are cleaned up).
bool has_errors(bool check_duplicate_filament_subtypes = false, bool check_preset_references = false) const;
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
bool check_preset_references() const;
private: private:
// Orca: validation only - flag any printer with two or more compatible // Orca: validation only - flag any printer with two or more compatible

View File

@@ -355,6 +355,40 @@ TEST_CASE("Renamed printer/process names are normalized into compatible lists on
CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector<std::string>{ "New Printer" }); CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector<std::string>{ "New Printer" });
} }
TEST_CASE("Renamed names are normalized into a SYSTEM preset's compatible lists", "[Preset][Rename]")
{
PresetBundle bundle;
// Current printer + process, each renamed from an older name.
add_inmemory_preset(bundle.printers, "New Printer");
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
add_inmemory_preset(bundle.prints, "New Process");
set_renamed_from(bundle.prints, "New Process", { "Old Process" });
// A *system* (vendor) filament whose own compatible lists still reference the OLD names. A vendor
// profile can point at a sibling preset that was later renamed, so system presets must be
// normalized too (they are skipped by neither collection walk).
add_inmemory_preset(bundle.filaments, "System Filament").is_system = true;
compatible_list(bundle.filaments, "System Filament", "compatible_printers") = { "Old Printer" };
compatible_list(bundle.filaments, "System Filament", "compatible_prints") = { "Old Process" };
AppConfig app_config;
bundle.load_installed_printers(app_config); // build the rename maps
bundle.normalize_compatible_presets();
// The stale references in the system preset are rewritten to the current names.
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") ==
std::vector<std::string>{ "New Printer" });
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_prints") ==
std::vector<std::string>{ "New Process" });
// The rewrite does not flag the system preset dirty, and is idempotent.
CHECK_FALSE(bundle.filaments.find_preset("System Filament", false, true)->is_dirty);
bundle.normalize_compatible_presets();
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") ==
std::vector<std::string>{ "New Printer" });
}
TEST_CASE("compatible_prints on SLA materials resolves against sla_prints, not prints", "[Preset][Rename]") TEST_CASE("compatible_prints on SLA materials resolves against sla_prints, not prints", "[Preset][Rename]")
{ {
PresetBundle bundle; PresetBundle bundle;
@@ -377,3 +411,56 @@ TEST_CASE("compatible_prints on SLA materials resolves against sla_prints, not p
std::vector<std::string>{ "New SLA Process" }); std::vector<std::string>{ "New SLA Process" });
} }
TEST_CASE("Profile validator flags dangling and renamed preset references", "[Preset][Validate]")
{
PresetBundle bundle;
// Current printers: a real one, and a renamed one (its old name resolves via renamed_from).
add_inmemory_preset(bundle.printers, "Real Printer");
add_inmemory_preset(bundle.printers, "New Printer");
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
// A real process, referenced from a filament's compatible_prints.
add_inmemory_preset(bundle.prints, "Real Process").is_system = true;
// A fully valid system filament: references only current names.
add_inmemory_preset(bundle.filaments, "Good Filament").is_system = true;
compatible_list(bundle.filaments, "Good Filament", "compatible_printers") = { "Real Printer" };
compatible_list(bundle.filaments, "Good Filament", "compatible_prints") = { "Real Process" };
AppConfig app_config;
bundle.load_installed_printers(app_config); // build the rename maps
// With only valid references, the validator is clean.
CHECK_FALSE(bundle.check_preset_references());
SECTION("deleted compatible_printers is flagged") {
add_inmemory_preset(bundle.filaments, "Ghost Ref Filament").is_system = true;
compatible_list(bundle.filaments, "Ghost Ref Filament", "compatible_printers") = { "Ghost Printer" };
CHECK(bundle.check_preset_references());
}
SECTION("renamed compatible_printers (old name) is flagged") {
add_inmemory_preset(bundle.filaments, "Old Ref Filament").is_system = true;
compatible_list(bundle.filaments, "Old Ref Filament", "compatible_printers") = { "Old Printer" };
CHECK(bundle.check_preset_references());
}
SECTION("deleted compatible_prints is flagged") {
add_inmemory_preset(bundle.filaments, "Bad Process Ref").is_system = true;
compatible_list(bundle.filaments, "Bad Process Ref", "compatible_prints") = { "Ghost Process" };
CHECK(bundle.check_preset_references());
}
SECTION("deleted inherits parent is flagged") {
add_inmemory_preset(bundle.filaments, "Orphan Filament", "Ghost Parent").is_system = true;
CHECK(bundle.check_preset_references());
}
SECTION("non-system preset with a dangling reference is ignored") {
add_inmemory_preset(bundle.filaments, "User Filament"); // is_system stays false
compatible_list(bundle.filaments, "User Filament", "compatible_printers") = { "Ghost Printer" };
CHECK_FALSE(bundle.check_preset_references());
}
}