factorio-test-cli 1.0.6 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +3 -3
- package/package.json +7 -7
- package/run.js +148 -18
package/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "commander";
|
|
3
3
|
import "./run.js";
|
|
4
|
-
program
|
|
4
|
+
await program
|
|
5
5
|
.name("factorio-test")
|
|
6
6
|
.description("cli for factorio testing")
|
|
7
|
-
.
|
|
7
|
+
.helpCommand(true)
|
|
8
8
|
.showHelpAfterError()
|
|
9
9
|
.showSuggestionAfterError()
|
|
10
|
-
.
|
|
10
|
+
.parseAsync();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factorio-test-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A CLI to run FactorioTest.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/GlassBricks/FactorioTest",
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
"*.js"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"chalk": "^5.
|
|
19
|
-
"commander": "^
|
|
20
|
-
"factoriomod-debug": "^
|
|
18
|
+
"chalk": "^5.3.0",
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"factoriomod-debug": "^2.0.3"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@commander-js/extra-typings": "^
|
|
24
|
-
"del-cli": "^
|
|
25
|
-
"typescript": "^5.
|
|
23
|
+
"@commander-js/extra-typings": "^12.1.0",
|
|
24
|
+
"del-cli": "^6.0.0",
|
|
25
|
+
"typescript": "^5.7.2"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "npm run clean && tsc",
|
package/run.js
CHANGED
|
@@ -6,17 +6,24 @@ import * as path from "path";
|
|
|
6
6
|
import { spawn, spawnSync } from "child_process";
|
|
7
7
|
import BufferLineSplitter from "./buffer-line-splitter.js";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
+
import * as process from "node:process";
|
|
10
|
+
import * as https from "https";
|
|
11
|
+
import * as readline from "readline";
|
|
12
|
+
const FACTORIO_TEST_MOD_VERSION = "2.0.1";
|
|
9
13
|
const thisCommand = program
|
|
10
14
|
.command("run")
|
|
11
15
|
.summary("Runs tests with Factorio test.")
|
|
12
16
|
.description("Runs tests for the specified mod with Factorio test. Exits with code 0 only if all tests pass.\n")
|
|
13
|
-
.argument("[mod-path]", "The path to the mod
|
|
14
|
-
.option("--mod-name <name>", "The name of the mod to test.
|
|
15
|
-
.option("--factorio-path <path>", "The path to the factorio binary. If not specified,
|
|
16
|
-
.option("-d --data-directory <path>", 'The path to the data directory. The "config.ini" file and the "mods" folder will be in this directory.', "./factorio-test-data")
|
|
17
|
-
.option("--
|
|
18
|
-
|
|
19
|
-
.
|
|
17
|
+
.argument("[mod-path]", "The path to the mod (folder containing info.json). A symlink will be created in the mods folder to this folder. Either this or --mod-name must be specified.")
|
|
18
|
+
.option("--mod-name <name>", "The name of the mod to test. To use this option, the mod must already be present in the mods directory (see --data-directory). Either this or [mod-path] must be specified.")
|
|
19
|
+
.option("--factorio-path <path>", "The path to the factorio binary. If not specified, attempts to auto-detect the path.")
|
|
20
|
+
.option("-d --data-directory <path>", 'The path to the factorio data directory that the testing instance will use. The "config.ini" file and the "mods" folder will be in this directory.', "./factorio-test-data-dir")
|
|
21
|
+
.option("--mods <mods...>", 'Adjust mods. By default, only the mod to test and "factorio-test" are enabled, and all others are disabled! ' +
|
|
22
|
+
'Same format as "fmtk mods adjust". Example: "--mods mod1 mod2=1.2.3" will enable mod1 any version, and mod2 version 1.2.3.')
|
|
23
|
+
.option("--show-output", "Print test output to stdout.", true)
|
|
24
|
+
.option("-v --verbose", "Enables more logging, and pipes the Factorio process output to stdout.")
|
|
25
|
+
.addHelpText("after", 'Arguments after "--" are passed to the Factorio process.')
|
|
26
|
+
.addHelpText("after", 'Suggested factorio arguments: "--cache-sprite-atlas", "--disable-audio"')
|
|
20
27
|
.action((modPath, options) => runTests(modPath, options));
|
|
21
28
|
async function runTests(modPath, options) {
|
|
22
29
|
if (modPath !== undefined && options.modName !== undefined) {
|
|
@@ -31,9 +38,14 @@ async function runTests(modPath, options) {
|
|
|
31
38
|
await fsp.mkdir(modsDir, { recursive: true });
|
|
32
39
|
const modToTest = await configureModToTest(modsDir, modPath, options.modName);
|
|
33
40
|
await installFactorioTest(modsDir);
|
|
41
|
+
const enableModsOptions = [
|
|
42
|
+
"factorio-test=true",
|
|
43
|
+
`${modToTest}=true`,
|
|
44
|
+
...(options.mods?.map((m) => (m.includes("=") ? m : `${m}=true`)) ?? []),
|
|
45
|
+
];
|
|
34
46
|
if (options.verbose)
|
|
35
|
-
console.log("
|
|
36
|
-
await runScript("fmtk mods adjust", "--modsPath", modsDir, "
|
|
47
|
+
console.log("Adjusting mods");
|
|
48
|
+
await runScript("fmtk mods adjust", "--modsPath", modsDir, "--disableExtra", ...enableModsOptions);
|
|
37
49
|
await ensureConfigIni(dataDir);
|
|
38
50
|
await setSettingsForAutorun(factorioPath, dataDir, modsDir, modToTest);
|
|
39
51
|
let resultMessage;
|
|
@@ -108,12 +120,121 @@ async function checkModExists(modsDir, modName) {
|
|
|
108
120
|
}
|
|
109
121
|
async function installFactorioTest(modsDir) {
|
|
110
122
|
await fsp.mkdir(modsDir, { recursive: true });
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
const modName = "factorio-test";
|
|
124
|
+
const version = FACTORIO_TEST_MOD_VERSION;
|
|
125
|
+
const expectedZipName = `${modName}_${version}.zip`;
|
|
126
|
+
const expectedZipPath = path.join(modsDir, expectedZipName);
|
|
127
|
+
if (fs.existsSync(expectedZipPath)) {
|
|
128
|
+
if (thisCommand.opts().verbose)
|
|
129
|
+
console.log(`${modName} version ${version} already installed`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(`Downloading ${modName} version ${version} from mod portal...`);
|
|
133
|
+
await downloadModVersion(modName, version, expectedZipPath);
|
|
134
|
+
}
|
|
135
|
+
async function downloadModVersion(modName, version, destPath) {
|
|
136
|
+
const modInfo = await fetchJson(`https://mods.factorio.com/api/mods/${modName}`);
|
|
137
|
+
const release = modInfo.releases.find((r) => r.version === version);
|
|
138
|
+
if (!release) {
|
|
139
|
+
const availableVersions = modInfo.releases.map((r) => r.version).join(", ");
|
|
140
|
+
throw new Error(`Version ${version} not found for mod ${modName}. Available: ${availableVersions}`);
|
|
141
|
+
}
|
|
142
|
+
const credentials = await getFactorioCredentials();
|
|
143
|
+
const downloadUrl = `https://mods.factorio.com${release.download_url}?username=${encodeURIComponent(credentials.username)}&token=${encodeURIComponent(credentials.token)}`;
|
|
144
|
+
await downloadFile(downloadUrl, destPath);
|
|
145
|
+
}
|
|
146
|
+
async function fetchJson(url) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
https.get(url, (res) => {
|
|
149
|
+
if (res.statusCode !== 200) {
|
|
150
|
+
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
let data = "";
|
|
154
|
+
res.on("data", (chunk) => (data += chunk));
|
|
155
|
+
res.on("end", () => {
|
|
156
|
+
try {
|
|
157
|
+
resolve(JSON.parse(data));
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
reject(new Error(`Failed to parse JSON from ${url}`, { cause: e }));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
res.on("error", reject);
|
|
164
|
+
}).on("error", reject);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async function downloadFile(url, destPath) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
https.get(url, (res) => {
|
|
170
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
171
|
+
const redirectUrl = res.headers.location;
|
|
172
|
+
if (!redirectUrl) {
|
|
173
|
+
reject(new Error("Redirect without location header"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
downloadFile(redirectUrl, destPath).then(resolve, reject);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (res.statusCode !== 200) {
|
|
180
|
+
reject(new Error(`HTTP ${res.statusCode} downloading mod`));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
184
|
+
res.pipe(fileStream);
|
|
185
|
+
fileStream.on("close", () => resolve());
|
|
186
|
+
fileStream.on("error", (err) => {
|
|
187
|
+
fs.unlink(destPath, () => { });
|
|
188
|
+
reject(err);
|
|
189
|
+
});
|
|
190
|
+
}).on("error", reject);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function getFactorioCredentials() {
|
|
194
|
+
const playerDataPath = getPlayerDataPath();
|
|
195
|
+
if (playerDataPath && fs.existsSync(playerDataPath)) {
|
|
196
|
+
try {
|
|
197
|
+
const playerData = JSON.parse(await fsp.readFile(playerDataPath, "utf8"));
|
|
198
|
+
if (playerData["service-username"] && playerData["service-token"]) {
|
|
199
|
+
return {
|
|
200
|
+
username: playerData["service-username"],
|
|
201
|
+
token: playerData["service-token"],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Fall through to prompt
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
console.log("Factorio credentials required for mod portal download.");
|
|
210
|
+
return promptForCredentials();
|
|
211
|
+
}
|
|
212
|
+
function getPlayerDataPath() {
|
|
213
|
+
const platform = os.platform();
|
|
214
|
+
if (platform === "linux") {
|
|
215
|
+
return path.join(os.homedir(), ".factorio", "player-data.json");
|
|
216
|
+
}
|
|
217
|
+
else if (platform === "darwin") {
|
|
218
|
+
return path.join(os.homedir(), "Library", "Application Support", "factorio", "player-data.json");
|
|
219
|
+
}
|
|
220
|
+
else if (platform === "win32") {
|
|
221
|
+
return path.join(os.homedir(), "AppData", "Roaming", "Factorio", "player-data.json");
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
async function promptForCredentials() {
|
|
226
|
+
const rl = readline.createInterface({
|
|
227
|
+
input: process.stdin,
|
|
228
|
+
output: process.stdout,
|
|
229
|
+
});
|
|
230
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
231
|
+
try {
|
|
232
|
+
const username = await question("Factorio username: ");
|
|
233
|
+
const token = await question("Factorio token (from https://factorio.com/profile): ");
|
|
234
|
+
return { username, token };
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
rl.close();
|
|
117
238
|
}
|
|
118
239
|
}
|
|
119
240
|
async function ensureConfigIni(dataDir) {
|
|
@@ -131,6 +252,14 @@ write-data=${dataDir}
|
|
|
131
252
|
locale=
|
|
132
253
|
`);
|
|
133
254
|
}
|
|
255
|
+
else {
|
|
256
|
+
// edit "^write-data=.*" to be dataDir
|
|
257
|
+
const content = await fsp.readFile(filePath, "utf8");
|
|
258
|
+
const newContent = content.replace(/^write-data=.*$/m, `write-data=${dataDir}`);
|
|
259
|
+
if (content !== newContent) {
|
|
260
|
+
await fsp.writeFile(filePath, newContent);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
134
263
|
}
|
|
135
264
|
async function setSettingsForAutorun(factorioPath, dataDir, modsDir, modToTest) {
|
|
136
265
|
// touch modsDir/mod-settings.dat
|
|
@@ -157,6 +286,7 @@ async function runFactorioTests(factorioPath, dataDir) {
|
|
|
157
286
|
const actualArgs = [
|
|
158
287
|
"--load-scenario",
|
|
159
288
|
"factorio-test/Test",
|
|
289
|
+
"--disable-migration-window",
|
|
160
290
|
"--mod-directory",
|
|
161
291
|
path.join(dataDir, "mods"),
|
|
162
292
|
"-c",
|
|
@@ -201,12 +331,12 @@ async function runFactorioTests(factorioPath, dataDir) {
|
|
|
201
331
|
}
|
|
202
332
|
});
|
|
203
333
|
await new Promise((resolve, reject) => {
|
|
204
|
-
factorioProcess.on("exit", (code) => {
|
|
205
|
-
if (code === 0
|
|
334
|
+
factorioProcess.on("exit", (code, signal) => {
|
|
335
|
+
if (code === 0 && resultMessage !== undefined) {
|
|
206
336
|
resolve();
|
|
207
337
|
}
|
|
208
338
|
else {
|
|
209
|
-
reject(new Error(`Factorio exited with code ${code}`));
|
|
339
|
+
reject(new Error(`Factorio exited with code ${code}, signal ${signal}`));
|
|
210
340
|
}
|
|
211
341
|
});
|
|
212
342
|
});
|