as-test 1.1.6 → 1.1.8
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 +17 -0
- package/README.md +4 -9
- package/assembly/index.ts +10 -15
- package/assembly/src/expectation.ts +11 -11
- package/assembly/src/fuzz.ts +11 -7
- package/assembly/src/log.ts +2 -2
- package/assembly/src/suite.ts +5 -5
- package/assembly/src/tests.ts +8 -8
- package/assembly/util/wipc.ts +5 -1
- package/bin/build-worker-pool.js +146 -142
- package/bin/build-worker.js +37 -34
- package/bin/commands/build-core.js +577 -465
- package/bin/commands/build.js +49 -29
- package/bin/commands/clean-core.js +120 -113
- package/bin/commands/clean.js +14 -8
- package/bin/commands/doctor-core.js +288 -289
- package/bin/commands/doctor.js +1 -1
- package/bin/commands/fuzz-core.js +467 -414
- package/bin/commands/fuzz.js +27 -10
- package/bin/commands/init-core.js +908 -794
- package/bin/commands/init.js +2 -2
- package/bin/commands/run-core.js +2675 -2344
- package/bin/commands/run.js +43 -25
- package/bin/commands/test.js +56 -32
- package/bin/commands/web-runner-source.js +1 -1
- package/bin/commands/web-session.js +516 -525
- package/bin/coverage-points.js +363 -341
- package/bin/crash-store.js +56 -66
- package/bin/index.js +4092 -3150
- package/bin/reporters/default.js +1090 -890
- package/bin/reporters/tap.js +319 -325
- package/bin/types.js +67 -67
- package/bin/util.js +1290 -1239
- package/bin/wipc.js +70 -73
- package/lib/build/index.d.ts +3 -1
- package/lib/build/index.js +1039 -1034
- package/lib/build/web-runner/client.js +1 -1
- package/lib/build/web-runner/html.js +1 -1
- package/lib/build/web-runner/worker.js +1 -1
- package/package.json +6 -3
- package/transform/lib/log.js +9 -5
- package/assembly/util/json.ts +0 -112
package/bin/reporters/default.js
CHANGED
|
@@ -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
|
-
|
|
8
|
+
return new DefaultReporter(context);
|
|
9
9
|
};
|
|
10
10
|
class DefaultReporter {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
248
|
+
this.clearRenderedBlock();
|
|
249
|
+
this.context.stdout.write(warnLine);
|
|
250
|
+
if (this.verboseMode) {
|
|
251
|
+
this.renderVerboseState();
|
|
252
|
+
} else {
|
|
253
|
+
this.renderLiveState();
|
|
298
254
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
context.stdout.write("\n");
|
|
338
|
+
if (!hasRenderedTestFiles) {
|
|
339
|
+
renderStandaloneFuzzTotals(event);
|
|
340
|
+
}
|
|
329
341
|
}
|
|
330
342
|
function renderFailedFuzzers(results) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
428
|
+
}
|
|
429
|
+
return rendered;
|
|
388
430
|
}
|
|
389
431
|
function groupFuzzResultsByFile(results) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
459
|
+
return (fuzzer.failures ?? [])
|
|
460
|
+
.map((failure) => String(failure.seed))
|
|
461
|
+
.join(", ");
|
|
422
462
|
}
|
|
423
463
|
function toRelativeResultPath(file) {
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
546
|
-
|
|
569
|
+
const buildCommand = String(suiteAny.buildCommand ?? "");
|
|
570
|
+
if (modeName.length && buildCommand.length) {
|
|
571
|
+
failure.buildCommands.set(modeName, buildCommand);
|
|
547
572
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
581
|
-
|
|
623
|
+
}
|
|
624
|
+
console.log("");
|
|
625
|
+
console.log(chalk.dim(" Here's a log dump too:"));
|
|
582
626
|
}
|
|
583
627
|
function renderRuntimeFailureDetails(failure, modes) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
|
|
607
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
706
|
+
return message.replace(/\r\n/g, "\n").trim();
|
|
654
707
|
}
|
|
655
708
|
function renderAssertionFailureDetails(leftRaw, rightRaw, messageRaw) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
695
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
798
|
+
renderSummaryLine("Modes:", summary, layout);
|
|
743
799
|
}
|
|
744
800
|
function renderFuzzTotals(summary, layout) {
|
|
745
|
-
|
|
801
|
+
renderSummaryLine("Fuzz:", summary, layout);
|
|
746
802
|
}
|
|
747
803
|
function renderStandaloneFuzzTotals(event) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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(
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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(
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
});
|
|
817
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
let
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
}
|