fix: collapse plugincapabilityid

This commit is contained in:
Ian Chua
2026-07-16 18:18:07 +08:00
parent b6f98d9592
commit f4414dd72b
14 changed files with 361 additions and 428 deletions

View File

@@ -2,6 +2,7 @@
#include <libslic3r/Utils.hpp>
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include "plugin_test_utils.hpp"
@@ -17,6 +18,11 @@ using json = nlohmann::json;
namespace {
PluginCapabilityId capability_id(PluginCapabilityType type, const char* name, const char* plugin_key)
{
return {type, name, plugin_key};
}
json read_config_file()
{
boost::nowide::ifstream ifs(PluginConfig::plugin_config_file().c_str());
@@ -40,24 +46,24 @@ TEST_CASE("PluginConfig creates, reads back and persists a capability config", "
ScopedDataDir data_dir_guard("plugin-config-roundtrip");
PluginConfig config;
const PluginCapabilityId id = capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a");
// A capability nobody has configured yet reads as an empty record rather than throwing.
CHECK_FALSE(config.has_config("plugin_a", "cap_a"));
CHECK(config.get_config("plugin_a", "cap_a").empty());
CHECK_FALSE(config.has_config(id));
CHECK_FALSE(config.get_config(id));
REQUIRE(config.store_capability_config("plugin_a", "cap_a", json{{"speed", 5}}));
REQUIRE(config.store_capability_config(id, json{{"speed", 5}}));
const BaseConfig stored = config.get_config("plugin_a", "cap_a");
REQUIRE_FALSE(stored.empty());
CHECK(stored.plugin_key == "plugin_a");
CHECK(stored.capability_name == "cap_a");
CHECK(stored.config == json{{"speed", 5}});
CHECK(config.has_config("plugin_a", "cap_a"));
const auto stored = config.get_config(id);
REQUIRE(stored);
CHECK(stored->id == id);
CHECK(stored->config == json{{"speed", 5}});
CHECK(config.has_config(id));
// store_capability_config writes through, so a fresh instance (a restart, in effect) sees it.
PluginConfig reloaded;
reloaded.load();
CHECK(reloaded.get_config("plugin_a", "cap_a").config == json{{"speed", 5}});
CHECK(reloaded.get_config(id)->config == json{{"speed", 5}});
}
TEST_CASE("PluginConfig updates only the target capability's cap_config", "[PluginConfig]")
@@ -65,23 +71,44 @@ TEST_CASE("PluginConfig updates only the target capability's cap_config", "[Plug
ScopedDataDir data_dir_guard("plugin-config-isolation");
PluginConfig config;
// The identity is the (plugin_key, capability) pair, so all three below are separate records.
REQUIRE(config.store_capability_config("plugin_a", "cap_a", json{{"value", 1}}));
REQUIRE(config.store_capability_config("plugin_a", "cap_b", json{{"value", 2}}));
REQUIRE(config.store_capability_config("plugin_b", "cap_a", json{{"value", 3}}));
const PluginCapabilityId a_a = capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a");
const PluginCapabilityId a_b = capability_id(PluginCapabilityType::Script, "cap_b", "plugin_a");
const PluginCapabilityId b_a = capability_id(PluginCapabilityType::Script, "cap_a", "plugin_b");
// The identity is the full (type, capability, plugin_key) tuple, so all three below are separate records.
REQUIRE(config.store_capability_config(a_a, json{{"value", 1}}));
REQUIRE(config.store_capability_config(a_b, json{{"value", 2}}));
REQUIRE(config.store_capability_config(b_a, json{{"value", 3}}));
REQUIRE(config.store_capability_config("plugin_a", "cap_a", json{{"value", 99}}));
REQUIRE(config.store_capability_config(a_a, json{{"value", 99}}));
CHECK(config.get_config("plugin_a", "cap_a").config == json{{"value", 99}});
CHECK(config.get_config("plugin_a", "cap_b").config == json{{"value", 2}});
CHECK(config.get_config("plugin_b", "cap_a").config == json{{"value", 3}});
CHECK(config.get_config(a_a)->config == json{{"value", 99}});
CHECK(config.get_config(a_b)->config == json{{"value", 2}});
CHECK(config.get_config(b_a)->config == json{{"value", 3}});
// The same holds on disk, not just in memory.
PluginConfig reloaded;
reloaded.load();
CHECK(reloaded.get_config("plugin_a", "cap_a").config == json{{"value", 99}});
CHECK(reloaded.get_config("plugin_a", "cap_b").config == json{{"value", 2}});
CHECK(reloaded.get_config("plugin_b", "cap_a").config == json{{"value", 3}});
CHECK(reloaded.get_config(a_a)->config == json{{"value", 99}});
CHECK(reloaded.get_config(a_b)->config == json{{"value", 2}});
CHECK(reloaded.get_config(b_a)->config == json{{"value", 3}});
}
TEST_CASE("PluginConfig isolates same-name capabilities by type", "[PluginConfig]")
{
ScopedDataDir data_dir_guard("plugin-config-type-isolation");
const PluginCapabilityId script = capability_id(PluginCapabilityType::Script, "shared", "plugin_a");
const PluginCapabilityId importer = capability_id(PluginCapabilityType::Importer, "shared", "plugin_a");
PluginConfig config;
REQUIRE(config.store_capability_config(script, json{{"value", "script"}}));
REQUIRE(config.store_capability_config(importer, json{{"value", "importer"}}));
CHECK(config.get_config(script)->config == json{{"value", "script"}});
CHECK(config.get_config(importer)->config == json{{"value", "importer"}});
PluginConfig reloaded;
reloaded.load();
CHECK(reloaded.get_config(script)->config == json{{"value", "script"}});
CHECK(reloaded.get_config(importer)->config == json{{"value", "importer"}});
}
TEST_CASE("PluginConfig serializes the documented on-disk schema", "[PluginConfig]")
@@ -89,7 +116,8 @@ TEST_CASE("PluginConfig serializes the documented on-disk schema", "[PluginConfi
ScopedDataDir data_dir_guard("plugin-config-schema");
PluginConfig config;
REQUIRE(config.store_capability_config("plugin_a", "cap_a", json{{"speed", 5}}));
const PluginCapabilityId id = capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a");
REQUIRE(config.store_capability_config(id, json{{"speed", 5}}));
// Locks the field names: an existing config.json must keep loading after any future change.
const json root = read_config_file();
@@ -100,10 +128,11 @@ TEST_CASE("PluginConfig serializes the documented on-disk schema", "[PluginConfi
const json& entry = root.at("config").front();
CHECK(entry.at("plugin_key") == "plugin_a");
CHECK(entry.at("capability") == "cap_a");
CHECK(entry.at("capability_type") == "script");
CHECK(entry.at("cap_config") == json{{"speed", 5}});
CHECK(entry.contains("plugin_version"));
// Only cap_config is user data; the rest of the record is host-managed.
CHECK(entry.size() == 4);
CHECK(entry.size() == 5);
}
TEST_CASE("PluginConfig keeps a capability's config after its plugin goes away", "[PluginConfig]")
@@ -112,14 +141,14 @@ TEST_CASE("PluginConfig keeps a capability's config after its plugin goes away",
{
PluginConfig config;
REQUIRE(config.store_capability_config("plugin_a", "cap_a", json{{"token", "keep me"}}));
REQUIRE(config.store_capability_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"), json{{"token", "keep me"}}));
}
// config.json is deliberately not keyed to installed plugins: a record outlives its plugin and is
// still there on reinstall. Asserts no cleanup path silently drops it.
PluginConfig after_removal;
after_removal.load();
CHECK(after_removal.get_config("plugin_a", "cap_a").config == json{{"token", "keep me"}});
CHECK(after_removal.get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"))->config == json{{"token", "keep me"}});
}
TEST_CASE("PluginConfig treats a missing config file as an empty store", "[PluginConfig]")
@@ -130,7 +159,7 @@ TEST_CASE("PluginConfig treats a missing config file as an empty store", "[Plugi
PluginConfig config;
REQUIRE_NOTHROW(config.load());
CHECK_FALSE(config.has_config("plugin_a", "cap_a"));
CHECK_FALSE(config.has_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a")));
CHECK_FALSE(config.dirty());
}
@@ -143,7 +172,7 @@ TEST_CASE("PluginConfig survives a malformed config file", "[PluginConfig]")
PluginConfig config;
REQUIRE_NOTHROW(config.load()); // a bad config must not block startup
CHECK_FALSE(config.has_config("plugin_a", "cap_a"));
CHECK_FALSE(config.has_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a")));
}
SECTION("valid JSON without the entries array")
@@ -153,7 +182,7 @@ TEST_CASE("PluginConfig survives a malformed config file", "[PluginConfig]")
PluginConfig config;
REQUIRE_NOTHROW(config.load());
CHECK_FALSE(config.has_config("plugin_a", "cap_a"));
CHECK_FALSE(config.has_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a")));
}
SECTION("entries without an identity are skipped, the rest still load")
@@ -161,26 +190,46 @@ TEST_CASE("PluginConfig survives a malformed config file", "[PluginConfig]")
ScopedDataDir data_dir_guard("plugin-config-partial");
write_config_file(R"({"config": [
{"cap_config": {"orphan": true}},
{"plugin_key": "plugin_a", "capability": "cap_a", "plugin_version": "1.0.0", "cap_config": {"kept": true}}
{"plugin_key": "plugin_a", "capability": "cap_a", "capability_type": "script", "plugin_version": "1.0.0", "cap_config": {"kept": true}}
]})");
PluginConfig config;
REQUIRE_NOTHROW(config.load());
CHECK(config.get_config("plugin_a", "cap_a").config == json{{"kept", true}});
CHECK(config.get_config("plugin_a", "cap_a").plugin_version == "1.0.0");
CHECK(config.get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"))->config == json{{"kept", true}});
CHECK(config.get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"))->plugin_version == "1.0.0");
}
SECTION("an entry with no cap_config reads as an empty object")
{
ScopedDataDir data_dir_guard("plugin-config-nocap");
write_config_file(R"({"config": [
{"plugin_key": "plugin_a", "capability": "cap_a", "plugin_version": "1.0.0"}
{"plugin_key": "plugin_a", "capability": "cap_a", "capability_type": "script", "plugin_version": "1.0.0"}
]})");
PluginConfig config;
REQUIRE_NOTHROW(config.load());
REQUIRE(config.has_config("plugin_a", "cap_a"));
CHECK(config.get_config("plugin_a", "cap_a").config == json::object());
REQUIRE(config.has_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a")));
CHECK(config.get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"))->config == json::object());
}
SECTION("legacy entries without capability_type remain addressable")
{
ScopedDataDir data_dir_guard("plugin-config-notype");
write_config_file(R"({"config": [
{"plugin_key": "plugin_a", "capability": "cap_a", "plugin_version": "1.0.0", "cap_config": {"old": true}}
]})");
PluginConfig config;
REQUIRE_NOTHROW(config.load());
const auto id = capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a");
REQUIRE(config.has_config(id));
CHECK(config.get_config(id)->config == json{{"old", true}});
REQUIRE(config.store_capability_config(id, json{{"migrated", true}}));
const json root = read_config_file();
REQUIRE(root.at("config").size() == 1);
CHECK(root.at("config").front().at("capability_type") == "script");
CHECK(root.at("config").front().at("cap_config") == json{{"migrated", true}});
}
}
@@ -189,12 +238,12 @@ TEST_CASE("PluginConfig refuses to store a record without an identity", "[Plugin
ScopedDataDir data_dir_guard("plugin-config-identity");
PluginConfig config;
config.save_config(BaseConfig{"", "cap_a", "1.0.0", json::object()});
config.save_config(BaseConfig{"plugin_a", "", "1.0.0", json::object()});
config.save_config(CapabilityConfigEntry{capability_id(PluginCapabilityType::Script, "cap_a", ""), "1.0.0", json::object()});
config.save_config(CapabilityConfigEntry{capability_id(PluginCapabilityType::Script, "", "plugin_a"), "1.0.0", json::object()});
// Neither could ever be looked up again, so neither is kept.
CHECK_FALSE(config.has_config("", "cap_a"));
CHECK_FALSE(config.has_config("plugin_a", ""));
CHECK_FALSE(config.has_config(capability_id(PluginCapabilityType::Script, "cap_a", "")));
CHECK_FALSE(config.has_config(capability_id(PluginCapabilityType::Script, "", "plugin_a")));
CHECK_FALSE(config.dirty());
}
@@ -206,9 +255,10 @@ TEST_CASE("PluginConfig preserves unknown keys inside cap_config", "[PluginConfi
const json nested = json{{"nested", {{"deep", json::array({1, 2, 3})}}}, {"flag", false}, {"name", "x"}};
PluginConfig config;
REQUIRE(config.store_capability_config("plugin_a", "cap_a", nested));
const PluginCapabilityId id = capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a");
REQUIRE(config.store_capability_config(id, nested));
PluginConfig reloaded;
reloaded.load();
CHECK(reloaded.get_config("plugin_a", "cap_a").config == nested);
CHECK(reloaded.get_config(id)->config == nested);
}