factorio-test-cli 3.0.2 → 3.0.4
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 +1 -1
- package/factorio-output-parser.js +6 -1
- package/factorio-output-parser.test.js +20 -1
- package/factorio-process.js +92 -26
- package/factorio-process.test.js +28 -1
- package/mod-setup.js +1 -1
- package/package.json +1 -1
- package/run.js +68 -68
- package/{output-formatter.js → test-output.js} +76 -9
- package/test-output.test.js +256 -0
- package/{test-run-collector.js → test-results.js} +30 -0
- package/{test-run-collector.test.js → test-results.test.js} +89 -2
- package/buffer-line-splitter.js +0 -31
- package/factorio-discovery.js +0 -53
- package/factorio-discovery.test.js +0 -24
- package/output-formatter.test.js +0 -137
- package/progress-renderer.js +0 -75
- package/progress-renderer.test.js +0 -111
- package/results-writer.js +0 -30
- package/results-writer.test.js +0 -89
package/cli.js
CHANGED
|
@@ -13,9 +13,14 @@ export function parseEvent(line) {
|
|
|
13
13
|
}
|
|
14
14
|
export class FactorioOutputHandler extends EventEmitter {
|
|
15
15
|
inMessage = false;
|
|
16
|
+
resultMessage;
|
|
17
|
+
getResultMessage() {
|
|
18
|
+
return this.resultMessage;
|
|
19
|
+
}
|
|
16
20
|
handleLine(line) {
|
|
17
21
|
if (line.startsWith("FACTORIO-TEST-RESULT:")) {
|
|
18
|
-
this.
|
|
22
|
+
this.resultMessage = line.slice("FACTORIO-TEST-RESULT:".length);
|
|
23
|
+
this.emit("result", this.resultMessage);
|
|
19
24
|
return;
|
|
20
25
|
}
|
|
21
26
|
if (line === "FACTORIO-TEST-MESSAGE-START") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseEvent } from "./factorio-output-parser.js";
|
|
2
|
+
import { FactorioOutputHandler, parseEvent } from "./factorio-output-parser.js";
|
|
3
3
|
describe("parseEvent", () => {
|
|
4
4
|
it("parses valid testStarted event", () => {
|
|
5
5
|
const line = 'FACTORIO-TEST-EVENT:{"type":"testStarted","test":{"path":"root > mytest"}}';
|
|
@@ -36,3 +36,22 @@ describe("parseEvent", () => {
|
|
|
36
36
|
expect(parseEvent("FACTORIO-TEST-EVENT:")).toBeUndefined();
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
describe("FactorioOutputHandler", () => {
|
|
40
|
+
it("getResultMessage returns undefined before result received", () => {
|
|
41
|
+
const handler = new FactorioOutputHandler();
|
|
42
|
+
expect(handler.getResultMessage()).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it("getResultMessage returns result after receiving FACTORIO-TEST-RESULT", () => {
|
|
45
|
+
const handler = new FactorioOutputHandler();
|
|
46
|
+
handler.handleLine("FACTORIO-TEST-RESULT:passed");
|
|
47
|
+
expect(handler.getResultMessage()).toBe("passed");
|
|
48
|
+
});
|
|
49
|
+
it("emits result event with message", () => {
|
|
50
|
+
const handler = new FactorioOutputHandler();
|
|
51
|
+
const results = [];
|
|
52
|
+
handler.on("result", (msg) => results.push(msg));
|
|
53
|
+
handler.handleLine("FACTORIO-TEST-RESULT:failed:focused");
|
|
54
|
+
expect(results).toEqual(["failed:focused"]);
|
|
55
|
+
expect(handler.getResultMessage()).toBe("failed:focused");
|
|
56
|
+
});
|
|
57
|
+
});
|
package/factorio-process.js
CHANGED
|
@@ -1,13 +1,84 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { spawn, spawnSync } from "child_process";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
2
5
|
import * as path from "path";
|
|
3
6
|
import { fileURLToPath } from "url";
|
|
4
|
-
import BufferLineSplitter from "./buffer-line-splitter.js";
|
|
5
7
|
import { FactorioOutputHandler } from "./factorio-output-parser.js";
|
|
6
|
-
import { OutputPrinter } from "./output
|
|
7
|
-
import {
|
|
8
|
-
import { TestRunCollector } from "./test-run-collector.js";
|
|
8
|
+
import { OutputPrinter, ProgressRenderer } from "./test-output.js";
|
|
9
|
+
import { TestRunCollector } from "./test-results.js";
|
|
9
10
|
import { CliError } from "./cli-error.js";
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
class BufferLineSplitter extends EventEmitter {
|
|
13
|
+
buf = "";
|
|
14
|
+
constructor(stream) {
|
|
15
|
+
super();
|
|
16
|
+
stream.on("close", () => {
|
|
17
|
+
if (this.buf.length > 0)
|
|
18
|
+
this.emit("line", this.buf);
|
|
19
|
+
});
|
|
20
|
+
stream.on("end", () => {
|
|
21
|
+
if (this.buf.length > 0)
|
|
22
|
+
this.emit("line", this.buf);
|
|
23
|
+
});
|
|
24
|
+
stream.on("data", (chunk) => {
|
|
25
|
+
this.buf += chunk.toString();
|
|
26
|
+
let index;
|
|
27
|
+
while ((index = this.buf.search(/\r?\n/)) !== -1) {
|
|
28
|
+
this.emit("line", this.buf.slice(0, index));
|
|
29
|
+
this.buf = this.buf.slice(index + 1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function getFactorioPlayerDataPath() {
|
|
35
|
+
const platform = os.platform();
|
|
36
|
+
if (platform === "win32") {
|
|
37
|
+
return path.join(process.env.APPDATA, "Factorio", "player-data.json");
|
|
38
|
+
}
|
|
39
|
+
if (platform === "darwin") {
|
|
40
|
+
return path.join(os.homedir(), "Library", "Application Support", "factorio", "player-data.json");
|
|
41
|
+
}
|
|
42
|
+
return path.join(os.homedir(), ".factorio", "player-data.json");
|
|
43
|
+
}
|
|
44
|
+
function factorioIsInPath() {
|
|
45
|
+
const result = spawnSync("factorio", ["--version"], { stdio: "ignore" });
|
|
46
|
+
return result.status === 0;
|
|
47
|
+
}
|
|
48
|
+
export function autoDetectFactorioPath() {
|
|
49
|
+
if (factorioIsInPath()) {
|
|
50
|
+
return "factorio";
|
|
51
|
+
}
|
|
52
|
+
let pathsToTry;
|
|
53
|
+
if (os.platform() === "linux" || os.platform() === "darwin") {
|
|
54
|
+
pathsToTry = [
|
|
55
|
+
"~/.local/share/Steam/steamapps/common/Factorio/bin/x64/factorio",
|
|
56
|
+
"~/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/MacOS/factorio",
|
|
57
|
+
"~/.factorio/bin/x64/factorio",
|
|
58
|
+
"/Applications/factorio.app/Contents/MacOS/factorio",
|
|
59
|
+
"/usr/share/factorio/bin/x64/factorio",
|
|
60
|
+
"/usr/share/games/factorio/bin/x64/factorio",
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
else if (os.platform() === "win32") {
|
|
64
|
+
pathsToTry = [
|
|
65
|
+
"factorio.exe",
|
|
66
|
+
process.env["ProgramFiles(x86)"] + "\\Steam\\steamapps\\common\\Factorio\\bin\\x64\\factorio.exe",
|
|
67
|
+
process.env["ProgramFiles"] + "\\Factorio\\bin\\x64\\factorio.exe",
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw new CliError(`Cannot auto-detect factorio path on platform ${os.platform()}`);
|
|
72
|
+
}
|
|
73
|
+
pathsToTry = pathsToTry.map((p) => p.replace(/^~\//, os.homedir() + "/"));
|
|
74
|
+
for (const testPath of pathsToTry) {
|
|
75
|
+
if (fs.statSync(testPath, { throwIfNoEntry: false })?.isFile()) {
|
|
76
|
+
return path.resolve(testPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new CliError(`Could not auto-detect factorio executable. Tried: ${pathsToTry.join(", ")}. ` +
|
|
80
|
+
"Either add the factorio bin to your path, or specify the path with --factorio-path");
|
|
81
|
+
}
|
|
11
82
|
export function getHeadlessSavePath(overridePath) {
|
|
12
83
|
if (overridePath) {
|
|
13
84
|
return path.resolve(overridePath);
|
|
@@ -29,11 +100,12 @@ export function parseResultMessage(message) {
|
|
|
29
100
|
function createOutputComponents(options) {
|
|
30
101
|
const handler = new FactorioOutputHandler();
|
|
31
102
|
const collector = new TestRunCollector();
|
|
103
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
32
104
|
const printer = new OutputPrinter({
|
|
33
105
|
verbose: options.verbose,
|
|
34
106
|
quiet: options.quiet,
|
|
35
107
|
});
|
|
36
|
-
const progress = new ProgressRenderer();
|
|
108
|
+
const progress = new ProgressRenderer(isTTY);
|
|
37
109
|
handler.on("event", (event) => {
|
|
38
110
|
collector.handleEvent(event);
|
|
39
111
|
progress.handleEvent(event);
|
|
@@ -52,7 +124,11 @@ function createOutputComponents(options) {
|
|
|
52
124
|
progress.handleTestFinished(test);
|
|
53
125
|
progress.withPermanentOutput(() => printer.printTestResult(test));
|
|
54
126
|
});
|
|
55
|
-
|
|
127
|
+
handler.on("result", () => {
|
|
128
|
+
progress.finish();
|
|
129
|
+
printer.resetMessage();
|
|
130
|
+
});
|
|
131
|
+
return { handler, collector };
|
|
56
132
|
}
|
|
57
133
|
export async function runFactorioTestsHeadless(factorioPath, dataDir, savePath, additionalArgs, options) {
|
|
58
134
|
const args = [
|
|
@@ -70,16 +146,10 @@ export async function runFactorioTestsHeadless(factorioPath, dataDir, savePath,
|
|
|
70
146
|
const factorioProcess = spawn(factorioPath, args, {
|
|
71
147
|
stdio: ["inherit", "pipe", "pipe"],
|
|
72
148
|
});
|
|
73
|
-
const { handler, collector
|
|
74
|
-
let resultMessage;
|
|
149
|
+
const { handler, collector } = createOutputComponents(options);
|
|
75
150
|
let testRunStarted = false;
|
|
76
151
|
let startupTimedOut = false;
|
|
77
152
|
let wasCancelled = false;
|
|
78
|
-
handler.on("result", (msg) => {
|
|
79
|
-
resultMessage = msg;
|
|
80
|
-
progress.finish();
|
|
81
|
-
printer.resetMessage();
|
|
82
|
-
});
|
|
83
153
|
handler.on("event", (event) => {
|
|
84
154
|
if (event.type === "testRunStarted") {
|
|
85
155
|
testRunStarted = true;
|
|
@@ -109,7 +179,7 @@ export async function runFactorioTestsHeadless(factorioPath, dataDir, savePath,
|
|
|
109
179
|
else if (startupTimedOut) {
|
|
110
180
|
reject(new CliError("Factorio unresponsive: no test run started within 10 seconds"));
|
|
111
181
|
}
|
|
112
|
-
else if (
|
|
182
|
+
else if (handler.getResultMessage() !== undefined) {
|
|
113
183
|
resolve();
|
|
114
184
|
}
|
|
115
185
|
else {
|
|
@@ -120,6 +190,7 @@ export async function runFactorioTestsHeadless(factorioPath, dataDir, savePath,
|
|
|
120
190
|
if (wasCancelled) {
|
|
121
191
|
return { status: "cancelled", hasFocusedTests: false };
|
|
122
192
|
}
|
|
193
|
+
const resultMessage = handler.getResultMessage();
|
|
123
194
|
const parsed = parseResultMessage(resultMessage);
|
|
124
195
|
return { ...parsed, message: resultMessage, data: collector.getData() };
|
|
125
196
|
}
|
|
@@ -137,22 +208,16 @@ export async function runFactorioTestsGraphics(factorioPath, dataDir, savePath,
|
|
|
137
208
|
const factorioProcess = spawn(factorioPath, args, {
|
|
138
209
|
stdio: ["inherit", "pipe", "inherit"],
|
|
139
210
|
});
|
|
140
|
-
const { handler, collector
|
|
141
|
-
let resultMessage;
|
|
211
|
+
const { handler, collector } = createOutputComponents(options);
|
|
142
212
|
let resolvePromise;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
printer.resetMessage();
|
|
147
|
-
if (options.resolveOnResult && resolvePromise) {
|
|
148
|
-
resolvePromise();
|
|
149
|
-
}
|
|
150
|
-
});
|
|
213
|
+
if (options.resolveOnResult) {
|
|
214
|
+
handler.on("result", () => resolvePromise?.());
|
|
215
|
+
}
|
|
151
216
|
new BufferLineSplitter(factorioProcess.stdout).on("line", (line) => handler.handleLine(line));
|
|
152
217
|
await new Promise((resolve, reject) => {
|
|
153
218
|
resolvePromise = resolve;
|
|
154
219
|
factorioProcess.on("exit", (code, signal) => {
|
|
155
|
-
if (
|
|
220
|
+
if (handler.getResultMessage() !== undefined) {
|
|
156
221
|
resolve();
|
|
157
222
|
}
|
|
158
223
|
else {
|
|
@@ -160,6 +225,7 @@ export async function runFactorioTestsGraphics(factorioPath, dataDir, savePath,
|
|
|
160
225
|
}
|
|
161
226
|
});
|
|
162
227
|
});
|
|
228
|
+
const resultMessage = handler.getResultMessage();
|
|
163
229
|
const parsed = parseResultMessage(resultMessage);
|
|
164
230
|
return { ...parsed, message: resultMessage, data: collector.getData() };
|
|
165
231
|
}
|
package/factorio-process.test.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { parseResultMessage } from "./factorio-process.js";
|
|
3
|
+
vi.mock("child_process", async (importOriginal) => {
|
|
4
|
+
const original = await importOriginal();
|
|
5
|
+
return {
|
|
6
|
+
...original,
|
|
7
|
+
spawnSync: vi.fn(() => ({ status: 1 })),
|
|
8
|
+
};
|
|
9
|
+
});
|
|
3
10
|
describe("parseResultMessage", () => {
|
|
4
11
|
it.each([
|
|
5
12
|
["passed", { status: "passed", hasFocusedTests: false }],
|
|
@@ -13,3 +20,23 @@ describe("parseResultMessage", () => {
|
|
|
13
20
|
expect(parseResultMessage(input)).toEqual(expected);
|
|
14
21
|
});
|
|
15
22
|
});
|
|
23
|
+
describe("autoDetectFactorioPath", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it("returns 'factorio' if in PATH", async () => {
|
|
31
|
+
const { spawnSync } = await import("child_process");
|
|
32
|
+
vi.mocked(spawnSync).mockReturnValue({ status: 0 });
|
|
33
|
+
const { autoDetectFactorioPath } = await import("./factorio-process.js");
|
|
34
|
+
expect(autoDetectFactorioPath()).toBe("factorio");
|
|
35
|
+
});
|
|
36
|
+
it("throws if no path found and factorio not in PATH", async () => {
|
|
37
|
+
const { spawnSync } = await import("child_process");
|
|
38
|
+
vi.mocked(spawnSync).mockReturnValue({ status: 1 });
|
|
39
|
+
const { autoDetectFactorioPath } = await import("./factorio-process.js");
|
|
40
|
+
expect(() => autoDetectFactorioPath()).toThrow(/Could not auto-detect/);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/mod-setup.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fsp from "fs/promises";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { runScript, runProcess } from "./process-utils.js";
|
|
5
|
-
import { getFactorioPlayerDataPath } from "./factorio-
|
|
5
|
+
import { getFactorioPlayerDataPath } from "./factorio-process.js";
|
|
6
6
|
import { CliError } from "./cli-error.js";
|
|
7
7
|
const MIN_FACTORIO_TEST_VERSION = "3.0.0";
|
|
8
8
|
function parseVersion(version) {
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
1
2
|
import { program } from "commander";
|
|
3
|
+
import * as dgram from "dgram";
|
|
2
4
|
import * as fsp from "fs/promises";
|
|
3
5
|
import * as path from "path";
|
|
4
|
-
import * as dgram from "dgram";
|
|
5
|
-
import chalk from "chalk";
|
|
6
|
-
import { loadConfig, mergeCliConfig, buildTestConfig, registerAllCliOptions, } from "./config/index.js";
|
|
7
|
-
import { setVerbose, runScript } from "./process-utils.js";
|
|
8
|
-
import { autoDetectFactorioPath } from "./factorio-discovery.js";
|
|
9
6
|
import { CliError } from "./cli-error.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { getHeadlessSavePath,
|
|
7
|
+
import { buildTestConfig, loadConfig, mergeCliConfig, registerAllCliOptions, } from "./config/index.js";
|
|
8
|
+
import { autoDetectFactorioPath } from "./factorio-process.js";
|
|
9
|
+
import { getHeadlessSavePath, runFactorioTestsGraphics, runFactorioTestsHeadless, } from "./factorio-process.js";
|
|
13
10
|
import { watchDirectory, watchFile } from "./file-watcher.js";
|
|
11
|
+
import { configureModToTest, ensureConfigIni, installFactorioTest, installModDependencies, resetAutorunSettings, resolveModWatchTarget, setSettingsForAutorun, } from "./mod-setup.js";
|
|
12
|
+
import { runScript, setVerbose } from "./process-utils.js";
|
|
13
|
+
import { getDefaultOutputPath, readPreviousFailedTests, writeResultsFile } from "./test-results.js";
|
|
14
14
|
const thisCommand = program
|
|
15
15
|
.command("run")
|
|
16
16
|
.summary("Runs tests with Factorio test.")
|
|
@@ -32,9 +32,7 @@ Examples:
|
|
|
32
32
|
`)
|
|
33
33
|
.argument("[filter...]", "Test patterns to filter (OR logic)");
|
|
34
34
|
registerAllCliOptions(thisCommand);
|
|
35
|
-
thisCommand.action((patterns, options) =>
|
|
36
|
-
runTests(patterns, options);
|
|
37
|
-
});
|
|
35
|
+
thisCommand.action((patterns, options) => runTests(patterns, options));
|
|
38
36
|
async function setupTestRun(patterns, options) {
|
|
39
37
|
const fileConfig = loadConfig(options.config);
|
|
40
38
|
mergeCliConfig(fileConfig, options);
|
|
@@ -146,68 +144,70 @@ async function executeTestRun(ctx, execOptions) {
|
|
|
146
144
|
return { exitCode: resultStatus === "passed" ? 0 : 1, status: resultStatus };
|
|
147
145
|
}
|
|
148
146
|
const DEFAULT_WATCH_PATTERNS = ["info.json", "**/*.lua"];
|
|
147
|
+
async function runGraphicsWatchMode(ctx) {
|
|
148
|
+
const watchPatterns = ctx.options.watchPatterns ?? ctx.fileConfig.watchPatterns ?? DEFAULT_WATCH_PATTERNS;
|
|
149
|
+
const target = await resolveModWatchTarget(ctx.modsDir, ctx.options.modPath, ctx.options.modName);
|
|
150
|
+
console.log(chalk.gray(`Watching ${target.path} for patterns: ${watchPatterns.join(", ")}`));
|
|
151
|
+
await executeTestRun(ctx, { skipResetAutorun: true, resolveOnResult: true });
|
|
152
|
+
const udpClient = dgram.createSocket("udp4");
|
|
153
|
+
const onFileChange = () => {
|
|
154
|
+
console.log(chalk.cyan("File change detected, triggering rerun..."));
|
|
155
|
+
udpClient.send("rerun", ctx.udpPort, "127.0.0.1");
|
|
156
|
+
};
|
|
157
|
+
const watcher = target.type === "directory"
|
|
158
|
+
? watchDirectory(target.path, onFileChange, { patterns: watchPatterns })
|
|
159
|
+
: watchFile(target.path, onFileChange);
|
|
160
|
+
process.on("SIGINT", () => {
|
|
161
|
+
watcher.close();
|
|
162
|
+
udpClient.close();
|
|
163
|
+
process.exit(0);
|
|
164
|
+
});
|
|
165
|
+
return new Promise(() => { });
|
|
166
|
+
}
|
|
167
|
+
async function runHeadlessWatchMode(ctx) {
|
|
168
|
+
const watchPatterns = ctx.options.watchPatterns ?? ctx.fileConfig.watchPatterns ?? DEFAULT_WATCH_PATTERNS;
|
|
169
|
+
const target = await resolveModWatchTarget(ctx.modsDir, ctx.options.modPath, ctx.options.modName);
|
|
170
|
+
let abortController;
|
|
171
|
+
const runOnce = async () => {
|
|
172
|
+
abortController?.abort();
|
|
173
|
+
abortController = new AbortController();
|
|
174
|
+
console.log("\n" + "─".repeat(60));
|
|
175
|
+
try {
|
|
176
|
+
await executeTestRun(ctx, { signal: abortController.signal });
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
if (e instanceof CliError) {
|
|
180
|
+
console.error(chalk.red(e.message));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
abortController = undefined;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
await runOnce();
|
|
191
|
+
const onFileChange = () => {
|
|
192
|
+
console.log(chalk.cyan("File change detected, rerunning tests..."));
|
|
193
|
+
runOnce();
|
|
194
|
+
};
|
|
195
|
+
const watcher = target.type === "directory"
|
|
196
|
+
? watchDirectory(target.path, onFileChange, { patterns: watchPatterns })
|
|
197
|
+
: watchFile(target.path, onFileChange);
|
|
198
|
+
process.on("SIGINT", () => {
|
|
199
|
+
watcher.close();
|
|
200
|
+
process.exit(0);
|
|
201
|
+
});
|
|
202
|
+
return new Promise(() => { });
|
|
203
|
+
}
|
|
149
204
|
async function runTests(patterns, options) {
|
|
150
205
|
const ctx = await setupTestRun(patterns, options);
|
|
151
206
|
if (options.watch && options.graphics) {
|
|
152
|
-
|
|
153
|
-
const target = await resolveModWatchTarget(ctx.modsDir, options.modPath, options.modName);
|
|
154
|
-
console.log(chalk.gray(`Watching ${target.path} for patterns: ${watchPatterns.join(", ")}`));
|
|
155
|
-
await executeTestRun(ctx, { skipResetAutorun: true, resolveOnResult: true });
|
|
156
|
-
const udpClient = dgram.createSocket("udp4");
|
|
157
|
-
const onFileChange = () => {
|
|
158
|
-
console.log(chalk.cyan("File change detected, triggering rerun..."));
|
|
159
|
-
udpClient.send("rerun", ctx.udpPort, "127.0.0.1");
|
|
160
|
-
};
|
|
161
|
-
const watcher = target.type === "directory"
|
|
162
|
-
? watchDirectory(target.path, onFileChange, { patterns: watchPatterns })
|
|
163
|
-
: watchFile(target.path, onFileChange);
|
|
164
|
-
process.on("SIGINT", () => {
|
|
165
|
-
watcher.close();
|
|
166
|
-
udpClient.close();
|
|
167
|
-
process.exit(0);
|
|
168
|
-
});
|
|
169
|
-
await new Promise(() => { });
|
|
207
|
+
await runGraphicsWatchMode(ctx);
|
|
170
208
|
}
|
|
171
209
|
else if (options.watch) {
|
|
172
|
-
|
|
173
|
-
const target = await resolveModWatchTarget(ctx.modsDir, options.modPath, options.modName);
|
|
174
|
-
let abortController;
|
|
175
|
-
let isRunning = false;
|
|
176
|
-
const runOnce = async () => {
|
|
177
|
-
if (isRunning) {
|
|
178
|
-
abortController?.abort();
|
|
179
|
-
}
|
|
180
|
-
abortController = new AbortController();
|
|
181
|
-
isRunning = true;
|
|
182
|
-
console.log("\n" + "─".repeat(60));
|
|
183
|
-
try {
|
|
184
|
-
await executeTestRun(ctx, { signal: abortController.signal });
|
|
185
|
-
}
|
|
186
|
-
catch (e) {
|
|
187
|
-
if (e instanceof CliError) {
|
|
188
|
-
console.error(chalk.red(e.message));
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
throw e;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
finally {
|
|
195
|
-
isRunning = false;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
await runOnce();
|
|
199
|
-
const onFileChange = () => {
|
|
200
|
-
console.log(chalk.cyan("File change detected, rerunning tests..."));
|
|
201
|
-
runOnce();
|
|
202
|
-
};
|
|
203
|
-
const watcher = target.type === "directory"
|
|
204
|
-
? watchDirectory(target.path, onFileChange, { patterns: watchPatterns })
|
|
205
|
-
: watchFile(target.path, onFileChange);
|
|
206
|
-
process.on("SIGINT", () => {
|
|
207
|
-
watcher.close();
|
|
208
|
-
process.exit(0);
|
|
209
|
-
});
|
|
210
|
-
await new Promise(() => { });
|
|
210
|
+
await runHeadlessWatchMode(ctx);
|
|
211
211
|
}
|
|
212
212
|
else {
|
|
213
213
|
const result = await executeTestRun(ctx);
|
|
@@ -1,4 +1,78 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import logUpdate from "log-update";
|
|
3
|
+
export class ProgressRenderer {
|
|
4
|
+
isTTY;
|
|
5
|
+
active = false;
|
|
6
|
+
total = 0;
|
|
7
|
+
ran = 0;
|
|
8
|
+
passed = 0;
|
|
9
|
+
failed = 0;
|
|
10
|
+
skipped = 0;
|
|
11
|
+
todo = 0;
|
|
12
|
+
currentTest;
|
|
13
|
+
constructor(isTTY = process.stdout.isTTY ?? false) {
|
|
14
|
+
this.isTTY = isTTY;
|
|
15
|
+
}
|
|
16
|
+
handleEvent(event) {
|
|
17
|
+
if (event.type === "testRunStarted") {
|
|
18
|
+
this.total = event.total;
|
|
19
|
+
}
|
|
20
|
+
else if (event.type === "testStarted") {
|
|
21
|
+
this.currentTest = event.test.path;
|
|
22
|
+
this.render();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
handleTestFinished(test) {
|
|
26
|
+
this.currentTest = undefined;
|
|
27
|
+
if (test.result === "passed") {
|
|
28
|
+
this.ran++;
|
|
29
|
+
this.passed++;
|
|
30
|
+
}
|
|
31
|
+
else if (test.result === "failed") {
|
|
32
|
+
this.ran++;
|
|
33
|
+
this.failed++;
|
|
34
|
+
}
|
|
35
|
+
else if (test.result === "skipped") {
|
|
36
|
+
this.skipped++;
|
|
37
|
+
}
|
|
38
|
+
else if (test.result === "todo") {
|
|
39
|
+
this.todo++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
withPermanentOutput(callback) {
|
|
43
|
+
if (!this.isTTY || !this.active) {
|
|
44
|
+
callback();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
logUpdate.clear();
|
|
48
|
+
callback();
|
|
49
|
+
this.render();
|
|
50
|
+
}
|
|
51
|
+
finish() {
|
|
52
|
+
if (this.active)
|
|
53
|
+
logUpdate.clear();
|
|
54
|
+
}
|
|
55
|
+
render() {
|
|
56
|
+
if (!this.isTTY)
|
|
57
|
+
return;
|
|
58
|
+
this.active = true;
|
|
59
|
+
const percent = this.total > 0 ? Math.floor((this.ran / this.total) * 100) : 0;
|
|
60
|
+
const barWidth = 20;
|
|
61
|
+
const filled = Math.min(barWidth, Math.floor((percent / 100) * barWidth));
|
|
62
|
+
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
|
63
|
+
const counts = [
|
|
64
|
+
chalk.green(`✓${this.passed}`),
|
|
65
|
+
this.failed > 0 ? chalk.red(`✗${this.failed}`) : null,
|
|
66
|
+
this.skipped > 0 ? chalk.yellow(`○${this.skipped}`) : null,
|
|
67
|
+
this.todo > 0 ? chalk.magenta(`◌${this.todo}`) : null,
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(" ");
|
|
71
|
+
const progress = `[${bar}] ${percent}% ${this.ran}/${this.total} ${counts}`;
|
|
72
|
+
const current = this.currentTest ? `Running: ${this.currentTest}` : " ";
|
|
73
|
+
logUpdate(progress + "\n" + current);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
2
76
|
function formatDuration(ms) {
|
|
3
77
|
if (ms >= 1000) {
|
|
4
78
|
return `${(ms / 1000).toFixed(2)}s`;
|
|
@@ -61,19 +135,12 @@ export class OutputPrinter {
|
|
|
61
135
|
});
|
|
62
136
|
}
|
|
63
137
|
printTestResult(test) {
|
|
64
|
-
if (this.options.quiet
|
|
138
|
+
if (this.options.quiet)
|
|
65
139
|
return;
|
|
66
|
-
if (
|
|
140
|
+
if (test.result === "skipped" && !this.options.verbose)
|
|
67
141
|
return;
|
|
68
142
|
this.formatter.formatTestResult(test);
|
|
69
143
|
}
|
|
70
|
-
printFailures(tests) {
|
|
71
|
-
for (const test of tests) {
|
|
72
|
-
if (test.result === "failed") {
|
|
73
|
-
this.formatter.formatTestResult(test);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
144
|
printMessage(line) {
|
|
78
145
|
if (this.options.quiet)
|
|
79
146
|
return;
|