Merge pull request #962 from bbassie/feature/plugin-system-tests

Add integration tests for the plugin system
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-08-11 15:25:17 +01:00
commit 3c9229087b
9 files changed
+242

No files matched your search

+26
View File
@@ -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,
};
+21
View File
@@ -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,
};
+26
View File
@@ -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,
};
+24
View File
@@ -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,
],
},
};
+12
View File
@@ -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,
};
+63
View File
@@ -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,
};
+10
View File
@@ -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"
]
}
+59
View File
@@ -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() {
+1
View File
@@ -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");