Add initializeCtl and control reuse in tests

This commit is contained in:
Cooldude2606 committed 2025-07-06 22:58:39 +01:00
1 parent 6b81e6383d
commit 2896ec1c15
6 files changed
+134 -73

No files matched your search

+61 -28
View File
@@ -143,7 +143,19 @@ interface CtlArguments {
pluginList: string,
}
async function startControl() {
interface InitializeParameters {
args: CtlArguments;
shouldRun: boolean;
ctlPlugins?: Map<string, BaseCtlPlugin>;
rootCommands?: lib.CommandTree;
controlConfig?: lib.ControlConfig;
}
export async function initialize(
argv: string | string[],
ctlPlugins?: Map<string, BaseCtlPlugin>,
noLoggerTransport?: boolean,
): Promise<InitializeParameters> {
yargs
.scriptName("clusterioctl")
.usage("$0 <command> [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) {
-1
View File
@@ -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();
});
+29 -28
View File
@@ -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");
+40 -11
View File
@@ -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,
+4 -4
View File
@@ -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}`);
-1
View File
@@ -215,7 +215,6 @@ class MockController {
}
async function createControllerPlugin(ControllerPluginClass, info) {
lib.registerPluginMessages([info]);
let controller = new MockController();
let metrics = {};
let logger = new MockLogger();