diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index b92e9bfa..77d223ce 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -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; - } - // Find plugin modules to patch in - let modules: Map = 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 = 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; - 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); } } diff --git a/packages/host/src/patch.ts b/packages/host/src/patch.ts index 111cf8da..d1d2071e 100644 --- a/packages/host/src/patch.ts +++ b/packages/host/src/patch.ts @@ -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, + ) { } + + 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, 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; - 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, 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 = { +const knownScenarios: Record = { // 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(); + let hold = new Map(); 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(); 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`; diff --git a/packages/lib/src/data/ModuleInfo.ts b/packages/lib/src/data/ModuleInfo.ts new file mode 100644 index 00000000..968a456e --- /dev/null +++ b/packages/lib/src/data/ModuleInfo.ts @@ -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(), + ) { } + + 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>(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) { + return new this( + json.name, + json.version, + json.load, + json.require, + json.dependencies ? new Map(Object.entries(json.dependencies)) : undefined, + ); + } +} diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index 46c1ed2a..55d07e21 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -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"; diff --git a/test/host/patch.js b/test/host/patch.js index aa64a7a1..cf48fc27 100644 --- a/test/host/patch.js +++ b/test/host/patch.js @@ -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"); }); }); diff --git a/test/integration/patch.js b/test/integration/patch.js index 79b5807e..ae3bb9a5 100644 --- a/test/integration/patch.js +++ b/test/integration/patch.js @@ -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");