mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 23:14:53 +09:00
Add integration tests for the plugin system
Nothing covered whether plugins are loaded at all, or whether the messages they declare are routed. The integration tests only loaded the first party plugins, so what coverage existed came incidentally from testing those. Add a plugin under test/file which exists purely to be loaded by the tests. It is plain JavaScript rather than TypeScript so that it needs no build step, which the plugin loader is happy with as it requires the entry point and reads the version from package.json like any other plugin. Covered so far: the plugin loads on the controller and on the host, its title and version are reported, the command it adds is available in clusterioctl, a request from control reaches the controller, a request from control reaches a host, and an event sent with broadcastEventToHosts arrives at the hosts. Left for a follow up, as all three need a running instance: a request to an instance, a Lua module reached over rcon, and log statements being tagged with the plugin and host or instance id. Part of #342, and a step towards #721 as core no longer needs a first party plugin to have the plugin system under test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5e0ab3423f
commit
b415d16408
9 files changed
+242
No files matched your search
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
const { Command, CommandTree } = require("@clusterio/lib");
|
||||
const { BaseCtlPlugin } = require("@clusterio/ctl");
|
||||
|
||||
const testPluginCommands = new CommandTree({
|
||||
name: "test-plugin", description: "Test plugin commands",
|
||||
});
|
||||
testPluginCommands.add(new Command({
|
||||
definition: ["echo <message>", "Print the message given to it", (yargs) => {
|
||||
yargs.positional("message", { describe: "message to print", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(args.message);
|
||||
},
|
||||
}));
|
||||
|
||||
class CtlPlugin extends BaseCtlPlugin {
|
||||
async addCommands(rootCommand) {
|
||||
rootCommand.add(testPluginCommands);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CtlPlugin,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||
|
||||
const { ControllerEcho, HostEcho } = require("./messages");
|
||||
|
||||
class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
this.logger.info("test_plugin controller loaded");
|
||||
this.controller.handle(ControllerEcho, this.handleControllerEcho.bind(this));
|
||||
}
|
||||
|
||||
async handleControllerEcho(request) {
|
||||
this.logger.info(`test_plugin controller echo ${request.text}`);
|
||||
this.broadcastEventToHosts(new HostEcho(request.text));
|
||||
return request.text;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControllerPlugin,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
const { BaseHostPlugin } = require("@clusterio/host");
|
||||
|
||||
const { HostEcho, HostEchoReceived } = require("./messages");
|
||||
|
||||
class HostPlugin extends BaseHostPlugin {
|
||||
async init() {
|
||||
this.logger.info("test_plugin host loaded");
|
||||
this.receivedEchoes = new Set();
|
||||
this.host.handle(HostEcho, this.handleHostEcho.bind(this));
|
||||
this.host.handle(HostEchoReceived, this.handleHostEchoReceived.bind(this));
|
||||
}
|
||||
|
||||
async handleHostEcho(event) {
|
||||
this.logger.info(`test_plugin host echo ${event.text}`);
|
||||
this.receivedEchoes.add(event.text);
|
||||
}
|
||||
|
||||
async handleHostEchoReceived(request) {
|
||||
return this.receivedEchoes.has(request.text);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HostPlugin,
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
// Plugin exercising the plugin system in the integration tests, see #342.
|
||||
// Written as plain JavaScript so that it does not need to be built.
|
||||
|
||||
const { ControllerEcho, HostEcho, HostEchoReceived } = require("./messages");
|
||||
|
||||
module.exports = {
|
||||
plugin: {
|
||||
name: "test_plugin",
|
||||
title: "Test Plugin",
|
||||
description: "Plugin used to test the plugin system.",
|
||||
|
||||
controllerEntrypoint: "controller",
|
||||
hostEntrypoint: "host",
|
||||
instanceEntrypoint: "instance",
|
||||
ctlEntrypoint: "control",
|
||||
|
||||
messages: [
|
||||
ControllerEcho,
|
||||
HostEcho,
|
||||
HostEchoReceived,
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
const { BaseInstancePlugin } = require("@clusterio/host");
|
||||
|
||||
class InstancePlugin extends BaseInstancePlugin {
|
||||
async init() {
|
||||
this.logger.info("test_plugin instance loaded");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InstancePlugin,
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
const { JsonBoolean, JsonString } = require("@clusterio/lib");
|
||||
|
||||
// Sent from ctl to the controller, which answers with the text it was given
|
||||
// and broadcasts a HostEcho to the hosts.
|
||||
class ControllerEcho {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static plugin = "test_plugin";
|
||||
static permission = null;
|
||||
static Response = JsonString;
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
static jsonSchema = { type: "string" };
|
||||
toJSON() { return this.text; }
|
||||
static fromJSON(json) { return new this(json); }
|
||||
}
|
||||
|
||||
// Broadcast from the controller to the hosts by broadcastEventToHosts.
|
||||
class HostEcho {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "host";
|
||||
static plugin = "test_plugin";
|
||||
static permission = null;
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
static jsonSchema = { type: "string" };
|
||||
toJSON() { return this.text; }
|
||||
static fromJSON(json) { return new this(json); }
|
||||
}
|
||||
|
||||
// Sent from ctl to a host, which answers with whether it received the given
|
||||
// text via a HostEcho.
|
||||
class HostEchoReceived {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "host";
|
||||
static plugin = "test_plugin";
|
||||
static permission = null;
|
||||
static Response = JsonBoolean;
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
static jsonSchema = { type: "string" };
|
||||
toJSON() { return this.text; }
|
||||
static fromJSON(json) { return new this(json); }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControllerEcho,
|
||||
HostEcho,
|
||||
HostEchoReceived,
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "test_plugin",
|
||||
"version": "0.0.1",
|
||||
"description": "Plugin used to test the plugin system, see #342",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"keywords": [
|
||||
"clusterio-plugin"
|
||||
]
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const {
|
||||
spawnNode, instancesDir, factorioDir, databaseDir, controllerConfigPath,
|
||||
requiresFactorio, hasFactorio,
|
||||
} = require("./index");
|
||||
const { ControllerEcho, HostEchoReceived } = require("../file/test_plugin/messages");
|
||||
|
||||
|
||||
/** @returns {Promise<Map<number, lib.InstanceDetails>>} */
|
||||
@@ -386,6 +387,64 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin system", function() {
|
||||
// Exercised with test/file/test_plugin, which is a plugin that exists
|
||||
// only to be loaded by these tests, see #342.
|
||||
async function pluginDetails(address) {
|
||||
const plugins = await getControl().sendTo(address, new lib.PluginListRequest());
|
||||
const details = plugins.find(plugin => plugin.name === "test_plugin");
|
||||
assert(details, `test_plugin missing from the plugin list of ${JSON.stringify(address)}`);
|
||||
return details;
|
||||
}
|
||||
|
||||
it("should load the plugin on the controller", async function() {
|
||||
const details = await pluginDetails("controller");
|
||||
assert(details.enabled, "test_plugin is not enabled on the controller");
|
||||
assert(details.loaded, "test_plugin is not loaded on the controller");
|
||||
});
|
||||
|
||||
it("should load the plugin on the host", async function() {
|
||||
const details = await pluginDetails({ hostId: 4 });
|
||||
assert(details.enabled, "test_plugin is not enabled on the host");
|
||||
assert(details.loaded, "test_plugin is not loaded on the host");
|
||||
});
|
||||
|
||||
it("should report the plugin's title and version", async function() {
|
||||
const details = await pluginDetails("controller");
|
||||
assert.equal(details.title, "Test Plugin");
|
||||
assert.equal(details.version, "0.0.1");
|
||||
});
|
||||
|
||||
it("should add the plugin's command to clusterioctl", async function() {
|
||||
const { stdout } = await execCtlProcess("test-plugin echo works");
|
||||
assert(stdout.includes("works"), `plugin command did not print its message, got ${stdout}`);
|
||||
});
|
||||
|
||||
it("should route a plugin request from control to the controller", async function() {
|
||||
const response = await getControl().sendTo("controller", new ControllerEcho("to-controller"));
|
||||
assert.equal(response, "to-controller");
|
||||
});
|
||||
|
||||
it("should route a plugin request from control to a host", async function() {
|
||||
const received = await getControl().sendTo({ hostId: 4 }, new HostEchoReceived("never-broadcast"));
|
||||
assert.equal(received, false);
|
||||
});
|
||||
|
||||
it("should deliver a plugin event sent with broadcastEventToHosts", async function() {
|
||||
await getControl().sendTo("controller", new ControllerEcho("to-hosts"));
|
||||
|
||||
// The broadcast is sent without waiting for the hosts to receive it.
|
||||
let received = false;
|
||||
for (let attempt = 0; attempt < 100 && !received; attempt++) {
|
||||
received = await getControl().sendTo({ hostId: 4 }, new HostEchoReceived("to-hosts"));
|
||||
if (!received) {
|
||||
await lib.wait(20);
|
||||
}
|
||||
}
|
||||
assert(received, "host did not receive the event broadcast to it");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clusterioctl", function() {
|
||||
describe("controller config list", function() {
|
||||
it("runs", async function() {
|
||||
|
||||
@@ -350,6 +350,7 @@ before(async function() {
|
||||
await execController("config set controller.tls_private_key ../../test/file/tls/key.pem");
|
||||
|
||||
console.log("Setting Controller Plugins");
|
||||
await execCtlProcess("plugin add ../../test/file/test_plugin");
|
||||
await execCtlProcess("plugin add ../../plugins/global_chat");
|
||||
await execCtlProcess("plugin add ../../plugins/research_sync");
|
||||
await execCtlProcess("plugin add ../../plugins/statistics_exporter");
|
||||
|
||||
Reference in new issue
Block a user