Files
factorio-clusterio/test/host/patch.js
T
Hornwitser 5dcaca4d0b Migrate to ECMAScript Modules
Migrate the usage of CommonJS specific code and imports to ESM.  Roughly
summarised this:
- Changes all require() calls to import statement or expressions.
- Changes usage of module.exports to export statements.
- Replaces require.resolve with import.meta.resolve.
- Replaces conditional imports with conditional subpath imports.
- Replaces module === require.main with import.meta.main.
- Adds the full filename with extention to all relative imports.
- Changes all usage of __dirname and __filename to their import.meta
  equivalents.
- Sets "type": "module" in package.json files.
- Removes usage of "use strict".
- Adds .ts an an alias of .js for the webpack build.

Due to differences in how require and import resolves files the plugin
entrypoints will have to include the filename and extension after this
migration in order to work.
2026-09-11 22:43:57 +02:00

242 lines
7.8 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs/promises";
import JSZip from "jszip";
import path from "node:path";
import * as lib from "@clusterio/lib";
import * as patch from "@clusterio/host/dist/node/src/patch.js";
describe("host/patch", function() {
describe("class SaveModule", function() {
describe("static moduleFilePath()", function() {
it("Should remap locale files", function() {
const mappings = [
["locale/en/foo.cfg", "locale/en/test-foo.cfg"],
["locale/en/foo.txt", "locale/en/test-foo.txt"],
["locale/en/foo.tmp.txt", "locale/en/test-foo.tmp.txt"],
["locale/en/foo", "locale/en/test-foo"],
["locale/en.cfg", "locale/en/test.cfg"],
["locale/en.txt", "locale/en/test.txt"],
["locale/en.tmp.txt", "locale/en/test.tmp.txt"],
["locale/en", "locale/en/test"],
["locale/en/bar/foo.cfg", "locale/en/test-bar/foo.cfg"],
];
for (let [input, expected] of mappings) {
assert.equal(patch.SaveModule.moduleFilePath(input, "test"), expected);
}
});
});
describe("loadFiles()", function() {
const testModule = new patch.SaveModule(new lib.ModuleInfo("test", "1.0.0"));
const referenceFiles = new Map([
["modules/test/test.lua", "-- test\n"],
["modules/test/module_exports.lua", "-- module_exports\n"],
["locale/en/test.cfg", "module-test=A Test\n"],
["locale/en/test-locale.cfg", "module-test-locale=Test Locale\n"],
["modules/test.lua", "return require(\"modules/test/module_exports\")"],
]);
before(async function() {
await testModule.loadFiles("test/file/modules/test");
});
it("Should read the contents of all files in a module", function() {
assert.equal(testModule.files.size, referenceFiles.size);
for (let [relativePath, contents] of referenceFiles) {
if (!testModule.files.has(relativePath)) {
assert.fail(`Module is missing file: ${relativePath}`);
} else {
assert.equal(testModule.files.get(relativePath).toString("utf-8"), contents);
}
}
});
});
});
describe("generateLoader()", function() {
let reference = [
"-- Auto generated scenario module loader created by Clusterio",
"-- Modifications to this file will be lost when loaded in Clusterio",
"clusterio_patch_number = 1",
"",
'local event_handler = require("event_handler")',
"",
"-- Scenario modules",
'event_handler.add_lib(require("foo"))',
"",
"-- Clusterio modules",
'event_handler.add_lib(require("modules/spam/bar"))',
'require("modules/spam/spam")',
"",
].join("\n");
let patchInfo = new patch.PatchInfo(
1,
new lib.ModuleInfo("foo", "0.0.0", ["foo"]),
[
new patch.SaveModule(
new lib.ModuleInfo(
"spam",
"0.0.0",
["bar.lua"],
["spam.lua"],
),
new Map([
["bar.lua", Buffer.from("")],
["spam.lua", Buffer.from("")],
["excluded.lua", Buffer.from("")],
]),
),
],
);
it("should produce output matching the reference", function() {
assert.equal(patch._generateLoader(patchInfo), reference);
});
});
function module({ name, version, dependencies = {} }) {
const dependencyMap = new Map(Object.entries(dependencies));
return new patch.SaveModule(new lib.ModuleInfo(name, version, [], [], dependencyMap));
}
describe("reorderDependencies()", function() {
it("should reorder to satisfy simple dependency", function() {
let modules = [
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
module({ name: "b", version: "1.0.0", dependencies: {} }),
];
patch._reorderDependencies(modules);
assert(modules[0].info.name === "b", "Dependency was not reorederd");
});
it("should throw on invalid version", function() {
let modules = [
module({ name: "a", version: "foo", dependencies: {} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Invalid version 'foo' for module a")
);
});
it("should throw on invalid version range", function() {
let modules = [
module({ name: "a", version: "1.0.0", dependencies: {"b": "invalid"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Invalid version range 'invalid' for dependency b on module a")
);
});
it("should throw on missing dependency", function() {
let modules = [
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Missing dependency b for module a")
);
modules = [
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
module({ name: "b", version: "1.0.0", dependencies: {"c": "*"} }),
module({ name: "c", version: "1.0.0", dependencies: {"d": "*"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Missing dependency d for module c")
);
});
it("should throw on outdated dependency", function() {
let modules = [
module({ name: "a", version: "1.0.0", dependencies: {} }),
module({ name: "b", version: "1.0.0", dependencies: {"a": ">=2"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Module b requires a >=2")
);
});
it("should throw on dependency loops", function() {
let modules = [
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
module({ name: "b", version: "1.0.0", dependencies: {"a": "*"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Module dependency loop detected: a -> b -> a")
);
modules = [
module({ name: "d", version: "1.0.0", dependencies: {"b": "*"} }),
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
module({ name: "b", version: "1.0.0", dependencies: {"c": "*"} }),
module({ name: "c", version: "1.0.0", dependencies: {"a": "*"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Module dependency loop detected: b -> c -> a -> b")
);
});
it("should reorder correctly for multiple dependencies", function() {
function validate(modules) {
let present = new Set();
for (let { info } of modules) {
for (let dependency of info.dependencies.keys()) {
if (!present.has(dependency)) {
assert.fail(`${info.name} depends on ${dependency}, but was ordered before it`);
}
}
present.add(info.name);
}
}
let names = ["a", "b", "c"];
let successCount = 0;
const bitCount = names.length * (names.length - 1);
for (let i=0; i < 2**bitCount; i++) {
let value = i;
let bits = [];
for (let j=0; j < bitCount; j++) {
bits.push(value & 1);
value >>= 1;
}
let modules = [];
for (let j=0; j < names.length; j++) {
let name = names[j];
let dependencies = {};
for (let k=0; k < names.length - 1; k++) {
if (bits[k + j * (names.length - 1)]) {
dependencies[names[k + (k >= j)]] = "*";
}
}
modules.push(module({ name, version: "1.0.0", dependencies }));
}
let success = false;
try {
patch._reorderDependencies(modules);
success = true;
} catch (err) { }
if (success) {
validate(modules);
successCount += 1;
}
}
// Deriving the formula for the number of graphs without
// dependencies loops is left as an excercise for the reader.
assert.equal(successCount, 25, "Incorrect number of successful orderings");
});
});
describe("patch()", function() {
it("should throw on unknown scenario", async function() {
let zip = new JSZip();
zip.file("world/control.lua", "-- unknown\n");
let zipPath = path.join("temp", "test", "patch.zip");
await fs.writeFile(zipPath, await zip.generateAsync({ type: "nodebuffer" }));
await assert.rejects(
patch.patch(zipPath, []),
new Error("Unable to patch save, unknown scenario (3acc3be3861144e55604f5ac2f2555071885ebc4)")
);
});
});
});