as-test 1.1.6 → 1.1.7

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -9
  3. package/assembly/index.ts +10 -15
  4. package/assembly/src/expectation.ts +11 -11
  5. package/assembly/src/fuzz.ts +11 -7
  6. package/assembly/src/log.ts +2 -2
  7. package/assembly/src/suite.ts +5 -5
  8. package/assembly/src/tests.ts +8 -8
  9. package/assembly/util/wipc.ts +5 -1
  10. package/bin/build-worker-pool.js +146 -142
  11. package/bin/build-worker.js +37 -34
  12. package/bin/commands/build-core.js +577 -465
  13. package/bin/commands/build.js +49 -29
  14. package/bin/commands/clean-core.js +120 -113
  15. package/bin/commands/clean.js +14 -8
  16. package/bin/commands/doctor-core.js +288 -289
  17. package/bin/commands/doctor.js +1 -1
  18. package/bin/commands/fuzz-core.js +467 -414
  19. package/bin/commands/fuzz.js +27 -10
  20. package/bin/commands/init-core.js +905 -794
  21. package/bin/commands/init.js +2 -2
  22. package/bin/commands/run-core.js +2675 -2344
  23. package/bin/commands/run.js +43 -25
  24. package/bin/commands/test.js +56 -32
  25. package/bin/commands/web-runner-source.js +1 -1
  26. package/bin/commands/web-session.js +516 -525
  27. package/bin/coverage-points.js +363 -341
  28. package/bin/crash-store.js +56 -66
  29. package/bin/index.js +4092 -3150
  30. package/bin/reporters/default.js +1090 -890
  31. package/bin/reporters/tap.js +319 -325
  32. package/bin/types.js +67 -67
  33. package/bin/util.js +1290 -1239
  34. package/bin/wipc.js +70 -73
  35. package/lib/build/index.d.ts +3 -1
  36. package/lib/build/index.js +1039 -1034
  37. package/lib/build/web-runner/client.js +1 -1
  38. package/lib/build/web-runner/html.js +1 -1
  39. package/lib/build/web-runner/worker.js +1 -1
  40. package/package.json +6 -3
  41. package/transform/lib/log.js +9 -5
  42. package/assembly/util/json.ts +0 -112
@@ -5,1004 +5,1204 @@ import * as path from "path";
5
5
  import { formatSpecDisplayPath, formatTime } from "../util.js";
6
6
  import { describeCoveragePoint } from "../coverage-points.js";
7
7
  export const createReporter = (context) => {
8
- return new DefaultReporter(context);
8
+ return new DefaultReporter(context);
9
9
  };
