factorio-test-cli 3.3.1 → 3.5.0
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/config/cli-config.js +1 -0
- package/config/loader.js +1 -0
- package/factorio-process.js +1 -1
- package/mod-setup.js +12 -9
- package/package.json +1 -1
- package/run.js +20 -2
- package/test-output.js +23 -1
- package/test-output.test.js +121 -0
package/config/cli-config.js
CHANGED
|
@@ -131,6 +131,7 @@ export function registerAllCliOptions(command) {
|
|
|
131
131
|
command.option("-c --config <path>", "Path to config file (default: factorio-test.json, or 'factorio-test' key in package.json)");
|
|
132
132
|
command.option("-g --graphics", "Launch Factorio with graphics (interactive mode) instead of headless");
|
|
133
133
|
command.option("-w --watch", "Watch for file changes and rerun tests");
|
|
134
|
+
command.option("--no-auto-start", "Configure tests but do not auto-start them (requires --graphics)");
|
|
134
135
|
addOption(command, f.modPath.cli);
|
|
135
136
|
addOption(command, f.modName.cli);
|
|
136
137
|
addOption(command, f.factorioPath.cli);
|
package/config/loader.js
CHANGED
|
@@ -58,6 +58,7 @@ export function resolveConfig({ cliOptions, patterns }) {
|
|
|
58
58
|
return {
|
|
59
59
|
graphics: cliOptions.graphics,
|
|
60
60
|
watch: cliOptions.watch,
|
|
61
|
+
noAutoStart: cliOptions.autoStart === false ? true : undefined,
|
|
61
62
|
modPath: baseConfig.modPath,
|
|
62
63
|
modName: baseConfig.modName,
|
|
63
64
|
factorioPath: baseConfig.factorioPath,
|
package/factorio-process.js
CHANGED
|
@@ -118,7 +118,7 @@ function createOutputComponents(options) {
|
|
|
118
118
|
collector.handleEvent(event);
|
|
119
119
|
progress.handleEvent(event);
|
|
120
120
|
if (options.verbose) {
|
|
121
|
-
progress.withPermanentOutput(() =>
|
|
121
|
+
progress.withPermanentOutput(() => printer.printEvent(event));
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
handler.on("log", (line) => {
|
package/mod-setup.js
CHANGED
|
@@ -154,17 +154,20 @@ locale=
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
export async function
|
|
157
|
+
export async function ensureModSettingsDat(factorioPath, dataDir, modsDir, verbose) {
|
|
158
158
|
const settingsDat = path.join(modsDir, "mod-settings.dat");
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
if (fs.existsSync(settingsDat))
|
|
160
|
+
return;
|
|
161
|
+
if (verbose)
|
|
162
|
+
console.log("Creating mod-settings.dat file by running factorio");
|
|
163
|
+
const dummySaveFile = path.join(dataDir, "____dummy_save_file.zip");
|
|
164
|
+
await runProcess(false, factorioPath, "--create", dummySaveFile, "--mod-directory", modsDir, "-c", path.join(dataDir, "config.ini"));
|
|
165
|
+
if (fs.existsSync(dummySaveFile)) {
|
|
166
|
+
await fsp.rm(dummySaveFile);
|
|
167
167
|
}
|
|
168
|
+
}
|
|
169
|
+
export async function setSettingsForAutorun(factorioPath, dataDir, modsDir, modToTest, mode, options) {
|
|
170
|
+
await ensureModSettingsDat(factorioPath, dataDir, modsDir, options?.verbose);
|
|
168
171
|
if (options?.verbose)
|
|
169
172
|
console.log("Setting autorun settings");
|
|
170
173
|
const autoStartConfig = JSON.stringify({
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -8,7 +8,7 @@ import { registerAllCliOptions, resolveConfig } from "./config/index.js";
|
|
|
8
8
|
import { autoDetectFactorioPath } from "./factorio-process.js";
|
|
9
9
|
import { getHeadlessSavePath, runFactorioTestsGraphics, runFactorioTestsHeadless, } from "./factorio-process.js";
|
|
10
10
|
import { watchDirectory, watchFile } from "./file-watcher.js";
|
|
11
|
-
import { configureModToTest, ensureConfigIni, installFactorioTest, installModDependencies, installMods, parseModRequirement, resetAutorunSettings, resolveModWatchTarget, setSettingsForAutorun, } from "./mod-setup.js";
|
|
11
|
+
import { configureModToTest, ensureConfigIni, ensureModSettingsDat, installFactorioTest, installModDependencies, installMods, parseModRequirement, resetAutorunSettings, resolveModWatchTarget, setSettingsForAutorun, } from "./mod-setup.js";
|
|
12
12
|
import { runScript, setVerbose } from "./process-utils.js";
|
|
13
13
|
import { OutputFormatter } from "./test-output.js";
|
|
14
14
|
import { readPreviousFailedTests, writeResultsFile } from "./test-results.js";
|
|
@@ -47,6 +47,9 @@ async function setupTestRun(patterns, cliOptions) {
|
|
|
47
47
|
if (config.modPath === undefined && config.modName === undefined) {
|
|
48
48
|
throw new CliError("One of --mod-path or --mod-name must be specified.");
|
|
49
49
|
}
|
|
50
|
+
if (config.noAutoStart && !config.graphics) {
|
|
51
|
+
throw new CliError("--no-auto-start requires --graphics.");
|
|
52
|
+
}
|
|
50
53
|
const factorioPath = config.factorioPath ?? autoDetectFactorioPath();
|
|
51
54
|
const dataDir = config.dataDirectory;
|
|
52
55
|
const modsDir = path.join(dataDir, "mods");
|
|
@@ -191,9 +194,24 @@ async function runHeadlessWatchMode(ctx) {
|
|
|
191
194
|
});
|
|
192
195
|
return new Promise(() => { });
|
|
193
196
|
}
|
|
197
|
+
async function launchWithoutAutoStart(ctx) {
|
|
198
|
+
const { config, factorioPath, dataDir, modsDir, modToTest, savePath, factorioArgs } = ctx;
|
|
199
|
+
await ensureModSettingsDat(factorioPath, dataDir, modsDir, config.verbose);
|
|
200
|
+
await runScript("fmtk", "settings", "set", "runtime-global", "factorio-test-mod-to-test", modToTest, "--modsPath", modsDir);
|
|
201
|
+
if (Object.keys(config.testConfig).length > 0) {
|
|
202
|
+
await runScript("fmtk", "settings", "set", "runtime-global", "factorio-test-config", JSON.stringify(config.testConfig), "--modsPath", modsDir);
|
|
203
|
+
}
|
|
204
|
+
await runFactorioTestsGraphics(factorioPath, dataDir, savePath, factorioArgs, {
|
|
205
|
+
verbose: config.verbose,
|
|
206
|
+
quiet: config.quiet,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
194
209
|
async function runTests(patterns, cliOptions) {
|
|
195
210
|
const ctx = await setupTestRun(patterns, cliOptions);
|
|
196
|
-
if (ctx.config.
|
|
211
|
+
if (ctx.config.noAutoStart) {
|
|
212
|
+
await launchWithoutAutoStart(ctx);
|
|
213
|
+
}
|
|
214
|
+
else if (ctx.config.watch && ctx.config.graphics) {
|
|
197
215
|
await runGraphicsWatchMode(ctx);
|
|
198
216
|
}
|
|
199
217
|
else if (ctx.config.watch) {
|
package/test-output.js
CHANGED
|
@@ -90,7 +90,8 @@ export class OutputFormatter {
|
|
|
90
90
|
const prefix = this.getPrefix(test.result);
|
|
91
91
|
const duration = test.durationMs !== undefined ? ` (${formatDuration(test.durationMs)})` : "";
|
|
92
92
|
console.log(`${prefix} ${test.path}${duration}`);
|
|
93
|
-
const showLogs =
|
|
93
|
+
const showLogs = this.options.showLogs !== false &&
|
|
94
|
+
(test.result === "failed" || test.result === "error" || this.options.showPassedLogs);
|
|
94
95
|
if (showLogs && test.logs.length > 0) {
|
|
95
96
|
console.log("Log messages:");
|
|
96
97
|
for (const log of test.logs) {
|
|
@@ -139,6 +140,20 @@ export class OutputFormatter {
|
|
|
139
140
|
const total = summary.passed + summary.failed + summary.skipped + summary.todo;
|
|
140
141
|
console.log(`Tests: ${segments.join(", ")} (${total} total)`);
|
|
141
142
|
}
|
|
143
|
+
formatEvent(event) {
|
|
144
|
+
switch (event.type) {
|
|
145
|
+
case "testRunStarted":
|
|
146
|
+
return chalk.dim(`Running ${event.total} tests...`);
|
|
147
|
+
case "testStarted":
|
|
148
|
+
return chalk.dim(`Starting: ${event.test.path}`);
|
|
149
|
+
case "loadError":
|
|
150
|
+
return chalk.red(`Load error: ${event.error}`);
|
|
151
|
+
case "testRunCancelled":
|
|
152
|
+
return chalk.yellow("Test run cancelled");
|
|
153
|
+
default:
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
142
157
|
getPrefix(result) {
|
|
143
158
|
switch (result) {
|
|
144
159
|
case "passed":
|
|
@@ -164,6 +179,7 @@ export class OutputPrinter {
|
|
|
164
179
|
verbose: options.verbose,
|
|
165
180
|
quiet: options.quiet,
|
|
166
181
|
showPassedLogs: options.verbose,
|
|
182
|
+
showLogs: !options.verbose,
|
|
167
183
|
});
|
|
168
184
|
}
|
|
169
185
|
printTestResult(test) {
|
|
@@ -187,6 +203,12 @@ export class OutputPrinter {
|
|
|
187
203
|
resetMessage() {
|
|
188
204
|
this.isMessageFirstLine = true;
|
|
189
205
|
}
|
|
206
|
+
printEvent(event) {
|
|
207
|
+
const formatted = this.formatter.formatEvent(event);
|
|
208
|
+
if (formatted !== undefined) {
|
|
209
|
+
console.log(formatted);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
190
212
|
printVerbose(line) {
|
|
191
213
|
if (this.options.verbose) {
|
|
192
214
|
console.log(line);
|
package/test-output.test.js
CHANGED
|
@@ -223,6 +223,104 @@ describe("OutputFormatter", () => {
|
|
|
223
223
|
expect(output.some((line) => line.includes("Oh no"))).toBe(true);
|
|
224
224
|
});
|
|
225
225
|
});
|
|
226
|
+
describe("OutputFormatter.formatEvent", () => {
|
|
227
|
+
it("formats testRunStarted", () => {
|
|
228
|
+
const formatter = new OutputFormatter({});
|
|
229
|
+
const result = formatter.formatEvent({ type: "testRunStarted", total: 42 });
|
|
230
|
+
expect(result).toContain("Running 42 tests...");
|
|
231
|
+
});
|
|
232
|
+
it("formats testStarted", () => {
|
|
233
|
+
const formatter = new OutputFormatter({});
|
|
234
|
+
const result = formatter.formatEvent({ type: "testStarted", test: { path: "root > my test" } });
|
|
235
|
+
expect(result).toContain("Starting: root > my test");
|
|
236
|
+
});
|
|
237
|
+
it("formats loadError", () => {
|
|
238
|
+
const formatter = new OutputFormatter({});
|
|
239
|
+
const result = formatter.formatEvent({ type: "loadError", error: "syntax error in foo.lua" });
|
|
240
|
+
expect(result).toContain("Load error: syntax error in foo.lua");
|
|
241
|
+
});
|
|
242
|
+
it("formats testRunCancelled", () => {
|
|
243
|
+
const formatter = new OutputFormatter({});
|
|
244
|
+
const result = formatter.formatEvent({ type: "testRunCancelled" });
|
|
245
|
+
expect(result).toContain("Test run cancelled");
|
|
246
|
+
});
|
|
247
|
+
it("returns undefined for testPassed", () => {
|
|
248
|
+
const formatter = new OutputFormatter({});
|
|
249
|
+
const result = formatter.formatEvent({ type: "testPassed", test: { path: "test" } });
|
|
250
|
+
expect(result).toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
it("returns undefined for testFailed", () => {
|
|
253
|
+
const formatter = new OutputFormatter({});
|
|
254
|
+
const result = formatter.formatEvent({ type: "testFailed", test: { path: "test" }, errors: ["err"] });
|
|
255
|
+
expect(result).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
it("returns undefined for testRunFinished", () => {
|
|
258
|
+
const formatter = new OutputFormatter({});
|
|
259
|
+
const result = formatter.formatEvent({
|
|
260
|
+
type: "testRunFinished",
|
|
261
|
+
results: {
|
|
262
|
+
ran: 1,
|
|
263
|
+
passed: 1,
|
|
264
|
+
failed: 0,
|
|
265
|
+
skipped: 0,
|
|
266
|
+
todo: 0,
|
|
267
|
+
cancelled: 0,
|
|
268
|
+
describeBlockErrors: 0,
|
|
269
|
+
status: "passed",
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
expect(result).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
describe("OutputFormatter showLogs option", () => {
|
|
276
|
+
let consoleSpy;
|
|
277
|
+
let output;
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
output = [];
|
|
280
|
+
consoleSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
|
|
281
|
+
output.push(args.join(" "));
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
consoleSpy.mockRestore();
|
|
286
|
+
});
|
|
287
|
+
it("showLogs: false suppresses logs for failed tests", () => {
|
|
288
|
+
const formatter = new OutputFormatter({ showLogs: false });
|
|
289
|
+
const test = {
|
|
290
|
+
path: "test",
|
|
291
|
+
result: "failed",
|
|
292
|
+
errors: ["assertion failed"],
|
|
293
|
+
logs: ["debug output"],
|
|
294
|
+
};
|
|
295
|
+
formatter.formatTestResult(test);
|
|
296
|
+
expect(output.some((l) => l.includes("Log messages:"))).toBe(false);
|
|
297
|
+
expect(output.some((l) => l.includes("debug output"))).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
it("showLogs: false still shows errors", () => {
|
|
300
|
+
const formatter = new OutputFormatter({ showLogs: false });
|
|
301
|
+
const test = {
|
|
302
|
+
path: "test",
|
|
303
|
+
result: "failed",
|
|
304
|
+
errors: ["assertion failed"],
|
|
305
|
+
logs: ["debug output"],
|
|
306
|
+
};
|
|
307
|
+
formatter.formatTestResult(test);
|
|
308
|
+
expect(output.some((l) => l.includes("Errors:"))).toBe(true);
|
|
309
|
+
expect(output.some((l) => l.includes("assertion failed"))).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
it("showLogs: false suppresses logs for error results", () => {
|
|
312
|
+
const formatter = new OutputFormatter({ showLogs: false });
|
|
313
|
+
const test = {
|
|
314
|
+
path: "block",
|
|
315
|
+
result: "error",
|
|
316
|
+
errors: ["hook error"],
|
|
317
|
+
logs: ["hook log"],
|
|
318
|
+
};
|
|
319
|
+
formatter.formatTestResult(test);
|
|
320
|
+
expect(output.some((l) => l.includes("hook log"))).toBe(false);
|
|
321
|
+
expect(output.some((l) => l.includes("hook error"))).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
226
324
|
describe("OutputFormatter.formatSummary", () => {
|
|
227
325
|
let consoleSpy;
|
|
228
326
|
let output;
|
|
@@ -403,6 +501,29 @@ describe("OutputPrinter", () => {
|
|
|
403
501
|
expect(output.some((line) => line.includes("FAIL"))).toBe(true);
|
|
404
502
|
expect(output.some((line) => line.includes("TODO"))).toBe(true);
|
|
405
503
|
});
|
|
504
|
+
it("verbose mode: printEvent prints formatted events", () => {
|
|
505
|
+
const printer = new OutputPrinter({ verbose: true });
|
|
506
|
+
printer.printEvent({ type: "testStarted", test: { path: "root > test" } });
|
|
507
|
+
expect(output.some((l) => l.includes("Starting: root > test"))).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
it("verbose mode: printEvent skips events that return undefined", () => {
|
|
510
|
+
const printer = new OutputPrinter({ verbose: true });
|
|
511
|
+
printer.printEvent({ type: "testPassed", test: { path: "test" } });
|
|
512
|
+
expect(output).toHaveLength(0);
|
|
513
|
+
});
|
|
514
|
+
it("verbose mode: printTestResult does not include logs", () => {
|
|
515
|
+
const printer = new OutputPrinter({ verbose: true });
|
|
516
|
+
const test = {
|
|
517
|
+
path: "test",
|
|
518
|
+
result: "failed",
|
|
519
|
+
errors: ["err"],
|
|
520
|
+
logs: ["debug log"],
|
|
521
|
+
};
|
|
522
|
+
printer.printTestResult(test);
|
|
523
|
+
expect(output.some((l) => l.includes("FAIL"))).toBe(true);
|
|
524
|
+
expect(output.some((l) => l.includes("debug log"))).toBe(false);
|
|
525
|
+
expect(output.some((l) => l.includes("err"))).toBe(true);
|
|
526
|
+
});
|
|
406
527
|
it("hides all tests in quiet mode", () => {
|
|
407
528
|
const printer = new OutputPrinter({ quiet: true });
|
|
408
529
|
printer.printTestResult(passedTest);
|