#include #include #include #include using Slic3r::GUI::compare_ascii_case_insensitive_natural; using Slic3r::GUI::PluginSortKey; using Slic3r::GUI::PluginSortOrder; using Slic3r::GUI::PluginSource; using Slic3r::GUI::PluginStatus; using Slic3r::GUI::plugin_sort_key_from_string; using Slic3r::GUI::plugin_sort_order_from_string; using Slic3r::GUI::sort_plugin_items_for_dialog; namespace { struct SortFixtureItem { std::string plugin_key; PluginSource source; PluginStatus status; std::string type_key; std::string display_name; std::string sort_version; }; std::vector keys(const std::vector& items) { std::vector result; result.reserve(items.size()); for (const SortFixtureItem& item : items) result.push_back(item.plugin_key); return result; } } // namespace TEST_CASE("plugin dialog status sort uses requested priority and base-order ties", "[plugin][sort]") { std::vector items = { {"local_inactive", PluginSource::Local, PluginStatus::Inactive, "script", "Local Inactive"}, {"mine_error", PluginSource::Mine, PluginStatus::Error, "script", "Mine Error"}, {"mine_activated", PluginSource::Mine, PluginStatus::Activated, "script", "Mine Activated"}, {"local_activated", PluginSource::Local, PluginStatus::Activated, "script", "Local Activated"}, {"subscribed_loading", PluginSource::Subscribed, PluginStatus::Loading, "script", "Subscribed Loading"}, }; sort_plugin_items_for_dialog(items, PluginSortKey::Status, PluginSortOrder::Asc); // why: local_activated and mine_activated tie on Status, so base order breaks the tie by // declared source priority - Mine (ordinal 0) before Local (ordinal 2). const std::vector expected = { "mine_activated", "local_activated", "mine_error", "local_inactive", "subscribed_loading", }; CHECK(keys(items) == expected); sort_plugin_items_for_dialog(items, PluginSortKey::Status, PluginSortOrder::Desc); // why: Desc reverses the status ordinal, but the Activated tie still resolves by ascending // base order (Mine before Local) - the direction only flips the primary key. const std::vector desc_expected = { "subscribed_loading", "local_inactive", "mine_error", "mine_activated", "local_activated", }; CHECK(keys(items) == desc_expected); } TEST_CASE("plugin dialog source sort uses enum priority", "[plugin][sort]") { std::vector items = { {"local", PluginSource::Local, PluginStatus::Activated, "script", "Local"}, {"mine", PluginSource::Mine, PluginStatus::Activated, "script", "Mine"}, {"subscribed", PluginSource::Subscribed, PluginStatus::Activated, "script", "Subscribed"}, }; sort_plugin_items_for_dialog(items, PluginSortKey::Source, PluginSortOrder::Asc); const std::vector asc_expected = {"mine", "subscribed", "local"}; CHECK(keys(items) == asc_expected); sort_plugin_items_for_dialog(items, PluginSortKey::Source, PluginSortOrder::Desc); const std::vector desc_expected = {"local", "subscribed", "mine"}; CHECK(keys(items) == desc_expected); } TEST_CASE("plugin dialog version sort is semver-aware with base-order ties", "[plugin][sort]") { std::vector items = { {"v_1_2_0", PluginSource::Local, PluginStatus::Activated, "script", "B", "1.2.0"}, {"v_1_10_0", PluginSource::Local, PluginStatus::Activated, "script", "A", "1.10.0"}, {"v_0_9_3", PluginSource::Local, PluginStatus::Activated, "script", "C", "0.9.3"}, }; sort_plugin_items_for_dialog(items, PluginSortKey::Version, PluginSortOrder::Asc); // why: semver numeric compare - 1.10.0 > 1.2.0 (not lexical "1.10" < "1.2"), so ascending is // 0.9.3 < 1.2.0 < 1.10.0. const std::vector asc_expected = {"v_0_9_3", "v_1_2_0", "v_1_10_0"}; CHECK(keys(items) == asc_expected); sort_plugin_items_for_dialog(items, PluginSortKey::Version, PluginSortOrder::Desc); const std::vector desc_expected = {"v_1_10_0", "v_1_2_0", "v_0_9_3"}; CHECK(keys(items) == desc_expected); } TEST_CASE("plugin dialog name sort is case-insensitive and numeric-aware", "[plugin][sort]") { std::vector items = { {"rig10", PluginSource::Local, PluginStatus::Activated, "script", "Rig 10"}, {"ada_lower", PluginSource::Local, PluginStatus::Activated, "script", "ada"}, {"rig2", PluginSource::Local, PluginStatus::Activated, "script", "Rig 2"}, {"ada_upper", PluginSource::Local, PluginStatus::Activated, "script", "Ada"}, }; sort_plugin_items_for_dialog(items, PluginSortKey::Name, PluginSortOrder::Asc); const std::vector expected = {"ada_upper", "ada_lower", "rig2", "rig10"}; CHECK(keys(items) == expected); sort_plugin_items_for_dialog(items, PluginSortKey::Name, PluginSortOrder::Desc); // why: names reverse ("Rig 10" before "Rig 2"), but "Ada"/"ada" tie on the case-insensitive // key and keep ascending base order (case-sensitive "Ada" < "ada"). const std::vector desc_expected = {"rig10", "rig2", "ada_upper", "ada_lower"}; CHECK(keys(items) == desc_expected); } TEST_CASE("natural compare handles digits, case, prefixes and leading zeros", "[plugin][sort]") { // numeric runs compare by value, not lexically CHECK(compare_ascii_case_insensitive_natural("item2", "item10") < 0); CHECK(compare_ascii_case_insensitive_natural("item10", "item2") > 0); CHECK(compare_ascii_case_insensitive_natural("2", "10") < 0); // case is ignored on the primary comparison CHECK(compare_ascii_case_insensitive_natural("Camera", "camera") == 0); // a prefix is less than the longer string it prefixes CHECK(compare_ascii_case_insensitive_natural("app", "apple") < 0); CHECK(compare_ascii_case_insensitive_natural("apple", "app") > 0); // equal numeric value: fewer leading zeros wins the tie CHECK(compare_ascii_case_insensitive_natural("1", "01") < 0); CHECK(compare_ascii_case_insensitive_natural("01", "1") > 0); // reflexivity and empty-string boundaries CHECK(compare_ascii_case_insensitive_natural("plugin", "plugin") == 0); CHECK(compare_ascii_case_insensitive_natural("", "") == 0); CHECK(compare_ascii_case_insensitive_natural("", "a") < 0); } TEST_CASE("plugin dialog None sort key falls to ascending base order in both directions", "[plugin][sort]") { std::vector items = { {"local_z", PluginSource::Local, PluginStatus::Activated, "script", "Zeta"}, {"mine_a", PluginSource::Mine, PluginStatus::Inactive, "script", "Alpha"}, {"sub_m", PluginSource::Subscribed, PluginStatus::Error, "script", "Mu"}, }; // why: base order is source priority (Mine, Subscribed, Local) then name - and it ignores the // requested status/order entirely, so the neutral baseline is deterministic. const std::vector base_expected = {"mine_a", "sub_m", "local_z"}; sort_plugin_items_for_dialog(items, PluginSortKey::None, PluginSortOrder::Asc); CHECK(keys(items) == base_expected); // why: None has no direction - Desc must not reverse the baseline. sort_plugin_items_for_dialog(items, PluginSortKey::None, PluginSortOrder::Desc); CHECK(keys(items) == base_expected); } TEST_CASE("plugin dialog sort request parsing keeps previous state on invalid values", "[plugin][sort]") { CHECK(plugin_sort_key_from_string("source", PluginSortKey::Status) == PluginSortKey::Source); CHECK(plugin_sort_key_from_string("none", PluginSortKey::Status) == PluginSortKey::None); CHECK(plugin_sort_key_from_string("missing", PluginSortKey::Name) == PluginSortKey::Name); CHECK(plugin_sort_order_from_string("desc", PluginSortOrder::Asc) == PluginSortOrder::Desc); CHECK(plugin_sort_order_from_string("down", PluginSortOrder::Asc) == PluginSortOrder::Asc); }