Files
2026-10-28 23:29:50 +00:00

214 lines
8.1 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs/promises";
import path from "node:path";
import * as lib from "@clusterio/lib";
import {
createScenario, createScenarioCommand, handleCreateScenarioCommand,
} from "@clusterio/host/dist/node/src/create_scenario.js";
import { SaveModule, PatchInfo } from "@clusterio/host/dist/node/src/patch.js";
import { createZipBuffer } from "../common.js";
describe("host/create_scenario", function() {
const tempDir = path.join("temp", "test", "create_scenario");
const patchInfo = new PatchInfo(0, new lib.ModuleInfo("test", "1.0.0", [], ["scenario"]), []);
const scenarioFiles = new Map([
["scenario.lua", "-- scenario\n"],
["description.json", "{}\n"],
["locale/en/test.cfg", "test=Test\n"],
["clusterio.json", JSON.stringify(patchInfo)],
]);
const module = new SaveModule(
new lib.ModuleInfo("spam", "1.0.0", ["spam.lua"]),
new Map([["modules/spam/spam.lua", Buffer.from("return {}\n")]]),
);
async function readDir(dirPath) {
const files = new Map();
for (const entry of await fs.readdir(dirPath, { recursive: true, withFileTypes: true })) {
if (entry.isFile()) {
const fsPath = path.join(entry.parentPath, entry.name);
files.set(path.relative(dirPath, fsPath).split(path.sep).join("/"), await fs.readFile(fsPath, "utf8"));
}
}
return files;
}
function checkOutput(files) {
assert(files.get("control.lua").includes('event_handler.add_lib(require("modules/spam/spam"))'));
assert.equal(JSON.parse(files.get("clusterio.json")).patch_number, 1);
assert.equal(files.get("modules/spam/spam.lua"), "return {}\n");
for (const name of ["scenario.lua", "description.json", "locale/en/test.cfg"]) {
assert.equal(files.get(name), scenarioFiles.get(name));
}
assert.equal(files.size, scenarioFiles.size + 2);
}
before(async function() {
await fs.rm(tempDir, { recursive: true, force: true });
await fs.mkdir(path.join(tempDir, "scenario"), { recursive: true });
for (const [name, content] of scenarioFiles) {
const fsPath = path.join(tempDir, "scenario", name);
await fs.mkdir(path.dirname(fsPath), { recursive: true });
await fs.writeFile(fsPath, content);
}
await fs.writeFile(
path.join(tempDir, "scenario.zip"),
await createZipBuffer([...scenarioFiles].map(([name, content]) => [`scenario/${name}`, content])),
);
});
it("should patch a scenario directory", async function() {
const output = path.join(tempDir, "from_dir");
await createScenario(path.join(tempDir, "scenario"), output, [module]);
checkOutput(await readDir(output));
});
it("should patch a zipped scenario", async function() {
const output = path.join(tempDir, "from_zip");
await createScenario(path.join(tempDir, "scenario.zip"), output, [module]);
checkOutput(await readDir(output));
});
it("should write a zip file when asked to", async function() {
const output = path.join(tempDir, "zipped.zip");
await createScenario(path.join(tempDir, "scenario"), output, [module], true);
const zip = await lib.ZipArchive.fromFile(output);
const files = new Map();
try {
for (const entry of zip.entries.values()) {
if (!entry.fileName.endsWith("/")) {
files.set(entry.fileName.replace(/^scenario\//, ""), (await zip.readEntry(entry)).toString());
}
}
} finally {
zip.close();
}
checkOutput(files);
});
it("should not modify the input scenario", async function() {
const files = await readDir(path.join(tempDir, "scenario"));
assert.deepEqual(files, scenarioFiles);
});
it("should throw if the output already exists", async function() {
const output = path.join(tempDir, "from_dir");
await assert.rejects(
createScenario(path.join(tempDir, "scenario"), output, [module]),
new Error(`${output} already exists`),
);
});
it("should throw on unknown scenario", async function() {
await fs.mkdir(path.join(tempDir, "unknown"));
await fs.writeFile(path.join(tempDir, "unknown", "control.lua"), "-- unknown\n");
await assert.rejects(
createScenario(path.join(tempDir, "unknown"), path.join(tempDir, "from_unknown"), [module]),
new Error("Unable to patch save, unknown scenario (3acc3be3861144e55604f5ac2f2555071885ebc4)")
);
});
it("should throw if a scenario file is unreadable", async function() {
if (process.platform === "win32" || process.getuid?.() === 0) {
this.skip();
}
const scenario = path.join(tempDir, "unreadable");
await fs.cp(path.join(tempDir, "scenario"), scenario, { recursive: true });
await fs.writeFile(path.join(scenario, "unreadable.lua"), "");
await fs.chmod(path.join(scenario, "unreadable.lua"), 0o000);
for (let i = 0; i < 20; i++) {
await fs.mkdir(path.join(scenario, `dir${i}`));
await fs.writeFile(path.join(scenario, `dir${i}`, "file.lua"), "");
}
await assert.rejects(
createScenario(scenario, path.join(tempDir, "from_unreadable"), [module]),
{ code: "EACCES" },
);
});
describe("createScenarioCommand()", function() {
it("should define the output positional and options", function() {
const positionals = [];
const options = [];
const yargs = {
positional(name) { positionals.push(name); return yargs; },
option(name) { options.push(name); return yargs; },
};
createScenarioCommand(yargs);
assert.deepEqual(positionals, ["output"]);
assert.deepEqual(options, ["zip", "scenario", "factorio-version", "plugins"]);
});
});
describe("handleCreateScenarioCommand()", function() {
const factorioDir = path.join(tempDir, "factorio");
const scenario = path.join(tempDir, "scenario");
let hostConfig;
let pluginInfos;
before(async function() {
// Fake install with the test scenario as freeplay
await fs.cp(path.join("test", "file", "factorio", "0.1.2"), factorioDir, { recursive: true });
const freeplay = path.join(factorioDir, "data", "base", "scenarios", "freeplay");
await fs.cp(scenario, freeplay, { recursive: true });
pluginInfos = await lib.loadPluginInfos(new Map([
["research_sync", path.resolve("plugins/research_sync")],
["test_plugin_no_module", path.resolve("test/file/test_plugin_no_module")],
]));
lib.addPluginConfigFields(pluginInfos);
hostConfig = new lib.HostConfig("host", { "host.factorio_directory": factorioDir });
});
it("should patch freeplay with the enabled plugins by default", async function() {
const output = path.join(tempDir, "default");
await handleCreateScenarioCommand({ output, factorioVersion: "latest" }, hostConfig, pluginInfos);
const files = await readDir(output);
assert.equal(files.get("scenario.lua"), scenarioFiles.get("scenario.lua"));
const info = JSON.parse(files.get("clusterio.json"));
assert.deepEqual(info.modules.map(m => m.name), ["clusterio", "research_sync"]);
});
it("should skip plugins disabled in the host config", async function() {
const output = path.join(tempDir, "disabled");
hostConfig.set("research_sync.load_plugin", false);
try {
await handleCreateScenarioCommand({ output, factorioVersion: "latest" }, hostConfig, pluginInfos);
} finally {
hostConfig.set("research_sync.load_plugin", true);
}
const info = JSON.parse((await readDir(output)).get("clusterio.json"));
assert.deepEqual(info.modules.map(m => m.name), ["clusterio"]);
});
it("should use the given scenario and plugins", async function() {
const output = path.join(tempDir, "selected");
await handleCreateScenarioCommand(
{ output, scenario, plugins: ["research_sync"] }, hostConfig, pluginInfos
);
const info = JSON.parse((await readDir(output)).get("clusterio.json"));
assert.deepEqual(info.modules.map(m => m.name), ["clusterio", "research_sync"]);
});
it("should throw on unknown plugin", async function() {
await assert.rejects(
handleCreateScenarioCommand(
{ output: path.join(tempDir, "unknown_plugin"), scenario, plugins: ["nope"] },
hostConfig,
pluginInfos,
),
new Error("Plugin nope is not in the plugin list")
);
});
it("should throw on invalid Factorio version", async function() {
await assert.rejects(
handleCreateScenarioCommand(
{ output: path.join(tempDir, "bad_version"), factorioVersion: "1..2" }, hostConfig, pluginInfos
),
new Error("Invalid Factorio version 1..2")
);
});
});
});