Refactor module patching logic

Break out the module info and patch info into data classes and pack all
the files for a module into one more easily modified structure.
This commit is contained in:
Hornwitser committed 2023-08-25 16:33:02 +02:00
1 parent b1a6628bfa
commit a1148c7ec9
6 files changed
+339 -206

No files matched your search

+9 -57
View File
@@ -9,7 +9,7 @@ import type { Static } from "@sinclair/typebox";
import * as lib from "@clusterio/lib";
import { FactorioServer } from "./server";
import { ModuleInfo, patch } from "./patch";
import { SaveModule, patch } from "./patch";
import { exportData } from "./export";
import type Host from "./Host"
@@ -776,74 +776,26 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
// Patch save with lua modules from plugins
this.logger.verbose("Patching save");
interface ModuleJson {
version: string,
name: string;
load?: string[];
require?: string[];
dependencies?: Record<string, string>;
}
// Find plugin modules to patch in
let modules: Map<string, ModuleInfo> = new Map();
for (let [pluginName, plugin] of this.plugins) {
let pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json"));
let modulePath = path.join(path.dirname(pluginPackagePath), "module");
if (!await fs.pathExists(modulePath)) {
let modules: Map<string, SaveModule> = new Map();
for (let plugin of this.plugins.values()) {
let module = await SaveModule.fromPlugin(plugin);
if (!module) {
continue;
}
let moduleJsonPath = path.join(modulePath, "module.json");
if (!await fs.pathExists(moduleJsonPath)) {
throw new Error(`Module for plugin ${pluginName} is missing module.json`);
}
let module: Omit<ModuleJson, "version">;
try {
module = JSON.parse(await fs.readFile(moduleJsonPath, "utf8"));
} catch (err: any) {
throw new Error(`Loading module/module.json in plugin ${pluginName} failed: ${err.message}`);
}
if (module.name !== pluginName) {
throw new Error(`Expected name of module for plugin ${pluginName} to match the plugin name`);
}
modules.set(module.name, {
version: plugin.info.version,
dependencies: { "clusterio": "*" },
path: modulePath,
load: [],
require: [],
...module,
});
modules.set(module.info.name, module);
}
// Find stand alone modules to load
// XXX for now only the included clusterio module is loaded
let modulesDirectory = path.join(__dirname, "..", "..", "modules");
for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) {
if (!entry.isFile()) {
if (entry.isDirectory()) {
if (modules.has(entry.name)) {
throw new Error(`Module with name ${entry.name} already exists in a plugin`);
}
let moduleJsonPath = path.join(modulesDirectory, entry.name, "module.json");
if (!await fs.pathExists(moduleJsonPath)) {
throw new Error(`Module ${entry.name} is missing module.json`);
}
let module: ModuleJson = JSON.parse(await fs.readFile(moduleJsonPath, "utf8"));
if (module.name !== entry.name) {
throw new Error(`Expected name of module ${entry.name} to match the directory name`);
}
modules.set(module.name, {
path: path.join(modulesDirectory, entry.name),
dependencies: { "clusterio": "*" },
load: [],
require: [],
...module,
});
let module = await SaveModule.fromDirectory(path.join(modulesDirectory, entry.name));
modules.set(module.info.name, module);
}
}
+223 -98
View File
@@ -1,44 +1,181 @@
// Library for patching Factorio saves with scenario code.
"use strict";
import events from "events";
import fs from "fs-extra";
import JSZip from "jszip";
import path from "path";
import semver from "semver";
import { Type, Static } from "@sinclair/typebox";
import * as lib from "@clusterio/lib";
interface ScenarioInfo {
name: string;
modules: string[];
/**
* Describes a module that can be patched into a save
*/
export class SaveModule {
constructor(
/** Module */
public info: lib.ModuleInfo,
/** Files and their content that will be patched into the save */
public files = new Map<string, Buffer>,
) { }
async loadFiles(moduleDirectory: string) {
let dirs: [string, string][] = [[moduleDirectory, ""]];
while (dirs.length) {
let [dir, relativeDir] = dirs.pop()!;
for (let entry of await fs.readdir(dir, { withFileTypes: true })) {
let fsPath = path.join(dir, entry.name);
let relativePath = path.posix.join(relativeDir, entry.name);
let savePath = path.posix.join("modules", this.info.name, relativePath);
if (entry.isFile()) {
this.files.set(savePath, await fs.readFile(fsPath));
} else if (entry.isDirectory()) {
dirs.push([fsPath, relativePath]);
}
}
}
}
static jsonSchema = Type.Object({
...lib.ModuleInfo.jsonSchema.properties,
"files": Type.Array(Type.String()),
});
toJSON() {
return {
...this.info.toJSON(),
files: [...this.files.keys()],
};
}
static async fromSave(json: Static<typeof SaveModule.jsonSchema>, root: JSZip) {
const module = new this(lib.ModuleInfo.fromJSON(json));
module.files = new Map(
await Promise.all(json.files.map(async f => [f, await root.file(f)!.async("nodebuffer")] as const))
);
return module;
}
static async fromPlugin(plugin: lib.BasePlugin) {
let pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json"));
let moduleDirectory = path.join(path.dirname(pluginPackagePath), "module");
if (!await fs.pathExists(moduleDirectory)) {
return null;
}
let moduleJsonPath = path.join(moduleDirectory, "module.json");
let moduleJson;
try {
moduleJson = {
name: plugin.info.name,
version: plugin.info.version,
dependencies: { "clusterio": "*" },
...JSON.parse(await fs.readFile(moduleJsonPath, "utf8")),
}
} catch (err: any) {
throw new Error(`Loading module/module.json in plugin ${plugin.info.name} failed: ${err.message}`);
}
if (!lib.ModuleInfo.validate(moduleJson)) {
throw new Error(
`module/module.json in plugin ${plugin.info.name} failed validation:\n` +
`${JSON.stringify(lib.ModuleInfo.validate.errors, null, 4)}`
);
}
let module = new SaveModule(lib.ModuleInfo.fromJSON(moduleJson));
await module.loadFiles(moduleDirectory);
return module;
}
static async fromDirectory(moduleDirectory: string) {
let name = path.basename(moduleDirectory);
let moduleJsonPath = path.join(moduleDirectory, "module.json");
let moduleJson;
try {
moduleJson = JSON.parse(await fs.readFile(moduleJsonPath, "utf8"));
} catch (err: any) {
throw new Error(`Loading ${name}/module.json failed: ${err.message}`);
}
if (!lib.ModuleInfo.validate(moduleJson)) {
throw new Error(
`${name}/module.json failed validation:\n` +
`${JSON.stringify(lib.ModuleInfo.validate.errors, null, 4)}`
);
}
if (moduleJson.name !== name) {
throw new Error(`Expected name of module ${moduleJson.name} to match the directory name ${name}`);
}
let module = new SaveModule(lib.ModuleInfo.fromJSON(moduleJson));
await module.loadFiles(moduleDirectory);
return module;
}
}
export interface ModuleInfo {
name: string;
version: string;
path: string;
dependencies: Record<string, string>;
load: string[];
require: string[];
export class PatchInfo {
static currentVersion = 1;
constructor(
public patchNumber: number,
public scenario: lib.ModuleInfo,
public modules: SaveModule[],
public version = PatchInfo.currentVersion,
) { }
static jsonSchema = Type.Object({
"version": Type.Number(),
"patch_number": Type.Number(),
"scenario": lib.ModuleInfo.jsonSchema,
"modules": Type.Array(SaveModule.jsonSchema),
});
toJSON() {
return {
version: PatchInfo.currentVersion,
patch_number: this.patchNumber,
scenario: this.scenario.toJSON(),
modules: this.modules.map(m => m.toJSON()),
};
}
static async fromSave(json: Static<typeof PatchInfo.jsonSchema>, root: JSZip) {
if (json.version === undefined) {
interface ScenarioInfoV0 {
name: string;
modules: string[];
}
interface PatchInfoV0 {
patch_number: number;
scenario: ScenarioInfoV0;
modules: { name: string, files: { path: string, load: boolean, require: boolean }[], }[];
}
const info = json as unknown as PatchInfoV0;
return new this(
info.patch_number,
new lib.ModuleInfo(info.scenario.name, "0.0.0", info.scenario.modules),
[],
0,
);
}
return new this(
json.patch_number,
lib.ModuleInfo.fromJSON(json.scenario),
await Promise.all(json.modules.map(m => SaveModule.fromSave(m, root))),
);
}
}
interface PatchInfo {
patch_number: number;
scenario: ScenarioInfo;
modules: { name: string, files: { path: string, load: boolean, require: boolean }[], }[];
}
const knownScenarios: Record<string, ScenarioInfo> = {
const knownScenarios: Record<string, lib.ModuleInfo> = {
// First seen in 0.17.63
"4e866186ebe297f1038fd325b09df1a1f5e2fdd1": {
"name": "freeplay",
"modules": [
"freeplay",
"silo-script",
],
},
"4e866186ebe297f1038fd325b09df1a1f5e2fdd1": new lib.ModuleInfo("freeplay", "0.17.63", ["freeplay", "silo-script"]),
};
/**
@@ -52,15 +189,18 @@ function generateLoader(patchInfo: PatchInfo) {
let lines = [
"-- Auto generated scenario module loader created by Clusterio",
"-- Modifications to this file will be lost when loaded in Clusterio",
`clusterio_patch_number = ${patchInfo.patch_number}`,
`clusterio_patch_number = ${patchInfo.patchNumber}`,
"",
'local event_handler = require("event_handler")',
"",
"-- Scenario modules",
];
for (let moduleName of patchInfo["scenario"]["modules"]) {
lines.push(`event_handler.add_lib(require("${moduleName}"))`);
for (let requirePath of patchInfo.scenario.load) {
lines.push(`event_handler.add_lib(require("${requirePath}"))`);
}
for (let requirePath of patchInfo.scenario.require) {
lines.push(`require("${requirePath}")`);
}
lines.push(...[
@@ -68,15 +208,14 @@ function generateLoader(patchInfo: PatchInfo) {
"-- Clusterio modules",
]);
for (let module of patchInfo["modules"]) {
for (let file of module["files"]) {
let requirePath = `modules/${module.name}/${file["path"].slice(0, -4)}`;
if (file["load"]) {
lines.push(`event_handler.add_lib(require("${requirePath}"))`);
}
if (file["require"]) {
lines.push(`require("${requirePath}")`);
}
for (let module of patchInfo.modules) {
for (let file of module.info.load) {
let requirePath = `modules/${module.info.name}/${file.replace(/\.lua$/i, "")}`;
lines.push(`event_handler.add_lib(require("${requirePath}"))`);
}
for (let file of module.info.require) {
let requirePath = `modules/${module.info.name}/${file.replace(/\.lua$/i, "")}`;
lines.push(`require("${requirePath}")`);
}
}
@@ -96,32 +235,32 @@ function generateLoader(patchInfo: PatchInfo) {
* @param modules - Array of modules to reorder
* @internal
*/
function reorderDependencies(modules: ModuleInfo[]) {
function reorderDependencies(modules: SaveModule[]) {
let index = 0;
let present = new Map();
let hold = new Map();
let present = new Map<string, string>();
let hold = new Map<string, [SaveModule]>();
reorder: while (index < modules.length) {
let module = modules[index];
if (semver.valid(module.version) === null) {
throw new Error(`Invalid version '${module.version}' for module ${module.name}`);
if (semver.valid(module.info.version) === null) {
throw new Error(`Invalid version '${module.info.version}' for module ${module.info.name}`);
}
for (let [dependency, requirement] of Object.entries(module.dependencies)) {
for (let [dependency, requirement] of module.info.dependencies) {
if (semver.validRange(requirement) === null) {
throw new Error(
`Invalid version range '${requirement}' for dependency ${dependency} on module ${module.name}`
`Invalid version range '${requirement}' for dependency ${dependency} on module ${module.info.name}`
);
}
if (present.has(dependency)) {
if (!semver.satisfies(present.get(dependency), requirement)) {
throw new Error(`Module ${module.name} requires ${dependency} ${requirement}`);
if (!semver.satisfies(present.get(dependency)!, requirement)) {
throw new Error(`Module ${module.info.name} requires ${dependency} ${requirement}`);
}
// We have an unmet dependency, take it out and continue
} else {
if (hold.has(dependency)) {
hold.get(dependency).push(module);
hold.get(dependency)!.push(module);
} else {
hold.set(dependency, [module]);
}
@@ -131,12 +270,12 @@ function reorderDependencies(modules: ModuleInfo[]) {
}
// No unmet dependencies, record and continue
present.set(module.name, module.version);
present.set(module.info.name, module.info.version);
index += 1;
if (hold.has(module.name)) {
modules.splice(index, 0, ...hold.get(module.name));
hold.delete(module.name);
if (hold.has(module.info.name)) {
modules.splice(index, 0, ...hold.get(module.info.name)!);
hold.delete(module.info.name);
}
}
@@ -148,10 +287,10 @@ function reorderDependencies(modules: ModuleInfo[]) {
// on a module that is missing, the module is part of a dependency loop, or the
// the module depends on a module that satisfy any of these conditions.
let remaining = new Map();
let remaining = new Map<string, SaveModule>();
for (let heldModules of hold.values()) {
for (let module of heldModules) {
remaining.set(module.name, module);
remaining.set(module.info.name, module);
}
}
@@ -160,22 +299,22 @@ function reorderDependencies(modules: ModuleInfo[]) {
let cycle: string[] = [];
while (true) {
// Find an unmet dependency
let dependency = Object.keys(module.dependencies).find(name => !present.has(name));
let dependency = [...module.info.dependencies.keys()].find(name => !present.has(name))!;
if (!remaining.has(dependency)) {
// There's no module being held up by this dependency, the
// dependency is missing.
throw new Error(`Missing dependency ${dependency} for module ${module.name}`);
throw new Error(`Missing dependency ${dependency} for module ${module.info.name}`);
}
if (cycle.includes(module.name)) {
cycle.push(module.name);
cycle.splice(0, cycle.indexOf(module.name));
if (cycle.includes(module.info.name)) {
cycle.push(module.info.name);
cycle.splice(0, cycle.indexOf(module.info.name));
throw new Error(`Module dependency loop detected: ${cycle.join(" -> ")}`);
}
cycle.push(module.name);
module = remaining.get(dependency);
cycle.push(module.info.name);
module = remaining.get(dependency)!;
}
}
}
@@ -191,7 +330,7 @@ function reorderDependencies(modules: ModuleInfo[]) {
* @param savePath - Path to the Factorio save to patch.
* @param modules - Description of the modules to patch.
*/
export async function patch(savePath: string, modules: ModuleInfo[]) {
export async function patch(savePath: string, modules: SaveModule[]) {
let zip = await JSZip.loadAsync(await fs.readFile(savePath));
let root = zip.folder(lib.findRoot(zip))!;
@@ -199,7 +338,13 @@ export async function patch(savePath: string, modules: ModuleInfo[]) {
let patchInfo: PatchInfo;
if (patchInfoFile !== null) {
let content = await patchInfoFile.async("string");
patchInfo = JSON.parse(content);
patchInfo = await PatchInfo.fromSave(JSON.parse(content), root);
if (patchInfo.version > PatchInfo.currentVersion) {
throw new Error(
`Save patch version ${patchInfo.version} is newer than the patch version this ` +
`version of Clusterio can load (${PatchInfo.currentVersion})`
);
}
// No info file present, try to detect if it's a known compatible scenario.
} else {
@@ -211,62 +356,42 @@ export async function patch(savePath: string, modules: ModuleInfo[]) {
let controlHash = await lib.hashStream(controlStream);
if (controlHash in knownScenarios) {
patchInfo = {
"patch_number": 0,
"scenario": knownScenarios[controlHash],
"modules": [],
};
patchInfo = new PatchInfo(0, knownScenarios[controlHash], []);
} else {
throw new Error(`Unable to patch save, unknown scenario (${controlHash})`);
}
}
// Increment patch number
patchInfo["patch_number"] = (patchInfo["patch_number"] || 0) + 1;
patchInfo.patchNumber = patchInfo.patchNumber + 1;
// Remove any existing modules from the save
patchInfo["modules"] = [];
for (let file of root.file(/^modules\//)) {
zip.remove(file.name);
if (patchInfo.version === 0) {
for (let file of root.file(/^modules\//)) {
zip.remove(file.name);
}
} else {
for (let module of patchInfo.modules) {
for (let file of module.files.keys()) {
zip.remove(root.file(file)!.name)
}
}
patchInfo.modules = [];
}
reorderDependencies(modules);
// Add the modules to the save.
for (let module of modules) {
let moduleDir = root.folder(`modules/${module.name}`)!;
let moduleInfo = {
"name": module.name,
"files": [] as { path: string, load: boolean, require: boolean }[],
};
let dirs: [string, string][] = [[module.path, ""]];
while (dirs.length) {
let [dir, relativeDir] = dirs.pop()!;
for (let entry of await fs.readdir(dir, { withFileTypes: true })) {
let fsPath = path.join(dir, entry.name);
let relativePath = path.posix.join(relativeDir, entry.name);
if (entry.isFile()) {
moduleDir.file(relativePath, await fs.readFile(fsPath));
moduleInfo["files"].push({
"path": relativePath,
"load": module.load.includes(relativePath),
"require": module.require.includes(relativePath),
});
} else if (entry.isDirectory()) {
dirs.push([fsPath, relativePath]);
}
}
for (let [relativePath, contents] of module.files) {
root.file(relativePath, contents);
}
patchInfo["modules"].push(moduleInfo);
patchInfo.modules.push(module);
}
// Add loading code and patch info
root.file("control.lua", generateLoader(patchInfo));
root.file("clusterio.json", JSON.stringify(patchInfo, null, 4));
root.file("clusterio.json", JSON.stringify(patchInfo, null, "\t"));
// Write back the save
let tempSavePath = `${savePath}.tmp`;
+51
View File
@@ -0,0 +1,51 @@
import { Type, Static } from "@sinclair/typebox";
import { compile } from "../schema";
/**
* Information about a module stored in its module.json file
*/
export default class ModuleInfo {
constructor(
/** Name of the module */
public name: string,
/** Version of this module */
public version: string,
/** Paths into the module that should be loaded into the event handler */
public load: string[] = [],
/** Paths into the module that should be required by control.lua */
public require: string[] = [],
/** Dependencies of this module */
public dependencies = new Map<string, string>(),
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"version": Type.String(),
"dependencies": Type.Optional(Type.Record(Type.String(), Type.String())),
"files": Type.Optional(Type.Array(Type.String())),
"require": Type.Optional(Type.Array(Type.String())),
"load": Type.Optional(Type.Array(Type.String())),
});
static validate = compile<Static<typeof this.jsonSchema>>(this.jsonSchema as any);
toJSON() {
return {
name: this.name,
version: this.version,
dependencies: Object.fromEntries(this.dependencies),
require: this.require,
load: this.load,
};
}
static fromJSON(json: Static<typeof ModuleInfo.jsonSchema>) {
return new this(
json.name,
json.version,
json.load,
json.require,
json.dependencies ? new Map(Object.entries(json.dependencies)) : undefined,
);
}
}
+1
View File
@@ -6,6 +6,7 @@
export { default as ExportManifest } from "./ExportManifest";
export { default as ModInfo } from "./ModInfo";
export { default as ModPack } from "./ModPack";
export { default as ModuleInfo } from "./ModuleInfo";
export * from "./composites";
export * from "./messages_core";
export * from "./messages_controller";
+48 -35
View File
@@ -4,6 +4,7 @@ const fs = require("fs-extra");
const JSZip = require("jszip");
const path = require("path");
const lib = require("@clusterio/lib");
const patch = require("@clusterio/host/dist/src/patch");
@@ -24,36 +25,48 @@ describe("host/patch", function() {
'require("modules/spam/spam")',
"",
].join("\n");
let patchInfo = {
"patch_number": 1,
"scenario": { "modules": ["foo"] },
"modules": [{
"name": "spam",
"files": [
{ "path": "bar.lua", "load": true, "require": false },
{ "path": "spam.lua", "load": false, "require": true },
{ "path": "excluded.lua", "load": false, "require": false },
],
}],
};
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 = [
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {} },
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
module({ name: "b", version: "1.0.0", dependencies: {} }),
];
patch._reorderDependencies(modules);
assert(modules[0].name === "b", "Dependency was not reorederd");
assert(modules[0].info.name === "b", "Dependency was not reorederd");
});
it("should throw on invalid version", function() {
let modules = [
{ name: "a", version: "foo", dependencies: {} },
module({ name: "a", version: "foo", dependencies: {} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
@@ -62,7 +75,7 @@ describe("host/patch", function() {
});
it("should throw on invalid version range", function() {
let modules = [
{ name: "a", version: "1.0.0", dependencies: {"b": "invalid"} },
module({ name: "a", version: "1.0.0", dependencies: {"b": "invalid"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
@@ -71,16 +84,16 @@ describe("host/patch", function() {
});
it("should throw on missing dependency", function() {
let modules = [
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
module({ name: "a", version: "1.0.0", dependencies: {"b": "*"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
new Error("Missing dependency b for module a")
);
modules = [
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {"c": "*"} },
{ name: "c", version: "1.0.0", dependencies: {"d": "*"} },
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),
@@ -89,8 +102,8 @@ describe("host/patch", function() {
});
it("should throw on outdated dependency", function() {
let modules = [
{ name: "a", version: "1.0.0", dependencies: {} },
{ name: "b", version: "1.0.0", dependencies: {"a": ">=2"} },
module({ name: "a", version: "1.0.0", dependencies: {} }),
module({ name: "b", version: "1.0.0", dependencies: {"a": ">=2"} }),
];
assert.throws(
() => patch._reorderDependencies(modules),
@@ -99,18 +112,18 @@ describe("host/patch", function() {
});
it("should throw on dependency loops", function() {
let modules = [
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {"a": "*"} },
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 = [
{ name: "d", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {"c": "*"} },
{ name: "c", version: "1.0.0", dependencies: {"a": "*"} },
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),
@@ -120,13 +133,13 @@ describe("host/patch", function() {
it("should reorder correctly for multiple dependencies", function() {
function validate(modules) {
let present = new Set();
for (let module of modules) {
for (let dependency of Object.keys(module.dependencies)) {
for (let { info } of modules) {
for (let dependency of info.dependencies.keys()) {
if (!present.has(dependency)) {
assert.fail(`${module.name} depends on ${dependency}, but was ordered before it`);
assert.fail(`${info.name} depends on ${dependency}, but was ordered before it`);
}
}
present.add(module.name);
present.add(info.name);
}
}
@@ -149,7 +162,7 @@ describe("host/patch", function() {
dependencies[names[k + (k >= j)]] = "*";
}
}
modules.push({ name, version: "1.0.0", dependencies });
modules.push(module({ name, version: "1.0.0", dependencies }));
}
let success = false;
try {
@@ -164,7 +177,7 @@ describe("host/patch", function() {
// Deriving the formula for the number of graphs without
// dependencies loops is left as an excercise for the reader.
assert(successCount === 25, "Incorrect number of successful orderings");
assert.equal(successCount, 25, "Incorrect number of successful orderings");
});
});
+7 -16
View File
@@ -4,7 +4,8 @@ const fs = require("fs-extra");
const JSZip = require("jszip");
const path = require("path");
const { patch } = require("@clusterio/host/dist/src/patch");
const { patch, SaveModule } = require("@clusterio/host/dist/src/patch");
const lib = require("@clusterio/lib");
const { slowTest } = require("./index");
@@ -17,21 +18,11 @@ describe("Integration of lib/factorio/patch", function() {
let savePath = path.join("temp", "test", "integration", "saves", "test.zip");
it("should patch a freeplay game", async function() {
slowTest(this);
await patch(savePath, [{
name: "test",
version: "1.0.0",
dependencies: {},
path: "test/file/modules/test",
load: [],
require: [],
}, {
name: "subdir",
version: "1.0.0",
dependencies: {},
path: "test/file/modules/subdir",
load: [],
require: [],
}]);
let testModule = new SaveModule(new lib.ModuleInfo("test", "1.0.0"));
let subdirModule = new SaveModule(new lib.ModuleInfo("subdir", "1.0.0"));
await testModule.loadFiles("test/file/modules/test");
await subdirModule.loadFiles("test/file/modules/subdir");
await patch(savePath, [testModule, subdirModule]);
let zip = await JSZip.loadAsync(await fs.readFile(savePath));
assert.equal(await zip.file("test/modules/test/test.lua").async("string"), "-- test\n");