10
10
  class DefaultReporter {
11
- constructor(context) {
12
- this.context = context;
13
- this.currentFile = null;
14
- this.openSuites = [];
15
- this.verboseSuites = [];
16
- this.renderedLines = 0;
17
- this.fileHasWarning = false;
18
- this.verboseMode = false;
19
- this.cleanMode = false;
20
- this.hasRenderedTestFiles = false;
21
- this.hasRenderedFuzzFiles = false;
22
- }
23
- canRewriteLine() {
24
- return (!this.cleanMode &&
25
- Boolean(this.context.stdout.isTTY));
26
- }
27
- badgeRunning() {
28
- return chalk.bgBlackBright.white(" .... ");
29
- }
30
- badgeFromVerdict(verdict) {
31
- if (verdict == "ok")
32
- return chalk.bgGreenBright.black(" PASS ");
33
- if (verdict == "fail")
34
- return chalk.bgRed.white(" FAIL ");
35
- return chalk.bgBlackBright.white(" SKIP ");
36
- }
37
- clearRenderedBlock() {
38
- if (!this.renderedLines || !this.canRewriteLine())
39
- return;
40
- for (let i = 0; i < this.renderedLines; i++) {
41
- this.context.stdout.write("\r\x1b[2K");
42
- if (i < this.renderedLines - 1) {
43
- this.context.stdout.write("\x1b[1A");
44
- }
45
- }
46
- this.renderedLines = 0;
47
- }
48
- drawLiveBlock(lines) {
49
- this.clearRenderedBlock();
50
- if (!lines.length)
51
- return;
52
- this.context.stdout.write(lines.join("\n"));
53
- this.renderedLines = lines.length;
54
- }
55
- renderLiveState() {
56
- if (!this.canRewriteLine() || !this.currentFile)
57
- return;
58
- const lines = [
59
- `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
60
- ];
61
- for (const suite of this.openSuites) {
62
- lines.push(`${" ".repeat(suite.depth + 1)}${this.badgeRunning()} ${suite.description}`);
63
- }
64
- this.drawLiveBlock(lines);
65
- }
66
- renderVerboseState(fileEnd) {
67
- if (!this.canRewriteLine() || !this.currentFile)
68
- return;
69
- const lines = [
70
- fileEnd
71
- ? this.renderFileResult(fileEnd)
72
- : `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
73
- ];
74
- for (const suite of this.verboseSuites) {
75
- const badge = suite.verdict == "running"
76
- ? this.badgeRunning()
77
- : this.badgeFromVerdict(suite.verdict);
78
- lines.push(`${" ".repeat(suite.depth + 1)}${badge} ${suite.description}`);
79
- }
80
- this.drawLiveBlock(lines);
81
- }
82
- setVerboseSuiteVerdict(depth, description, verdict) {
83
- for (let i = this.verboseSuites.length - 1; i >= 0; i--) {
84
- const suite = this.verboseSuites[i];
85
- if (suite.depth == depth &&
86
- (!description.length || suite.description == description) &&
87
- suite.verdict == "running") {
88
- if (description.length)
89
- suite.description = description;
90
- suite.verdict = verdict;
91
- return;
92
- }
93
- }
94
- this.verboseSuites.push({ depth, description, verdict });
11
+ constructor(context) {
12
+ this.context = context;
13
+ this.currentFile = null;
14
+ this.openSuites = [];
15
+ this.verboseSuites = [];
16
+ this.renderedLines = 0;
17
+ this.fileHasWarning = false;
18
+ this.verboseMode = false;
19
+ this.cleanMode = false;
20
+ this.hasRenderedTestFiles = false;
21
+ this.hasRenderedFuzzFiles = false;
22
+ }
23
+ canRewriteLine() {
24
+ return !this.cleanMode && Boolean(this.context.stdout.isTTY);
25
+ }
26
+ badgeRunning() {
27
+ return chalk.bgBlackBright.white(" .... ");
28
+ }
29
+ badgeFromVerdict(verdict) {
30
+ if (verdict == "ok") return chalk.bgGreenBright.black(" PASS ");
31
+ if (verdict == "fail") return chalk.bgRed.white(" FAIL ");
32
+ return chalk.bgBlackBright.white(" SKIP ");
33
+ }
34
+ clearRenderedBlock() {
35
+ if (!this.renderedLines || !this.canRewriteLine()) return;
36
+ for (let i = 0; i < this.renderedLines; i++) {
37
+ this.context.stdout.write("\r\x1b[2K");
38
+ if (i < this.renderedLines - 1) {
39
+ this.context.stdout.write("\x1b[1A");
40
+ }
95
41
  }
96
- collapseToDepth(depth) {
97
- while (this.openSuites.length > depth) {
98
- this.openSuites.pop();
99
- }
42
+ this.renderedLines = 0;
43
+ }
44
+ drawLiveBlock(lines) {
45
+ this.clearRenderedBlock();
46
+ if (!lines.length) return;
47
+ this.context.stdout.write(lines.join("\n"));
48
+ this.renderedLines = lines.length;
49
+ }
50
+ renderLiveState() {
51
+ if (!this.canRewriteLine() || !this.currentFile) return;
52
+ const lines = [
53
+ `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
54
+ ];
55
+ for (const suite of this.openSuites) {
56
+ lines.push(
57
+ `${" ".repeat(suite.depth + 1)}${this.badgeRunning()} ${suite.description}`,
58
+ );
100
59
  }
101
- renderSuiteCompleteFrame(depth, description, verdict) {
102
- if (!this.canRewriteLine() || !this.currentFile)
103
- return;
104
- const lines = [`${this.badgeRunning()} ${this.currentFile}`];
105
- for (let i = 0; i < depth; i++) {
106
- const suite = this.openSuites[i];
107
- if (!suite)
108
- continue;
109
- lines.push(`${" ".repeat(suite.depth + 1)}${this.badgeRunning()} ${suite.description}`);
110
- }
111
- lines.push(`${" ".repeat(depth + 1)}${this.badgeFromVerdict(verdict)} ${description}`);
112
- this.drawLiveBlock(lines);
113
- }
114
- renderFileResult(event) {
115
- const verdict = event.verdict ?? "none";
116
- const time = event.time ? ` ${chalk.dim(event.time)}` : "";
117
- const file = formatSpecDisplayPath(event.file);
118
- if (verdict == "fail")
119
- return `${chalk.bgRed.white(" FAIL ")} ${file}${time}`;
120
- if (this.fileHasWarning)
121
- return `${chalk.bgYellow.black(" WARN ")} ${file}${time}`;
122
- if (verdict == "ok")
123
- return `${chalk.bgGreenBright.black(" PASS ")} ${file}${time}`;
124
- return `${chalk.bgBlackBright.white(" SKIP ")} ${file}${time}`;
125
- }
126
- onRunStart(event) {
127
- this.verboseMode = Boolean(event.verbose);
128
- this.cleanMode = Boolean(event.clean);
129
- this.hasRenderedTestFiles = false;
130
- this.hasRenderedFuzzFiles = false;
131
- }
132
- onFileStart(event) {
133
- this.currentFile = event.file;
134
- this.openSuites = [];
135
- this.verboseSuites = [];
136
- this.fileHasWarning = false;
137
- if (this.cleanMode)
138
- return;
139
- if (this.verboseMode && this.canRewriteLine()) {
140
- this.renderVerboseState();
141
- return;
142
- }
143
- if (!this.verboseMode) {
144
- if (!this.canRewriteLine()) {
145
- this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`);
146
- return;
147
- }
148
- this.clearRenderedBlock();
149
- this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}`);
150
- this.renderedLines = 1;
151
- return;
152
- }
153
- if (!this.canRewriteLine()) {
154
- this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`);
155
- return;
156
- }
157
- this.renderLiveState();
158
- }
159
- onFileEnd(event) {
160
- this.hasRenderedTestFiles = true;
161
- if (this.verboseMode && this.canRewriteLine()) {
162
- this.renderVerboseState(event);
163
- this.context.stdout.write("\n");
164
- this.renderedLines = 0;
165
- this.currentFile = null;
166
- this.openSuites = [];
167
- this.verboseSuites = [];
168
- this.fileHasWarning = false;
169
- return;
170
- }
171
- const result = this.renderFileResult(event);
172
- this.clearRenderedBlock();
173
- this.context.stdout.write(`${result}\n`);
174
- this.currentFile = null;
175
- this.openSuites = [];
176
- this.verboseSuites = [];
177
- this.fileHasWarning = false;
178
- }
179
- onSuiteStart(event) {
180
- if (this.cleanMode)
181
- return;
182
- if (!this.verboseMode)
183
- return;
184
- const depth = Math.max(event.depth, 0);
185
- if (this.verboseMode && this.canRewriteLine()) {
186
- if (this.currentFile !== event.file)
187
- return;
188
- this.verboseSuites.push({
189
- depth,
190
- description: event.description,
191
- verdict: "running",
192
- });
193
- this.renderVerboseState();
194
- return;
195
- }
196
- if (this.verboseMode || !this.canRewriteLine()) {
197
- this.context.stdout.write(`${" ".repeat(depth + 1)}${this.badgeRunning()} ${event.description}\n`);
198
- return;
199
- }
200
- if (this.currentFile !== event.file)
201
- return;
202
- this.collapseToDepth(depth);
203
- this.openSuites.push({ depth, description: event.description });
204
- this.renderLiveState();
205
- }
206
- onSuiteEnd(event) {
207
- if (this.cleanMode)
208
- return;
209
- if (!this.verboseMode)
210
- return;
211
- const depth = Math.max(event.depth, 0);
212
- const verdict = String(event.verdict ?? "none");
213
- if (this.verboseMode && this.canRewriteLine()) {
214
- if (this.currentFile !== event.file)
215
- return;
216
- this.setVerboseSuiteVerdict(depth, event.description, verdict);
217
- this.renderVerboseState();
218
- return;
219
- }
220
- if (this.verboseMode || !this.canRewriteLine()) {
221
- this.context.stdout.write(`${" ".repeat(depth + 1)}${this.badgeFromVerdict(verdict)} ${event.description}\n`);
222
- return;
223
- }
224
- if (this.currentFile !== event.file)
225
- return;
226
- this.collapseToDepth(depth + 1);
227
- const current = this.openSuites[depth];
228
- const description = event.description || current?.description || "suite";
229
- if (!current) {
230
- this.openSuites.push({ depth, description });
231
- }
232
- else {
233
- current.description = description;
234
- }
235
- this.renderSuiteCompleteFrame(depth, description, verdict);
236
- this.collapseToDepth(depth);
237
- this.renderLiveState();
238
- }
239
- onAssertionFail(_event) { }
240
- onSnapshotMissing(event) {
241
- this.fileHasWarning = true;
242
- const warnLine = `${chalk.bgYellow.black(" WARN ")} missing snapshot for ${chalk.dim(event.key)}. Re-run with ${chalk.bold("--create-snapshots")} to create it.\n`;
243
- if (!this.canRewriteLine() || !this.currentFile) {
244
- this.context.stdout.write(warnLine);
245
- return;
246
- }
247
- this.clearRenderedBlock();
248
- this.context.stdout.write(warnLine);
249
- if (this.verboseMode) {
250
- this.renderVerboseState();
251
- }
252
- else {
253
- this.renderLiveState();
254
- }
60
+ this.drawLiveBlock(lines);
61
+ }
62
+ renderVerboseState(fileEnd) {
63
+ if (!this.canRewriteLine() || !this.currentFile) return;
64
+ const lines = [
65
+ fileEnd
66
+ ? this.renderFileResult(fileEnd)
67
+ : `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
68
+ ];
69
+ for (const suite of this.verboseSuites) {
70
+ const badge =
71
+ suite.verdict == "running"
72
+ ? this.badgeRunning()
73
+ : this.badgeFromVerdict(suite.verdict);
74
+ lines.push(
75
+ `${" ".repeat(suite.depth + 1)}${badge} ${suite.description}`,
76
+ );
255
77
  }
256
- onWarning(event) {
257
- this.fileHasWarning = true;
258
- const warnLine = `${chalk.bgYellow.black(" WARN ")} ${event.message}\n`;
259
- if (!this.canRewriteLine() || !this.currentFile) {
260
- this.context.stdout.write(warnLine);
261
- return;
262
- }
263
- this.clearRenderedBlock();
264
- this.context.stdout.write(warnLine);
265
- if (this.verboseMode) {
266
- this.renderVerboseState();
267
- }
268
- else {
269
- this.renderLiveState();
270
- }
78
+ this.drawLiveBlock(lines);
79
+ }
80
+ setVerboseSuiteVerdict(depth, description, verdict) {
81
+ for (let i = this.verboseSuites.length - 1; i >= 0; i--) {
82
+ const suite = this.verboseSuites[i];
83
+ if (
84
+ suite.depth == depth &&
85
+ (!description.length || suite.description == description) &&
86
+ suite.verdict == "running"
87
+ ) {
88
+ if (description.length) suite.description = description;
89
+ suite.verdict = verdict;
90
+ return;
91
+ }
271
92
  }
272
- onLog(event) {
273
- if (this.cleanMode)
274
- return;
275
- if (this.verboseMode || !this.canRewriteLine()) {
276
- const depth = Math.max(event.depth, 0);
277
- this.context.stdout.write(`${" ".repeat(depth + 1)}${chalk.dim("LOG")} ${event.text}\n`);
278
- }
93
+ this.verboseSuites.push({ depth, description, verdict });
94
+ }
95
+ collapseToDepth(depth) {
96
+ while (this.openSuites.length > depth) {
97
+ this.openSuites.pop();
279
98
  }
280
- onRunComplete(event) {
281
- this.clearRenderedBlock();
282
- if (!event.clean) {
283
- renderFailedSuites(event.stats.failedEntries);
284
- }
285
- if (event.snapshotEnabled) {
286
- renderSnapshotSummary(event.snapshotSummary, true);
287
- }
288
- if (event.coverageSummary.enabled) {
289
- renderCoverageSummary(event.coverageSummary, event.showCoverage);
290
- if (event.showCoverage && event.coverageSummary.uncovered) {
291
- renderCoveragePoints(event.coverageSummary.files, Boolean(event.verbose || event.showCoverageAll));
292
- }
293
- }
294
- renderTotals(event.stats, event);
99
+ }
100
+ renderSuiteCompleteFrame(depth, description, verdict) {
101
+ if (!this.canRewriteLine() || !this.currentFile) return;
102
+ const lines = [`${this.badgeRunning()} ${this.currentFile}`];
103
+ for (let i = 0; i < depth; i++) {
104
+ const suite = this.openSuites[i];
105
+ if (!suite) continue;
106
+ lines.push(
107
+ `${" ".repeat(suite.depth + 1)}${this.badgeRunning()} ${suite.description}`,
108
+ );
109
+ }
110
+ lines.push(
111
+ `${" ".repeat(depth + 1)}${this.badgeFromVerdict(verdict)} ${description}`,
112
+ );
113
+ this.drawLiveBlock(lines);
114
+ }
115
+ renderFileResult(event) {
116
+ const verdict = event.verdict ?? "none";
117
+ const time = event.time ? ` ${chalk.dim(event.time)}` : "";
118
+ const file = formatSpecDisplayPath(event.file);
119
+ if (verdict == "fail")
120
+ return `${chalk.bgRed.white(" FAIL ")} ${file}${time}`;
121
+ if (this.fileHasWarning)
122
+ return `${chalk.bgYellow.black(" WARN ")} ${file}${time}`;
123
+ if (verdict == "ok")
124
+ return `${chalk.bgGreenBright.black(" PASS ")} ${file}${time}`;
125
+ return `${chalk.bgBlackBright.white(" SKIP ")} ${file}${time}`;
126
+ }
127
+ onRunStart(event) {
128
+ this.verboseMode = Boolean(event.verbose);
129
+ this.cleanMode = Boolean(event.clean);
130
+ this.hasRenderedTestFiles = false;
131
+ this.hasRenderedFuzzFiles = false;
132
+ }
133
+ onFileStart(event) {
134
+ this.currentFile = event.file;
135
+ this.openSuites = [];
136
+ this.verboseSuites = [];
137
+ this.fileHasWarning = false;
138
+ if (this.cleanMode) return;
139
+ if (this.verboseMode && this.canRewriteLine()) {
140
+ this.renderVerboseState();
141
+ return;
142
+ }
143
+ if (!this.verboseMode) {
144
+ if (!this.canRewriteLine()) {
145
+ this.context.stdout.write(
146
+ `${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`,
147
+ );
148
+ return;
149
+ }
150
+ this.clearRenderedBlock();
151
+ this.context.stdout.write(
152
+ `${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}`,
153
+ );
154
+ this.renderedLines = 1;
155
+ return;
156
+ }
157
+ if (!this.canRewriteLine()) {
158
+ this.context.stdout.write(
159
+ `${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`,
160
+ );
161
+ return;
162
+ }
163
+ this.renderLiveState();
164
+ }
165
+ onFileEnd(event) {
166
+ this.hasRenderedTestFiles = true;
167
+ if (this.verboseMode && this.canRewriteLine()) {
168
+ this.renderVerboseState(event);
169
+ this.context.stdout.write("\n");
170
+ this.renderedLines = 0;
171
+ this.currentFile = null;
172
+ this.openSuites = [];
173
+ this.verboseSuites = [];
174
+ this.fileHasWarning = false;
175
+ return;
176
+ }
177
+ const result = this.renderFileResult(event);
178
+ this.clearRenderedBlock();
179
+ this.context.stdout.write(`${result}\n`);
180
+ this.currentFile = null;
181
+ this.openSuites = [];
182
+ this.verboseSuites = [];
183
+ this.fileHasWarning = false;
184
+ }
185
+ onSuiteStart(event) {
186
+ if (this.cleanMode) return;
187
+ if (!this.verboseMode) return;
188
+ const depth = Math.max(event.depth, 0);
189
+ if (this.verboseMode && this.canRewriteLine()) {
190
+ if (this.currentFile !== event.file) return;
191
+ this.verboseSuites.push({
192
+ depth,
193
+ description: event.description,
194
+ verdict: "running",
195
+ });
196
+ this.renderVerboseState();
197
+ return;
198
+ }
199
+ if (this.verboseMode || !this.canRewriteLine()) {
200
+ this.context.stdout.write(
201
+ `${" ".repeat(depth + 1)}${this.badgeRunning()} ${event.description}\n`,
202
+ );
203
+ return;
204
+ }
205
+ if (this.currentFile !== event.file) return;
206
+ this.collapseToDepth(depth);
207
+ this.openSuites.push({ depth, description: event.description });
208
+ this.renderLiveState();
209
+ }
210
+ onSuiteEnd(event) {
211
+ if (this.cleanMode) return;
212
+ if (!this.verboseMode) return;
213
+ const depth = Math.max(event.depth, 0);
214
+ const verdict = String(event.verdict ?? "none");
215
+ if (this.verboseMode && this.canRewriteLine()) {
216
+ if (this.currentFile !== event.file) return;
217
+ this.setVerboseSuiteVerdict(depth, event.description, verdict);
218
+ this.renderVerboseState();
219
+ return;
220
+ }
221
+ if (this.verboseMode || !this.canRewriteLine()) {
222
+ this.context.stdout.write(
223
+ `${" ".repeat(depth + 1)}${this.badgeFromVerdict(verdict)} ${event.description}\n`,
224
+ );
225
+ return;
226
+ }
227
+ if (this.currentFile !== event.file) return;
228
+ this.collapseToDepth(depth + 1);
229
+ const current = this.openSuites[depth];
230
+ const description = event.description || current?.description || "suite";
231
+ if (!current) {
232
+ this.openSuites.push({ depth, description });
233
+ } else {
234
+ current.description = description;
235
+ }
236
+ this.renderSuiteCompleteFrame(depth, description, verdict);
237
+ this.collapseToDepth(depth);
238
+ this.renderLiveState();
239
+ }
240
+ onAssertionFail(_event) {}
241
+ onSnapshotMissing(event) {
242
+ this.fileHasWarning = true;
243
+ const warnLine = `${chalk.bgYellow.black(" WARN ")} missing snapshot for ${chalk.dim(event.key)}. Re-run with ${chalk.bold("--create-snapshots")} to create it.\n`;
244
+ if (!this.canRewriteLine() || !this.currentFile) {
245
+ this.context.stdout.write(warnLine);
246
+ return;
295
247
  }
296
- onFuzzComplete(event) {
297
- renderFuzzSummary(this.context, event, this.hasRenderedTestFiles);
248
+ this.clearRenderedBlock();
249
+ this.context.stdout.write(warnLine);
250
+ if (this.verboseMode) {
251
+ this.renderVerboseState();
252
+ } else {
253
+ this.renderLiveState();
298
254
  }
299
- onFuzzFileComplete(event) {
300
- this.hasRenderedFuzzFiles = true;
301
- renderFuzzFileSummary(this.context, event.results);
255
+ }
256
+ onWarning(event) {
257
+ this.fileHasWarning = true;
258
+ const warnLine = `${chalk.bgYellow.black(" WARN ")} ${event.message}\n`;
259
+ if (!this.canRewriteLine() || !this.currentFile) {
260
+ this.context.stdout.write(warnLine);
261
+ return;
302
262
  }
263
+ this.clearRenderedBlock();
264
+ this.context.stdout.write(warnLine);
265
+ if (this.verboseMode) {
266
+ this.renderVerboseState();
267
+ } else {
268
+ this.renderLiveState();
269
+ }
270
+ }
271
+ onLog(event) {
272
+ if (this.cleanMode) return;
273
+ if (this.verboseMode || !this.canRewriteLine()) {
274
+ const depth = Math.max(event.depth, 0);
275
+ this.context.stdout.write(
276
+ `${" ".repeat(depth + 1)}${chalk.dim("LOG")} ${event.text}\n`,
277
+ );
278
+ }
279
+ }
280
+ onRunComplete(event) {
281
+ this.clearRenderedBlock();
282
+ if (!event.clean) {
283
+ renderFailedSuites(event.stats.failedEntries);
284
+ }
285
+ if (event.snapshotEnabled) {
286
+ renderSnapshotSummary(event.snapshotSummary, true);
287
+ }
288
+ if (event.coverageSummary.enabled) {
289
+ renderCoverageSummary(event.coverageSummary, event.showCoverage);
290
+ if (event.showCoverage && event.coverageSummary.uncovered) {
291
+ renderCoveragePoints(
292
+ event.coverageSummary.files,
293
+ Boolean(event.verbose || event.showCoverageAll),
294
+ );
295
+ }
296
+ }
297
+ renderTotals(event.stats, event);
298
+ }
299
+ onFuzzComplete(event) {
300
+ renderFuzzSummary(this.context, event, this.hasRenderedTestFiles);
301
+ }
302
+ onFuzzFileComplete(event) {
303
+ this.hasRenderedFuzzFiles = true;
304
+ renderFuzzFileSummary(this.context, event.results);
305
+ }
303
306
  }
304
307
  function renderFuzzFileSummary(context, results) {
305
- if (!results.length)
306
- return;
307
- const file = results[0].file;
308
- const itemFailed = results.some((mode) => mode.crashes > 0 || mode.fuzzers.some((fuzzer) => fuzzer.failed > 0));
309
- const itemSkipped = !itemFailed &&
310
- results.length > 0 &&
311
- results.every((mode) => mode.fuzzers.length > 0 &&
312
- mode.fuzzers.every((fuzzer) => fuzzer.skipped > 0));
313
- const itemBadge = itemFailed
314
- ? chalk.bgRed.white(" FAIL ")
315
- : itemSkipped
316
- ? chalk.bgBlackBright.white(" SKIP ")
317
- : chalk.bgGreenBright.black(" PASS ");
318
- const detail = formatTime(averageFuzzModeTime(results));
319
- const crashFile = firstFuzzCrashFile(results);
320
- const crashSuffix = crashFile != null ? ` ${chalk.dim(`-> ${crashFile}`)}` : "";
321
- context.stdout.write(`${itemBadge} ${formatSpecDisplayPath(file)} ${chalk.dim(detail)}${crashSuffix}\n`);
322
- renderFailedFuzzers(groupFuzzResultsByFile(results));
308
+ if (!results.length) return;
309
+ const file = results[0].file;
310
+ const itemFailed = results.some(
311
+ (mode) =>
312
+ mode.crashes > 0 || mode.fuzzers.some((fuzzer) => fuzzer.failed > 0),
313
+ );
314
+ const itemSkipped =
315
+ !itemFailed &&
316
+ results.length > 0 &&
317
+ results.every(
318
+ (mode) =>
319
+ mode.fuzzers.length > 0 &&
320
+ mode.fuzzers.every((fuzzer) => fuzzer.skipped > 0),
321
+ );
322
+ const itemBadge = itemFailed
323
+ ? chalk.bgRed.white(" FAIL ")
324
+ : itemSkipped
325
+ ? chalk.bgBlackBright.white(" SKIP ")
326
+ : chalk.bgGreenBright.black(" PASS ");
327
+ const detail = formatTime(averageFuzzModeTime(results));
328
+ const crashFile = firstFuzzCrashFile(results);
329
+ const crashSuffix =
330
+ crashFile != null ? ` ${chalk.dim(`-> ${crashFile}`)}` : "";
331
+ context.stdout.write(
332
+ `${itemBadge} ${formatSpecDisplayPath(file)} ${chalk.dim(detail)}${crashSuffix}\n`,
333
+ );
334
+ renderFailedFuzzers(groupFuzzResultsByFile(results));
323
335
  }
324
336
  function renderFuzzSummary(context, event, hasRenderedTestFiles) {
325
- context.stdout.write("\n");
326
- if (!hasRenderedTestFiles) {
327
- renderStandaloneFuzzTotals(event);
328
- }
337
+ context.stdout.write("\n");
338
+ if (!hasRenderedTestFiles) {
339
+ renderStandaloneFuzzTotals(event);
340
+ }
329
341
  }
330
342
  function renderFailedFuzzers(results) {
331
- let rendered = false;
332
- for (const result of results) {
333
- for (const modeResult of result.modes) {
334
- const relativeFile = toRelativeResultPath(modeResult.file);
335
- const repro = buildFuzzReproCommand(relativeFile, modeResult.seed, modeResult.modeName, modeResult.fuzzers[0]?.selector);
336
- if (modeResult.crashes > 0 && !modeResult.fuzzers.length) {
337
- if (!rendered) {
338
- console.log("");
339
- rendered = true;
340
- }
341
- console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(formatSpecDisplayPath(modeResult.file))} ${chalk.dim("(crash)")}`);
342
- console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
343
- console.log(chalk.dim(`Runs: ${modeResult.runs} configured`));
344
- console.log(chalk.dim(`Repro: ${repro}`));
345
- console.log(chalk.dim(`Seed: ${modeResult.seed}`));
346
- if (modeResult.crashFiles.length) {
347
- console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
348
- }
349
- console.log("");
350
- continue;
351
- }
352
- for (const fuzzer of modeResult.fuzzers) {
353
- if (fuzzer.failed <= 0 && fuzzer.crashed <= 0)
354
- continue;
355
- if (!rendered) {
356
- console.log("");
357
- rendered = true;
358
- }
359
- const fuzzerRepro = buildFuzzReproCommand(relativeFile, modeResult.seed, modeResult.modeName, fuzzer.selector);
360
- console.log(`${chalk.bgRed(" FAIL ")} ${formatFuzzFailureTitle(modeResult.file, fuzzer.name)}`);
361
- if (fuzzer.failure) {
362
- renderAssertionFailureDetails(fuzzer.failure.left, fuzzer.failure.right, fuzzer.failure.message);
363
- }
364
- console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
365
- console.log(chalk.dim(`Runs: ${fuzzer.passed + fuzzer.failed + fuzzer.crashed} completed (${fuzzer.passed} passed, ${fuzzer.failed} failed, ${fuzzer.crashed} crashed)`));
366
- console.log(chalk.dim(`Repro: ${fuzzerRepro}`));
367
- console.log(chalk.dim(`Seed: ${modeResult.seed}`));
368
- if (fuzzer.failures?.length) {
369
- console.log(chalk.dim(`Failing seeds: ${formatFailingSeeds(fuzzer)}`));
370
- for (const failure of fuzzer.failures) {
371
- console.log(chalk.dim(`Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, fuzzer.selector, 1)}`));
372
- if (failure.input) {
373
- console.log(chalk.dim(`Input ${failure.run + 1}: ${JSON.stringify(failure.input)}`));
374
- }
375
- }
376
- }
377
- if (fuzzer.crashFile?.length) {
378
- console.log(chalk.dim(`Crash: ${fuzzer.crashFile}`));
379
- }
380
- else if (modeResult.crashFiles.length) {
381
- console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
382
- }
383
- console.log("");
343
+ let rendered = false;
344
+ for (const result of results) {
345
+ for (const modeResult of result.modes) {
346
+ const relativeFile = toRelativeResultPath(modeResult.file);
347
+ const repro = buildFuzzReproCommand(
348
+ relativeFile,
349
+ modeResult.seed,
350
+ modeResult.modeName,
351
+ modeResult.fuzzers[0]?.selector,
352
+ );
353
+ if (modeResult.crashes > 0 && !modeResult.fuzzers.length) {
354
+ if (!rendered) {
355
+ console.log("");
356
+ rendered = true;
357
+ }
358
+ console.log(
359
+ `${chalk.bgRed(" FAIL ")} ${chalk.dim(formatSpecDisplayPath(modeResult.file))} ${chalk.dim("(crash)")}`,
360
+ );
361
+ console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
362
+ console.log(chalk.dim(`Runs: ${modeResult.runs} configured`));
363
+ console.log(chalk.dim(`Repro: ${repro}`));
364
+ console.log(chalk.dim(`Seed: ${modeResult.seed}`));
365
+ if (modeResult.crashFiles.length) {
366
+ console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
367
+ }
368
+ console.log("");
369
+ continue;
370
+ }
371
+ for (const fuzzer of modeResult.fuzzers) {
372
+ if (fuzzer.failed <= 0 && fuzzer.crashed <= 0) continue;
373
+ if (!rendered) {
374
+ console.log("");
375
+ rendered = true;
376
+ }
377
+ const fuzzerRepro = buildFuzzReproCommand(
378
+ relativeFile,
379
+ modeResult.seed,
380
+ modeResult.modeName,
381
+ fuzzer.selector,
382
+ );
383
+ console.log(
384
+ `${chalk.bgRed(" FAIL ")} ${formatFuzzFailureTitle(modeResult.file, fuzzer.name)}`,
385
+ );
386
+ if (fuzzer.failure) {
387
+ renderAssertionFailureDetails(
388
+ fuzzer.failure.left,
389
+ fuzzer.failure.right,
390
+ fuzzer.failure.message,
391
+ );
392
+ }
393
+ console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
394
+ console.log(
395
+ chalk.dim(
396
+ `Runs: ${fuzzer.passed + fuzzer.failed + fuzzer.crashed} completed (${fuzzer.passed} passed, ${fuzzer.failed} failed, ${fuzzer.crashed} crashed)`,
397
+ ),
398
+ );
399
+ console.log(chalk.dim(`Repro: ${fuzzerRepro}`));
400
+ console.log(chalk.dim(`Seed: ${modeResult.seed}`));
401
+ if (fuzzer.failures?.length) {
402
+ console.log(
403
+ chalk.dim(`Failing seeds: ${formatFailingSeeds(fuzzer)}`),
404
+ );
405
+ for (const failure of fuzzer.failures) {
406
+ console.log(
407
+ chalk.dim(
408
+ `Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, fuzzer.selector, 1)}`,
409
+ ),
410
+ );
411
+ if (failure.input) {
412
+ console.log(
413
+ chalk.dim(
414
+ `Input ${failure.run + 1}: ${JSON.stringify(failure.input)}`,
415
+ ),
416
+ );
384
417
  }
418
+ }
385
419
  }
420
+ if (fuzzer.crashFile?.length) {
421
+ console.log(chalk.dim(`Crash: ${fuzzer.crashFile}`));
422
+ } else if (modeResult.crashFiles.length) {
423
+ console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
424
+ }
425
+ console.log("");
426
+ }
386
427
  }
387
- return rendered;
428
+ }
429
+ return rendered;
388
430
  }
389
431
  function groupFuzzResultsByFile(results) {
390
- const grouped = new Map();
391
- for (const result of results) {
392
- const current = grouped.get(result.file) ?? [];
393
- current.push(result);
394
- grouped.set(result.file, current);
395
- }
396
- return [...grouped.entries()]
397
- .sort((a, b) => a[0].localeCompare(b[0]))
398
- .map(([file, modes]) => ({ file, modes }));
432
+ const grouped = new Map();
433
+ for (const result of results) {
434
+ const current = grouped.get(result.file) ?? [];
435
+ current.push(result);
436
+ grouped.set(result.file, current);
437
+ }
438
+ return [...grouped.entries()]
439
+ .sort((a, b) => a[0].localeCompare(b[0]))
440
+ .map(([file, modes]) => ({ file, modes }));
399
441
  }
400
442
  function firstFuzzCrashFile(results) {
401
- for (const result of results) {
402
- if (result.crashFiles.length)
403
- return result.crashFiles[0];
404
- }
405
- return null;
443
+ for (const result of results) {
444
+ if (result.crashFiles.length) return result.crashFiles[0];
445
+ }
446
+ return null;
406
447
  }
407
448
  function averageFuzzModeTime(results) {
408
- if (!results.length)
409
- return 0;
410
- return results.reduce((sum, result) => sum + result.time, 0) / results.length;
449
+ if (!results.length) return 0;
450
+ return results.reduce((sum, result) => sum + result.time, 0) / results.length;
411
451
  }
412
452
  function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
413
- const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
414
- const fuzzerArg = fuzzer?.length ? ` --fuzzer ${fuzzer}` : "";
415
- const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
416
- return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
453
+ const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
454
+ const fuzzerArg = fuzzer?.length ? ` --fuzzer ${fuzzer}` : "";
455
+ const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
456
+ return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
417
457
  }
418
458
  function formatFailingSeeds(fuzzer) {
419
- return (fuzzer.failures ?? [])
420
- .map((failure) => String(failure.seed))
421
- .join(", ");
459
+ return (fuzzer.failures ?? [])
460
+ .map((failure) => String(failure.seed))
461
+ .join(", ");
422
462
  }
423
463
  function toRelativeResultPath(file) {
424
- const relative = path.relative(process.cwd(), path.resolve(process.cwd(), file));
425
- return relative.length ? relative : file;
464
+ const relative = path.relative(
465
+ process.cwd(),
466
+ path.resolve(process.cwd(), file),
467
+ );
468
+ return relative.length ? relative : file;
426
469
  }
427
470
  function formatFuzzFailureTitle(file, name) {
428
- const location = findFuzzLocation(file, name);
429
- const suffix = location
430
- ? ` (${formatSpecDisplayPath(file)}:${location})`
431
- : ` (${formatSpecDisplayPath(file)})`;
432
- return `${chalk.dim(name)}${chalk.dim(suffix)}`;
471
+ const location = findFuzzLocation(file, name);
472
+ const suffix = location
473
+ ? ` (${formatSpecDisplayPath(file)}:${location})`
474
+ : ` (${formatSpecDisplayPath(file)})`;
475
+ return `${chalk.dim(name)}${chalk.dim(suffix)}`;
433
476
  }
434
477
  function findFuzzLocation(file, name) {
435
- try {
436
- const source = readFileSync(path.resolve(process.cwd(), file), "utf8");
437
- const patterns = [`fuzz("${name}"`, `fuzz('${name}'`];
438
- patterns.push(`xfuzz("${name}"`, `xfuzz('${name}'`);
439
- let index = -1;
440
- for (const pattern of patterns) {
441
- index = source.indexOf(pattern);
442
- if (index != -1)
443
- break;
444
- }
445
- if (index == -1)
446
- return null;
447
- let line = 1;
448
- let column = 1;
449
- for (let i = 0; i < index; i++) {
450
- if (source.charCodeAt(i) == 10) {
451
- line++;
452
- column = 1;
453
- }
454
- else {
455
- column++;
456
- }
457
- }
458
- return `${line}:${column}`;
478
+ try {
479
+ const source = readFileSync(path.resolve(process.cwd(), file), "utf8");
480
+ const patterns = [`fuzz("${name}"`, `fuzz('${name}'`];
481
+ patterns.push(`xfuzz("${name}"`, `xfuzz('${name}'`);
482
+ let index = -1;
483
+ for (const pattern of patterns) {
484
+ index = source.indexOf(pattern);
485
+ if (index != -1) break;
459
486
  }
460
- catch {
461
- return null;
487
+ if (index == -1) return null;
488
+ let line = 1;
489
+ let column = 1;
490
+ for (let i = 0; i < index; i++) {
491
+ if (source.charCodeAt(i) == 10) {
492
+ line++;
493
+ column = 1;
494
+ } else {
495
+ column++;
496
+ }
462
497
  }
498
+ return `${line}:${column}`;
499
+ } catch {
500
+ return null;
501
+ }
463
502
  }
464
503
  function renderFailedSuites(failedEntries) {
465
- if (!failedEntries.length)
466
- return;
467
- console.log("");
468
- const grouped = new Map();
469
- for (const failed of failedEntries) {
470
- const failedAny = failed;
471
- if (!failedAny?.file)
472
- continue;
473
- const file = String(failedAny.file);
474
- collectSuiteFailures(failed, file, [], grouped);
475
- }
476
- for (const failure of grouped.values()) {
477
- renderCollectedFailure(failure);
478
- }
479
- }
480
- function collectSuiteFailures(suite, file, path, grouped, inheritedModeName = "") {
481
- const suiteAny = suite;
482
- const nextPath = [...path, String(suiteAny.description ?? "unknown")];
483
- const modeName = String(suiteAny.modeName ?? inheritedModeName);
484
- const isRuntimeErrorSuite = String(suiteAny.kind ?? "") == "runtime-error";
485
- const isBuildErrorSuite = String(suiteAny.kind ?? "") == "build-error";
486
- const tests = Array.isArray(suiteAny.tests)
487
- ? suiteAny.tests
488
- : [];
489
- for (let i = 0; i < tests.length; i++) {
490
- const test = tests[i];
491
- if (test.verdict != "fail")
492
- continue;
493
- const assertionIndex = i + 1;
494
- const title = `${nextPath.join(" > ")}#${assertionIndex}`;
495
- const loc = String(test.location ?? "");
496
- const where = loc.length ? `${file}:${loc}` : file;
497
- const suitePath = String(suiteAny.path ?? "");
498
- const message = String(test.message ?? "");
499
- const left = test.left;
500
- const right = test.right;
501
- const dedupeKey = `${file}::${title}::${String(left)}::${String(right)}::${message}`;
502
- let failure = grouped.get(dedupeKey);
503
- if (!failure) {
504
- failure = {
505
- title,
506
- where,
507
- file,
508
- suitePath,
509
- left,
510
- right,
511
- message,
512
- isRuntimeError: isRuntimeErrorSuite || String(test.type ?? "") == "runtime-error",
513
- isBuildError: isBuildErrorSuite || String(test.type ?? "") == "build-error",
514
- modes: new Set(),
515
- runCommands: new Map(),
516
- buildCommands: new Map(),
517
- };
518
- grouped.set(dedupeKey, failure);
519
- }
520
- if (modeName.length) {
521
- failure.modes.add(modeName);
522
- }
523
- const runCommand = String(suiteAny.runCommand ?? "");
524
- if (modeName.length && runCommand.length) {
525
- failure.runCommands.set(modeName, runCommand);
526
- }
527
- const buildCommand = String(suiteAny.buildCommand ?? "");
528
- if (modeName.length && buildCommand.length) {
529
- failure.buildCommands.set(modeName, buildCommand);
530
- }
504
+ if (!failedEntries.length) return;
505
+ console.log("");
506
+ const grouped = new Map();
507
+ for (const failed of failedEntries) {
508
+ const failedAny = failed;
509
+ if (!failedAny?.file) continue;
510
+ const file = String(failedAny.file);
511
+ collectSuiteFailures(failed, file, [], grouped);
512
+ }
513
+ for (const failure of grouped.values()) {
514
+ renderCollectedFailure(failure);
515
+ }
516
+ }
517
+ function collectSuiteFailures(
518
+ suite,
519
+ file,
520
+ path,
521
+ grouped,
522
+ inheritedModeName = "",
523
+ ) {
524
+ const suiteAny = suite;
525
+ const nextPath = [...path, String(suiteAny.description ?? "unknown")];
526
+ const modeName = String(suiteAny.modeName ?? inheritedModeName);
527
+ const isRuntimeErrorSuite = String(suiteAny.kind ?? "") == "runtime-error";
528
+ const isBuildErrorSuite = String(suiteAny.kind ?? "") == "build-error";
529
+ const tests = Array.isArray(suiteAny.tests) ? suiteAny.tests : [];
530
+ for (let i = 0; i < tests.length; i++) {
531
+ const test = tests[i];
532
+ if (test.verdict != "fail") continue;
533
+ const assertionIndex = i + 1;
534
+ const title = `${nextPath.join(" > ")}#${assertionIndex}`;
535
+ const loc = String(test.location ?? "");
536
+ const where = loc.length ? `${file}:${loc}` : file;
537
+ const suitePath = String(suiteAny.path ?? "");
538
+ const message = String(test.message ?? "");
539
+ const left = test.left;
540
+ const right = test.right;
541
+ const dedupeKey = `${file}::${title}::${String(left)}::${String(right)}::${message}`;
542
+ let failure = grouped.get(dedupeKey);
543
+ if (!failure) {
544
+ failure = {
545
+ title,
546
+ where,
547
+ file,
548
+ suitePath,
549
+ left,
550
+ right,
551
+ message,
552
+ isRuntimeError:
553
+ isRuntimeErrorSuite || String(test.type ?? "") == "runtime-error",
554
+ isBuildError:
555
+ isBuildErrorSuite || String(test.type ?? "") == "build-error",
556
+ modes: new Set(),
557
+ runCommands: new Map(),
558
+ buildCommands: new Map(),
559
+ };
560
+ grouped.set(dedupeKey, failure);
531
561
  }
532
- const suites = Array.isArray(suiteAny.suites)
533
- ? suiteAny.suites
534
- : [];
535
- for (const sub of suites) {
536
- collectSuiteFailures(sub, file, nextPath, grouped, modeName);
562
+ if (modeName.length) {
563
+ failure.modes.add(modeName);
537
564
  }
538
- }
539
- function renderCollectedFailure(failure) {
540
- console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(failure.title)} ${chalk.dim("(" + failure.where + ")")}`);
541
- const modes = [...failure.modes].filter(Boolean).sort();
542
- if (failure.isBuildError) {
543
- renderBuildFailureDetails(failure, modes);
565
+ const runCommand = String(suiteAny.runCommand ?? "");
566
+ if (modeName.length && runCommand.length) {
567
+ failure.runCommands.set(modeName, runCommand);
544
568
  }
545
- else if (failure.isRuntimeError) {
546
- renderRuntimeFailureDetails(failure, modes);
569
+ const buildCommand = String(suiteAny.buildCommand ?? "");
570
+ if (modeName.length && buildCommand.length) {
571
+ failure.buildCommands.set(modeName, buildCommand);
547
572
  }
548
- else {
549
- if (modes.length == 1) {
550
- console.log(chalk.dim(`Mode: ${modes[0]}`));
551
- }
552
- else if (modes.length > 1) {
553
- console.log(chalk.dim(`Modes: ${modes.join(", ")}`));
554
- }
555
- const relativeFile = toRelativeResultPath(failure.file);
556
- const repro = failure.suitePath.length && modes.length == 1
557
- ? buildSuiteReproCommand(relativeFile, failure.suitePath, modes[0])
558
- : buildFileReproCommand(relativeFile, modes);
559
- console.log(chalk.dim(`Repro: ${repro}`));
560
- renderModeCommands("Build", failure.buildCommands, modes);
561
- renderModeCommands("Run", failure.runCommands, modes);
573
+ }
574
+ const suites = Array.isArray(suiteAny.suites) ? suiteAny.suites : [];
575
+ for (const sub of suites) {
576
+ collectSuiteFailures(sub, file, nextPath, grouped, modeName);
577
+ }
578
+ }
579
+ function renderCollectedFailure(failure) {
580
+ console.log(
581
+ `${chalk.bgRed(" FAIL ")} ${chalk.dim(failure.title)} ${chalk.dim("(" + failure.where + ")")}`,
582
+ );
583
+ const modes = [...failure.modes].filter(Boolean).sort();
584
+ if (failure.isBuildError) {
585
+ renderBuildFailureDetails(failure, modes);
586
+ } else if (failure.isRuntimeError) {
587
+ renderRuntimeFailureDetails(failure, modes);
588
+ } else {
589
+ if (modes.length == 1) {
590
+ console.log(chalk.dim(`Mode: ${modes[0]}`));
591
+ } else if (modes.length > 1) {
592
+ console.log(chalk.dim(`Modes: ${modes.join(", ")}`));
562
593
  }
563
- renderAssertionFailureDetails(failure.left, failure.right, failure.message);
594
+ const relativeFile = toRelativeResultPath(failure.file);
595
+ const repro =
596
+ failure.suitePath.length && modes.length == 1
597
+ ? buildSuiteReproCommand(relativeFile, failure.suitePath, modes[0])
598
+ : buildFileReproCommand(relativeFile, modes);
599
+ console.log(chalk.dim(`Repro: ${repro}`));
600
+ renderModeCommands("Build", failure.buildCommands, modes);
601
+ renderModeCommands("Run", failure.runCommands, modes);
602
+ }
603
+ renderAssertionFailureDetails(failure.left, failure.right, failure.message);
564
604
  }
565
605
  function renderBuildFailureDetails(failure, modes) {
566
- console.log("");
567
- console.log(chalk.bold(" Oops! Looks like the test failed to build!"));
568
- console.log(chalk.dim(" Here's some details and reproduction instructions if that helps:"));
569
- console.log("");
570
- console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
571
- console.log("");
572
- console.log(chalk.dim(" To reproduce, run the following commands:"));
573
- for (const mode of modes.length ? modes : ["default"]) {
574
- console.log(chalk.dim(` Mode: ${mode}`));
575
- const buildCommand = failure.buildCommands.get(mode);
576
- if (buildCommand?.length) {
577
- console.log(chalk.dim(` Build: ${buildCommand}`));
578
- }
606
+ console.log("");
607
+ console.log(chalk.bold(" Oops! Looks like the test failed to build!"));
608
+ console.log(
609
+ chalk.dim(
610
+ " Here's some details and reproduction instructions if that helps:",
611
+ ),
612
+ );
613
+ console.log("");
614
+ console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
615
+ console.log("");
616
+ console.log(chalk.dim(" To reproduce, run the following commands:"));
617
+ for (const mode of modes.length ? modes : ["default"]) {
618
+ console.log(chalk.dim(` Mode: ${mode}`));
619
+ const buildCommand = failure.buildCommands.get(mode);
620
+ if (buildCommand?.length) {
621
+ console.log(chalk.dim(` Build: ${buildCommand}`));
579
622
  }
580
- console.log("");
581
- console.log(chalk.dim(" Here's a log dump too:"));
623
+ }
624
+ console.log("");
625
+ console.log(chalk.dim(" Here's a log dump too:"));
582
626
  }
583
627
  function renderRuntimeFailureDetails(failure, modes) {
584
- console.log("");
585
- console.log(chalk.bold(" Oops! Looks like the runtime crashed!"));
586
- console.log(chalk.dim(" Here's some details and reproduction instructions if that helps:"));
587
- console.log("");
588
- console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
589
- console.log("");
590
- console.log(chalk.dim(" To reproduce, run the following commands:"));
591
- for (const mode of modes.length ? modes : ["default"]) {
592
- console.log(chalk.dim(` Mode: ${mode}`));
593
- const buildCommand = failure.buildCommands.get(mode);
594
- if (buildCommand?.length) {
595
- console.log(chalk.dim(` Build: ${buildCommand}`));
596
- }
597
- const runCommand = buildRuntimeReproRunCommand(failure.runCommands.get(mode) ?? "", buildCommand ?? "");
598
- if (runCommand.length) {
599
- console.log(chalk.dim(` Run: ${runCommand}`));
600
- }
628
+ console.log("");
629
+ console.log(chalk.bold(" Oops! Looks like the runtime crashed!"));
630
+ console.log(
631
+ chalk.dim(
632
+ " Here's some details and reproduction instructions if that helps:",
633
+ ),
634
+ );
635
+ console.log("");
636
+ console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
637
+ console.log("");
638
+ console.log(chalk.dim(" To reproduce, run the following commands:"));
639
+ for (const mode of modes.length ? modes : ["default"]) {
640
+ console.log(chalk.dim(` Mode: ${mode}`));
641
+ const buildCommand = failure.buildCommands.get(mode);
642
+ if (buildCommand?.length) {
643
+ console.log(chalk.dim(` Build: ${buildCommand}`));
601
644
  }
602
- console.log("");
603
- console.log(chalk.dim(" Here's a log dump too:"));
645
+ const runCommand = buildRuntimeReproRunCommand(
646
+ failure.runCommands.get(mode) ?? "",
647
+ buildCommand ?? "",
648
+ );
649
+ if (runCommand.length) {
650
+ console.log(chalk.dim(` Run: ${runCommand}`));
651
+ }
652
+ }
653
+ console.log("");
654
+ console.log(chalk.dim(" Here's a log dump too:"));
604
655
  }
605
656
  function buildSuiteReproCommand(file, suitePath, modeName) {
606
- const modeArg = modeName && modeName != "default" ? ` --mode ${modeName}` : "";
607
- return `ast run ${file}${modeArg} --suite ${suitePath}`;
657
+ const modeArg =
658
+ modeName && modeName != "default" ? ` --mode ${modeName}` : "";
659
+ return `ast run ${file}${modeArg} --suite ${suitePath}`;
608
660
  }
609
661
  function buildFileReproCommand(file, modes) {
610
- const normalizedModes = modes.filter(Boolean).sort();
611
- if (normalizedModes.length == 1 && normalizedModes[0] != "default") {
612
- return `ast run ${file} --mode ${normalizedModes[0]}`;
613
- }
614
- if (normalizedModes.length > 1 &&
615
- normalizedModes.every((mode) => mode != "default")) {
616
- return `ast run ${file} --mode ${normalizedModes.join(",")}`;
617
- }
618
- return `ast run ${file}`;
662
+ const normalizedModes = modes.filter(Boolean).sort();
663
+ if (normalizedModes.length == 1 && normalizedModes[0] != "default") {
664
+ return `ast run ${file} --mode ${normalizedModes[0]}`;
665
+ }
666
+ if (
667
+ normalizedModes.length > 1 &&
668
+ normalizedModes.every((mode) => mode != "default")
669
+ ) {
670
+ return `ast run ${file} --mode ${normalizedModes.join(",")}`;
671
+ }
672
+ return `ast run ${file}`;
619
673
  }
620
674
  function renderModeCommands(label, commands, modes) {
621
- if (!commands.size)
622
- return;
623
- const uniqueCommands = new Set([...commands.values()].filter(Boolean));
624
- if (uniqueCommands.size == 1) {
625
- console.log(chalk.dim(`${label}: ${[...uniqueCommands][0]}`));
626
- return;
627
- }
628
- console.log(chalk.dim(`${label} commands:`));
629
- for (const mode of modes) {
630
- const command = commands.get(mode);
631
- if (!command)
632
- continue;
633
- console.log(chalk.dim(` [${mode}] ${command}`));
634
- }
675
+ if (!commands.size) return;
676
+ const uniqueCommands = new Set([...commands.values()].filter(Boolean));
677
+ if (uniqueCommands.size == 1) {
678
+ console.log(chalk.dim(`${label}: ${[...uniqueCommands][0]}`));
679
+ return;
680
+ }
681
+ console.log(chalk.dim(`${label} commands:`));
682
+ for (const mode of modes) {
683
+ const command = commands.get(mode);
684
+ if (!command) continue;
685
+ console.log(chalk.dim(` [${mode}] ${command}`));
686
+ }
635
687
  }
636
688
  function buildRuntimeReproRunCommand(runCommand, buildCommand) {
637
- if (!runCommand.length)
638
- return "";
639
- const artifactPath = extractBuildArtifactPath(buildCommand);
640
- if (!artifactPath) {
641
- return runCommand;
642
- }
643
- if (runCommand.includes(".as-test/runners/default.")) {
644
- return `${runCommand} ${artifactPath}`;
645
- }
689
+ if (!runCommand.length) return "";
690
+ const artifactPath = extractBuildArtifactPath(buildCommand);
691
+ if (!artifactPath) {
646
692
  return runCommand;
693
+ }
694
+ if (runCommand.includes(".as-test/runners/default.")) {
695
+ return `${runCommand} ${artifactPath}`;
696
+ }
697
+ return runCommand;
647
698
  }
648
699
  function extractBuildArtifactPath(buildCommand) {
649
- const outMatch = buildCommand.match(/(?:^|\s)(?:-o|--outFile)\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
650
- return outMatch?.[1] ?? outMatch?.[2] ?? outMatch?.[3] ?? null;
700
+ const outMatch = buildCommand.match(
701
+ /(?:^|\s)(?:-o|--outFile)\s+(?:"([^"]+)"|'([^']+)'|(\S+))/,
702
+ );
703
+ return outMatch?.[1] ?? outMatch?.[2] ?? outMatch?.[3] ?? null;
651
704
  }
652
705
  function normalizeFailureMessage(message) {
653
- return message.replace(/\r\n/g, "\n").trim();
706
+ return message.replace(/\r\n/g, "\n").trim();
654
707
  }
655
708
  function renderAssertionFailureDetails(leftRaw, rightRaw, messageRaw) {
656
- const left = JSON.stringify(leftRaw);
657
- const right = JSON.stringify(rightRaw);
658
- const message = String(messageRaw ?? "");
659
- if (left == "null" && right == "null") {
660
- const normalizedMessage = normalizeFailureMessage(message);
661
- if (normalizedMessage.length) {
662
- console.log("");
663
- for (const line of normalizedMessage.split("\n")) {
664
- console.log(chalk.dim(line));
665
- }
666
- }
667
- else {
668
- console.log("");
669
- console.log(chalk.dim("runtime error"));
670
- }
671
- return;
709
+ const left = JSON.stringify(leftRaw);
710
+ const right = JSON.stringify(rightRaw);
711
+ const message = String(messageRaw ?? "");
712
+ if (left == "null" && right == "null") {
713
+ const normalizedMessage = normalizeFailureMessage(message);
714
+ if (normalizedMessage.length) {
715
+ console.log("");
716
+ for (const line of normalizedMessage.split("\n")) {
717
+ console.log(chalk.dim(line));
718
+ }
719
+ } else {
720
+ console.log("");
721
+ console.log(chalk.dim("runtime error"));
672
722
  }
673
- const diffResult = diff(left, right);
674
- let expected = "";
675
- for (const res of diffResult.diff) {
676
- switch (res.type) {
677
- case "correct":
678
- expected += chalk.dim(res.value);
679
- break;
680
- case "extra":
681
- expected += chalk.red.strikethrough(res.value);
682
- break;
683
- case "missing":
684
- expected += chalk.bgBlack(res.value);
685
- break;
686
- case "wrong":
687
- expected += chalk.bgRed(res.value);
688
- break;
689
- case "untouched":
690
- case "spacer":
691
- break;
692
- }
723
+ return;
724
+ }
725
+ const diffResult = diff(left, right);
726
+ let expected = "";
727
+ for (const res of diffResult.diff) {
728
+ switch (res.type) {
729
+ case "correct":
730
+ expected += chalk.dim(res.value);
731
+ break;
732
+ case "extra":
733
+ expected += chalk.red.strikethrough(res.value);
734
+ break;
735
+ case "missing":
736
+ expected += chalk.bgBlack(res.value);
737
+ break;
738
+ case "wrong":
739
+ expected += chalk.bgRed(res.value);
740
+ break;
741
+ case "untouched":
742
+ case "spacer":
743
+ break;
693
744
  }
694
- console.log(`${chalk.dim("(expected) ->")} ${expected}`);
695
- console.log(`${chalk.dim("(received) ->")} ${chalk.dim(left)}\n`);
745
+ }
746
+ console.log(`${chalk.dim("(expected) ->")} ${expected}`);
747
+ console.log(`${chalk.dim("(received) ->")} ${chalk.dim(left)}\n`);
696
748
  }
697
749
  function renderSnapshotSummary(snapshotSummary, leadingGap = true) {
698
- if (leadingGap) {
699
- console.log("");
700
- }
701
- console.log(`${chalk.bold("Snapshots:")} ${chalk.greenBright(snapshotSummary.matched)} matched, ${chalk.blueBright(snapshotSummary.created)} created, ${chalk.blueBright(snapshotSummary.updated)} updated, ${snapshotSummary.failed ? chalk.red(snapshotSummary.failed) : chalk.greenBright("0")} failed`);
750
+ if (leadingGap) {
751
+ console.log("");
752
+ }
753
+ console.log(
754
+ `${chalk.bold("Snapshots:")} ${chalk.greenBright(snapshotSummary.matched)} matched, ${chalk.blueBright(snapshotSummary.created)} created, ${chalk.blueBright(snapshotSummary.updated)} updated, ${snapshotSummary.failed ? chalk.red(snapshotSummary.failed) : chalk.greenBright("0")} failed`,
755
+ );
702
756
  }
703
757
  function renderTotals(stats, event) {
704
- console.log("");
705
- const filesSummary = {
706
- failed: stats.failedFiles,
707
- skipped: stats.skippedFiles,
708
- total: stats.failedFiles + stats.passedFiles + stats.skippedFiles,
709
- };
710
- const suitesSummary = {
711
- failed: stats.failedSuites,
712
- skipped: stats.skippedSuites,
713
- total: stats.failedSuites + stats.passedSuites + stats.skippedSuites,
714
- };
715
- const testsSummary = {
716
- failed: stats.failedTests,
717
- skipped: stats.skippedTests,
718
- total: stats.failedTests + stats.passedTests + stats.skippedTests,
719
- };
720
- const layout = createSummaryLayout([
721
- event.fuzzSummary,
722
- filesSummary,
723
- suitesSummary,
724
- testsSummary,
725
- event.modeSummary,
726
- ]);
727
- if (event.fuzzSummary) {
728
- renderFuzzTotals(event.fuzzSummary, layout);
729
- }
730
- renderSummaryLine("Files:", filesSummary, layout);
731
- renderSummaryLine("Suites:", suitesSummary, layout);
732
- renderSummaryLine("Tests:", testsSummary, layout);
733
- if (event.modeSummary) {
734
- renderModeSummary(event.modeSummary, layout);
735
- }
736
- process.stdout.write(chalk.bold("Time:".padEnd(9)) +
737
- formatTime(stats.time) +
738
- chalk.dim(` (${formatTime(event.buildTime)} build)`) +
739
- "\n");
758
+ console.log("");
759
+ const filesSummary = {
760
+ failed: stats.failedFiles,
761
+ skipped: stats.skippedFiles,
762
+ total: stats.failedFiles + stats.passedFiles + stats.skippedFiles,
763
+ };
764
+ const suitesSummary = {
765
+ failed: stats.failedSuites,
766
+ skipped: stats.skippedSuites,
767
+ total: stats.failedSuites + stats.passedSuites + stats.skippedSuites,
768
+ };
769
+ const testsSummary = {
770
+ failed: stats.failedTests,
771
+ skipped: stats.skippedTests,
772
+ total: stats.failedTests + stats.passedTests + stats.skippedTests,
773
+ };
774
+ const layout = createSummaryLayout([
775
+ event.fuzzSummary,
776
+ filesSummary,
777
+ suitesSummary,
778
+ testsSummary,
779
+ event.modeSummary,
780
+ ]);
781
+ if (event.fuzzSummary) {
782
+ renderFuzzTotals(event.fuzzSummary, layout);
783
+ }
784
+ renderSummaryLine("Files:", filesSummary, layout);
785
+ renderSummaryLine("Suites:", suitesSummary, layout);
786
+ renderSummaryLine("Tests:", testsSummary, layout);
787
+ if (event.modeSummary) {
788
+ renderModeSummary(event.modeSummary, layout);
789
+ }
790
+ process.stdout.write(
791
+ chalk.bold("Time:".padEnd(9)) +
792
+ formatTime(stats.time) +
793
+ chalk.dim(` (${formatTime(event.buildTime)} build)`) +
794
+ "\n",
795
+ );
740
796
  }
741
797
  function renderModeSummary(summary, layout) {
742
- renderSummaryLine("Modes:", summary, layout);
798
+ renderSummaryLine("Modes:", summary, layout);
743
799
  }
744
800
  function renderFuzzTotals(summary, layout) {
745
- renderSummaryLine("Fuzz:", summary, layout);
801
+ renderSummaryLine("Fuzz:", summary, layout);
746
802
  }
747
803
  function renderStandaloneFuzzTotals(event) {
748
- console.log("");
749
- const layout = createSummaryLayout([
750
- event.fuzzingSummary,
751
- event.suiteSummary,
752
- event.modeSummary,
753
- ]);
754
- renderSummaryLine("Fuzz:", event.fuzzingSummary, layout);
755
- renderSummaryLine("Suites:", event.suiteSummary, layout);
756
- renderSummaryLine("Modes:", event.modeSummary, layout);
757
- process.stdout.write(chalk.bold("Time:".padEnd(9)) +
758
- formatTime(event.time) +
759
- chalk.dim(` (${formatTime(event.buildTime)} build)`) +
760
- "\n");
804
+ console.log("");
805
+ const layout = createSummaryLayout([
806
+ event.fuzzingSummary,
807
+ event.suiteSummary,
808
+ event.modeSummary,
809
+ ]);
810
+ renderSummaryLine("Fuzz:", event.fuzzingSummary, layout);
811
+ renderSummaryLine("Suites:", event.suiteSummary, layout);
812
+ renderSummaryLine("Modes:", event.modeSummary, layout);
813
+ process.stdout.write(
814
+ chalk.bold("Time:".padEnd(9)) +
815
+ formatTime(event.time) +
816
+ chalk.dim(` (${formatTime(event.buildTime)} build)`) +
817
+ "\n",
818
+ );
761
819
  }
762
820
  function createSummaryLayout(summaries) {
763
- return {
764
- failedWidth: Math.max(...summaries.map((summary) => summary ? `${summary.failed} failed`.length : 0)),
765
- skippedWidth: Math.max(...summaries.map((summary) => summary ? `${summary.skipped} skipped`.length : 0)),
766
- totalWidth: Math.max(...summaries.map((summary) => summary ? `${summary.total} total`.length : 0)),
767
- };
821
+ return {
822
+ failedWidth: Math.max(
823
+ ...summaries.map((summary) =>
824
+ summary ? `${summary.failed} failed`.length : 0,
825
+ ),
826
+ ),
827
+ skippedWidth: Math.max(
828
+ ...summaries.map((summary) =>
829
+ summary ? `${summary.skipped} skipped`.length : 0,
830
+ ),
831
+ ),
832
+ totalWidth: Math.max(
833
+ ...summaries.map((summary) =>
834
+ summary ? `${summary.total} total`.length : 0,
835
+ ),
836
+ ),
837
+ };
768
838
  }
769
- function renderSummaryLine(label, summary, layout = {
839
+ function renderSummaryLine(
840
+ label,
841
+ summary,
842
+ layout = {
770
843
  failedWidth: `${summary.failed} failed`.length,
771
844
  skippedWidth: `${summary.skipped} skipped`.length,
772
845
  totalWidth: `${summary.total} total`.length,
773
- }) {
774
- const failedText = `${summary.failed} failed`;
775
- const skippedText = `${summary.skipped} skipped`;
776
- const totalText = `${summary.total} total`;
777
- process.stdout.write(chalk.bold(label.padEnd(9)));
778
- process.stdout.write(summary.failed
779
- ? chalk.bold.red(failedText.padStart(layout.failedWidth))
780
- : chalk.bold.greenBright(failedText.padStart(layout.failedWidth)));
781
- process.stdout.write(", ");
782
- process.stdout.write(chalk.gray(skippedText.padStart(layout.skippedWidth)));
783
- process.stdout.write(", ");
784
- process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
846
+ },
847
+ ) {
848
+ const failedText = `${summary.failed} failed`;
849
+ const skippedText = `${summary.skipped} skipped`;
850
+ const totalText = `${summary.total} total`;
851
+ process.stdout.write(chalk.bold(label.padEnd(9)));
852
+ process.stdout.write(
853
+ summary.failed
854
+ ? chalk.bold.red(failedText.padStart(layout.failedWidth))
855
+ : chalk.bold.greenBright(failedText.padStart(layout.failedWidth)),
856
+ );
857
+ process.stdout.write(", ");
858
+ process.stdout.write(chalk.gray(skippedText.padStart(layout.skippedWidth)));
859
+ process.stdout.write(", ");
860
+ process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
785
861
  }
786
862
  function renderCoverageSummary(summary, showCoverage) {
787
- console.log("");
788
- const shouldShowCoverageHint = !showCoverage && summary.total > 0 && summary.uncovered > 0;
789
- const coverageHeading = shouldShowCoverageHint
790
- ? "Coverage (run with --show-coverage to display uncovered points)"
791
- : "Coverage";
792
- console.log(chalk.bold(coverageHeading));
793
- if (!summary.files.length || summary.total <= 0) {
794
- console.log(` ${chalk.dim("No eligible source files were tracked for coverage.")}`);
795
- return;
796
- }
797
- const pct = summary.total
798
- ? ((summary.covered * 100) / summary.total).toFixed(2)
799
- : "100.00";
800
- const missingLabel = summary.uncovered == 1
801
- ? "1 point missing"
802
- : `${summary.uncovered} points missing`;
803
- const fileLabel = summary.files.length == 1 ? "1 file" : `${summary.files.length} files`;
804
- const color = Number(pct) >= 90
863
+ console.log("");
864
+ const shouldShowCoverageHint =
865
+ !showCoverage && summary.total > 0 && summary.uncovered > 0;
866
+ const coverageHeading = shouldShowCoverageHint
867
+ ? "Coverage (run with --show-coverage to display uncovered points)"
868
+ : "Coverage";
869
+ console.log(chalk.bold(coverageHeading));
870
+ if (!summary.files.length || summary.total <= 0) {
871
+ console.log(
872
+ ` ${chalk.dim("No eligible source files were tracked for coverage.")}`,
873
+ );
874
+ return;
875
+ }
876
+ const pct = summary.total
877
+ ? ((summary.covered * 100) / summary.total).toFixed(2)
878
+ : "100.00";
879
+ const missingLabel =
880
+ summary.uncovered == 1
881
+ ? "1 point missing"
882
+ : `${summary.uncovered} points missing`;
883
+ const fileLabel =
884
+ summary.files.length == 1 ? "1 file" : `${summary.files.length} files`;
885
+ const color =
886
+ Number(pct) >= 90
887
+ ? chalk.greenBright
888
+ : Number(pct) >= 75
889
+ ? chalk.yellowBright
890
+ : chalk.redBright;
891
+ console.log(
892
+ ` ${color(pct + "%")} ${renderCoverageBar(summary.percent)} ${chalk.dim(`(${summary.covered}/${summary.total} covered, ${missingLabel}, ${fileLabel})`)}`,
893
+ );
894
+ const ranked = [...summary.files].sort((a, b) => {
895
+ if (a.percent != b.percent) return a.percent - b.percent;
896
+ if (a.uncovered != b.uncovered) return b.uncovered - a.uncovered;
897
+ return a.file.localeCompare(b.file);
898
+ });
899
+ console.log(chalk.bold(" File Breakdown"));
900
+ for (const file of ranked.slice(0, 8)) {
901
+ const filePct = file.total
902
+ ? ((file.covered * 100) / file.total).toFixed(2)
903
+ : "100.00";
904
+ const fileColor =
905
+ Number(filePct) >= 90
805
906
  ? chalk.greenBright
806
- : Number(pct) >= 75
807
- ? chalk.yellowBright
808
- : chalk.redBright;
809
- console.log(` ${color(pct + "%")} ${renderCoverageBar(summary.percent)} ${chalk.dim(`(${summary.covered}/${summary.total} covered, ${missingLabel}, ${fileLabel})`)}`);
810
- const ranked = [...summary.files].sort((a, b) => {
811
- if (a.percent != b.percent)
812
- return a.percent - b.percent;
813
- if (a.uncovered != b.uncovered)
814
- return b.uncovered - a.uncovered;
815
- return a.file.localeCompare(b.file);
816
- });
817
- console.log(chalk.bold(" File Breakdown"));
818
- for (const file of ranked.slice(0, 8)) {
819
- const filePct = file.total
820
- ? ((file.covered * 100) / file.total).toFixed(2)
821
- : "100.00";
822
- const fileColor = Number(filePct) >= 90
823
- ? chalk.greenBright
824
- : Number(filePct) >= 75
825
- ? chalk.yellowBright
826
- : chalk.redBright;
827
- const suffix = file.uncovered > 0 ? `${file.uncovered} missing` : "fully covered";
828
- console.log(` ${fileColor(filePct.padStart(6) + "%")} ${toRelativeResultPath(file.file).padEnd(36)} ${chalk.dim(`${file.covered}/${file.total} covered, ${suffix}`)}`);
829
- }
830
- if (ranked.length > 8) {
831
- console.log(chalk.dim(` ... ${ranked.length - 8} more files`));
832
- }
907
+ : Number(filePct) >= 75
908
+ ? chalk.yellowBright
909
+ : chalk.redBright;
910
+ const suffix =
911
+ file.uncovered > 0 ? `${file.uncovered} missing` : "fully covered";
912
+ console.log(
913
+ ` ${fileColor(filePct.padStart(6) + "%")} ${toRelativeResultPath(file.file).padEnd(36)} ${chalk.dim(`${file.covered}/${file.total} covered, ${suffix}`)}`,
914
+ );
915
+ }
916
+ if (ranked.length > 8) {
917
+ console.log(chalk.dim(` ... ${ranked.length - 8} more files`));
918
+ }
833
919
  }
834
920
  function renderCoveragePoints(files, expandNested) {
835
- console.log("");
836
- console.log(chalk.bold("Coverage Gaps"));
837
- const sortedFiles = [...files].sort((a, b) => a.file.localeCompare(b.file));
838
- const missingPoints = sortedFiles.flatMap((file) => file.points
839
- .filter((point) => !point.executed)
840
- .map((point) => ({
921
+ console.log("");
922
+ console.log(chalk.bold("Coverage Gaps"));
923
+ const sortedFiles = [...files].sort((a, b) => a.file.localeCompare(b.file));
924
+ const missingPoints = sortedFiles.flatMap((file) =>
925
+ file.points
926
+ .filter((point) => !point.executed)
927
+ .map((point) => ({
841
928
  ...point,
842
- displayType: describeCoveragePoint(point.file, point.line, point.column, point.type).displayType,
843
- })));
844
- const layout = createCoverageGapLayout(missingPoints);
845
- let renderedFileCount = 0;
846
- let collapsedNestedPoints = 0;
847
- for (const file of sortedFiles) {
848
- const points = [...file.points].sort(compareCoverageGapPoints);
849
- const missing = points.filter((point) => !point.executed);
850
- if (!missing.length)
851
- continue;
852
- if (renderedFileCount > 0) {
853
- console.log("");
854
- }
855
- console.log(` ${chalk.bold(toRelativeResultPath(file.file))} ${chalk.dim(`(${missing.length} uncovered)`)}`);
856
- const pointsByHash = new Map(points.map((point) => [point.hash, point]));
857
- const childrenByParent = new Map();
858
- const roots = [];
859
- for (const point of points) {
860
- const parentHash = point.parentHash ?? "";
861
- if (parentHash.length && pointsByHash.has(parentHash)) {
862
- const children = childrenByParent.get(parentHash) ?? [];
863
- children.push(point);
864
- childrenByParent.set(parentHash, children);
865
- }
866
- else {
867
- roots.push(point);
868
- }
869
- }
870
- const visibleRoots = roots.filter((point) => shouldRenderCoveragePoint(point, childrenByParent));
871
- for (let i = 0; i < visibleRoots.length; i++) {
872
- collapsedNestedPoints += renderCoveragePointTree(visibleRoots[i], childrenByParent, layout, [], i == visibleRoots.length - 1, expandNested);
873
- }
874
- renderedFileCount++;
929
+ displayType: describeCoveragePoint(
930
+ point.file,
931
+ point.line,
932
+ point.column,
933
+ point.type,
934
+ ).displayType,
935
+ })),
936
+ );
937
+ const layout = createCoverageGapLayout(missingPoints);
938
+ let renderedFileCount = 0;
939
+ let collapsedNestedPoints = 0;
940
+ for (const file of sortedFiles) {
941
+ const points = [...file.points].sort(compareCoverageGapPoints);
942
+ const missing = points.filter((point) => !point.executed);
943
+ if (!missing.length) continue;
944
+ if (renderedFileCount > 0) {
945
+ console.log("");
875
946
  }
876
- if (!expandNested && collapsedNestedPoints > 0) {
877
- console.log("");
878
- console.log(chalk.dim(" Run with --show-coverage=all or --verbose to expand nested coverage gaps."));
879
- }
880
- }
881
- function renderCoveragePointTree(point, childrenByParent, layout, ancestorHasNext, isLast, expandNested) {
882
- const visibleChildren = [...(childrenByParent.get(point.hash) ?? [])]
883
- .filter((child) => shouldRenderCoveragePoint(child, childrenByParent))
884
- .sort(compareCoverageGapPoints);
885
- const nestedUncoveredCount = countNestedUncoveredPoints(visibleChildren, childrenByParent);
886
- if (!point.executed) {
887
- renderCoverageGapLine(point, layout, ancestorHasNext, isLast);
888
- if (nestedUncoveredCount > 0) {
889
- if (expandNested) {
890
- let rendered = 0;
891
- for (let i = 0; i < visibleChildren.length; i++) {
892
- rendered += renderCoveragePointTree(visibleChildren[i], childrenByParent, layout, [...ancestorHasNext, !isLast], i == visibleChildren.length - 1, expandNested);
893
- }
894
- return 1 + rendered;
895
- }
896
- const treePrefix = buildCoverageTreePrefix([...ancestorHasNext, !isLast], true);
897
- console.log(` ${treePrefix}${chalk.dim(`(+${nestedUncoveredCount} nested uncovered point${nestedUncoveredCount == 1 ? "" : "s"})`)}`);
898
- return nestedUncoveredCount;
899
- }
900
- return 0;
947
+ console.log(
948
+ ` ${chalk.bold(toRelativeResultPath(file.file))} ${chalk.dim(`(${missing.length} uncovered)`)}`,
949
+ );
950
+ const pointsByHash = new Map(points.map((point) => [point.hash, point]));
951
+ const childrenByParent = new Map();
952
+ const roots = [];
953
+ for (const point of points) {
954
+ const parentHash = point.parentHash ?? "";
955
+ if (parentHash.length && pointsByHash.has(parentHash)) {
956
+ const children = childrenByParent.get(parentHash) ?? [];
957
+ children.push(point);
958
+ childrenByParent.set(parentHash, children);
959
+ } else {
960
+ roots.push(point);
961
+ }
901
962
  }
902
- if (nestedUncoveredCount <= 0)
903
- return 0;
904
- renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast);
905
- let rendered = 0;
906
- for (let i = 0; i < visibleChildren.length; i++) {
907
- rendered += renderCoveragePointTree(visibleChildren[i], childrenByParent, layout, [...ancestorHasNext, !isLast], i == visibleChildren.length - 1, expandNested);
963
+ const visibleRoots = roots.filter((point) =>
964
+ shouldRenderCoveragePoint(point, childrenByParent),
965
+ );
966
+ for (let i = 0; i < visibleRoots.length; i++) {
967
+ collapsedNestedPoints += renderCoveragePointTree(
968
+ visibleRoots[i],
969
+ childrenByParent,
970
+ layout,
971
+ [],
972
+ i == visibleRoots.length - 1,
973
+ expandNested,
974
+ );
908
975
  }
909
- return rendered;
976
+ renderedFileCount++;
977
+ }
978
+ if (!expandNested && collapsedNestedPoints > 0) {
979
+ console.log("");
980
+ console.log(
981
+ chalk.dim(
982
+ " Run with --show-coverage=all or --verbose to expand nested coverage gaps.",
983
+ ),
984
+ );
985
+ }
986
+ }
987
+ function renderCoveragePointTree(
988
+ point,
989
+ childrenByParent,
990
+ layout,
991
+ ancestorHasNext,
992
+ isLast,
993
+ expandNested,
994
+ ) {
995
+ const visibleChildren = [...(childrenByParent.get(point.hash) ?? [])]
996
+ .filter((child) => shouldRenderCoveragePoint(child, childrenByParent))
997
+ .sort(compareCoverageGapPoints);
998
+ const nestedUncoveredCount = countNestedUncoveredPoints(
999
+ visibleChildren,
1000
+ childrenByParent,
1001
+ );
1002
+ if (!point.executed) {
1003
+ renderCoverageGapLine(point, layout, ancestorHasNext, isLast);
1004
+ if (nestedUncoveredCount > 0) {
1005
+ if (expandNested) {
1006
+ let rendered = 0;
1007
+ for (let i = 0; i < visibleChildren.length; i++) {
1008
+ rendered += renderCoveragePointTree(
1009
+ visibleChildren[i],
1010
+ childrenByParent,
1011
+ layout,
1012
+ [...ancestorHasNext, !isLast],
1013
+ i == visibleChildren.length - 1,
1014
+ expandNested,
1015
+ );
1016
+ }
1017
+ return 1 + rendered;
1018
+ }
1019
+ const treePrefix = buildCoverageTreePrefix(
1020
+ [...ancestorHasNext, !isLast],
1021
+ true,
1022
+ );
1023
+ console.log(
1024
+ ` ${treePrefix}${chalk.dim(`(+${nestedUncoveredCount} nested uncovered point${nestedUncoveredCount == 1 ? "" : "s"})`)}`,
1025
+ );
1026
+ return nestedUncoveredCount;
1027
+ }
1028
+ return 0;
1029
+ }
1030
+ if (nestedUncoveredCount <= 0) return 0;
1031
+ renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast);
1032
+ let rendered = 0;
1033
+ for (let i = 0; i < visibleChildren.length; i++) {
1034
+ rendered += renderCoveragePointTree(
1035
+ visibleChildren[i],
1036
+ childrenByParent,
1037
+ layout,
1038
+ [...ancestorHasNext, !isLast],
1039
+ i == visibleChildren.length - 1,
1040
+ expandNested,
1041
+ );
1042
+ }
1043
+ return rendered;
910
1044
  }
911
1045
  function shouldRenderCoveragePoint(point, childrenByParent) {
912
- if (!point.executed)
913
- return true;
914
- return (countNestedUncoveredPoints(childrenByParent.get(point.hash) ?? [], childrenByParent) > 0);
1046
+ if (!point.executed) return true;
1047
+ return (
1048
+ countNestedUncoveredPoints(
1049
+ childrenByParent.get(point.hash) ?? [],
1050
+ childrenByParent,
1051
+ ) > 0
1052
+ );
915
1053
  }
916
1054
  function countNestedUncoveredPoints(points, childrenByParent) {
917
- let count = 0;
918
- for (const point of points) {
919
- if (!point.executed)
920
- count++;
921
- count += countNestedUncoveredPoints(childrenByParent.get(point.hash) ?? [], childrenByParent);
922
- }
923
- return count;
1055
+ let count = 0;
1056
+ for (const point of points) {
1057
+ if (!point.executed) count++;
1058
+ count += countNestedUncoveredPoints(
1059
+ childrenByParent.get(point.hash) ?? [],
1060
+ childrenByParent,
1061
+ );
1062
+ }
1063
+ return count;
924
1064
  }
925
1065
  function renderCoverageGapLine(point, layout, ancestorHasNext, isLast) {
926
- const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
927
- const snippet = formatCoverageSnippet(point.file, point.line, point.column, point.type, ancestorHasNext.length);
928
- const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 6);
929
- const locationLabel = location.padEnd(layout.locationWidth + 6);
930
- const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
931
- const meta = `${typeLabel}${locationLabel}`;
932
- console.log(` ${treePrefix}${chalk.dim(meta)} ${snippet}`);
1066
+ const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
1067
+ const snippet = formatCoverageSnippet(
1068
+ point.file,
1069
+ point.line,
1070
+ point.column,
1071
+ point.type,
1072
+ ancestorHasNext.length,
1073
+ );
1074
+ const typeLabel = describeCoveragePoint(
1075
+ point.file,
1076
+ point.line,
1077
+ point.column,
1078
+ point.type,
1079
+ ).displayType.padEnd(layout.typeWidth + 6);
1080
+ const locationLabel = location.padEnd(layout.locationWidth + 6);
1081
+ const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
1082
+ const meta = `${typeLabel}${locationLabel}`;
1083
+ console.log(` ${treePrefix}${chalk.dim(meta)} ${snippet}`);
933
1084
  }
934
1085
  function renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast) {
935
- const descriptor = describeCoveragePoint(point.file, point.line, point.column, point.type);
936
- const label = point.scopeKind || descriptor.displayType;
937
- const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
938
- const locationLabel = location.padEnd(layout.locationWidth + 6);
939
- const typeLabel = label.padEnd(layout.typeWidth + 6);
940
- const snippet = formatCoverageSnippet(point.file, point.line, point.column, point.type, ancestorHasNext.length);
941
- const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
942
- const meta = `${typeLabel}${locationLabel}`;
943
- console.log(` ${treePrefix}${chalk.dim(meta)} ${chalk.dim(snippet)}`);
1086
+ const descriptor = describeCoveragePoint(
1087
+ point.file,
1088
+ point.line,
1089
+ point.column,
1090
+ point.type,
1091
+ );
1092
+ const label = point.scopeKind || descriptor.displayType;
1093
+ const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
1094
+ const locationLabel = location.padEnd(layout.locationWidth + 6);
1095
+ const typeLabel = label.padEnd(layout.typeWidth + 6);
1096
+ const snippet = formatCoverageSnippet(
1097
+ point.file,
1098
+ point.line,
1099
+ point.column,
1100
+ point.type,
1101
+ ancestorHasNext.length,
1102
+ );
1103
+ const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
1104
+ const meta = `${typeLabel}${locationLabel}`;
1105
+ console.log(` ${treePrefix}${chalk.dim(meta)} ${chalk.dim(snippet)}`);
944
1106
  }
945
1107
  function buildCoverageTreePrefix(ancestorHasNext, isLast) {
946
- let out = "";
947
- for (const hasNext of ancestorHasNext) {
948
- out += hasNext ? "│ " : " ";
949
- }
950
- out += isLast ? "└─" : "├─";
951
- return chalk.dim(out);
1108
+ let out = "";
1109
+ for (const hasNext of ancestorHasNext) {
1110
+ out += hasNext ? "│ " : " ";
1111
+ }
1112
+ out += isLast ? "└─" : "├─";
1113
+ return chalk.dim(out);
952
1114
  }
953
1115
  function compareCoverageGapPoints(a, b) {
954
- if (a.line != b.line)
955
- return a.line - b.line;
956
- if (a.column != b.column)
957
- return a.column - b.column;
958
- if ((a.depth ?? 0) != (b.depth ?? 0))
959
- return (a.depth ?? 0) - (b.depth ?? 0);
960
- if (a.type != b.type)
961
- return a.type.localeCompare(b.type);
962
- return a.hash.localeCompare(b.hash);
1116
+ if (a.line != b.line) return a.line - b.line;
1117
+ if (a.column != b.column) return a.column - b.column;
1118
+ if ((a.depth ?? 0) != (b.depth ?? 0)) return (a.depth ?? 0) - (b.depth ?? 0);
1119
+ if (a.type != b.type) return a.type.localeCompare(b.type);
1120
+ return a.hash.localeCompare(b.hash);
963
1121
  }
964
1122
  function renderCoverageBar(percent) {
965
- const slots = 12;
966
- const filled = Math.max(0, Math.min(slots, Math.round((Math.max(0, Math.min(100, percent)) / 100) * slots)));
967
- return `[${"=".repeat(filled)}${"-".repeat(slots - filled)}]`;
1123
+ const slots = 12;
1124
+ const filled = Math.max(
1125
+ 0,
1126
+ Math.min(
1127
+ slots,
1128
+ Math.round((Math.max(0, Math.min(100, percent)) / 100) * slots),
1129
+ ),
1130
+ );
1131
+ return `[${"=".repeat(filled)}${"-".repeat(slots - filled)}]`;
968
1132
  }
969
1133
  function createCoverageGapLayout(points) {
970
- return {
971
- typeWidth: Math.max(...points.map((point) => point.displayType.length), 5),
972
- locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`
973
- .length), 1),
974
- };
1134
+ return {
1135
+ typeWidth: Math.max(...points.map((point) => point.displayType.length), 5),
1136
+ locationWidth: Math.max(
1137
+ ...points.map(
1138
+ (point) =>
1139
+ `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`
1140
+ .length,
1141
+ ),
1142
+ 1,
1143
+ ),
1144
+ };
975
1145
  }
976
1146
  function formatCoverageSnippet(file, line, column, fallbackType, _depth) {
977
- const descriptor = describeCoveragePoint(file, line, column, fallbackType);
978
- const visible = descriptor.visible;
979
- if (!visible.length)
980
- return "";
981
- const maxWidth = 72;
982
- const focus = Math.max(0, Math.min(visible.length - 1, descriptor.focus));
983
- if (visible.length <= maxWidth) {
984
- return styleCoverageSnippetWindow(visible, 0, visible.length, focus, descriptor.highlightStart, descriptor.highlightEnd);
985
- }
986
- const start = Math.max(0, Math.min(visible.length - maxWidth, focus - Math.floor(maxWidth / 2)));
987
- const end = Math.min(visible.length, start + maxWidth);
988
- return styleCoverageSnippetWindow(visible, start, end, focus, descriptor.highlightStart, descriptor.highlightEnd);
989
- }
990
- function styleCoverageSnippetWindow(visible, start, end, focus, highlightStart, highlightEnd) {
991
- const prefix = start > 0 ? "..." : "";
992
- const suffix = end < visible.length ? "..." : "";
993
- const slice = visible.slice(start, end);
994
- const localFocus = Math.max(0, Math.min(slice.length - 1, focus - start));
995
- const localStart = Math.max(0, Math.min(slice.length, highlightStart - start));
996
- const localEnd = Math.max(localStart + 1, Math.min(slice.length, highlightEnd - start));
997
- if (!slice.length)
998
- return "";
999
- if (localStart >= slice.length) {
1000
- return chalk.dim(`${prefix}${slice}${suffix}`);
1001
- }
1002
- const head = slice.slice(0, localStart);
1003
- const body = slice.slice(localStart, localEnd || localStart + 1);
1004
- const tail = slice.slice(localEnd || localStart + 1);
1005
- return (chalk.dim(prefix + head) +
1006
- chalk.dim.underline(body.length ? body : slice.charAt(localFocus)) +
1007
- chalk.dim(tail + suffix));
1147
+ const descriptor = describeCoveragePoint(file, line, column, fallbackType);
1148
+ const visible = descriptor.visible;
1149
+ if (!visible.length) return "";
1150
+ const maxWidth = 72;
1151
+ const focus = Math.max(0, Math.min(visible.length - 1, descriptor.focus));
1152
+ if (visible.length <= maxWidth) {
1153
+ return styleCoverageSnippetWindow(
1154
+ visible,
1155
+ 0,
1156
+ visible.length,
1157
+ focus,
1158
+ descriptor.highlightStart,
1159
+ descriptor.highlightEnd,
1160
+ );
1161
+ }
1162
+ const start = Math.max(
1163
+ 0,
1164
+ Math.min(visible.length - maxWidth, focus - Math.floor(maxWidth / 2)),
1165
+ );
1166
+ const end = Math.min(visible.length, start + maxWidth);
1167
+ return styleCoverageSnippetWindow(
1168
+ visible,
1169
+ start,
1170
+ end,
1171
+ focus,
1172
+ descriptor.highlightStart,
1173
+ descriptor.highlightEnd,
1174
+ );
1175
+ }
1176
+ function styleCoverageSnippetWindow(
1177
+ visible,
1178
+ start,
1179
+ end,
1180
+ focus,
1181
+ highlightStart,
1182
+ highlightEnd,
1183
+ ) {
1184
+ const prefix = start > 0 ? "..." : "";
1185
+ const suffix = end < visible.length ? "..." : "";
1186
+ const slice = visible.slice(start, end);
1187
+ const localFocus = Math.max(0, Math.min(slice.length - 1, focus - start));
1188
+ const localStart = Math.max(
1189
+ 0,
1190
+ Math.min(slice.length, highlightStart - start),
1191
+ );
1192
+ const localEnd = Math.max(
1193
+ localStart + 1,
1194
+ Math.min(slice.length, highlightEnd - start),
1195
+ );
1196
+ if (!slice.length) return "";
1197
+ if (localStart >= slice.length) {
1198
+ return chalk.dim(`${prefix}${slice}${suffix}`);
1199
+ }
1200
+ const head = slice.slice(0, localStart);
1201
+ const body = slice.slice(localStart, localEnd || localStart + 1);
1202
+ const tail = slice.slice(localEnd || localStart + 1);
1203
+ return (
1204
+ chalk.dim(prefix + head) +
1205
+ chalk.dim.underline(body.length ? body : slice.charAt(localFocus)) +
1206
+ chalk.dim(tail + suffix)
1207
+ );
1008
1208
  }