as-test 1.0.1 → 1.0.3
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/CHANGELOG.md +112 -1
- package/README.md +138 -406
- package/as-test.config.schema.json +210 -17
- package/assembly/__fuzz__/array.fuzz.ts +10 -0
- package/assembly/__fuzz__/bytes.fuzz.ts +8 -0
- package/assembly/__fuzz__/math.fuzz.ts +9 -0
- package/assembly/__fuzz__/string.fuzz.ts +21 -0
- package/assembly/index.ts +141 -86
- package/assembly/src/expectation.ts +104 -19
- package/assembly/src/fuzz.ts +723 -0
- package/assembly/src/log.ts +6 -1
- package/assembly/src/suite.ts +45 -3
- package/assembly/util/json.ts +38 -4
- package/assembly/util/wipc.ts +35 -26
- package/bin/build-worker-pool.js +149 -0
- package/bin/build-worker.js +43 -0
- package/bin/commands/build-core.js +214 -28
- package/bin/commands/build.js +1 -0
- package/bin/commands/fuzz-core.js +306 -0
- package/bin/commands/fuzz.js +10 -0
- package/bin/commands/init-core.js +129 -24
- package/bin/commands/run-core.js +525 -123
- package/bin/commands/run.js +4 -1
- package/bin/commands/test.js +8 -3
- package/bin/commands/web-runner-source.js +634 -0
- package/bin/crash-store.js +64 -0
- package/bin/index.js +1484 -169
- package/bin/reporters/default.js +281 -49
- package/bin/reporters/tap.js +83 -2
- package/bin/types.js +19 -2
- package/bin/util.js +315 -33
- package/bin/wipc.js +79 -0
- package/package.json +19 -9
- package/transform/lib/coverage.js +1 -2
- package/transform/lib/index.js +3 -3
- package/transform/lib/log.js +1 -1
package/bin/commands/run-core.js
CHANGED
|
@@ -1,91 +1,16 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { glob } from "glob";
|
|
4
|
-
import {
|
|
4
|
+
import { Channel, MessageType } from "../wipc.js";
|
|
5
|
+
import { applyMode, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
|
|
5
6
|
import * as path from "path";
|
|
6
7
|
import { pathToFileURL } from "url";
|
|
7
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { buildWebRunnerSource } from "./web-runner-source.js";
|
|
8
10
|
import { createReporter as createDefaultReporter } from "../reporters/default.js";
|
|
9
11
|
import { createTapReporter } from "../reporters/tap.js";
|
|
12
|
+
import { persistCrashRecord } from "../crash-store.js";
|
|
10
13
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
11
|
-
var MessageType;
|
|
12
|
-
(function (MessageType) {
|
|
13
|
-
MessageType[MessageType["OPEN"] = 0] = "OPEN";
|
|
14
|
-
MessageType[MessageType["CLOSE"] = 1] = "CLOSE";
|
|
15
|
-
MessageType[MessageType["CALL"] = 2] = "CALL";
|
|
16
|
-
MessageType[MessageType["DATA"] = 3] = "DATA";
|
|
17
|
-
})(MessageType || (MessageType = {}));
|
|
18
|
-
class Channel {
|
|
19
|
-
constructor(input, output) {
|
|
20
|
-
this.input = input;
|
|
21
|
-
this.output = output;
|
|
22
|
-
this.buffer = Buffer.alloc(0);
|
|
23
|
-
this.input.on("data", (chunk) => this.onData(chunk));
|
|
24
|
-
}
|
|
25
|
-
send(type, payload) {
|
|
26
|
-
const body = payload ?? Buffer.alloc(0);
|
|
27
|
-
const header = Buffer.alloc(Channel.HEADER_SIZE);
|
|
28
|
-
Channel.MAGIC.copy(header, 0);
|
|
29
|
-
header.writeUInt8(type, 4);
|
|
30
|
-
header.writeUInt32LE(body.length, 5);
|
|
31
|
-
this.output.write(Buffer.concat([header, body]));
|
|
32
|
-
}
|
|
33
|
-
sendJSON(type, msg) {
|
|
34
|
-
this.send(type, Buffer.from(JSON.stringify(msg), "utf8"));
|
|
35
|
-
}
|
|
36
|
-
onData(chunk) {
|
|
37
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
38
|
-
while (true) {
|
|
39
|
-
if (this.buffer.length === 0)
|
|
40
|
-
return;
|
|
41
|
-
const idx = this.buffer.indexOf(Channel.MAGIC);
|
|
42
|
-
if (idx === -1) {
|
|
43
|
-
this.onPassthrough(this.buffer);
|
|
44
|
-
this.buffer = Buffer.alloc(0);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (idx > 0) {
|
|
48
|
-
this.onPassthrough(this.buffer.subarray(0, idx));
|
|
49
|
-
this.buffer = this.buffer.subarray(idx);
|
|
50
|
-
}
|
|
51
|
-
if (this.buffer.length < Channel.HEADER_SIZE)
|
|
52
|
-
return;
|
|
53
|
-
const type = this.buffer.readUInt8(4);
|
|
54
|
-
const length = this.buffer.readUInt32LE(5);
|
|
55
|
-
const frameSize = Channel.HEADER_SIZE + length;
|
|
56
|
-
if (this.buffer.length < frameSize)
|
|
57
|
-
return;
|
|
58
|
-
const payload = this.buffer.subarray(Channel.HEADER_SIZE, frameSize);
|
|
59
|
-
this.buffer = this.buffer.subarray(frameSize);
|
|
60
|
-
this.handleFrame(type, payload);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
handleFrame(type, payload) {
|
|
64
|
-
switch (type) {
|
|
65
|
-
case MessageType.OPEN:
|
|
66
|
-
this.onOpen();
|
|
67
|
-
break;
|
|
68
|
-
case MessageType.CLOSE:
|
|
69
|
-
this.onClose();
|
|
70
|
-
break;
|
|
71
|
-
case MessageType.CALL:
|
|
72
|
-
this.onCall(JSON.parse(payload.toString("utf8")));
|
|
73
|
-
break;
|
|
74
|
-
case MessageType.DATA:
|
|
75
|
-
this.onDataMessage(payload);
|
|
76
|
-
break;
|
|
77
|
-
default:
|
|
78
|
-
this.onPassthrough(payload);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
onPassthrough(_data) { }
|
|
82
|
-
onOpen() { }
|
|
83
|
-
onClose() { }
|
|
84
|
-
onCall(_msg) { }
|
|
85
|
-
onDataMessage(_data) { }
|
|
86
|
-
}
|
|
87
|
-
Channel.MAGIC = Buffer.from("WIPC");
|
|
88
|
-
Channel.HEADER_SIZE = 9;
|
|
89
14
|
class SnapshotStore {
|
|
90
15
|
constructor(specFile, snapshotDir, duplicateSpecBasenames = new Set()) {
|
|
91
16
|
this.dirty = false;
|
|
@@ -94,30 +19,24 @@ class SnapshotStore {
|
|
|
94
19
|
this.matched = 0;
|
|
95
20
|
this.failed = 0;
|
|
96
21
|
this.warnedMissing = new Set();
|
|
97
|
-
const base = path.basename(specFile, ".ts");
|
|
98
|
-
const disambiguator = resolveDisambiguator(specFile, duplicateSpecBasenames);
|
|
99
|
-
const snapshotBase = disambiguator.length
|
|
100
|
-
? `${base}.${disambiguator}`
|
|
101
|
-
: base;
|
|
102
22
|
const dir = path.join(process.cwd(), snapshotDir);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
assert(key, actual, allowSnapshot, updateSnapshots) {
|
|
23
|
+
const relative = resolveArtifactRelativePath(specFile, "__tests__").replace(/\.ts$/, ".snap");
|
|
24
|
+
this.filePath = path.join(dir, relative);
|
|
25
|
+
const sourcePath = resolveSnapshotSourcePath(specFile, dir, duplicateSpecBasenames, this.filePath) ?? null;
|
|
26
|
+
const loaded = sourcePath
|
|
27
|
+
? readSnapshotFile(sourcePath, specFile)
|
|
28
|
+
: { data: {}, normalized: false, preamble: "" };
|
|
29
|
+
this.data = loaded.data;
|
|
30
|
+
this.preamble = loaded.preamble;
|
|
31
|
+
this.existed = Boolean(sourcePath && existsSync(sourcePath));
|
|
32
|
+
this.dirty = Boolean((sourcePath && sourcePath != this.filePath) || loaded.normalized);
|
|
33
|
+
}
|
|
34
|
+
assert(key, actual, allowSnapshot, createSnapshots, overwriteSnapshots) {
|
|
35
|
+
key = canonicalizeSnapshotKey(key);
|
|
117
36
|
if (!allowSnapshot)
|
|
118
37
|
return { ok: true, expected: actual, warnMissing: false };
|
|
119
38
|
if (!(key in this.data)) {
|
|
120
|
-
if (!
|
|
39
|
+
if (!createSnapshots) {
|
|
121
40
|
this.failed++;
|
|
122
41
|
const warnMissing = !this.warnedMissing.has(key);
|
|
123
42
|
if (warnMissing)
|
|
@@ -138,7 +57,7 @@ class SnapshotStore {
|
|
|
138
57
|
this.matched++;
|
|
139
58
|
return { ok: true, expected, warnMissing: false };
|
|
140
59
|
}
|
|
141
|
-
if (!
|
|
60
|
+
if (!overwriteSnapshots) {
|
|
142
61
|
this.failed++;
|
|
143
62
|
return { ok: false, expected, warnMissing: false };
|
|
144
63
|
}
|
|
@@ -150,8 +69,335 @@ class SnapshotStore {
|
|
|
150
69
|
flush() {
|
|
151
70
|
if (!this.dirty)
|
|
152
71
|
return;
|
|
153
|
-
|
|
72
|
+
const outDir = path.dirname(this.filePath);
|
|
73
|
+
if (!existsSync(outDir))
|
|
74
|
+
mkdirSync(outDir, { recursive: true });
|
|
75
|
+
writeFileSync(this.filePath, formatSnapshotFile(this.data, this.filePath, this.existed ? this.preamble : defaultSnapshotPreamble()));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resolveSnapshotSourcePath(specFile, snapshotDir, duplicateSpecBasenames, preferredPath) {
|
|
79
|
+
if (existsSync(preferredPath))
|
|
80
|
+
return preferredPath;
|
|
81
|
+
const base = path.basename(specFile, ".ts");
|
|
82
|
+
const legacyFlat = path.join(snapshotDir, `${base}.snap.json`);
|
|
83
|
+
if (existsSync(legacyFlat))
|
|
84
|
+
return legacyFlat;
|
|
85
|
+
const disambiguator = resolveDisambiguator(specFile, duplicateSpecBasenames);
|
|
86
|
+
if (disambiguator.length) {
|
|
87
|
+
const legacyDisambiguated = path.join(snapshotDir, `${base}.${disambiguator}.snap.json`);
|
|
88
|
+
if (existsSync(legacyDisambiguated))
|
|
89
|
+
return legacyDisambiguated;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function readSnapshotFile(filePath, specFile) {
|
|
94
|
+
const raw = readFileSync(filePath, "utf8");
|
|
95
|
+
if (filePath.endsWith(".json")) {
|
|
96
|
+
const normalized = normalizeSnapshotRecord(JSON.parse(raw));
|
|
97
|
+
return { ...normalized, preamble: "" };
|
|
98
|
+
}
|
|
99
|
+
return parseSnapshotText(raw, specFile);
|
|
100
|
+
}
|
|
101
|
+
function parseSnapshotText(source, specFile) {
|
|
102
|
+
const out = {};
|
|
103
|
+
const lines = source.split(/\r?\n/);
|
|
104
|
+
let i = 0;
|
|
105
|
+
let normalized = false;
|
|
106
|
+
const preambleLines = [];
|
|
107
|
+
while (i < lines.length) {
|
|
108
|
+
const header = lines[i] ?? "";
|
|
109
|
+
if (isSnapshotOuterComment(header) || !header.length) {
|
|
110
|
+
if (!Object.keys(out).length)
|
|
111
|
+
preambleLines.push(header);
|
|
112
|
+
i++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const match = header.match(/^=== (.+) ===$/);
|
|
116
|
+
if (!match) {
|
|
117
|
+
i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const localKey = match[1];
|
|
121
|
+
i++;
|
|
122
|
+
let value = "";
|
|
123
|
+
if ((lines[i] ?? "") == "<<<") {
|
|
124
|
+
i++;
|
|
125
|
+
const block = [];
|
|
126
|
+
while (i < lines.length && (lines[i] ?? "") != ">>>") {
|
|
127
|
+
block.push(lines[i] ?? "");
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
value = block.join("\n");
|
|
131
|
+
if ((lines[i] ?? "") == ">>>")
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
value = lines[i] ?? "";
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
while (i < lines.length && !(lines[i] ?? "").startsWith("=== ")) {
|
|
139
|
+
if (!lines[i]?.length || isSnapshotOuterComment(lines[i] ?? "")) {
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
while (i < lines.length && isSnapshotOuterComment(lines[i] ?? "")) {
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
148
|
+
const qualified = qualifySnapshotKey(specFile, localKey);
|
|
149
|
+
const canonical = canonicalizeSnapshotKey(qualified);
|
|
150
|
+
if (canonical != qualified)
|
|
151
|
+
normalized = true;
|
|
152
|
+
out[canonical] = value;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
data: out,
|
|
156
|
+
normalized,
|
|
157
|
+
preamble: trimSnapshotPreamble(preambleLines),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function normalizeSnapshotRecord(data) {
|
|
161
|
+
const out = {};
|
|
162
|
+
let normalized = false;
|
|
163
|
+
for (const [key, value] of Object.entries(data)) {
|
|
164
|
+
const canonical = canonicalizeSnapshotKey(key);
|
|
165
|
+
if (canonical != key)
|
|
166
|
+
normalized = true;
|
|
167
|
+
out[canonical] = value;
|
|
168
|
+
}
|
|
169
|
+
return { data: out, normalized };
|
|
170
|
+
}
|
|
171
|
+
function isSnapshotOuterComment(line) {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
return trimmed.startsWith("#") || trimmed.startsWith("//");
|
|
174
|
+
}
|
|
175
|
+
function formatSnapshotFile(data, filePath, preamble) {
|
|
176
|
+
const specFile = resolveSnapshotSpecFile(filePath);
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
const sections = [];
|
|
179
|
+
for (const key of Object.keys(data)) {
|
|
180
|
+
const localKey = canonicalizeSnapshotLocalKey(localizeSnapshotKey(specFile, key));
|
|
181
|
+
if (seen.has(localKey))
|
|
182
|
+
continue;
|
|
183
|
+
seen.add(localKey);
|
|
184
|
+
const value = data[key] ?? "";
|
|
185
|
+
if (value.includes("\n")) {
|
|
186
|
+
sections.push(`=== ${localKey} ===\n<<<\n${value}\n>>>`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
sections.push(`=== ${localKey} ===\n${value}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!sections.length)
|
|
193
|
+
return "";
|
|
194
|
+
const prefix = preamble.length ? preamble + "\n\n" : "";
|
|
195
|
+
return prefix + sections.join("\n\n") + "\n";
|
|
196
|
+
}
|
|
197
|
+
function defaultSnapshotPreamble() {
|
|
198
|
+
return [
|
|
199
|
+
"# as-test snapshot file",
|
|
200
|
+
"#",
|
|
201
|
+
"# IDs use this format:",
|
|
202
|
+
"# Suite > test",
|
|
203
|
+
"# Suite > test [name]",
|
|
204
|
+
"# Suite > test #2",
|
|
205
|
+
"#",
|
|
206
|
+
"# Examples:",
|
|
207
|
+
'# test("renders card", () => {',
|
|
208
|
+
"# expect(view()).toMatchSnapshot();",
|
|
209
|
+
"# })",
|
|
210
|
+
"# -> renders card",
|
|
211
|
+
"#",
|
|
212
|
+
'# test("renders card", () => {',
|
|
213
|
+
'# expect(view()).toMatchSnapshot("mobile");',
|
|
214
|
+
"# })",
|
|
215
|
+
"# -> renders card [mobile]",
|
|
216
|
+
"#",
|
|
217
|
+
'# test("renders card", () => {',
|
|
218
|
+
"# expect(header()).toMatchSnapshot();",
|
|
219
|
+
"# expect(body()).toMatchSnapshot();",
|
|
220
|
+
"# })",
|
|
221
|
+
"# -> renders card",
|
|
222
|
+
"# -> renders card #2",
|
|
223
|
+
"#",
|
|
224
|
+
'# describe("Card", () => {',
|
|
225
|
+
'# test("renders", () => {',
|
|
226
|
+
"# expect(view()).toMatchSnapshot();",
|
|
227
|
+
"# })",
|
|
228
|
+
"# })",
|
|
229
|
+
"# -> Card > renders",
|
|
230
|
+
"#",
|
|
231
|
+
"# Single-line values are written directly below the ID.",
|
|
232
|
+
"# Multi-line values use delimiters:",
|
|
233
|
+
"# <<<",
|
|
234
|
+
"# ...",
|
|
235
|
+
"# >>>",
|
|
236
|
+
].join("\n");
|
|
237
|
+
}
|
|
238
|
+
function trimSnapshotPreamble(lines) {
|
|
239
|
+
let end = lines.length;
|
|
240
|
+
while (end > 0 && !(lines[end - 1] ?? "").trim().length)
|
|
241
|
+
end--;
|
|
242
|
+
return lines.slice(0, end).join("\n");
|
|
243
|
+
}
|
|
244
|
+
function resolveSnapshotSpecFile(filePath) {
|
|
245
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
246
|
+
const marker = "/snapshots/";
|
|
247
|
+
const markerIndex = normalized.lastIndexOf(marker);
|
|
248
|
+
const suffix = markerIndex >= 0
|
|
249
|
+
? normalized.slice(markerIndex + marker.length)
|
|
250
|
+
: path.basename(normalized);
|
|
251
|
+
const withoutMode = suffix.replace(/^default\//, "");
|
|
252
|
+
const relative = withoutMode.replace(/\.snap$/, ".ts");
|
|
253
|
+
return `assembly/__tests__/${relative}`;
|
|
254
|
+
}
|
|
255
|
+
function localizeSnapshotKey(specFile, key) {
|
|
256
|
+
const prefix = `${path.basename(specFile)}::`;
|
|
257
|
+
return key.startsWith(prefix) ? key.slice(prefix.length) : key;
|
|
258
|
+
}
|
|
259
|
+
function qualifySnapshotKey(specFile, key) {
|
|
260
|
+
return `${path.basename(specFile)}::${key}`;
|
|
261
|
+
}
|
|
262
|
+
function canonicalizeSnapshotKey(key) {
|
|
263
|
+
const sep = key.indexOf("::");
|
|
264
|
+
if (sep < 0)
|
|
265
|
+
return canonicalizeSnapshotLocalKey(key);
|
|
266
|
+
const prefix = key.slice(0, sep + 2);
|
|
267
|
+
const local = key.slice(sep + 2);
|
|
268
|
+
return prefix + canonicalizeSnapshotLocalKey(local);
|
|
269
|
+
}
|
|
270
|
+
function canonicalizeSnapshotLocalKey(localKey) {
|
|
271
|
+
const named = localKey.match(/^(.*)::\d+::(.+)$/);
|
|
272
|
+
if (named) {
|
|
273
|
+
return `${named[1]} [${named[2]}]`;
|
|
274
|
+
}
|
|
275
|
+
const simpleNamed = localKey.match(/^(.*)::([^:]+)$/);
|
|
276
|
+
if (simpleNamed && !/^\d+$/.test(simpleNamed[2])) {
|
|
277
|
+
return `${simpleNamed[1]} [${simpleNamed[2]}]`;
|
|
278
|
+
}
|
|
279
|
+
const unnamed = localKey.match(/^(.*)::(\d+)$/);
|
|
280
|
+
if (unnamed) {
|
|
281
|
+
const index = Number(unnamed[2]);
|
|
282
|
+
if (!Number.isFinite(index) || index < 0)
|
|
283
|
+
return localKey;
|
|
284
|
+
return index === 0 ? unnamed[1] : `${unnamed[1]} #${index + 1}`;
|
|
285
|
+
}
|
|
286
|
+
return localKey;
|
|
287
|
+
}
|
|
288
|
+
function resolveArtifactRelativePath(sourceFile, segment) {
|
|
289
|
+
const normalized = sourceFile.replace(/\\/g, "/");
|
|
290
|
+
const marker = `/${segment}/`;
|
|
291
|
+
const index = normalized.lastIndexOf(marker);
|
|
292
|
+
if (index >= 0)
|
|
293
|
+
return normalized.slice(index + marker.length);
|
|
294
|
+
return path.basename(normalized);
|
|
295
|
+
}
|
|
296
|
+
function writeReadableLog(logRoot, file, suites, modeName, buildCommand, runCommand, snapshotSummary) {
|
|
297
|
+
const relative = resolveArtifactRelativePath(file, "__tests__").replace(/\.ts$/, ".log");
|
|
298
|
+
const filePath = path.join(logRoot, relative);
|
|
299
|
+
const dir = path.dirname(filePath);
|
|
300
|
+
if (!existsSync(dir))
|
|
301
|
+
mkdirSync(dir, { recursive: true });
|
|
302
|
+
writeFileSync(filePath, formatReadableLog(file, suites, modeName, buildCommand, runCommand, snapshotSummary));
|
|
303
|
+
}
|
|
304
|
+
function formatReadableLog(file, suites, modeName, buildCommand, runCommand, snapshotSummary) {
|
|
305
|
+
const stats = collectRunStats([suites]);
|
|
306
|
+
const verdict = stats.failedFiles
|
|
307
|
+
? "FAIL"
|
|
308
|
+
: stats.passedFiles
|
|
309
|
+
? "PASS"
|
|
310
|
+
: "SKIP";
|
|
311
|
+
const lines = [
|
|
312
|
+
`Mode: ${modeName ?? "default"}`,
|
|
313
|
+
`Build: ${buildCommand || "(unknown)"}`,
|
|
314
|
+
`Run: ${runCommand}`,
|
|
315
|
+
"",
|
|
316
|
+
`${verdict} ${file}`,
|
|
317
|
+
"",
|
|
318
|
+
`Snapshots: ${snapshotSummary.matched} matched, ${snapshotSummary.created} created, ${snapshotSummary.updated} updated, ${snapshotSummary.failed} failed`,
|
|
319
|
+
"",
|
|
320
|
+
`Suites: ${stats.passedSuites} passed, ${stats.failedSuites} failed, ${stats.skippedSuites} skipped`,
|
|
321
|
+
`Tests: ${stats.passedTests} passed, ${stats.failedTests} failed, ${stats.skippedTests} skipped`,
|
|
322
|
+
`Time: ${formatTime(stats.time)}`,
|
|
323
|
+
];
|
|
324
|
+
const failures = collectReadableFailures(suites, file, []);
|
|
325
|
+
if (failures.length) {
|
|
326
|
+
lines.push("", "Failures:");
|
|
327
|
+
for (const failure of failures) {
|
|
328
|
+
lines.push(`FAIL ${failure.title}${failure.where.length ? ` (${failure.where})` : ""}`);
|
|
329
|
+
if (failure.message.length)
|
|
330
|
+
lines.push(`Message: ${failure.message}`);
|
|
331
|
+
if (failure.left.length)
|
|
332
|
+
lines.push(`Expected: ${failure.right}`);
|
|
333
|
+
if (failure.right.length)
|
|
334
|
+
lines.push(`Received: ${failure.left}`);
|
|
335
|
+
lines.push("");
|
|
336
|
+
}
|
|
337
|
+
if (!lines[lines.length - 1].length)
|
|
338
|
+
lines.pop();
|
|
339
|
+
}
|
|
340
|
+
const logs = collectReadableLogs(suites);
|
|
341
|
+
if (logs.length) {
|
|
342
|
+
lines.push("", "Log:");
|
|
343
|
+
for (const entry of logs) {
|
|
344
|
+
lines.push(entry);
|
|
345
|
+
}
|
|
154
346
|
}
|
|
347
|
+
return lines.join("\n") + "\n";
|
|
348
|
+
}
|
|
349
|
+
function formatInvocation(invocation) {
|
|
350
|
+
return [invocation.command, ...invocation.args]
|
|
351
|
+
.map((part) => (/[\s"'\\]/.test(part) ? JSON.stringify(part) : part))
|
|
352
|
+
.join(" ");
|
|
353
|
+
}
|
|
354
|
+
function collectReadableFailures(suites, file, pathParts) {
|
|
355
|
+
const out = [];
|
|
356
|
+
for (const suite of suites) {
|
|
357
|
+
const suiteAny = suite;
|
|
358
|
+
const nextPath = [...pathParts, String(suiteAny.description ?? "unknown")];
|
|
359
|
+
const tests = Array.isArray(suiteAny.tests)
|
|
360
|
+
? suiteAny.tests
|
|
361
|
+
: [];
|
|
362
|
+
for (let i = 0; i < tests.length; i++) {
|
|
363
|
+
const test = tests[i];
|
|
364
|
+
if (String(test.verdict ?? "none") != "fail")
|
|
365
|
+
continue;
|
|
366
|
+
out.push({
|
|
367
|
+
title: `${nextPath.join(" > ")}#${i + 1}`,
|
|
368
|
+
where: String(test.location ?? "").length
|
|
369
|
+
? `${path.basename(file)}:${String(test.location ?? "")}`
|
|
370
|
+
: path.basename(file),
|
|
371
|
+
message: String(test.message ?? ""),
|
|
372
|
+
left: JSON.stringify(test.left ?? ""),
|
|
373
|
+
right: JSON.stringify(test.right ?? ""),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const childSuites = Array.isArray(suiteAny.suites)
|
|
377
|
+
? suiteAny.suites
|
|
378
|
+
: [];
|
|
379
|
+
out.push(...collectReadableFailures(childSuites, file, nextPath));
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
function collectReadableLogs(suites) {
|
|
384
|
+
const out = [];
|
|
385
|
+
for (const suite of suites) {
|
|
386
|
+
const suiteAny = suite;
|
|
387
|
+
const logs = Array.isArray(suiteAny.logs)
|
|
388
|
+
? suiteAny.logs
|
|
389
|
+
: [];
|
|
390
|
+
for (const log of logs) {
|
|
391
|
+
const value = String(log.value ?? log.message ?? "");
|
|
392
|
+
if (value.length)
|
|
393
|
+
out.push(value);
|
|
394
|
+
}
|
|
395
|
+
const childSuites = Array.isArray(suiteAny.suites)
|
|
396
|
+
? suiteAny.suites
|
|
397
|
+
: [];
|
|
398
|
+
out.push(...collectReadableLogs(childSuites));
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
155
401
|
}
|
|
156
402
|
export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selectors = [], shouldExit = true, options = {}) {
|
|
157
403
|
const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
|
|
@@ -163,7 +409,8 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
163
409
|
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
164
410
|
const duplicateSpecBasenames = await resolveDuplicateSpecBasenames(config.input);
|
|
165
411
|
const snapshotEnabled = flags.snapshot !== false;
|
|
166
|
-
const
|
|
412
|
+
const createSnapshots = Boolean(flags.createSnapshots);
|
|
413
|
+
const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
|
|
167
414
|
const cleanOutput = Boolean(flags.clean);
|
|
168
415
|
const showCoverage = Boolean(flags.showCoverage);
|
|
169
416
|
const coverage = resolveCoverageOptions(config.coverage);
|
|
@@ -200,7 +447,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
200
447
|
clean: cleanOutput,
|
|
201
448
|
verbose: Boolean(flags.verbose),
|
|
202
449
|
snapshotEnabled,
|
|
203
|
-
|
|
450
|
+
createSnapshots,
|
|
204
451
|
});
|
|
205
452
|
}
|
|
206
453
|
if (showCoverage && !coverageEnabled) {
|
|
@@ -229,10 +476,14 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
229
476
|
.slice(1)
|
|
230
477
|
.map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
|
|
231
478
|
};
|
|
479
|
+
const runCommandForLog = formatInvocation(invocation);
|
|
232
480
|
const snapshotStore = new SnapshotStore(file, config.snapshotDir, duplicateSpecBasenames);
|
|
233
481
|
let report;
|
|
234
482
|
try {
|
|
235
|
-
report = await runProcess(invocation, snapshotStore, snapshotEnabled,
|
|
483
|
+
report = await runProcess(invocation, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", {
|
|
484
|
+
...mode.env,
|
|
485
|
+
...config.runOptions.env,
|
|
486
|
+
});
|
|
236
487
|
}
|
|
237
488
|
catch (error) {
|
|
238
489
|
const modeLabel = options.modeName ?? "default";
|
|
@@ -247,21 +498,30 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
247
498
|
snapshotSummary.failed += snapshotStore.failed;
|
|
248
499
|
reports.push({
|
|
249
500
|
file,
|
|
501
|
+
modeName: options.modeName ?? "default",
|
|
250
502
|
suites: normalized.suites,
|
|
251
503
|
coverage: normalized.coverage,
|
|
504
|
+
runCommand: runCommandForLog,
|
|
505
|
+
snapshotSummary: {
|
|
506
|
+
matched: snapshotStore.matched,
|
|
507
|
+
created: snapshotStore.created,
|
|
508
|
+
updated: snapshotStore.updated,
|
|
509
|
+
failed: snapshotStore.failed,
|
|
510
|
+
},
|
|
252
511
|
});
|
|
253
512
|
}
|
|
254
513
|
if (config.logs && config.logs != "none") {
|
|
255
|
-
|
|
256
|
-
|
|
514
|
+
const logRoot = path.join(process.cwd(), config.logs);
|
|
515
|
+
if (!existsSync(logRoot)) {
|
|
516
|
+
mkdirSync(logRoot, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
for (const report of reports) {
|
|
519
|
+
writeReadableLog(logRoot, report.file, report.suites, options.modeName, options.buildCommandsByFile?.[report.file] ??
|
|
520
|
+
options.buildCommand ??
|
|
521
|
+
"", report.runCommand, report.snapshotSummary);
|
|
257
522
|
}
|
|
258
|
-
const logReports = reports.map((report) => ({
|
|
259
|
-
file: report.file,
|
|
260
|
-
suites: report.suites,
|
|
261
|
-
}));
|
|
262
|
-
writeFileSync(path.join(process.cwd(), config.logs, options.logFileName ?? "test.log.json"), JSON.stringify(logReports, null, 2));
|
|
263
523
|
}
|
|
264
|
-
const stats = collectRunStats(reports
|
|
524
|
+
const stats = collectRunStats(reports);
|
|
265
525
|
if (options.fileSummaryTotal != undefined) {
|
|
266
526
|
applyConfiguredFileTotalToStats(stats, options.fileSummaryTotal);
|
|
267
527
|
}
|
|
@@ -285,6 +545,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
285
545
|
clean: cleanOutput,
|
|
286
546
|
snapshotEnabled,
|
|
287
547
|
showCoverage,
|
|
548
|
+
buildTime: 0,
|
|
288
549
|
snapshotSummary,
|
|
289
550
|
coverageSummary,
|
|
290
551
|
stats,
|
|
@@ -295,6 +556,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
295
556
|
total: totalModes,
|
|
296
557
|
},
|
|
297
558
|
});
|
|
559
|
+
reporter.flush?.();
|
|
298
560
|
}
|
|
299
561
|
const failed = Boolean(stats.failedFiles || snapshotSummary.failed);
|
|
300
562
|
if (shouldExit) {
|
|
@@ -302,6 +564,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
302
564
|
}
|
|
303
565
|
return {
|
|
304
566
|
failed,
|
|
567
|
+
buildTime: 0,
|
|
305
568
|
stats,
|
|
306
569
|
snapshotSummary,
|
|
307
570
|
coverageSummary,
|
|
@@ -362,6 +625,12 @@ function resolveLegacyRuntime(runtimeRun, target, emitWarnings) {
|
|
|
362
625
|
return runtimeRun.replace(legacyPath, preferredPath);
|
|
363
626
|
}
|
|
364
627
|
}
|
|
628
|
+
if (target == "web") {
|
|
629
|
+
const preferredPath = "./.as-test/runners/default.web.js";
|
|
630
|
+
if (runtimeRun.includes(preferredPath)) {
|
|
631
|
+
ensureDefaultRuntimeRunner("web", emitWarnings);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
365
634
|
return runtimeRun;
|
|
366
635
|
}
|
|
367
636
|
function fallbackToDefaultRuntime(runtimeRun, target, emitWarnings) {
|
|
@@ -399,6 +668,12 @@ function getDefaultRuntimeFallback(target) {
|
|
|
399
668
|
scriptPath: "./.as-test/runners/default.bindings.js",
|
|
400
669
|
};
|
|
401
670
|
}
|
|
671
|
+
if (target == "web") {
|
|
672
|
+
return {
|
|
673
|
+
command: "node ./.as-test/runners/default.web.js <file>",
|
|
674
|
+
scriptPath: "./.as-test/runners/default.web.js",
|
|
675
|
+
};
|
|
676
|
+
}
|
|
402
677
|
return null;
|
|
403
678
|
}
|
|
404
679
|
function ensureDefaultRuntimeRunner(target, emitWarnings) {
|
|
@@ -456,7 +731,18 @@ try {
|
|
|
456
731
|
|
|
457
732
|
const binary = readFileSync(wasmPath);
|
|
458
733
|
const module = new WebAssembly.Module(binary);
|
|
734
|
+
const envImports = {
|
|
735
|
+
__as_test_request_fuzz_config() {
|
|
736
|
+
return 0;
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
for (const entry of WebAssembly.Module.imports(module)) {
|
|
740
|
+
if (entry.module == "env" && entry.kind == "function" && !(entry.name in envImports)) {
|
|
741
|
+
envImports[entry.name] = () => 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
459
744
|
const instance = new WebAssembly.Instance(module, {
|
|
745
|
+
env: envImports,
|
|
460
746
|
wasi_snapshot_preview1: wasi.wasiImport,
|
|
461
747
|
});
|
|
462
748
|
wasi.start(instance);
|
|
@@ -537,6 +823,9 @@ try {
|
|
|
537
823
|
}
|
|
538
824
|
`;
|
|
539
825
|
}
|
|
826
|
+
if (target == "web") {
|
|
827
|
+
return buildWebRunnerSource();
|
|
828
|
+
}
|
|
540
829
|
return null;
|
|
541
830
|
}
|
|
542
831
|
function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
|
|
@@ -884,8 +1173,57 @@ function isIgnoredCoverageFile(file, coverage) {
|
|
|
884
1173
|
return true;
|
|
885
1174
|
if (!coverage.includeSpecs && normalized.endsWith(".spec.ts"))
|
|
886
1175
|
return true;
|
|
1176
|
+
if (coverage.include.length &&
|
|
1177
|
+
!coverage.include.some((pattern) => matchesCoverageGlob(normalized, pattern))) {
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
if (coverage.exclude.some((pattern) => matchesCoverageGlob(normalized, pattern))) {
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
887
1183
|
return false;
|
|
888
1184
|
}
|
|
1185
|
+
function matchesCoverageGlob(file, pattern) {
|
|
1186
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").trim();
|
|
1187
|
+
if (!normalizedPattern.length)
|
|
1188
|
+
return false;
|
|
1189
|
+
const regex = globPatternToRegExp(normalizedPattern);
|
|
1190
|
+
return regex.test(file);
|
|
1191
|
+
}
|
|
1192
|
+
function globPatternToRegExp(pattern) {
|
|
1193
|
+
let source = "^";
|
|
1194
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1195
|
+
const char = pattern[i];
|
|
1196
|
+
if (char == "*") {
|
|
1197
|
+
const next = pattern[i + 1];
|
|
1198
|
+
if (next == "*") {
|
|
1199
|
+
const after = pattern[i + 2];
|
|
1200
|
+
if (after == "/") {
|
|
1201
|
+
source += "(?:.*/)?";
|
|
1202
|
+
i += 2;
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
source += ".*";
|
|
1206
|
+
i += 1;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
source += "[^/]*";
|
|
1211
|
+
}
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (char == "?") {
|
|
1215
|
+
source += "[^/]";
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if ("\\.[]{}()+-^$|".includes(char)) {
|
|
1219
|
+
source += `\\${char}`;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
source += char;
|
|
1223
|
+
}
|
|
1224
|
+
source += "$";
|
|
1225
|
+
return new RegExp(source);
|
|
1226
|
+
}
|
|
889
1227
|
function isAllowedCoverageSourceFile(file) {
|
|
890
1228
|
const lower = file.toLowerCase();
|
|
891
1229
|
return lower.endsWith(".ts") || lower.endsWith(".as");
|
|
@@ -906,18 +1244,28 @@ function resolveCoverageOptions(raw) {
|
|
|
906
1244
|
return {
|
|
907
1245
|
enabled: raw,
|
|
908
1246
|
includeSpecs: false,
|
|
1247
|
+
include: [],
|
|
1248
|
+
exclude: [],
|
|
909
1249
|
};
|
|
910
1250
|
}
|
|
911
1251
|
if (raw && typeof raw == "object") {
|
|
912
1252
|
const obj = raw;
|
|
913
1253
|
return {
|
|
914
|
-
enabled: obj.enabled == null ?
|
|
1254
|
+
enabled: obj.enabled == null ? false : Boolean(obj.enabled),
|
|
915
1255
|
includeSpecs: Boolean(obj.includeSpecs),
|
|
1256
|
+
include: Array.isArray(obj.include)
|
|
1257
|
+
? obj.include.filter((item) => typeof item == "string")
|
|
1258
|
+
: [],
|
|
1259
|
+
exclude: Array.isArray(obj.exclude)
|
|
1260
|
+
? obj.exclude.filter((item) => typeof item == "string")
|
|
1261
|
+
: [],
|
|
916
1262
|
};
|
|
917
1263
|
}
|
|
918
1264
|
return {
|
|
919
1265
|
enabled: false,
|
|
920
1266
|
includeSpecs: false,
|
|
1267
|
+
include: [],
|
|
1268
|
+
exclude: [],
|
|
921
1269
|
};
|
|
922
1270
|
}
|
|
923
1271
|
function compareCoveragePoints(a, b) {
|
|
@@ -929,7 +1277,7 @@ function compareCoveragePoints(a, b) {
|
|
|
929
1277
|
return a.type.localeCompare(b.type);
|
|
930
1278
|
return a.hash.localeCompare(b.hash);
|
|
931
1279
|
}
|
|
932
|
-
async function runProcess(invocation, snapshots, snapshotEnabled,
|
|
1280
|
+
async function runProcess(invocation, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
|
|
933
1281
|
const child = spawn(invocation.command, invocation.args, {
|
|
934
1282
|
stdio: ["pipe", "pipe", "pipe"],
|
|
935
1283
|
shell: false,
|
|
@@ -938,6 +1286,7 @@ async function runProcess(invocation, snapshots, snapshotEnabled, updateSnapshot
|
|
|
938
1286
|
let report = null;
|
|
939
1287
|
let parseError = null;
|
|
940
1288
|
let stderrBuffer = "";
|
|
1289
|
+
let stdoutBuffer = "";
|
|
941
1290
|
let suppressTraceWarningLine = false;
|
|
942
1291
|
let spawnError = null;
|
|
943
1292
|
child.on("error", (error) => {
|
|
@@ -961,6 +1310,7 @@ async function runProcess(invocation, snapshots, snapshotEnabled, updateSnapshot
|
|
|
961
1310
|
});
|
|
962
1311
|
class TestChannel extends Channel {
|
|
963
1312
|
onPassthrough(data) {
|
|
1313
|
+
stdoutBuffer += data.toString("utf8");
|
|
964
1314
|
if (tapMode) {
|
|
965
1315
|
process.stderr.write(data);
|
|
966
1316
|
}
|
|
@@ -1020,10 +1370,24 @@ async function runProcess(invocation, snapshots, snapshotEnabled, updateSnapshot
|
|
|
1020
1370
|
});
|
|
1021
1371
|
return;
|
|
1022
1372
|
}
|
|
1373
|
+
if (kind === "event:warn") {
|
|
1374
|
+
reporter.onWarning?.({
|
|
1375
|
+
message: String(event.message ?? ""),
|
|
1376
|
+
});
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (kind === "event:log") {
|
|
1380
|
+
reporter.onLog?.({
|
|
1381
|
+
file: String(event.file ?? "unknown"),
|
|
1382
|
+
depth: Number(event.depth ?? 0),
|
|
1383
|
+
text: String(event.text ?? ""),
|
|
1384
|
+
});
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1023
1387
|
if (kind === "snapshot:assert") {
|
|
1024
1388
|
const key = String(event.key ?? "");
|
|
1025
1389
|
const actual = String(event.actual ?? "");
|
|
1026
|
-
const result = snapshots.assert(key, actual, snapshotEnabled,
|
|
1390
|
+
const result = snapshots.assert(key, actual, snapshotEnabled, createSnapshots, overwriteSnapshots);
|
|
1027
1391
|
if (result.warnMissing) {
|
|
1028
1392
|
reporter.onSnapshotMissing?.({ key });
|
|
1029
1393
|
}
|
|
@@ -1051,12 +1415,36 @@ async function runProcess(invocation, snapshots, snapshotEnabled, updateSnapshot
|
|
|
1051
1415
|
}
|
|
1052
1416
|
}
|
|
1053
1417
|
if (spawnError) {
|
|
1418
|
+
persistCrashRecord(crashDir, {
|
|
1419
|
+
kind: "test",
|
|
1420
|
+
file: specFile,
|
|
1421
|
+
mode: modeName ?? "default",
|
|
1422
|
+
error: spawnError.stack ?? spawnError.message,
|
|
1423
|
+
stdout: stdoutBuffer,
|
|
1424
|
+
stderr: stderrBuffer,
|
|
1425
|
+
});
|
|
1054
1426
|
throw spawnError;
|
|
1055
1427
|
}
|
|
1056
1428
|
if (parseError) {
|
|
1429
|
+
persistCrashRecord(crashDir, {
|
|
1430
|
+
kind: "test",
|
|
1431
|
+
file: specFile,
|
|
1432
|
+
mode: modeName ?? "default",
|
|
1433
|
+
error: `could not parse report payload: ${parseError}`,
|
|
1434
|
+
stdout: stdoutBuffer,
|
|
1435
|
+
stderr: stderrBuffer,
|
|
1436
|
+
});
|
|
1057
1437
|
throw new Error(`could not parse report payload: ${parseError}`);
|
|
1058
1438
|
}
|
|
1059
1439
|
if (!report) {
|
|
1440
|
+
persistCrashRecord(crashDir, {
|
|
1441
|
+
kind: "test",
|
|
1442
|
+
file: specFile,
|
|
1443
|
+
mode: modeName ?? "default",
|
|
1444
|
+
error: "missing report payload from test runtime",
|
|
1445
|
+
stdout: stdoutBuffer,
|
|
1446
|
+
stderr: stderrBuffer,
|
|
1447
|
+
});
|
|
1060
1448
|
throw new Error("missing report payload from test runtime");
|
|
1061
1449
|
}
|
|
1062
1450
|
if (code !== 0) {
|
|
@@ -1094,10 +1482,17 @@ function collectRunStats(reports) {
|
|
|
1094
1482
|
return stats;
|
|
1095
1483
|
}
|
|
1096
1484
|
function readFileReport(stats, fileReport) {
|
|
1097
|
-
const
|
|
1485
|
+
const fileReportAny = fileReport;
|
|
1486
|
+
const suites = Array.isArray(fileReportAny.suites)
|
|
1487
|
+
? fileReportAny.suites
|
|
1488
|
+
: Array.isArray(fileReport)
|
|
1489
|
+
? fileReport
|
|
1490
|
+
: [];
|
|
1491
|
+
const file = String(fileReportAny.file ?? "");
|
|
1492
|
+
const modeName = String(fileReportAny.modeName ?? "");
|
|
1098
1493
|
let fileVerdict = "none";
|
|
1099
1494
|
for (const suite of suites) {
|
|
1100
|
-
fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite));
|
|
1495
|
+
fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite, file, modeName));
|
|
1101
1496
|
}
|
|
1102
1497
|
if (fileVerdict == "fail") {
|
|
1103
1498
|
stats.failedFiles++;
|
|
@@ -1109,7 +1504,7 @@ function readFileReport(stats, fileReport) {
|
|
|
1109
1504
|
stats.skippedFiles++;
|
|
1110
1505
|
}
|
|
1111
1506
|
}
|
|
1112
|
-
function readSuite(stats, suite) {
|
|
1507
|
+
function readSuite(stats, suite, file, modeName) {
|
|
1113
1508
|
const suiteAny = suite;
|
|
1114
1509
|
let verdict = normalizeVerdict(suiteAny.verdict);
|
|
1115
1510
|
const time = suiteAny.time;
|
|
@@ -1120,7 +1515,7 @@ function readSuite(stats, suite) {
|
|
|
1120
1515
|
? suiteAny.suites
|
|
1121
1516
|
: [];
|
|
1122
1517
|
for (const subSuite of subSuites) {
|
|
1123
|
-
verdict = mergeVerdict(verdict, readSuite(stats, subSuite));
|
|
1518
|
+
verdict = mergeVerdict(verdict, readSuite(stats, subSuite, file, modeName));
|
|
1124
1519
|
}
|
|
1125
1520
|
const tests = Array.isArray(suiteAny.tests)
|
|
1126
1521
|
? suiteAny.tests
|
|
@@ -1140,7 +1535,11 @@ function readSuite(stats, suite) {
|
|
|
1140
1535
|
}
|
|
1141
1536
|
if (verdict == "fail") {
|
|
1142
1537
|
stats.failedSuites++;
|
|
1143
|
-
stats.failedEntries.push(
|
|
1538
|
+
stats.failedEntries.push({
|
|
1539
|
+
...suiteAny,
|
|
1540
|
+
file,
|
|
1541
|
+
modeName,
|
|
1542
|
+
});
|
|
1144
1543
|
}
|
|
1145
1544
|
else if (verdict == "ok") {
|
|
1146
1545
|
stats.passedSuites++;
|
|
@@ -1169,15 +1568,18 @@ function mergeVerdict(current, next) {
|
|
|
1169
1568
|
return "skip";
|
|
1170
1569
|
return "none";
|
|
1171
1570
|
}
|
|
1172
|
-
export async function createRunReporter(configPath = DEFAULT_CONFIG_PATH, reporterPath, modeName
|
|
1571
|
+
export async function createRunReporter(configPath = DEFAULT_CONFIG_PATH, reporterPath, modeName, context = {
|
|
1572
|
+
stdout: process.stdout,
|
|
1573
|
+
stderr: process.stderr,
|
|
1574
|
+
}) {
|
|
1173
1575
|
const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
|
|
1174
1576
|
const loadedConfig = loadConfig(resolvedConfigPath);
|
|
1175
1577
|
const mode = applyMode(loadedConfig, modeName);
|
|
1176
1578
|
const config = mode.config;
|
|
1177
1579
|
const selection = resolveReporterSelection(reporterPath, config.runOptions.reporter);
|
|
1178
1580
|
const reporter = await loadReporter(selection, resolvedConfigPath, {
|
|
1179
|
-
stdout:
|
|
1180
|
-
stderr:
|
|
1581
|
+
stdout: context.stdout,
|
|
1582
|
+
stderr: context.stderr,
|
|
1181
1583
|
});
|
|
1182
1584
|
const runtimeCommand = resolveRuntimeCommand(getConfiguredRuntimeCmd(config), config.buildOptions.target, false);
|
|
1183
1585
|
return {
|