mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-16 07:24:53 +09:00
Write plugin versions while loading configs
The staging copy made by Config.set() went through the constructor, so plugin migrations ran a second time on the live fields. A version-gated migration was applied twice when setPluginVersions() changed the version. Staging now copies the fields with update() instead. A config saved without <plugin>.version got the running version as its default, so setPluginVersions() saw no change and nothing saved the stamp. The constructor now writes the registered plugin versions after the migrations run and marks the config dirty when that changes a loaded value. Controller.bootstrap() saves the instances that came out dirty, and setPluginVersions() is gone. Restructure the SyncHook and plugin config tests by method, and suggest the controller config version for role migrations in the docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aec0fc7a1e
commit
e455d17bb7
10 files changed
+188
-119
No files matched your search
+47
-34
@@ -10,43 +10,56 @@ describe("lib/SyncHook", function() {
|
||||
hook = new lib.SyncHook();
|
||||
});
|
||||
|
||||
it("should track attached handlers", function() {
|
||||
assert.equal(hook.size, 0);
|
||||
hook.attach("alpha", value => value);
|
||||
hook.attach("beta", value => value);
|
||||
assert.equal(hook.size, 2);
|
||||
assert.deepEqual([...hook.attached], ["alpha", "beta"]);
|
||||
describe(".attach()", function() {
|
||||
it("should track attached handlers", function() {
|
||||
assert.equal(hook.size, 0);
|
||||
hook.attach("alpha", value => value);
|
||||
hook.attach("beta", value => value);
|
||||
assert.equal(hook.size, 2);
|
||||
assert.deepEqual([...hook.attached], ["alpha", "beta"]);
|
||||
});
|
||||
it("should reject duplicate handler names", function() {
|
||||
hook.attach("alpha", value => value);
|
||||
assert.throws(
|
||||
() => hook.attach("alpha", value => value),
|
||||
new Error("Handler with name alpha is already attached")
|
||||
);
|
||||
});
|
||||
});
|
||||
it("should reject duplicate handler names", function() {
|
||||
hook.attach("alpha", value => value);
|
||||
assert.throws(
|
||||
() => hook.attach("alpha", value => value),
|
||||
new Error("Handler with name alpha is already attached")
|
||||
);
|
||||
describe(".detach()", function() {
|
||||
it("should remove the named handler", function() {
|
||||
hook.attach("alpha", value => value);
|
||||
hook.attach("beta", value => value);
|
||||
hook.detach("alpha");
|
||||
assert.deepEqual([...hook.attached], ["beta"]);
|
||||
});
|
||||
});
|
||||
it("should detach and clear handlers", function() {
|
||||
hook.attach("alpha", value => value);
|
||||
hook.attach("beta", value => value);
|
||||
hook.detach("alpha");
|
||||
assert.deepEqual([...hook.attached], ["beta"]);
|
||||
hook.clear();
|
||||
assert.equal(hook.size, 0);
|
||||
describe(".clear()", function() {
|
||||
it("should remove all handlers", function() {
|
||||
hook.attach("alpha", value => value);
|
||||
hook.attach("beta", value => value);
|
||||
hook.clear();
|
||||
assert.equal(hook.size, 0);
|
||||
});
|
||||
});
|
||||
it("should pass the value through handlers in attach order", function() {
|
||||
hook.attach("alpha", value => [...value, "alpha"]);
|
||||
hook.attach("beta", value => [...value, "beta"]);
|
||||
assert.deepEqual(hook.reduce([]), ["alpha", "beta"]);
|
||||
});
|
||||
it("should return the value unchanged with no handlers", function() {
|
||||
const value = {};
|
||||
assert.equal(hook.reduce(value), value);
|
||||
});
|
||||
it("should rethrow errors as a PluginError named after the handler", function() {
|
||||
hook.attach("alpha", () => { throw new Error("failed"); });
|
||||
assert.throws(
|
||||
() => hook.reduce(1),
|
||||
err => err instanceof lib.PluginError && err.pluginName === "alpha" && err.original.message === "failed"
|
||||
);
|
||||
describe(".reduce()", function() {
|
||||
it("should pass the value through handlers in attach order", function() {
|
||||
hook.attach("alpha", value => [...value, "alpha"]);
|
||||
hook.attach("beta", value => [...value, "beta"]);
|
||||
assert.deepEqual(hook.reduce([]), ["alpha", "beta"]);
|
||||
});
|
||||
it("should return the value unchanged with no handlers", function() {
|
||||
const value = {};
|
||||
assert.equal(hook.reduce(value), value);
|
||||
});
|
||||
it("should rethrow errors as a PluginError named after the handler", function() {
|
||||
hook.attach("alpha", () => { throw new Error("failed"); });
|
||||
assert.throws(
|
||||
() => hook.reduce(1),
|
||||
err => err instanceof lib.PluginError && err.pluginName === "alpha"
|
||||
&& err.original.message === "failed"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe("lib/config/definitions/migrations", function() {
|
||||
describe("Ctl Config", function() {
|
||||
|
||||
});
|
||||
describe("Plugin migrations", function() {
|
||||
describe("Plugin Config", function() {
|
||||
const pluginInfo = {
|
||||
name: "test_plugin",
|
||||
version: "1.2.0",
|
||||
@@ -59,6 +59,7 @@ describe("lib/config/definitions/migrations", function() {
|
||||
});
|
||||
afterEach(function() {
|
||||
lib.InstanceConfig.pluginMigrations.clear();
|
||||
lib.Config.pluginVersions.clear();
|
||||
});
|
||||
after(function() {
|
||||
for (const Config of [lib.ControllerConfig, lib.HostConfig, lib.InstanceConfig]) {
|
||||
@@ -70,44 +71,81 @@ describe("lib/config/definitions/migrations", function() {
|
||||
}
|
||||
});
|
||||
|
||||
it("should run attached migrations on load", function() {
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => (
|
||||
lib.renameConfigPrefix(config, "old_plugin.", "test_plugin.")
|
||||
));
|
||||
const config = lib.InstanceConfig.fromJSON({
|
||||
"old_plugin.load_plugin": false,
|
||||
"old_plugin.delay": 5,
|
||||
}, "controller");
|
||||
assert.equal(config.get("test_plugin.load_plugin"), false);
|
||||
assert.equal(config.get("test_plugin.delay"), 5);
|
||||
assert.equal(config.toJSON()["old_plugin.delay"], undefined);
|
||||
});
|
||||
it("should run plugin migrations after core migrations", function() {
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
|
||||
assert.equal(config["factorio.sync_adminlist"], "enabled");
|
||||
return config;
|
||||
describe("static .pluginMigrations", function() {
|
||||
it("should run on load", function() {
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => (
|
||||
lib.renameConfigPrefix(config, "old_plugin.", "test_plugin.")
|
||||
));
|
||||
const config = lib.InstanceConfig.fromJSON({
|
||||
"old_plugin.load_plugin": false,
|
||||
"old_plugin.delay": 5,
|
||||
}, "controller");
|
||||
assert.equal(config.get("test_plugin.load_plugin"), false);
|
||||
assert.equal(config.get("test_plugin.delay"), 5);
|
||||
assert.equal(config.toJSON()["old_plugin.delay"], undefined);
|
||||
});
|
||||
lib.InstanceConfig.fromJSON({ "factorio.sync_adminlist": true }, "controller");
|
||||
});
|
||||
it("should give migrations the version the config was saved with", function() {
|
||||
let seen;
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
|
||||
seen = config["test_plugin.version"];
|
||||
return config;
|
||||
it("should run after core migrations", function() {
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
|
||||
assert.equal(config["factorio.sync_adminlist"], "enabled");
|
||||
return config;
|
||||
});
|
||||
lib.InstanceConfig.fromJSON({ "factorio.sync_adminlist": true }, "controller");
|
||||
});
|
||||
it("should see the version the config was saved with", function() {
|
||||
let seen;
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
|
||||
seen = config["test_plugin.version"];
|
||||
return config;
|
||||
});
|
||||
lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.1.0" }, "controller");
|
||||
assert.equal(seen, "1.1.0");
|
||||
});
|
||||
it("should not run again when the config is changed", function() {
|
||||
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
|
||||
let calls = 0;
|
||||
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
|
||||
calls += 1;
|
||||
if (config["test_plugin.version"] === "1.1.0") {
|
||||
config["test_plugin.delay"] *= 60;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
const config = lib.InstanceConfig.fromJSON({
|
||||
"test_plugin.version": "1.1.0",
|
||||
"test_plugin.delay": 2,
|
||||
}, "controller");
|
||||
config.set("instance.name", "changed");
|
||||
assert.equal(config.get("test_plugin.delay"), 120);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
const config = lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.1.0" }, "controller");
|
||||
assert.equal(seen, "1.1.0");
|
||||
assert.equal(config.get("test_plugin.version"), "1.1.0");
|
||||
});
|
||||
it("should default the version of new configs to the loaded plugin version", function() {
|
||||
const config = new lib.InstanceConfig("controller");
|
||||
assert.equal(config.get("test_plugin.version"), "1.2.0");
|
||||
});
|
||||
it("should update versions with setPluginVersions", function() {
|
||||
const config = lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.1.0" }, "controller");
|
||||
assert.equal(lib.setPluginVersions(config, [pluginInfo]), true);
|
||||
assert.equal(config.get("test_plugin.version"), "1.2.0");
|
||||
assert.equal(lib.setPluginVersions(config, [pluginInfo]), false);
|
||||
describe("static .pluginVersions", function() {
|
||||
it("should write the version after migrations run", function() {
|
||||
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
|
||||
const config = lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.1.0" }, "controller");
|
||||
assert.equal(config.get("test_plugin.version"), "1.2.0");
|
||||
assert.equal(config.dirty, true);
|
||||
});
|
||||
it("should mark configs saved without a version as dirty", function() {
|
||||
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
|
||||
const config = lib.InstanceConfig.fromJSON({}, "controller");
|
||||
assert.equal(config.get("test_plugin.version"), "1.2.0");
|
||||
assert.equal(config.dirty, true);
|
||||
});
|
||||
it("should not mark configs already at the version as dirty", function() {
|
||||
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
|
||||
const config = lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.2.0" }, "controller");
|
||||
assert.equal(config.dirty, false);
|
||||
});
|
||||
it("should write the version to new configs", function() {
|
||||
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
|
||||
const config = new lib.InstanceConfig("controller");
|
||||
assert.equal(config.get("test_plugin.version"), "1.2.0");
|
||||
});
|
||||
it("should leave the version empty when no versions are registered", function() {
|
||||
const config = new lib.InstanceConfig("controller");
|
||||
assert.equal(config.get("test_plugin.version"), "");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -128,11 +128,13 @@ describe("lib/plugin_loader", function() {
|
||||
});
|
||||
afterEach(function() {
|
||||
lib.ControllerConfig.pluginMigrations.clear();
|
||||
lib.Config.pluginVersions.clear();
|
||||
});
|
||||
|
||||
function info(entrypoint) {
|
||||
return {
|
||||
name: "test",
|
||||
version: "1.0.0",
|
||||
packagePath: path.join(baseDir, "package.json"),
|
||||
migrationEntrypoint: entrypoint,
|
||||
};
|
||||
@@ -143,6 +145,10 @@ describe("lib/plugin_loader", function() {
|
||||
assert.deepEqual([...lib.ControllerConfig.pluginMigrations.attached], ["test"]);
|
||||
assert.deepEqual(lib.ControllerConfig.pluginMigrations.reduce({}), { migratedBy: "test" });
|
||||
});
|
||||
it("should register the version of every plugin", async function() {
|
||||
await lib.loadPluginMigrations([info("migrations.js"), { name: "other", version: "2.0.0" }]);
|
||||
assert.deepEqual([...lib.Config.pluginVersions], [["test", "1.0.0"], ["other", "2.0.0"]]);
|
||||
});
|
||||
it("should reject with a PluginError if the entrypoint throws", async function() {
|
||||
await assert.rejects(
|
||||
lib.loadPluginMigrations([info("throwing.js")]),
|
||||
|
||||
Reference in new issue
Block a user