From 2896ec1c152ef9a464fe53236c819bcc1ad0c970 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 6 Jul 2025 22:58:39 +0100 Subject: [PATCH] Add initializeCtl and control reuse in tests --- packages/ctl/ctl.ts | 89 ++++++++++++++++++++---------- plugins/global_chat/test/plugin.js | 1 - test/integration/clusterio.js | 57 +++++++++---------- test/integration/index.js | 51 +++++++++++++---- test/integration/instance.js | 8 +-- test/mock.js | 1 - 6 files changed, 134 insertions(+), 73 deletions(-) diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index ddaf1933..349cc7ec 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -143,7 +143,19 @@ interface CtlArguments { pluginList: string, } -async function startControl() { +interface InitializeParameters { + args: CtlArguments; + shouldRun: boolean; + ctlPlugins?: Map; + rootCommands?: lib.CommandTree; + controlConfig?: lib.ControlConfig; +} + +export async function initialize( + argv: string | string[], + ctlPlugins?: Map, + noLoggerTransport?: boolean, +): Promise { yargs .scriptName("clusterioctl") .usage("$0 [options]") @@ -175,38 +187,41 @@ async function startControl() { // Parse the args first to get the configured plugin list. // eslint-disable-next-line node/no-sync - let args = yargs.parseSync() as CtlArguments; + let args = yargs.parseSync(argv) as CtlArguments; - // Log stream for the ctl session. - logger.add( - new ConsoleTransport({ - errorLevels: Object.keys(levels), - level: args.logLevel, - format: new lib.TerminalFormat(), - }) - ); - lib.handleUnhandledErrors(); - - logger.verbose(`Loading available plugins from ${args.pluginList}`); - let pluginList = await lib.loadPluginList(args.pluginList); - - // If the command is plugin management we don't try to load plugins - if (args._[0] === "plugin") { - await lib.handlePluginCommand(args, pluginList, args.pluginList); - return; + // Log stream for the ctl session. Skipped in testing. + if (!noLoggerTransport) { + logger.add( + new ConsoleTransport({ + errorLevels: Object.keys(levels), + level: args.logLevel, + format: new lib.TerminalFormat(), + }) + ); + lib.handleUnhandledErrors(); } - logger.verbose("Loading Plugins"); - let ctlPlugins = await loadPlugins(pluginList); + // Discover and load plugins. This check exists to allow tests to inject plugins. + if (!ctlPlugins || args._[0] === "plugin") { + logger.verbose(`Loading available plugins from ${args.pluginList}`); + const pluginList = await lib.loadPluginList(args.pluginList); - // Add all cluster management commands including ones from plugins - let rootCommands = await commands.registerCommands(ctlPlugins, yargs); + // If the command is plugin management we don't try to load plugins + if (args._[0] === "plugin") { + await lib.handlePluginCommand(args, pluginList, args.pluginList); + return { args, shouldRun: false }; + } - // Reparse after commands have been added with help and strict checking. + logger.verbose("Loading Plugins"); + ctlPlugins = await loadPlugins(pluginList); + } + + // Add all commands including from plugins and reparse with help and strict checking. + const rootCommands = await commands.registerCommands(ctlPlugins, yargs); args = yargs .help() .strict() - .parse() as CtlArguments + .parse(argv) as CtlArguments ; let controlConfig; @@ -233,18 +248,34 @@ async function startControl() { // Handle the control-config command before trying to connect. if (args._[0] === "control-config") { await lib.handleConfigCommand(args, controlConfig); - return; + return { args, controlConfig, ctlPlugins, rootCommands, shouldRun: false }; } - // Determine which command is being executed. + return { args, controlConfig, ctlPlugins, rootCommands, shouldRun: true }; +} + +export function selectTargetCommand(args: CtlArguments, rootCommands: lib.CommandTree): lib.Command { let commandPath = [...args._] as string[]; let targetCommand: lib.CommandTree | lib.Command = rootCommands; while (commandPath.length && targetCommand instanceof lib.CommandTree) { targetCommand = targetCommand.get(commandPath.shift()!)!; } assert(targetCommand instanceof lib.Command); + return targetCommand; +} + +async function startControl() { + const { + args, + shouldRun, + ctlPlugins, + rootCommands, + controlConfig, + } = await initialize(process.argv.slice(2)); + if (!shouldRun || !ctlPlugins || !rootCommands || !controlConfig) { + return; + } - // The remaining commands require connecting to the controller. if (!controlConfig.get("control.controller_url") || !controlConfig.get("control.controller_token")) { logger.error("Missing URL and/or token to connect with. See README.md for setting up access."); process.exitCode = 1; @@ -263,6 +294,7 @@ async function startControl() { tlsCa, controlConfig.get("control.controller_token")!, ); + let control = new Control(controlConnector, controlConfig, tlsCa, ctlPlugins); try { await controlConnector.connect(); @@ -284,6 +316,7 @@ async function startControl() { }); try { + const targetCommand = selectTargetCommand(args, rootCommands); await targetCommand.run(args, control); } catch (err) { diff --git a/plugins/global_chat/test/plugin.js b/plugins/global_chat/test/plugin.js index 315df7eb..0d22c5e8 100644 --- a/plugins/global_chat/test/plugin.js +++ b/plugins/global_chat/test/plugin.js @@ -26,7 +26,6 @@ describe("global_chat plugin", function() { let instancePlugin; before(async function() { - lib.Link.register(ChatEvent); instancePlugin = new instance.InstancePlugin(info, new mock.MockInstance(), new mock.MockHost()); await instancePlugin.init(); }); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index b345ed44..7c860302 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -13,7 +13,8 @@ const { wait } = lib; const testStrings = require("../lib/factorio/test_strings"); const { TestControl, TestControlConnector, url, controlToken, slowTest, - exec, execCtl, sendRcon, getControl, spawnNode, instancesDir, factorioDir, + exec, execCtl, execCtlProcess, execHost, sendRcon, getControl, + spawnNode, instancesDir, factorioDir, } = require("./index"); @@ -39,11 +40,11 @@ async function startAltHost() { await fs.remove(path.join("temp", "test", "alt-instances")); await fs.remove(path.join("temp", "test", "alt-mods")); await execCtl(`host create-config --id 5 --name alt-host --generate-token --output ${config}`); - await exec(`node ../../packages/host --config ${config} config set host.tls_ca ../../test/file/tls/cert.pem`); - await exec(`node ../../packages/host --config ${config} config set host.instances_directory alt-instances`); + await execHost(`--config ${config} config set host.tls_ca ../../test/file/tls/cert.pem`); + await execHost(`--config ${config} config set host.instances_directory alt-instances`); const dir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir); - await exec(`node ../../packages/host --config ${config} config set host.factorio_directory ${dir}`); - await exec(`node ../../packages/host --config ${config} config set host.mods_directory alt-mods`); + await execHost(`--config ${config} config set host.factorio_directory ${dir}`); + await execHost(`--config ${config} config set host.mods_directory alt-mods`); return await spawnAltHost(config); } @@ -259,9 +260,9 @@ describe("Integration of Clusterio", function() { describe("controller config set", function() { it("sets given config option", async function() { - await execCtl('controller config set controller.name "Test Cluster"'); + await execCtl("controller config set controller.name Test-Cluster"); let result = await getControl().send(new lib.ControllerConfigGetRequest()); - assert.equal(result["controller.name"], "Test Cluster"); + assert.equal(result["controller.name"], "Test-Cluster"); }); it("should not allow setting auth_secret", async function() { await assert.rejects(execCtl("controller config set controller.auth_secret root")); @@ -329,7 +330,7 @@ describe("Integration of Clusterio", function() { describe("host config", function() { it("changes host config", async function() { await execCtl("host config set 4 host.name My-Host"); - const result = await execCtl("host config list 4"); + const result = await execCtlProcess("host config list 4"); assert(/My-Host/.test(result.stdout), "New host name not in config output"); }); it("should not allow setting host.controller_token", async function() { @@ -416,9 +417,12 @@ describe("Integration of Clusterio", function() { slowTest(this); let sawDisconnected; await runWithAltHost(async () => { - getControl().hostUpdates = []; + const control = getControl(); + control.hostUpdates = []; + await wait(50); // Allow for host to update instances await execCtl("host revoke-token 5"); - for (let update of getControl().hostUpdates) { + await wait(50); // Allow controller to notify control + for (let update of control.hostUpdates) { if (update.name !== "alt-host") { continue; } @@ -448,10 +452,8 @@ describe("Integration of Clusterio", function() { assert.equal(instances.get(44).status, "unassigned", "incorrect instance status"); // Make sure the following tests does not fail due to not having internet - let value = JSON.stringify({ lan: true, public: false }).replace( - /"/g, process.platform === "win32" ? '""' : '\\"' - ); - await execCtl(`instance config set-prop test factorio.settings visibility "${value}"`); + const value = jsonArg({ lan: true, public: false }); + await execCtlProcess(`instance config set-prop test factorio.settings visibility ${value}`); await execCtl("instance config set-prop test factorio.settings require_user_verification false"); }); it("can clone an instance", async function() { @@ -500,7 +502,7 @@ describe("Integration of Clusterio", function() { describe("instance save list", function() { it("lists the created save", async function() { slowTest(this); - let result = await execCtl("instance save list test"); + let result = await execCtlProcess("instance save list test"); assert(/world\.zip/.test(result.stdout), "world.zip not present in list save output"); }); }); @@ -698,8 +700,8 @@ describe("Integration of Clusterio", function() { ]; for (let [prop, value] of testConfigs) { - let args = `test factorio.settings ${prop} ${jsonArg(value)}`; - await execCtl(`instance config set-prop ${args}`); + const args = `test factorio.settings ${prop} ${jsonArg(value)}`; + await execCtlProcess(`instance config set-prop ${args}`); } // Do this afterwards to leave enough to time for the @@ -909,6 +911,7 @@ describe("Integration of Clusterio", function() { await execCtl("instance config set 44 research_sync.load_plugin false"); await execCtl("instance config set 44 statistics_exporter.load_plugin false"); await execCtl("instance config set 44 subspace_storage.load_plugin false"); + await execCtl("instance config set 44 inventory_sync.load_plugin false"); await execCtl("instance start 44"); const instance = (await getInstances()).get(44); @@ -1188,7 +1191,7 @@ describe("Integration of Clusterio", function() { }); it("should allow setting all fields", async function() { const color = { r: 1, g: 0, b: 1, a: 0 }; - await execCtl( + await execCtlProcess( "mod-pack create full-pack 0.17.59 " + "--description Description " + "--mods empty_mod:1.0.0 " + @@ -1220,7 +1223,7 @@ describe("Integration of Clusterio", function() { describe("mod-pack list", function() { let result = null; it("runs", async function() { - result = await execCtl("mod-pack list"); + result = await execCtlProcess("mod-pack list"); }); it("contains defaults", function() { assert(result !== null, "Failed to return a value"); @@ -1249,7 +1252,7 @@ describe("Integration of Clusterio", function() { reference.settings["runtime-global"].set("MyDouble", { value: 12.25 }); reference.settings["runtime-per-user"].set("MyString", { value: "a-string" }); await execCtl(`mod-pack import ${reference.toModPackString()}`); - const result = await execCtl("mod-pack export imported-pack"); + const result = await execCtlProcess("mod-pack export imported-pack"); const roundtrip = lib.ModPack.fromModPackString(result.stdout.trim()); roundtrip.id = reference.id; assert.deepEqual(roundtrip, reference); @@ -1308,7 +1311,7 @@ describe("Integration of Clusterio", function() { describe("mod show", function() { it("gives details of a mod", async function() { - let result = await execCtl("mod show empty_mod 1.0.0"); + let result = await execCtlProcess("mod show empty_mod 1.0.0"); let hash = await lib.hashFile(path.join("temp", "test", "mods", "empty_mod_1.0.0.zip")); let stat = await fs.stat(path.join("temp", "test", "mods", "empty_mod_1.0.0.zip")); assert.equal( @@ -1333,14 +1336,14 @@ describe("Integration of Clusterio", function() { describe("mod list", function() { it("shows the list of mods", async function() { - let result = await execCtl("mod list"); + let result = await execCtlProcess("mod list"); assert(result.stdout.indexOf("empty_mod") !== -1, "empty_mod is not in the list"); }); }); describe("mod search", function() { it("searches the list of mods", async function() { - let result = await execCtl("mod search 1.1 name:empty_mod"); + let result = await execCtlProcess("mod search 1.1 name:empty_mod"); assert(result.stdout.indexOf("empty_mod") !== -1, "empty_mod is not in the result"); }); }); @@ -1414,7 +1417,7 @@ describe("Integration of Clusterio", function() { describe("role edit", function() { it("should modify the given role", async function() { let args = "--name new --description \"A new role\" --set-perms"; - await execCtl(`role edit temp ${args}`); + await execCtlProcess(`role edit temp ${args}`); let roles = await getControl().send(new lib.RoleListRequest()); let newRole = roles.find(role => role.name === "new"); assert.deepEqual(newRole, new lib.Role(5, "new", "A new role", new Set())); @@ -1495,7 +1498,7 @@ describe("Integration of Clusterio", function() { describe("user set-roles", function() { it("should set the roles on the user", async function() { getControl().userUpdates = []; - await execCtl('user set-roles temp "Cluster Admin"'); + await execCtlProcess('user set-roles temp "Cluster Admin"'); let users = await getControl().send(new lib.UserListRequest()); let tempUser = users.find(user => user.id === "temp"); assert.deepEqual(tempUser.roleIds, new Set([0])); @@ -1503,7 +1506,7 @@ describe("Integration of Clusterio", function() { }); it("should restrict actions based on roles", async function() { - await execCtl('user set-roles temp "Player"'); + await execCtl("user set-roles temp Player"); let tempControl; try { let tlsCa = await fs.readFile("test/file/tls/cert.pem"); @@ -1657,7 +1660,6 @@ describe("Integration of Clusterio", function() { await execCtl("user create export_control"); }); it("should export users", async function() { - slowTest(this); await execCtl("user set-admin export_user --create"); await execCtl("user set-whitelisted export_user"); await execCtl("user export --users user-export.json"); @@ -1669,7 +1671,6 @@ describe("Integration of Clusterio", function() { }); }); it("should export bans", async function() { - slowTest(this); await execCtl("user set-banned export_ban --create"); await execCtl("user set-banned export_ban_reason --create --reason banned"); await execCtl("user export --bans ban-export.json"); diff --git a/test/integration/index.js b/test/integration/index.js index baaaa7a0..7a31d23e 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -11,6 +11,8 @@ const events = require("events"); const lib = require("@clusterio/lib"); const { LineSplitter, ConsoleTransport, logger } = lib; +const { selectTargetCommand, initialize: initializeCtl } = require("@clusterio/ctl"); + // Make sure permissions from plugins are loaded require("../../plugins/global_chat/dist/node/index"); require("../../plugins/player_auth/dist/node/index"); @@ -84,6 +86,10 @@ class TestControl extends lib.Link { async handleUserUpdatesEvent(event) { this.userUpdates.push(...event.updates); } + + async setLogSubscriptions(args) { + // Do nothing + } } class TestControlConnector extends lib.WebSocketClientConnector { @@ -185,11 +191,27 @@ async function execHost(...args) { return await exec(...args); } -async function execCtl(...args) { +async function execCtlProcess(...args) { args[0] = `node --enable-source-maps ../../packages/ctl ${args[0]}`; return await exec(...args); } +async function execCtl(command) { + process.chdir("temp/test"); + try { + const initArgs = await initializeCtl(command, control.plugins, true); + if (!initArgs.shouldRun) { + return; + } + + control.config = initArgs.controlConfig; + const targetCommand = selectTargetCommand(initArgs.args, initArgs.rootCommands); + await targetCommand.run(initArgs.args, control); + } finally { + process.chdir("../.."); + } +} + async function sendRcon(instanceId, command) { return await control.sendTo({ instanceId }, new lib.InstanceSendRconRequest(command)); } @@ -291,33 +313,38 @@ before(async function() { await execController("config set controller.tls_private_key ../../test/file/tls/key.pem"); console.log("Setting Controller Plugins"); - await execCtl("plugin add ../../plugins/global_chat"); - await execCtl("plugin add ../../plugins/research_sync"); - await execCtl("plugin add ../../plugins/statistics_exporter"); - await execCtl("plugin add ../../plugins/subspace_storage"); - await execCtl("plugin add ../../plugins/player_auth"); + await execCtlProcess("plugin add ../../plugins/global_chat"); + await execCtlProcess("plugin add ../../plugins/research_sync"); + await execCtlProcess("plugin add ../../plugins/statistics_exporter"); + await execCtlProcess("plugin add ../../plugins/subspace_storage"); + await execCtlProcess("plugin add ../../plugins/inventory_sync"); + await execCtlProcess("plugin add ../../plugins/player_auth"); console.log("Bootstrapping"); await execController("bootstrap create-admin test"); await execController("bootstrap create-ctl-config test"); - await execCtl("control-config set control.tls_ca ../../test/file/tls/cert.pem"); + await execCtlProcess("control-config set control.tls_ca ../../test/file/tls/cert.pem"); controllerProcess = await spawnNode("controller:", "../../packages/controller run", /Started controller/); - await execCtl("host create-config --id 4 --name host --generate-token"); - const relativeFactorioDir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir); + await execCtlProcess("host create-config --id 4 --name host --generate-token"); await execHost(`config set host.factorio_directory ${relativeFactorioDir}`); await execHost("config set host.tls_ca ../../test/file/tls/cert.pem"); hostProcess = await spawnNode("host:", "../../packages/host run", /Started host/); - let tlsCa = await fs.readFile("test/file/tls/cert.pem"); - let controlConnector = new TestControlConnector(url, 2, tlsCa); + const tlsCa = await fs.readFile("test/file/tls/cert.pem"); + const controlConnector = new TestControlConnector(url, 2, tlsCa); controlConnector.token = controlToken; control = new TestControl(controlConnector); await controlConnector.connect(); + const initArgs = await initializeCtl(`--plugin-list ${pluginListPath} control-config list`, undefined, true); + control.config = initArgs.controlConfig; + control.plugins = initArgs.ctlPlugins; + control.tlsCa = tlsCa; + const testPack = lib.ModPack.fromJSON({}); testPack.id = 12; testPack.name = "subspace_storage-pack"; @@ -358,10 +385,12 @@ module.exports = { TestControl, TestControlConnector, TestHostConnector, + execCtlProcess, slowTest, get, exec, execCtl, + execHost, sendRcon, getControl, spawn, diff --git a/test/integration/instance.js b/test/integration/instance.js index 8b8ad605..77a654b8 100644 --- a/test/integration/instance.js +++ b/test/integration/instance.js @@ -3,7 +3,7 @@ const assert = require("assert").strict; const lib = require("@clusterio/lib"); const { - slowTest, exec, execCtl, sendRcon, getControl, + slowTest, exec, execCtl, execCtlProcess, sendRcon, getControl, } = require("./index"); const instId = 48; @@ -31,12 +31,12 @@ describe("Clusterio Instance", function() { // Create a new instance await execCtl(`instance create ${instName} --id ${instId}`); await execCtl(`instance config set ${instName} factorio.enable_whitelist true`); - await execCtl(`instance config set-prop ${instName} factorio.settings visibility "${visibility}"`); + await execCtlProcess(`instance config set-prop ${instName} factorio.settings visibility "${visibility}"`); await execCtl(`instance config set-prop ${instName} factorio.settings require_user_verification false`); await execCtl(`instance assign ${instName} 4`); for (const plugin of [ - "global_chat", "research_sync", "statistics_exporter", "subspace_storage", "player_auth", + "global_chat", "research_sync", "statistics_exporter", "subspace_storage", "player_auth", "inventory_sync", ]) { await execCtl(`instance config set ${instName} ${plugin}.load_plugin false`); } @@ -44,7 +44,7 @@ describe("Clusterio Instance", function() { // Create a new alt instance await execCtl(`instance create ${instAltName} --id ${instAltId}`); await execCtl(`instance config set ${instAltName} factorio.enable_whitelist true`); - await execCtl(`instance config set-prop ${instAltName} factorio.settings visibility "${visibility}"`); + await execCtlProcess(`instance config set-prop ${instAltName} factorio.settings visibility "${visibility}"`); await execCtl(`instance config set-prop ${instAltName} factorio.settings require_user_verification false`); await execCtl(`instance assign ${instAltName} 4`); await execCtl(`instance start ${instAltName}`); diff --git a/test/mock.js b/test/mock.js index 460fc5db..06c18852 100644 --- a/test/mock.js +++ b/test/mock.js @@ -215,7 +215,6 @@ class MockController { } async function createControllerPlugin(ControllerPluginClass, info) { - lib.registerPluginMessages([info]); let controller = new MockController(); let metrics = {}; let logger = new MockLogger();