vitest 3.0.9 → 3.1.0-beta.2

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 (85) hide show
  1. package/dist/browser.js +4 -4
  2. package/dist/chunks/base.CylSMlTD.js +41 -0
  3. package/dist/chunks/benchmark.BKUatJGy.js +39 -0
  4. package/dist/chunks/cac.JtTXbKz0.js +1525 -0
  5. package/dist/chunks/{cli-api.Ckwz_xSb.js → cli-api.BTtPTYMs.js} +4638 -5072
  6. package/dist/chunks/console.D6t261w0.js +173 -0
  7. package/dist/chunks/constants.BZZyIeIE.js +43 -0
  8. package/dist/chunks/coverage.0iPg4Wrz.js +33 -0
  9. package/dist/chunks/{coverage.gV8doR2Y.js → coverage.C2ohxaN0.js} +2216 -2479
  10. package/dist/chunks/creator.BEXek7yQ.js +640 -0
  11. package/dist/chunks/date.CDOsz-HY.js +53 -0
  12. package/dist/chunks/defaults.DmfNPoe5.js +114 -0
  13. package/dist/chunks/{env.D4Lgay0q.js → env.Dq0hM4Xv.js} +1 -1
  14. package/dist/chunks/execute.DZKwfrTs.js +791 -0
  15. package/dist/chunks/git.DXfdBEfR.js +74 -0
  16. package/dist/chunks/{globals.BEpDe-k3.js → globals.DCbUWjip.js} +10 -10
  17. package/dist/chunks/{index.D7Ny8f_s.js → index.BDobFbcz.js} +6 -7
  18. package/dist/chunks/index.DFXFpH3w.js +607 -0
  19. package/dist/chunks/index.VfYQ6MXY.js +104 -0
  20. package/dist/chunks/index.ZIOEXBQB.js +2382 -0
  21. package/dist/chunks/inspector.DbDkSkFn.js +54 -0
  22. package/dist/chunks/node.IqGoMrm4.js +15 -0
  23. package/dist/chunks/{reporters.d.CqBhtcTq.d.ts → reporters.d.5g6jXhoW.d.ts} +25 -8
  24. package/dist/chunks/rpc.DGgL5dw7.js +92 -0
  25. package/dist/chunks/run-once.I7PpBOk1.js +47 -0
  26. package/dist/chunks/runBaseTests.CqmKSG99.js +134 -0
  27. package/dist/chunks/setup-common.DEGDGBiA.js +88 -0
  28. package/dist/chunks/{typechecker.BlF3eHsb.js → typechecker.C2IpOhid.js} +620 -622
  29. package/dist/chunks/utils.BfxieIyZ.js +66 -0
  30. package/dist/chunks/utils.CtocqOoE.js +72 -0
  31. package/dist/chunks/utils.OLmtDstN.js +194 -0
  32. package/dist/chunks/{vi.nSCvwQ7l.js → vi.B-PuvDzu.js} +878 -1019
  33. package/dist/chunks/vite.d.Dh1jE-_V.d.ts +23 -0
  34. package/dist/chunks/vm.BW5voG-u.js +789 -0
  35. package/dist/cli.js +2 -2
  36. package/dist/config.cjs +97 -103
  37. package/dist/config.d.ts +3 -3
  38. package/dist/config.js +6 -6
  39. package/dist/coverage.d.ts +1 -1
  40. package/dist/coverage.js +6 -6
  41. package/dist/environments.js +1 -1
  42. package/dist/execute.js +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +6 -6
  45. package/dist/node.d.ts +3 -3
  46. package/dist/node.js +36 -45
  47. package/dist/path.js +1 -4
  48. package/dist/reporters.d.ts +1 -1
  49. package/dist/reporters.js +4 -4
  50. package/dist/runners.js +231 -267
  51. package/dist/snapshot.js +2 -2
  52. package/dist/suite.js +2 -2
  53. package/dist/worker.js +98 -114
  54. package/dist/workers/forks.js +22 -22
  55. package/dist/workers/runVmTests.js +61 -66
  56. package/dist/workers/threads.js +13 -13
  57. package/dist/workers/vmForks.js +24 -24
  58. package/dist/workers/vmThreads.js +15 -15
  59. package/dist/workers.js +10 -10
  60. package/package.json +11 -11
  61. package/dist/chunks/base.DV59CbtV.js +0 -45
  62. package/dist/chunks/benchmark.DL72EVN-.js +0 -40
  63. package/dist/chunks/cac.CeVHgzve.js +0 -1659
  64. package/dist/chunks/console.CN7AiMGV.js +0 -179
  65. package/dist/chunks/constants.DTYd6dNH.js +0 -46
  66. package/dist/chunks/coverage.A3sS5-Wm.js +0 -40
  67. package/dist/chunks/creator.BsBnpTzI.js +0 -670
  68. package/dist/chunks/date.W2xKR2qe.js +0 -53
  69. package/dist/chunks/defaults.C2Ndd9wx.js +0 -119
  70. package/dist/chunks/execute.eDH0aFFd.js +0 -839
  71. package/dist/chunks/git.B5SDxu-n.js +0 -69
  72. package/dist/chunks/index.B8tIoLPT.js +0 -2526
  73. package/dist/chunks/index.K90BXFOx.js +0 -658
  74. package/dist/chunks/index.uXkkC4xl.js +0 -111
  75. package/dist/chunks/inspector.DKLceBVD.js +0 -54
  76. package/dist/chunks/node.AKq966Jp.js +0 -15
  77. package/dist/chunks/rpc.TVf73xOu.js +0 -102
  78. package/dist/chunks/run-once.2ogXb3JV.js +0 -28
  79. package/dist/chunks/runBaseTests.BVrL_ow3.js +0 -142
  80. package/dist/chunks/setup-common.CPvtqi8q.js +0 -96
  81. package/dist/chunks/utils.C8RiOc4B.js +0 -77
  82. package/dist/chunks/utils.Cn0zI1t3.js +0 -68
  83. package/dist/chunks/utils.bLM2atbD.js +0 -198
  84. package/dist/chunks/vite.d.BUZTGxQ3.d.ts +0 -11
  85. package/dist/chunks/vm.jEFQDlX_.js +0 -852
@@ -0,0 +1,2382 @@
1
+ import fs, { existsSync, readFileSync, promises } from 'node:fs';
2
+ import { getSuites, getTests, getTestName, hasFailed, getFullName, getTasks } from '@vitest/runner/utils';
3
+ import * as pathe from 'pathe';
4
+ import { relative, normalize, resolve, dirname } from 'pathe';
5
+ import c from 'tinyrainbow';
6
+ import { t as truncateString, d as divider, F as F_POINTER, f as formatTimeString, g as getStateSymbol, a as formatProjectName, b as taskFail, c as F_CHECK, e as F_RIGHT, w as withLabel, r as renderSnapshotSummary, p as padSummaryTitle, h as getStateString$1, i as formatTime, j as countTestErrors, k as F_TREE_NODE_END, l as F_TREE_NODE_MIDDLE } from './utils.OLmtDstN.js';
7
+ import { stripVTControlCharacters } from 'node:util';
8
+ import { positionToOffset, lineSplitRE, isPrimitive, inspect, toArray, notNullish } from '@vitest/utils';
9
+ import { performance as performance$1 } from 'node:perf_hooks';
10
+ import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
11
+ import { i as isTTY } from './env.Dq0hM4Xv.js';
12
+ import { T as TypeCheckError, g as getOutputFile, h as hasFailedSnapshot } from './typechecker.C2IpOhid.js';
13
+ import { mkdir, writeFile, readdir, stat, readFile } from 'node:fs/promises';
14
+ import { Console } from 'node:console';
15
+ import { Writable } from 'node:stream';
16
+ import { createRequire } from 'node:module';
17
+ import { hostname } from 'node:os';
18
+
19
+ /// <reference types="../types/index.d.ts" />
20
+
21
+ // (c) 2020-present Andrea Giammarchi
22
+
23
+ const {parse: $parse, stringify: $stringify} = JSON;
24
+ const {keys} = Object;
25
+
26
+ const Primitive = String; // it could be Number
27
+ const primitive = 'string'; // it could be 'number'
28
+
29
+ const ignore = {};
30
+ const object = 'object';
31
+
32
+ const noop = (_, value) => value;
33
+
34
+ const primitives = value => (
35
+ value instanceof Primitive ? Primitive(value) : value
36
+ );
37
+
38
+ const Primitives = (_, value) => (
39
+ typeof value === primitive ? new Primitive(value) : value
40
+ );
41
+
42
+ const revive = (input, parsed, output, $) => {
43
+ const lazy = [];
44
+ for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
45
+ const k = ke[y];
46
+ const value = output[k];
47
+ if (value instanceof Primitive) {
48
+ const tmp = input[value];
49
+ if (typeof tmp === object && !parsed.has(tmp)) {
50
+ parsed.add(tmp);
51
+ output[k] = ignore;
52
+ lazy.push({k, a: [input, parsed, tmp, $]});
53
+ }
54
+ else
55
+ output[k] = $.call(output, k, tmp);
56
+ }
57
+ else if (output[k] !== ignore)
58
+ output[k] = $.call(output, k, value);
59
+ }
60
+ for (let {length} = lazy, i = 0; i < length; i++) {
61
+ const {k, a} = lazy[i];
62
+ output[k] = $.call(output, k, revive.apply(null, a));
63
+ }
64
+ return output;
65
+ };
66
+
67
+ const set = (known, input, value) => {
68
+ const index = Primitive(input.push(value) - 1);
69
+ known.set(value, index);
70
+ return index;
71
+ };
72
+
73
+ /**
74
+ * Converts a specialized flatted string into a JS value.
75
+ * @param {string} text
76
+ * @param {(this: any, key: string, value: any) => any} [reviver]
77
+ * @returns {any}
78
+ */
79
+ const parse = (text, reviver) => {
80
+ const input = $parse(text, Primitives).map(primitives);
81
+ const value = input[0];
82
+ const $ = reviver || noop;
83
+ const tmp = typeof value === object && value ?
84
+ revive(input, new Set, value, $) :
85
+ value;
86
+ return $.call({'': tmp}, '', tmp);
87
+ };
88
+
89
+ /**
90
+ * Converts a JS value into a specialized flatted string.
91
+ * @param {any} value
92
+ * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
93
+ * @param {string | number | undefined} [space]
94
+ * @returns {string}
95
+ */
96
+ const stringify = (value, replacer, space) => {
97
+ const $ = replacer && typeof replacer === object ?
98
+ (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
99
+ (replacer || noop);
100
+ const known = new Map;
101
+ const input = [];
102
+ const output = [];
103
+ let i = +set(known, input, $.call({'': value}, '', value));
104
+ let firstRun = !i;
105
+ while (i < input.length) {
106
+ firstRun = true;
107
+ output[i] = $stringify(input[i++], replace, space);
108
+ }
109
+ return '[' + output.join(',') + ']';
110
+ function replace(key, value) {
111
+ if (firstRun) {
112
+ firstRun = !firstRun;
113
+ return value;
114
+ }
115
+ const after = $.call(this, key, value);
116
+ switch (typeof after) {
117
+ case object:
118
+ if (after === null) return after;
119
+ case primitive:
120
+ return known.get(after) || set(known, input, after);
121
+ }
122
+ return after;
123
+ }
124
+ };
125
+
126
+ function capturePrintError(error, ctx, options) {
127
+ let output = "";
128
+ const writable = new Writable({ write(chunk, _encoding, callback) {
129
+ output += String(chunk);
130
+ callback();
131
+ } });
132
+ const console = new Console(writable);
133
+ const logger = {
134
+ error: console.error.bind(console),
135
+ highlight: ctx.logger.highlight.bind(ctx.logger)
136
+ };
137
+ const result = printError(error, ctx, logger, {
138
+ showCodeFrame: false,
139
+ ...options
140
+ });
141
+ return {
142
+ nearest: result?.nearest,
143
+ output
144
+ };
145
+ }
146
+ function printError(error, ctx, logger, options) {
147
+ const project = options.project ?? ctx.coreWorkspaceProject ?? ctx.projects[0];
148
+ return printErrorInner(error, project, {
149
+ logger,
150
+ type: options.type,
151
+ showCodeFrame: options.showCodeFrame,
152
+ screenshotPaths: options.screenshotPaths,
153
+ printProperties: options.verbose,
154
+ parseErrorStacktrace(error) {
155
+ if (options.task?.file.pool === "browser" && project.browser) {
156
+ return project.browser.parseErrorStacktrace(error, { ignoreStackEntries: options.fullStack ? [] : undefined });
157
+ }
158
+ return parseErrorStacktrace(error, {
159
+ frameFilter: project.config.onStackTrace,
160
+ ignoreStackEntries: options.fullStack ? [] : undefined
161
+ });
162
+ }
163
+ });
164
+ }
165
+ function printErrorInner(error, project, options) {
166
+ const { showCodeFrame = true, type, printProperties = true } = options;
167
+ const logger = options.logger;
168
+ let e = error;
169
+ if (isPrimitive(e)) {
170
+ e = {
171
+ message: String(error).split(/\n/g)[0],
172
+ stack: String(error)
173
+ };
174
+ }
175
+ if (!e) {
176
+ const error = new Error("unknown error");
177
+ e = {
178
+ message: e ?? error.message,
179
+ stack: error.stack
180
+ };
181
+ }
182
+ if (!project) {
183
+ printErrorMessage(e, logger);
184
+ return;
185
+ }
186
+ const stacks = options.parseErrorStacktrace(e);
187
+ const nearest = error instanceof TypeCheckError ? error.stacks[0] : stacks.find((stack) => {
188
+ try {
189
+ return project.server && project.getModuleById(stack.file) && existsSync(stack.file);
190
+ } catch {
191
+ return false;
192
+ }
193
+ });
194
+ if (type) {
195
+ printErrorType(type, project.vitest);
196
+ }
197
+ printErrorMessage(e, logger);
198
+ if (options.screenshotPaths?.length) {
199
+ const length = options.screenshotPaths.length;
200
+ logger.error(`\nFailure screenshot${length > 1 ? "s" : ""}:`);
201
+ logger.error(options.screenshotPaths.map((p) => ` - ${c.dim(relative(process.cwd(), p))}`).join("\n"));
202
+ if (!e.diff) {
203
+ logger.error();
204
+ }
205
+ }
206
+ if (e.codeFrame) {
207
+ logger.error(`${e.codeFrame}\n`);
208
+ }
209
+ if ("__vitest_rollup_error__" in e) {
210
+ const err = e.__vitest_rollup_error__;
211
+ logger.error([
212
+ err.plugin && ` Plugin: ${c.magenta(err.plugin)}`,
213
+ err.id && ` File: ${c.cyan(err.id)}${err.loc ? `:${err.loc.line}:${err.loc.column}` : ""}`,
214
+ err.frame && c.yellow(err.frame.split(/\r?\n/g).map((l) => ` `.repeat(2) + l).join(`\n`))
215
+ ].filter(Boolean).join("\n"));
216
+ }
217
+ if (e.diff) {
218
+ logger.error(`\n${e.diff}\n`);
219
+ }
220
+ if (e.frame) {
221
+ logger.error(c.yellow(e.frame));
222
+ } else {
223
+ const errorProperties = printProperties ? getErrorProperties(e) : {};
224
+ printStack(logger, project, stacks, nearest, errorProperties, (s) => {
225
+ if (showCodeFrame && s === nearest && nearest) {
226
+ const sourceCode = readFileSync(nearest.file, "utf-8");
227
+ logger.error(generateCodeFrame(sourceCode.length > 1e5 ? sourceCode : logger.highlight(nearest.file, sourceCode), 4, s));
228
+ }
229
+ });
230
+ }
231
+ const testPath = e.VITEST_TEST_PATH;
232
+ const testName = e.VITEST_TEST_NAME;
233
+ const afterEnvTeardown = e.VITEST_AFTER_ENV_TEARDOWN;
234
+ if (testPath) {
235
+ logger.error(c.red(`This error originated in "${c.bold(relative(project.config.root, testPath))}" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.`));
236
+ }
237
+ if (testName) {
238
+ logger.error(c.red(`The latest test that might've caused the error is "${c.bold(testName)}". It might mean one of the following:` + "\n- The error was thrown, while Vitest was running this test." + "\n- If the error occurred after the test had been completed, this was the last documented test before it was thrown."));
239
+ }
240
+ if (afterEnvTeardown) {
241
+ logger.error(c.red("This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:" + "\n- cancel timeouts using clearTimeout and clearInterval" + "\n- wait for promises to resolve using the await keyword"));
242
+ }
243
+ if (typeof e.cause === "object" && e.cause && "name" in e.cause) {
244
+ e.cause.name = `Caused by: ${e.cause.name}`;
245
+ printErrorInner(e.cause, project, {
246
+ showCodeFrame: false,
247
+ logger: options.logger,
248
+ parseErrorStacktrace: options.parseErrorStacktrace
249
+ });
250
+ }
251
+ handleImportOutsideModuleError(e.stack || e.stackStr || "", logger);
252
+ return { nearest };
253
+ }
254
+ function printErrorType(type, ctx) {
255
+ ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(` ${type} `))))}`);
256
+ }
257
+ const skipErrorProperties = new Set([
258
+ "nameStr",
259
+ "stack",
260
+ "cause",
261
+ "stacks",
262
+ "stackStr",
263
+ "type",
264
+ "showDiff",
265
+ "ok",
266
+ "operator",
267
+ "diff",
268
+ "codeFrame",
269
+ "actual",
270
+ "expected",
271
+ "diffOptions",
272
+ "sourceURL",
273
+ "column",
274
+ "line",
275
+ "VITEST_TEST_NAME",
276
+ "VITEST_TEST_PATH",
277
+ "VITEST_AFTER_ENV_TEARDOWN",
278
+ ...Object.getOwnPropertyNames(Error.prototype),
279
+ ...Object.getOwnPropertyNames(Object.prototype)
280
+ ]);
281
+ function getErrorProperties(e) {
282
+ const errorObject = Object.create(null);
283
+ if (e.name === "AssertionError") {
284
+ return errorObject;
285
+ }
286
+ for (const key of Object.getOwnPropertyNames(e)) {
287
+ if (!skipErrorProperties.has(key)) {
288
+ errorObject[key] = e[key];
289
+ }
290
+ }
291
+ return errorObject;
292
+ }
293
+ const esmErrors = ["Cannot use import statement outside a module", "Unexpected token 'export'"];
294
+ function handleImportOutsideModuleError(stack, logger) {
295
+ if (!esmErrors.some((e) => stack.includes(e))) {
296
+ return;
297
+ }
298
+ const path = normalize(stack.split("\n")[0].trim());
299
+ let name = path.split("/node_modules/").pop() || "";
300
+ if (name?.startsWith("@")) {
301
+ name = name.split("/").slice(0, 2).join("/");
302
+ } else {
303
+ name = name.split("/")[0];
304
+ }
305
+ if (name) {
306
+ printModuleWarningForPackage(logger, path, name);
307
+ } else {
308
+ printModuleWarningForSourceCode(logger, path);
309
+ }
310
+ }
311
+ function printModuleWarningForPackage(logger, path, name) {
312
+ logger.error(c.yellow(`Module ${path} seems to be an ES Module but shipped in a CommonJS package. ` + `You might want to create an issue to the package ${c.bold(`"${name}"`)} asking ` + "them to ship the file in .mjs extension or add \"type\": \"module\" in their package.json." + "\n\n" + "As a temporary workaround you can try to inline the package by updating your config:" + "\n\n" + c.gray(c.dim("// vitest.config.js")) + "\n" + c.green(`export default {
313
+ test: {
314
+ server: {
315
+ deps: {
316
+ inline: [
317
+ ${c.yellow(c.bold(`"${name}"`))}
318
+ ]
319
+ }
320
+ }
321
+ }
322
+ }\n`)));
323
+ }
324
+ function printModuleWarningForSourceCode(logger, path) {
325
+ logger.error(c.yellow(`Module ${path} seems to be an ES Module but shipped in a CommonJS package. ` + "To fix this issue, change the file extension to .mjs or add \"type\": \"module\" in your package.json."));
326
+ }
327
+ function printErrorMessage(error, logger) {
328
+ const errorName = error.name || error.nameStr || "Unknown Error";
329
+ if (!error.message) {
330
+ logger.error(error);
331
+ return;
332
+ }
333
+ if (error.message.length > 5e3) {
334
+ logger.error(`${c.red(c.bold(errorName))}: ${error.message}`);
335
+ } else {
336
+ logger.error(c.red(`${c.bold(errorName)}: ${error.message}`));
337
+ }
338
+ }
339
+ function printStack(logger, project, stack, highlight, errorProperties, onStack) {
340
+ for (const frame of stack) {
341
+ const color = frame === highlight ? c.cyan : c.gray;
342
+ const path = relative(project.config.root, frame.file);
343
+ logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ")}`));
344
+ onStack?.(frame);
345
+ }
346
+ if (stack.length) {
347
+ logger.error();
348
+ }
349
+ if (hasProperties(errorProperties)) {
350
+ logger.error(c.red(c.dim(divider())));
351
+ const propertiesString = inspect(errorProperties);
352
+ logger.error(c.red(c.bold("Serialized Error:")), c.gray(propertiesString));
353
+ }
354
+ }
355
+ function hasProperties(obj) {
356
+ for (const _key in obj) {
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+ function generateCodeFrame(source, indent = 0, loc, range = 2) {
362
+ const start = typeof loc === "object" ? positionToOffset(source, loc.line, loc.column) : loc;
363
+ const end = start;
364
+ const lines = source.split(lineSplitRE);
365
+ const nl = /\r\n/.test(source) ? 2 : 1;
366
+ let count = 0;
367
+ let res = [];
368
+ const columns = process.stdout?.columns || 80;
369
+ for (let i = 0; i < lines.length; i++) {
370
+ count += lines[i].length + nl;
371
+ if (count >= start) {
372
+ for (let j = i - range; j <= i + range || end > count; j++) {
373
+ if (j < 0 || j >= lines.length) {
374
+ continue;
375
+ }
376
+ const lineLength = lines[j].length;
377
+ if (stripVTControlCharacters(lines[j]).length > 200) {
378
+ return "";
379
+ }
380
+ res.push(lineNo(j + 1) + truncateString(lines[j].replace(/\t/g, " "), columns - 5 - indent));
381
+ if (j === i) {
382
+ const pad = start - (count - lineLength) + (nl - 1);
383
+ const length = Math.max(1, end > count ? lineLength - pad : end - start);
384
+ res.push(lineNo() + " ".repeat(pad) + c.red("^".repeat(length)));
385
+ } else if (j > i) {
386
+ if (end > count) {
387
+ const length = Math.max(1, Math.min(end - count, lineLength));
388
+ res.push(lineNo() + c.red("^".repeat(length)));
389
+ }
390
+ count += lineLength + 1;
391
+ }
392
+ }
393
+ break;
394
+ }
395
+ }
396
+ if (indent) {
397
+ res = res.map((line) => " ".repeat(indent) + line);
398
+ }
399
+ return res.join("\n");
400
+ }
401
+ function lineNo(no = "") {
402
+ return c.gray(`${String(no).padStart(3, " ")}| `);
403
+ }
404
+
405
+ class BlobReporter {
406
+ ctx;
407
+ options;
408
+ constructor(options) {
409
+ this.options = options;
410
+ }
411
+ onInit(ctx) {
412
+ if (ctx.config.watch) {
413
+ throw new Error("Blob reporter is not supported in watch mode");
414
+ }
415
+ this.ctx = ctx;
416
+ }
417
+ async onFinished(files = [], errors = [], coverage) {
418
+ let outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "blob");
419
+ if (!outputFile) {
420
+ const shard = this.ctx.config.shard;
421
+ outputFile = shard ? `.vitest-reports/blob-${shard.index}-${shard.count}.json` : ".vitest-reports/blob.json";
422
+ }
423
+ const modules = this.ctx.projects.map((project) => {
424
+ return [project.name, [...project.vite.moduleGraph.idToModuleMap.entries()].map((mod) => {
425
+ if (!mod[1].file) {
426
+ return null;
427
+ }
428
+ return [
429
+ mod[0],
430
+ mod[1].file,
431
+ mod[1].url
432
+ ];
433
+ }).filter((x) => x != null)];
434
+ });
435
+ const report = stringify([
436
+ this.ctx.version,
437
+ files,
438
+ errors,
439
+ modules,
440
+ coverage
441
+ ]);
442
+ const reportFile = resolve(this.ctx.config.root, outputFile);
443
+ const dir = dirname(reportFile);
444
+ if (!existsSync(dir)) {
445
+ await mkdir(dir, { recursive: true });
446
+ }
447
+ await writeFile(reportFile, report, "utf-8");
448
+ this.ctx.logger.log("blob report written to", reportFile);
449
+ }
450
+ }
451
+ async function readBlobs(currentVersion, blobsDirectory, projectsArray) {
452
+ const resolvedDir = resolve(process.cwd(), blobsDirectory);
453
+ const blobsFiles = await readdir(resolvedDir);
454
+ const promises = blobsFiles.map(async (filename) => {
455
+ const fullPath = resolve(resolvedDir, filename);
456
+ const stats = await stat(fullPath);
457
+ if (!stats.isFile()) {
458
+ throw new TypeError(`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a file`);
459
+ }
460
+ const content = await readFile(fullPath, "utf-8");
461
+ const [version, files, errors, moduleKeys, coverage] = parse(content);
462
+ if (!version) {
463
+ throw new TypeError(`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`);
464
+ }
465
+ return {
466
+ version,
467
+ files,
468
+ errors,
469
+ moduleKeys,
470
+ coverage,
471
+ file: filename
472
+ };
473
+ });
474
+ const blobs = await Promise.all(promises);
475
+ if (!blobs.length) {
476
+ throw new Error(`vitest.mergeReports() requires at least one blob file in "${blobsDirectory}" directory, but none were found`);
477
+ }
478
+ const versions = new Set(blobs.map((blob) => blob.version));
479
+ if (versions.size > 1) {
480
+ throw new Error(`vitest.mergeReports() requires all blob files to be generated by the same Vitest version, received\n\n${blobs.map((b) => `- "${b.file}" uses v${b.version}`).join("\n")}`);
481
+ }
482
+ if (!versions.has(currentVersion)) {
483
+ throw new Error(`the blobs in "${blobsDirectory}" were generated by a different version of Vitest. Expected v${currentVersion}, but received v${blobs[0].version}`);
484
+ }
485
+ const projects = Object.fromEntries(projectsArray.map((p) => [p.name, p]));
486
+ blobs.forEach((blob) => {
487
+ blob.moduleKeys.forEach(([projectName, moduleIds]) => {
488
+ const project = projects[projectName];
489
+ if (!project) {
490
+ return;
491
+ }
492
+ moduleIds.forEach(([moduleId, file, url]) => {
493
+ const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file);
494
+ moduleNode.url = url;
495
+ moduleNode.id = moduleId;
496
+ project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode);
497
+ });
498
+ });
499
+ });
500
+ const files = blobs.flatMap((blob) => blob.files).sort((f1, f2) => {
501
+ const time1 = f1.result?.startTime || 0;
502
+ const time2 = f2.result?.startTime || 0;
503
+ return time1 - time2;
504
+ });
505
+ const errors = blobs.flatMap((blob) => blob.errors);
506
+ const coverages = blobs.map((blob) => blob.coverage);
507
+ return {
508
+ files,
509
+ errors,
510
+ coverages
511
+ };
512
+ }
513
+
514
+ class HangingProcessReporter {
515
+ whyRunning;
516
+ onInit() {
517
+ const _require = createRequire(import.meta.url);
518
+ this.whyRunning = _require("why-is-node-running");
519
+ }
520
+ onProcessTimeout() {
521
+ this.whyRunning?.();
522
+ }
523
+ }
524
+
525
+ const BADGE_PADDING = " ";
526
+ class BaseReporter {
527
+ start = 0;
528
+ end = 0;
529
+ watchFilters;
530
+ failedUnwatchedFiles = [];
531
+ isTTY;
532
+ ctx = undefined;
533
+ renderSucceed = false;
534
+ verbose = false;
535
+ _filesInWatchMode = new Map();
536
+ _timeStart = formatTimeString(new Date());
537
+ constructor(options = {}) {
538
+ this.isTTY = options.isTTY ?? isTTY;
539
+ }
540
+ onInit(ctx) {
541
+ this.ctx = ctx;
542
+ this.ctx.logger.printBanner();
543
+ this.start = performance$1.now();
544
+ }
545
+ log(...messages) {
546
+ this.ctx.logger.log(...messages);
547
+ }
548
+ error(...messages) {
549
+ this.ctx.logger.error(...messages);
550
+ }
551
+ relative(path) {
552
+ return relative(this.ctx.config.root, path);
553
+ }
554
+ onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
555
+ this.end = performance$1.now();
556
+ if (!files.length && !errors.length) {
557
+ this.ctx.logger.printNoTestFound(this.ctx.filenamePattern);
558
+ } else {
559
+ this.reportSummary(files, errors);
560
+ }
561
+ }
562
+ onTestCaseResult(testCase) {
563
+ if (testCase.result().state === "failed") {
564
+ this.logFailedTask(testCase.task);
565
+ }
566
+ }
567
+ onTestSuiteResult(testSuite) {
568
+ if (testSuite.state() === "failed") {
569
+ this.logFailedTask(testSuite.task);
570
+ }
571
+ }
572
+ onTestModuleEnd(testModule) {
573
+ if (testModule.state() === "failed") {
574
+ this.logFailedTask(testModule.task);
575
+ }
576
+ }
577
+ logFailedTask(task) {
578
+ if (this.ctx.config.silent === "passed-only") {
579
+ for (const log of task.logs || []) {
580
+ this.onUserConsoleLog(log, "failed");
581
+ }
582
+ }
583
+ }
584
+ onTaskUpdate(packs) {
585
+ for (const pack of packs) {
586
+ const task = this.ctx.state.idMap.get(pack[0]);
587
+ if (task) {
588
+ this.printTask(task);
589
+ }
590
+ }
591
+ }
592
+ /**
593
+ * Callback invoked with a single `Task` from `onTaskUpdate`
594
+ */
595
+ printTask(task) {
596
+ if (!("filepath" in task) || !task.result?.state || task.result?.state === "run" || task.result?.state === "queued") {
597
+ return;
598
+ }
599
+ const suites = getSuites(task);
600
+ const allTests = getTests(task);
601
+ const failed = allTests.filter((t) => t.result?.state === "fail");
602
+ const skipped = allTests.filter((t) => t.mode === "skip" || t.mode === "todo");
603
+ let state = c.dim(`${allTests.length} test${allTests.length > 1 ? "s" : ""}`);
604
+ if (failed.length) {
605
+ state += c.dim(" | ") + c.red(`${failed.length} failed`);
606
+ }
607
+ if (skipped.length) {
608
+ state += c.dim(" | ") + c.yellow(`${skipped.length} skipped`);
609
+ }
610
+ let suffix = c.dim("(") + state + c.dim(")") + this.getDurationPrefix(task);
611
+ if (this.ctx.config.logHeapUsage && task.result.heap != null) {
612
+ suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`);
613
+ }
614
+ let title = getStateSymbol(task);
615
+ if (task.meta.typecheck) {
616
+ title += ` ${c.bgBlue(c.bold(" TS "))}`;
617
+ }
618
+ if (task.projectName) {
619
+ title += ` ${formatProjectName(task.projectName, "")}`;
620
+ }
621
+ this.log(` ${title} ${task.name} ${suffix}`);
622
+ for (const suite of suites) {
623
+ if (this.ctx.config.hideSkippedTests && (suite.mode === "skip" || suite.result?.state === "skip")) {
624
+ continue;
625
+ }
626
+ const tests = suite.tasks.filter((task) => task.type === "test");
627
+ if (!("filepath" in suite)) {
628
+ this.printSuite(suite);
629
+ }
630
+ for (const test of tests) {
631
+ const { duration, retryCount, repeatCount } = test.result || {};
632
+ const padding = this.getTestIndentation(test);
633
+ let suffix = this.getDurationPrefix(test);
634
+ if (retryCount != null && retryCount > 0) {
635
+ suffix += c.yellow(` (retry x${retryCount})`);
636
+ }
637
+ if (repeatCount != null && repeatCount > 0) {
638
+ suffix += c.yellow(` (repeat x${repeatCount})`);
639
+ }
640
+ if (test.result?.state === "fail") {
641
+ this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test, c.dim(" > "))}`) + suffix);
642
+ test.result?.errors?.forEach((error) => {
643
+ const message = this.formatShortError(error);
644
+ if (message) {
645
+ this.log(c.red(` ${padding}${message}`));
646
+ }
647
+ });
648
+ } else if (duration && duration > this.ctx.config.slowTestThreshold) {
649
+ this.log(` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test, c.dim(" > "))} ${suffix}`);
650
+ } else if (this.ctx.config.hideSkippedTests && (test.mode === "skip" || test.result?.state === "skip")) ; else if (test.result?.state === "skip" && test.result.note) {
651
+ this.log(` ${padding}${getStateSymbol(test)} ${this.getTestName(test)}${c.dim(c.gray(` [${test.result.note}]`))}`);
652
+ } else if (this.renderSucceed || failed.length > 0) {
653
+ this.log(` ${padding}${getStateSymbol(test)} ${this.getTestName(test, c.dim(" > "))}${suffix}`);
654
+ }
655
+ }
656
+ }
657
+ }
658
+ printSuite(_task) {}
659
+ getTestName(test, separator) {
660
+ return getTestName(test, separator);
661
+ }
662
+ formatShortError(error) {
663
+ return `${F_RIGHT} ${error.message}`;
664
+ }
665
+ getTestIndentation(_test) {
666
+ return " ";
667
+ }
668
+ getDurationPrefix(task) {
669
+ if (!task.result?.duration) {
670
+ return "";
671
+ }
672
+ const color = task.result.duration > this.ctx.config.slowTestThreshold ? c.yellow : c.green;
673
+ return color(` ${Math.round(task.result.duration)}${c.dim("ms")}`);
674
+ }
675
+ onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
676
+ const failed = errors.length > 0 || hasFailed(files);
677
+ if (failed) {
678
+ this.log(withLabel("red", "FAIL", "Tests failed. Watching for file changes..."));
679
+ } else if (this.ctx.isCancelling) {
680
+ this.log(withLabel("red", "CANCELLED", "Test run cancelled. Watching for file changes..."));
681
+ } else {
682
+ this.log(withLabel("green", "PASS", "Waiting for file changes..."));
683
+ }
684
+ const hints = [c.dim("press ") + c.bold("h") + c.dim(" to show help")];
685
+ if (hasFailedSnapshot(files)) {
686
+ hints.unshift(c.dim("press ") + c.bold(c.yellow("u")) + c.dim(" to update snapshot"));
687
+ } else {
688
+ hints.push(c.dim("press ") + c.bold("q") + c.dim(" to quit"));
689
+ }
690
+ this.log(BADGE_PADDING + hints.join(c.dim(", ")));
691
+ }
692
+ onWatcherRerun(files, trigger) {
693
+ this.watchFilters = files;
694
+ this.failedUnwatchedFiles = this.ctx.state.getFiles().filter((file) => !files.includes(file.filepath) && hasFailed(file));
695
+ files.forEach((filepath) => {
696
+ let reruns = this._filesInWatchMode.get(filepath) ?? 0;
697
+ this._filesInWatchMode.set(filepath, ++reruns);
698
+ });
699
+ let banner = trigger ? c.dim(`${this.relative(trigger)} `) : "";
700
+ if (files.length === 1) {
701
+ const rerun = this._filesInWatchMode.get(files[0]) ?? 1;
702
+ banner += c.blue(`x${rerun} `);
703
+ }
704
+ this.ctx.logger.clearFullScreen();
705
+ this.log(withLabel("blue", "RERUN", banner));
706
+ if (this.ctx.configOverride.project) {
707
+ this.log(BADGE_PADDING + c.dim(" Project name: ") + c.blue(toArray(this.ctx.configOverride.project).join(", ")));
708
+ }
709
+ if (this.ctx.filenamePattern) {
710
+ this.log(BADGE_PADDING + c.dim(" Filename pattern: ") + c.blue(this.ctx.filenamePattern.join(", ")));
711
+ }
712
+ if (this.ctx.configOverride.testNamePattern) {
713
+ this.log(BADGE_PADDING + c.dim(" Test name pattern: ") + c.blue(String(this.ctx.configOverride.testNamePattern)));
714
+ }
715
+ this.log("");
716
+ for (const task of this.failedUnwatchedFiles) {
717
+ this.printTask(task);
718
+ }
719
+ this._timeStart = formatTimeString(new Date());
720
+ this.start = performance$1.now();
721
+ }
722
+ onUserConsoleLog(log, taskState) {
723
+ if (!this.shouldLog(log, taskState)) {
724
+ return;
725
+ }
726
+ const output = log.type === "stdout" ? this.ctx.logger.outputStream : this.ctx.logger.errorStream;
727
+ const write = (msg) => output.write(msg);
728
+ let headerText = "unknown test";
729
+ const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined;
730
+ if (task) {
731
+ headerText = getFullName(task, c.dim(" > "));
732
+ } else if (log.taskId && log.taskId !== "__vitest__unknown_test__") {
733
+ headerText = log.taskId;
734
+ }
735
+ write(c.gray(log.type + c.dim(` | ${headerText}\n`)) + log.content);
736
+ if (log.origin) {
737
+ if (log.browser) {
738
+ write("\n");
739
+ }
740
+ const project = task ? this.ctx.getProjectByName(task.file.projectName || "") : this.ctx.getRootProject();
741
+ const stack = log.browser ? project.browser?.parseStacktrace(log.origin) || [] : parseStacktrace(log.origin);
742
+ const highlight = task && stack.find((i) => i.file === task.file.filepath);
743
+ for (const frame of stack) {
744
+ const color = frame === highlight ? c.cyan : c.gray;
745
+ const path = relative(project.config.root, frame.file);
746
+ const positions = [frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ");
747
+ write(color(` ${c.dim(F_POINTER)} ${positions}\n`));
748
+ }
749
+ }
750
+ write("\n");
751
+ }
752
+ onTestRemoved(trigger) {
753
+ this.log(c.yellow("Test removed...") + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : ""));
754
+ }
755
+ shouldLog(log, taskState) {
756
+ if (this.ctx.config.silent === true) {
757
+ return false;
758
+ }
759
+ if (this.ctx.config.silent === "passed-only" && taskState !== "failed") {
760
+ return false;
761
+ }
762
+ const shouldLog = this.ctx.config.onConsoleLog?.(log.content, log.type);
763
+ if (shouldLog === false) {
764
+ return shouldLog;
765
+ }
766
+ return true;
767
+ }
768
+ onServerRestart(reason) {
769
+ this.log(c.bold(c.magenta(reason === "config" ? "\nRestarting due to config changes..." : "\nRestarting Vitest...")));
770
+ }
771
+ reportSummary(files, errors) {
772
+ this.printErrorsSummary(files, errors);
773
+ if (this.ctx.config.mode === "benchmark") {
774
+ this.reportBenchmarkSummary(files);
775
+ } else {
776
+ this.reportTestSummary(files, errors);
777
+ }
778
+ }
779
+ reportTestSummary(files, errors) {
780
+ this.log();
781
+ const affectedFiles = [...this.failedUnwatchedFiles, ...files];
782
+ const tests = getTests(affectedFiles);
783
+ const snapshotOutput = renderSnapshotSummary(this.ctx.config.root, this.ctx.snapshot.summary);
784
+ for (const [index, snapshot] of snapshotOutput.entries()) {
785
+ const title = index === 0 ? "Snapshots" : "";
786
+ this.log(`${padSummaryTitle(title)} ${snapshot}`);
787
+ }
788
+ if (snapshotOutput.length > 1) {
789
+ this.log();
790
+ }
791
+ this.log(padSummaryTitle("Test Files"), getStateString$1(affectedFiles));
792
+ this.log(padSummaryTitle("Tests"), getStateString$1(tests));
793
+ if (this.ctx.projects.some((c) => c.config.typecheck.enabled)) {
794
+ const failed = tests.filter((t) => t.meta?.typecheck && t.result?.errors?.length);
795
+ this.log(padSummaryTitle("Type Errors"), failed.length ? c.bold(c.red(`${failed.length} failed`)) : c.dim("no errors"));
796
+ }
797
+ if (errors.length) {
798
+ this.log(padSummaryTitle("Errors"), c.bold(c.red(`${errors.length} error${errors.length > 1 ? "s" : ""}`)));
799
+ }
800
+ this.log(padSummaryTitle("Start at"), this._timeStart);
801
+ const collectTime = sum(files, (file) => file.collectDuration);
802
+ const testsTime = sum(files, (file) => file.result?.duration);
803
+ const setupTime = sum(files, (file) => file.setupDuration);
804
+ if (this.watchFilters) {
805
+ this.log(padSummaryTitle("Duration"), formatTime(collectTime + testsTime + setupTime));
806
+ } else {
807
+ const executionTime = this.end - this.start;
808
+ const environmentTime = sum(files, (file) => file.environmentLoad);
809
+ const prepareTime = sum(files, (file) => file.prepareDuration);
810
+ const transformTime = sum(this.ctx.projects, (project) => project.vitenode.getTotalDuration());
811
+ const typecheck = sum(this.ctx.projects, (project) => project.typechecker?.getResult().time);
812
+ const timers = [
813
+ `transform ${formatTime(transformTime)}`,
814
+ `setup ${formatTime(setupTime)}`,
815
+ `collect ${formatTime(collectTime)}`,
816
+ `tests ${formatTime(testsTime)}`,
817
+ `environment ${formatTime(environmentTime)}`,
818
+ `prepare ${formatTime(prepareTime)}`,
819
+ typecheck && `typecheck ${formatTime(typecheck)}`
820
+ ].filter(Boolean).join(", ");
821
+ this.log(padSummaryTitle("Duration"), formatTime(executionTime) + c.dim(` (${timers})`));
822
+ }
823
+ this.log();
824
+ }
825
+ printErrorsSummary(files, errors) {
826
+ const suites = getSuites(files);
827
+ const tests = getTests(files);
828
+ const failedSuites = suites.filter((i) => i.result?.errors);
829
+ const failedTests = tests.filter((i) => i.result?.state === "fail");
830
+ const failedTotal = countTestErrors(failedSuites) + countTestErrors(failedTests);
831
+ let current = 1;
832
+ const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)))}\n`);
833
+ if (failedSuites.length) {
834
+ this.error(`\n${errorBanner(`Failed Suites ${failedSuites.length}`)}\n`);
835
+ this.printTaskErrors(failedSuites, errorDivider);
836
+ }
837
+ if (failedTests.length) {
838
+ this.error(`\n${errorBanner(`Failed Tests ${failedTests.length}`)}\n`);
839
+ this.printTaskErrors(failedTests, errorDivider);
840
+ }
841
+ if (errors.length) {
842
+ this.ctx.logger.printUnhandledErrors(errors);
843
+ this.error();
844
+ }
845
+ }
846
+ reportBenchmarkSummary(files) {
847
+ const benches = getTests(files);
848
+ const topBenches = benches.filter((i) => i.result?.benchmark?.rank === 1);
849
+ this.log(`\n${withLabel("cyan", "BENCH", "Summary\n")}`);
850
+ for (const bench of topBenches) {
851
+ const group = bench.suite || bench.file;
852
+ if (!group) {
853
+ continue;
854
+ }
855
+ const groupName = getFullName(group, c.dim(" > "));
856
+ this.log(` ${formatProjectName(bench.file.projectName)}${bench.name}${c.dim(` - ${groupName}`)}`);
857
+ const siblings = group.tasks.filter((i) => i.meta.benchmark && i.result?.benchmark && i !== bench).sort((a, b) => a.result.benchmark.rank - b.result.benchmark.rank);
858
+ for (const sibling of siblings) {
859
+ const number = (sibling.result.benchmark.mean / bench.result.benchmark.mean).toFixed(2);
860
+ this.log(c.green(` ${number}x `) + c.gray("faster than ") + sibling.name);
861
+ }
862
+ this.log("");
863
+ }
864
+ }
865
+ printTaskErrors(tasks, errorDivider) {
866
+ const errorsQueue = [];
867
+ for (const task of tasks) {
868
+ task.result?.errors?.forEach((error) => {
869
+ let previous;
870
+ if (error?.stackStr) {
871
+ previous = errorsQueue.find((i) => {
872
+ if (i[0]?.stackStr !== error.stackStr) {
873
+ return false;
874
+ }
875
+ const currentProjectName = task?.projectName || task.file?.projectName || "";
876
+ const projectName = i[1][0]?.projectName || i[1][0].file?.projectName || "";
877
+ return projectName === currentProjectName;
878
+ });
879
+ }
880
+ if (previous) {
881
+ previous[1].push(task);
882
+ } else {
883
+ errorsQueue.push([error, [task]]);
884
+ }
885
+ });
886
+ }
887
+ for (const [error, tasks] of errorsQueue) {
888
+ for (const task of tasks) {
889
+ const filepath = task?.filepath || "";
890
+ const projectName = task?.projectName || task.file?.projectName || "";
891
+ let name = getFullName(task, c.dim(" > "));
892
+ if (filepath) {
893
+ name += c.dim(` [ ${this.relative(filepath)} ]`);
894
+ }
895
+ this.ctx.logger.error(`${c.red(c.bold(c.inverse(" FAIL ")))} ${formatProjectName(projectName)}${name}`);
896
+ }
897
+ const screenshotPaths = tasks.map((t) => t.meta?.failScreenshotPath).filter((screenshot) => screenshot != null);
898
+ this.ctx.logger.printError(error, {
899
+ project: this.ctx.getProjectByName(tasks[0].file.projectName || ""),
900
+ verbose: this.verbose,
901
+ screenshotPaths,
902
+ task: tasks[0]
903
+ });
904
+ errorDivider();
905
+ }
906
+ }
907
+ }
908
+ function errorBanner(message) {
909
+ return c.red(divider(c.bold(c.inverse(` ${message} `))));
910
+ }
911
+ function sum(items, cb) {
912
+ return items.reduce((total, next) => {
913
+ return total + Math.max(cb(next) || 0, 0);
914
+ }, 0);
915
+ }
916
+
917
+ class BasicReporter extends BaseReporter {
918
+ constructor() {
919
+ super();
920
+ this.isTTY = false;
921
+ }
922
+ onInit(ctx) {
923
+ super.onInit(ctx);
924
+ ctx.logger.log(c.inverse(c.bold(c.yellow(" DEPRECATED "))), c.yellow(`'basic' reporter is deprecated and will be removed in Vitest v3.\n` + `Remove 'basic' from 'reporters' option. To match 'basic' reporter 100%, use configuration:\n${JSON.stringify({ test: { reporters: [["default", { summary: false }]] } }, null, 2)}`));
925
+ }
926
+ reportSummary(files, errors) {
927
+ this.ctx.logger.log();
928
+ return super.reportSummary(files, errors);
929
+ }
930
+ }
931
+
932
+ const DEFAULT_RENDER_INTERVAL_MS = 1e3;
933
+ const ESC = "\x1B[";
934
+ const CLEAR_LINE = `${ESC}K`;
935
+ const MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`;
936
+ const SYNC_START = `${ESC}?2026h`;
937
+ const SYNC_END = `${ESC}?2026l`;
938
+ /**
939
+ * Renders content of `getWindow` at the bottom of the terminal and
940
+ * forwards all other intercepted `stdout` and `stderr` logs above it.
941
+ */
942
+ class WindowRenderer {
943
+ options;
944
+ streams;
945
+ buffer = [];
946
+ renderInterval = undefined;
947
+ renderScheduled = false;
948
+ windowHeight = 0;
949
+ finished = false;
950
+ cleanups = [];
951
+ constructor(options) {
952
+ this.options = {
953
+ interval: DEFAULT_RENDER_INTERVAL_MS,
954
+ ...options
955
+ };
956
+ this.streams = {
957
+ output: options.logger.outputStream.write.bind(options.logger.outputStream),
958
+ error: options.logger.errorStream.write.bind(options.logger.errorStream)
959
+ };
960
+ this.cleanups.push(this.interceptStream(process.stdout, "output"), this.interceptStream(process.stderr, "error"));
961
+ this.options.logger.onTerminalCleanup(() => {
962
+ this.flushBuffer();
963
+ this.stop();
964
+ });
965
+ this.start();
966
+ }
967
+ start() {
968
+ this.finished = false;
969
+ this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref();
970
+ }
971
+ stop() {
972
+ this.cleanups.splice(0).map((fn) => fn());
973
+ clearInterval(this.renderInterval);
974
+ }
975
+ /**
976
+ * Write all buffered output and stop buffering.
977
+ * All intercepted writes are forwarded to actual write after this.
978
+ */
979
+ finish() {
980
+ this.finished = true;
981
+ this.flushBuffer();
982
+ clearInterval(this.renderInterval);
983
+ }
984
+ /**
985
+ * Queue new render update
986
+ */
987
+ schedule() {
988
+ if (!this.renderScheduled) {
989
+ this.renderScheduled = true;
990
+ this.flushBuffer();
991
+ setTimeout(() => {
992
+ this.renderScheduled = false;
993
+ }, 100).unref();
994
+ }
995
+ }
996
+ flushBuffer() {
997
+ if (this.buffer.length === 0) {
998
+ return this.render();
999
+ }
1000
+ let current;
1001
+ for (const next of this.buffer.splice(0)) {
1002
+ if (!current) {
1003
+ current = next;
1004
+ continue;
1005
+ }
1006
+ if (current.type !== next.type) {
1007
+ this.render(current.message, current.type);
1008
+ current = next;
1009
+ continue;
1010
+ }
1011
+ current.message += next.message;
1012
+ }
1013
+ if (current) {
1014
+ this.render(current?.message, current?.type);
1015
+ }
1016
+ }
1017
+ render(message, type = "output") {
1018
+ if (this.finished) {
1019
+ this.clearWindow();
1020
+ return this.write(message || "", type);
1021
+ }
1022
+ const windowContent = this.options.getWindow();
1023
+ const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns());
1024
+ let padding = this.windowHeight - rowCount;
1025
+ if (padding > 0 && message) {
1026
+ padding -= getRenderedRowCount([message], this.options.logger.getColumns());
1027
+ }
1028
+ this.write(SYNC_START);
1029
+ this.clearWindow();
1030
+ if (message) {
1031
+ this.write(message, type);
1032
+ }
1033
+ if (padding > 0) {
1034
+ this.write("\n".repeat(padding));
1035
+ }
1036
+ this.write(windowContent.join("\n"));
1037
+ this.write(SYNC_END);
1038
+ this.windowHeight = rowCount + Math.max(0, padding);
1039
+ }
1040
+ clearWindow() {
1041
+ if (this.windowHeight === 0) {
1042
+ return;
1043
+ }
1044
+ this.write(CLEAR_LINE);
1045
+ for (let i = 1; i < this.windowHeight; i++) {
1046
+ this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`);
1047
+ }
1048
+ this.windowHeight = 0;
1049
+ }
1050
+ interceptStream(stream, type) {
1051
+ const original = stream.write;
1052
+ stream.write = (chunk, _, callback) => {
1053
+ if (chunk) {
1054
+ if (this.finished) {
1055
+ this.write(chunk.toString(), type);
1056
+ } else {
1057
+ this.buffer.push({
1058
+ type,
1059
+ message: chunk.toString()
1060
+ });
1061
+ }
1062
+ }
1063
+ callback?.();
1064
+ };
1065
+ return function restore() {
1066
+ stream.write = original;
1067
+ };
1068
+ }
1069
+ write(message, type = "output") {
1070
+ this.streams[type](message);
1071
+ }
1072
+ }
1073
+ /** Calculate the actual row count needed to render `rows` into `stream` */
1074
+ function getRenderedRowCount(rows, columns) {
1075
+ let count = 0;
1076
+ for (const row of rows) {
1077
+ const text = stripVTControlCharacters(row);
1078
+ count += Math.max(1, Math.ceil(text.length / columns));
1079
+ }
1080
+ return count;
1081
+ }
1082
+
1083
+ const DURATION_UPDATE_INTERVAL_MS = 100;
1084
+ const FINISHED_TEST_CLEANUP_TIME_MS = 1e3;
1085
+ /**
1086
+ * Reporter extension that renders summary and forwards all other logs above itself.
1087
+ * Intended to be used by other reporters, not as a standalone reporter.
1088
+ */
1089
+ class SummaryReporter {
1090
+ ctx;
1091
+ options;
1092
+ renderer;
1093
+ modules = emptyCounters();
1094
+ tests = emptyCounters();
1095
+ maxParallelTests = 0;
1096
+ /** Currently running test modules, may include finished test modules too */
1097
+ runningModules = new Map();
1098
+ /** ID of finished `this.runningModules` that are currently being shown */
1099
+ finishedModules = new Map();
1100
+ startTime = "";
1101
+ currentTime = 0;
1102
+ duration = 0;
1103
+ durationInterval = undefined;
1104
+ onInit(ctx, options = {}) {
1105
+ this.ctx = ctx;
1106
+ this.options = {
1107
+ verbose: false,
1108
+ ...options
1109
+ };
1110
+ this.renderer = new WindowRenderer({
1111
+ logger: ctx.logger,
1112
+ getWindow: () => this.createSummary()
1113
+ });
1114
+ this.ctx.onClose(() => {
1115
+ clearInterval(this.durationInterval);
1116
+ this.renderer.stop();
1117
+ });
1118
+ }
1119
+ onTestRunStart(specifications) {
1120
+ this.runningModules.clear();
1121
+ this.finishedModules.clear();
1122
+ this.modules = emptyCounters();
1123
+ this.tests = emptyCounters();
1124
+ this.startTimers();
1125
+ this.renderer.start();
1126
+ this.modules.total = specifications.length;
1127
+ }
1128
+ onTestRunEnd() {
1129
+ this.runningModules.clear();
1130
+ this.finishedModules.clear();
1131
+ this.renderer.finish();
1132
+ clearInterval(this.durationInterval);
1133
+ }
1134
+ onTestModuleQueued(module) {
1135
+ if (this.finishedModules.size) {
1136
+ const finished = this.finishedModules.keys().next().value;
1137
+ this.removeTestModule(finished);
1138
+ }
1139
+ this.runningModules.set(module.id, initializeStats(module));
1140
+ this.renderer.schedule();
1141
+ }
1142
+ onTestModuleCollected(module) {
1143
+ let stats = this.runningModules.get(module.id);
1144
+ if (!stats) {
1145
+ stats = initializeStats(module);
1146
+ this.runningModules.set(module.id, stats);
1147
+ }
1148
+ const total = Array.from(module.children.allTests()).length;
1149
+ this.tests.total += total;
1150
+ stats.total = total;
1151
+ this.maxParallelTests = Math.max(this.maxParallelTests, this.runningModules.size);
1152
+ this.renderer.schedule();
1153
+ }
1154
+ onHookStart(options) {
1155
+ const stats = this.getHookStats(options);
1156
+ if (!stats) {
1157
+ return;
1158
+ }
1159
+ const hook = {
1160
+ name: options.name,
1161
+ visible: false,
1162
+ startTime: performance.now(),
1163
+ onFinish: () => {}
1164
+ };
1165
+ stats.hook?.onFinish?.();
1166
+ stats.hook = hook;
1167
+ const timeout = setTimeout(() => {
1168
+ hook.visible = true;
1169
+ }, this.ctx.config.slowTestThreshold).unref();
1170
+ hook.onFinish = () => clearTimeout(timeout);
1171
+ }
1172
+ onHookEnd(options) {
1173
+ const stats = this.getHookStats(options);
1174
+ if (stats?.hook?.name !== options.name) {
1175
+ return;
1176
+ }
1177
+ stats.hook.onFinish();
1178
+ stats.hook.visible = false;
1179
+ }
1180
+ onTestCaseReady(test) {
1181
+ if (!this.options.verbose) {
1182
+ return;
1183
+ }
1184
+ const stats = this.runningModules.get(test.module.id);
1185
+ if (!stats || stats.tests.has(test.id)) {
1186
+ return;
1187
+ }
1188
+ const slowTest = {
1189
+ name: test.name,
1190
+ visible: false,
1191
+ startTime: performance.now(),
1192
+ onFinish: () => {}
1193
+ };
1194
+ const timeout = setTimeout(() => {
1195
+ slowTest.visible = true;
1196
+ }, this.ctx.config.slowTestThreshold).unref();
1197
+ slowTest.onFinish = () => {
1198
+ slowTest.hook?.onFinish();
1199
+ clearTimeout(timeout);
1200
+ };
1201
+ stats.tests.set(test.id, slowTest);
1202
+ }
1203
+ onTestCaseResult(test) {
1204
+ const stats = this.runningModules.get(test.module.id);
1205
+ if (!stats) {
1206
+ return;
1207
+ }
1208
+ stats.tests.get(test.id)?.onFinish();
1209
+ stats.tests.delete(test.id);
1210
+ stats.completed++;
1211
+ const result = test.result();
1212
+ if (result?.state === "passed") {
1213
+ this.tests.passed++;
1214
+ } else if (result?.state === "failed") {
1215
+ this.tests.failed++;
1216
+ } else if (!result?.state || result?.state === "skipped") {
1217
+ this.tests.skipped++;
1218
+ }
1219
+ this.renderer.schedule();
1220
+ }
1221
+ onTestModuleEnd(module) {
1222
+ const state = module.state();
1223
+ this.modules.completed++;
1224
+ if (state === "passed") {
1225
+ this.modules.passed++;
1226
+ } else if (state === "failed") {
1227
+ this.modules.failed++;
1228
+ } else if (module.task.mode === "todo" && state === "skipped") {
1229
+ this.modules.todo++;
1230
+ } else if (state === "skipped") {
1231
+ this.modules.skipped++;
1232
+ }
1233
+ const left = this.modules.total - this.modules.completed;
1234
+ if (left > this.maxParallelTests) {
1235
+ this.finishedModules.set(module.id, setTimeout(() => {
1236
+ this.removeTestModule(module.id);
1237
+ }, FINISHED_TEST_CLEANUP_TIME_MS).unref());
1238
+ } else {
1239
+ this.removeTestModule(module.id);
1240
+ }
1241
+ this.renderer.schedule();
1242
+ }
1243
+ getHookStats({ entity }) {
1244
+ if (!this.options.verbose) {
1245
+ return;
1246
+ }
1247
+ const module = entity.type === "module" ? entity : entity.module;
1248
+ const stats = this.runningModules.get(module.id);
1249
+ if (!stats) {
1250
+ return;
1251
+ }
1252
+ return entity.type === "test" ? stats.tests.get(entity.id) : stats;
1253
+ }
1254
+ createSummary() {
1255
+ const summary = [""];
1256
+ for (const testFile of Array.from(this.runningModules.values()).sort(sortRunningModules)) {
1257
+ const typecheck = testFile.typecheck ? `${c.bgBlue(c.bold(" TS "))} ` : "";
1258
+ summary.push(c.bold(c.yellow(` ${F_POINTER} `)) + formatProjectName(testFile.projectName) + typecheck + testFile.filename + c.dim(!testFile.completed && !testFile.total ? " [queued]" : ` ${testFile.completed}/${testFile.total}`));
1259
+ const slowTasks = [testFile.hook, ...Array.from(testFile.tests.values())].filter((t) => t != null && t.visible);
1260
+ for (const [index, task] of slowTasks.entries()) {
1261
+ const elapsed = this.currentTime - task.startTime;
1262
+ const icon = index === slowTasks.length - 1 ? F_TREE_NODE_END : F_TREE_NODE_MIDDLE;
1263
+ summary.push(c.bold(c.yellow(` ${icon} `)) + task.name + c.bold(c.yellow(` ${formatTime(Math.max(0, elapsed))}`)));
1264
+ if (task.hook?.visible) {
1265
+ summary.push(c.bold(c.yellow(` ${F_TREE_NODE_END} `)) + task.hook.name);
1266
+ }
1267
+ }
1268
+ }
1269
+ if (this.runningModules.size > 0) {
1270
+ summary.push("");
1271
+ }
1272
+ summary.push(padSummaryTitle("Test Files") + getStateString(this.modules));
1273
+ summary.push(padSummaryTitle("Tests") + getStateString(this.tests));
1274
+ summary.push(padSummaryTitle("Start at") + this.startTime);
1275
+ summary.push(padSummaryTitle("Duration") + formatTime(this.duration));
1276
+ summary.push("");
1277
+ return summary;
1278
+ }
1279
+ startTimers() {
1280
+ const start = performance.now();
1281
+ this.startTime = formatTimeString(new Date());
1282
+ this.durationInterval = setInterval(() => {
1283
+ this.currentTime = performance.now();
1284
+ this.duration = this.currentTime - start;
1285
+ }, DURATION_UPDATE_INTERVAL_MS).unref();
1286
+ }
1287
+ removeTestModule(id) {
1288
+ if (!id) {
1289
+ return;
1290
+ }
1291
+ const testFile = this.runningModules.get(id);
1292
+ testFile?.hook?.onFinish();
1293
+ testFile?.tests?.forEach((test) => test.onFinish());
1294
+ this.runningModules.delete(id);
1295
+ clearTimeout(this.finishedModules.get(id));
1296
+ this.finishedModules.delete(id);
1297
+ }
1298
+ }
1299
+ function emptyCounters() {
1300
+ return {
1301
+ completed: 0,
1302
+ passed: 0,
1303
+ failed: 0,
1304
+ skipped: 0,
1305
+ todo: 0,
1306
+ total: 0
1307
+ };
1308
+ }
1309
+ function getStateString(entry) {
1310
+ return [
1311
+ entry.failed ? c.bold(c.red(`${entry.failed} failed`)) : null,
1312
+ c.bold(c.green(`${entry.passed} passed`)),
1313
+ entry.skipped ? c.yellow(`${entry.skipped} skipped`) : null,
1314
+ entry.todo ? c.gray(`${entry.todo} todo`) : null
1315
+ ].filter(Boolean).join(c.dim(" | ")) + c.gray(` (${entry.total})`);
1316
+ }
1317
+ function sortRunningModules(a, b) {
1318
+ if ((a.projectName || "") > (b.projectName || "")) {
1319
+ return 1;
1320
+ }
1321
+ if ((a.projectName || "") < (b.projectName || "")) {
1322
+ return -1;
1323
+ }
1324
+ return a.filename.localeCompare(b.filename);
1325
+ }
1326
+ function initializeStats(module) {
1327
+ return {
1328
+ total: 0,
1329
+ completed: 0,
1330
+ filename: module.task.name,
1331
+ projectName: module.project.name,
1332
+ tests: new Map(),
1333
+ typecheck: !!module.task.meta.typecheck
1334
+ };
1335
+ }
1336
+
1337
+ class DefaultReporter extends BaseReporter {
1338
+ options;
1339
+ summary;
1340
+ constructor(options = {}) {
1341
+ super(options);
1342
+ this.options = {
1343
+ summary: true,
1344
+ ...options
1345
+ };
1346
+ if (!this.isTTY) {
1347
+ this.options.summary = false;
1348
+ }
1349
+ if (this.options.summary) {
1350
+ this.summary = new SummaryReporter();
1351
+ }
1352
+ }
1353
+ onTestRunStart(specifications) {
1354
+ this.summary?.onTestRunStart(specifications);
1355
+ }
1356
+ onTestModuleQueued(file) {
1357
+ this.summary?.onTestModuleQueued(file);
1358
+ }
1359
+ onTestModuleCollected(module) {
1360
+ this.summary?.onTestModuleCollected(module);
1361
+ }
1362
+ onTestModuleEnd(module) {
1363
+ super.onTestModuleEnd(module);
1364
+ this.summary?.onTestModuleEnd(module);
1365
+ }
1366
+ onTestCaseReady(test) {
1367
+ this.summary?.onTestCaseReady(test);
1368
+ }
1369
+ onTestCaseResult(test) {
1370
+ super.onTestCaseResult(test);
1371
+ this.summary?.onTestCaseResult(test);
1372
+ }
1373
+ onHookStart(hook) {
1374
+ this.summary?.onHookStart(hook);
1375
+ }
1376
+ onHookEnd(hook) {
1377
+ this.summary?.onHookEnd(hook);
1378
+ }
1379
+ onInit(ctx) {
1380
+ super.onInit(ctx);
1381
+ this.summary?.onInit(ctx, { verbose: this.verbose });
1382
+ }
1383
+ onPathsCollected(paths = []) {
1384
+ if (this.isTTY) {
1385
+ if (this.renderSucceed === undefined) {
1386
+ this.renderSucceed = !!this.renderSucceed;
1387
+ }
1388
+ if (this.renderSucceed !== true) {
1389
+ this.renderSucceed = paths.length <= 1;
1390
+ }
1391
+ }
1392
+ }
1393
+ onTestRunEnd() {
1394
+ this.summary?.onTestRunEnd();
1395
+ }
1396
+ }
1397
+
1398
+ class DotReporter extends BaseReporter {
1399
+ renderer;
1400
+ tests = new Map();
1401
+ finishedTests = new Set();
1402
+ onInit(ctx) {
1403
+ super.onInit(ctx);
1404
+ if (this.isTTY) {
1405
+ this.renderer = new WindowRenderer({
1406
+ logger: ctx.logger,
1407
+ getWindow: () => this.createSummary()
1408
+ });
1409
+ this.ctx.onClose(() => this.renderer?.stop());
1410
+ }
1411
+ }
1412
+ printTask(task) {
1413
+ if (!this.isTTY) {
1414
+ super.printTask(task);
1415
+ }
1416
+ }
1417
+ onWatcherRerun(files, trigger) {
1418
+ this.tests.clear();
1419
+ this.renderer?.start();
1420
+ super.onWatcherRerun(files, trigger);
1421
+ }
1422
+ onFinished(files, errors) {
1423
+ if (this.isTTY) {
1424
+ const finalLog = formatTests(Array.from(this.tests.values()));
1425
+ this.ctx.logger.log(finalLog);
1426
+ }
1427
+ this.tests.clear();
1428
+ this.renderer?.finish();
1429
+ super.onFinished(files, errors);
1430
+ }
1431
+ onTestModuleCollected(module) {
1432
+ for (const test of module.children.allTests()) {
1433
+ this.onTestCaseReady(test);
1434
+ }
1435
+ }
1436
+ onTestCaseReady(test) {
1437
+ if (this.finishedTests.has(test.id)) {
1438
+ return;
1439
+ }
1440
+ this.tests.set(test.id, test.result().state || "run");
1441
+ this.renderer?.schedule();
1442
+ }
1443
+ onTestCaseResult(test) {
1444
+ super.onTestCaseResult(test);
1445
+ this.finishedTests.add(test.id);
1446
+ this.tests.set(test.id, test.result().state || "skipped");
1447
+ this.renderer?.schedule();
1448
+ }
1449
+ onTestModuleEnd(testModule) {
1450
+ super.onTestModuleEnd(testModule);
1451
+ if (!this.isTTY) {
1452
+ return;
1453
+ }
1454
+ const columns = this.ctx.logger.getColumns();
1455
+ if (this.tests.size < columns) {
1456
+ return;
1457
+ }
1458
+ const finishedTests = Array.from(this.tests).filter((entry) => entry[1] !== "pending");
1459
+ if (finishedTests.length < columns) {
1460
+ return;
1461
+ }
1462
+ const states = [];
1463
+ let count = 0;
1464
+ for (const [id, state] of finishedTests) {
1465
+ if (count++ >= columns) {
1466
+ break;
1467
+ }
1468
+ this.tests.delete(id);
1469
+ states.push(state);
1470
+ }
1471
+ this.ctx.logger.log(formatTests(states));
1472
+ this.renderer?.schedule();
1473
+ }
1474
+ createSummary() {
1475
+ return [formatTests(Array.from(this.tests.values())), ""];
1476
+ }
1477
+ }
1478
+ const pass = {
1479
+ char: "·",
1480
+ color: c.green
1481
+ };
1482
+ const fail = {
1483
+ char: "x",
1484
+ color: c.red
1485
+ };
1486
+ const pending = {
1487
+ char: "*",
1488
+ color: c.yellow
1489
+ };
1490
+ const skip = {
1491
+ char: "-",
1492
+ color: (char) => c.dim(c.gray(char))
1493
+ };
1494
+ function getIcon(state) {
1495
+ switch (state) {
1496
+ case "passed": return pass;
1497
+ case "failed": return fail;
1498
+ case "skipped": return skip;
1499
+ default: return pending;
1500
+ }
1501
+ }
1502
+ /**
1503
+ * Format test states into string while keeping ANSI escapes at minimal.
1504
+ * Sibling icons with same color are merged into a single c.color() call.
1505
+ */
1506
+ function formatTests(states) {
1507
+ let currentIcon = pending;
1508
+ let count = 0;
1509
+ let output = "";
1510
+ for (const state of states) {
1511
+ const icon = getIcon(state);
1512
+ if (currentIcon === icon) {
1513
+ count++;
1514
+ continue;
1515
+ }
1516
+ output += currentIcon.color(currentIcon.char.repeat(count));
1517
+ count = 1;
1518
+ currentIcon = icon;
1519
+ }
1520
+ output += currentIcon.color(currentIcon.char.repeat(count));
1521
+ return output;
1522
+ }
1523
+
1524
+ class GithubActionsReporter {
1525
+ ctx = undefined;
1526
+ onInit(ctx) {
1527
+ this.ctx = ctx;
1528
+ }
1529
+ onFinished(files = [], errors = []) {
1530
+ const projectErrors = new Array();
1531
+ for (const error of errors) {
1532
+ projectErrors.push({
1533
+ project: this.ctx.getRootProject(),
1534
+ title: "Unhandled error",
1535
+ error
1536
+ });
1537
+ }
1538
+ for (const file of files) {
1539
+ const tasks = getTasks(file);
1540
+ const project = this.ctx.getProjectByName(file.projectName || "");
1541
+ for (const task of tasks) {
1542
+ if (task.result?.state !== "fail") {
1543
+ continue;
1544
+ }
1545
+ const title = getFullName(task, " > ");
1546
+ for (const error of task.result?.errors ?? []) {
1547
+ projectErrors.push({
1548
+ project,
1549
+ title,
1550
+ error,
1551
+ file
1552
+ });
1553
+ }
1554
+ }
1555
+ }
1556
+ for (const { project, title, error, file } of projectErrors) {
1557
+ const result = capturePrintError(error, this.ctx, {
1558
+ project,
1559
+ task: file
1560
+ });
1561
+ const stack = result?.nearest;
1562
+ if (!stack) {
1563
+ continue;
1564
+ }
1565
+ const formatted = formatMessage({
1566
+ command: "error",
1567
+ properties: {
1568
+ file: stack.file,
1569
+ title,
1570
+ line: String(stack.line),
1571
+ column: String(stack.column)
1572
+ },
1573
+ message: stripVTControlCharacters(result.output)
1574
+ });
1575
+ this.ctx.logger.log(`\n${formatted}`);
1576
+ }
1577
+ }
1578
+ }
1579
+ function formatMessage({ command, properties, message }) {
1580
+ let result = `::${command}`;
1581
+ Object.entries(properties).forEach(([k, v], i) => {
1582
+ result += i === 0 ? " " : ",";
1583
+ result += `${k}=${escapeProperty(v)}`;
1584
+ });
1585
+ result += `::${escapeData(message)}`;
1586
+ return result;
1587
+ }
1588
+ function escapeData(s) {
1589
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
1590
+ }
1591
+ function escapeProperty(s) {
1592
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
1593
+ }
1594
+
1595
+ const StatusMap = {
1596
+ fail: "failed",
1597
+ only: "pending",
1598
+ pass: "passed",
1599
+ run: "pending",
1600
+ skip: "skipped",
1601
+ todo: "todo",
1602
+ queued: "pending"
1603
+ };
1604
+ class JsonReporter {
1605
+ start = 0;
1606
+ ctx;
1607
+ options;
1608
+ constructor(options) {
1609
+ this.options = options;
1610
+ }
1611
+ onInit(ctx) {
1612
+ this.ctx = ctx;
1613
+ this.start = Date.now();
1614
+ }
1615
+ async logTasks(files, coverageMap) {
1616
+ const suites = getSuites(files);
1617
+ const numTotalTestSuites = suites.length;
1618
+ const tests = getTests(files);
1619
+ const numTotalTests = tests.length;
1620
+ const numFailedTestSuites = suites.filter((s) => s.result?.state === "fail").length;
1621
+ const numPendingTestSuites = suites.filter((s) => s.result?.state === "run" || s.result?.state === "queued" || s.mode === "todo").length;
1622
+ const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites - numPendingTestSuites;
1623
+ const numFailedTests = tests.filter((t) => t.result?.state === "fail").length;
1624
+ const numPassedTests = tests.filter((t) => t.result?.state === "pass").length;
1625
+ const numPendingTests = tests.filter((t) => t.result?.state === "run" || t.result?.state === "queued" || t.mode === "skip" || t.result?.state === "skip").length;
1626
+ const numTodoTests = tests.filter((t) => t.mode === "todo").length;
1627
+ const testResults = [];
1628
+ const success = !!(files.length > 0 || this.ctx.config.passWithNoTests) && numFailedTestSuites === 0 && numFailedTests === 0;
1629
+ for (const file of files) {
1630
+ const tests = getTests([file]);
1631
+ let startTime = tests.reduce((prev, next) => Math.min(prev, next.result?.startTime ?? Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
1632
+ if (startTime === Number.POSITIVE_INFINITY) {
1633
+ startTime = this.start;
1634
+ }
1635
+ const endTime = tests.reduce((prev, next) => Math.max(prev, (next.result?.startTime ?? 0) + (next.result?.duration ?? 0)), startTime);
1636
+ const assertionResults = tests.map((t) => {
1637
+ const ancestorTitles = [];
1638
+ let iter = t.suite;
1639
+ while (iter) {
1640
+ ancestorTitles.push(iter.name);
1641
+ iter = iter.suite;
1642
+ }
1643
+ ancestorTitles.reverse();
1644
+ return {
1645
+ ancestorTitles,
1646
+ fullName: t.name ? [...ancestorTitles, t.name].join(" ") : ancestorTitles.join(" "),
1647
+ status: StatusMap[t.result?.state || t.mode] || "skipped",
1648
+ title: t.name,
1649
+ duration: t.result?.duration,
1650
+ failureMessages: t.result?.errors?.map((e) => e.stack || e.message) || [],
1651
+ location: t.location,
1652
+ meta: t.meta
1653
+ };
1654
+ });
1655
+ if (tests.some((t) => t.result?.state === "run" || t.result?.state === "queued")) {
1656
+ this.ctx.logger.warn("WARNING: Some tests are still running when generating the JSON report." + "This is likely an internal bug in Vitest." + "Please report it to https://github.com/vitest-dev/vitest/issues");
1657
+ }
1658
+ const hasFailedTests = tests.some((t) => t.result?.state === "fail");
1659
+ testResults.push({
1660
+ assertionResults,
1661
+ startTime,
1662
+ endTime,
1663
+ status: file.result?.state === "fail" || hasFailedTests ? "failed" : "passed",
1664
+ message: file.result?.errors?.[0]?.message ?? "",
1665
+ name: file.filepath
1666
+ });
1667
+ }
1668
+ const result = {
1669
+ numTotalTestSuites,
1670
+ numPassedTestSuites,
1671
+ numFailedTestSuites,
1672
+ numPendingTestSuites,
1673
+ numTotalTests,
1674
+ numPassedTests,
1675
+ numFailedTests,
1676
+ numPendingTests,
1677
+ numTodoTests,
1678
+ snapshot: this.ctx.snapshot.summary,
1679
+ startTime: this.start,
1680
+ success,
1681
+ testResults,
1682
+ coverageMap
1683
+ };
1684
+ await this.writeReport(JSON.stringify(result));
1685
+ }
1686
+ async onFinished(files = this.ctx.state.getFiles(), _errors = [], coverageMap) {
1687
+ await this.logTasks(files, coverageMap);
1688
+ }
1689
+ /**
1690
+ * Writes the report to an output file if specified in the config,
1691
+ * or logs it to the console otherwise.
1692
+ * @param report
1693
+ */
1694
+ async writeReport(report) {
1695
+ const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "json");
1696
+ if (outputFile) {
1697
+ const reportFile = resolve(this.ctx.config.root, outputFile);
1698
+ const outputDirectory = dirname(reportFile);
1699
+ if (!existsSync(outputDirectory)) {
1700
+ await promises.mkdir(outputDirectory, { recursive: true });
1701
+ }
1702
+ await promises.writeFile(reportFile, report, "utf-8");
1703
+ this.ctx.logger.log(`JSON report written to ${reportFile}`);
1704
+ } else {
1705
+ this.ctx.logger.log(report);
1706
+ }
1707
+ }
1708
+ }
1709
+
1710
+ class IndentedLogger {
1711
+ currentIndent = "";
1712
+ constructor(baseLog) {
1713
+ this.baseLog = baseLog;
1714
+ }
1715
+ indent() {
1716
+ this.currentIndent += " ";
1717
+ }
1718
+ unindent() {
1719
+ this.currentIndent = this.currentIndent.substring(0, this.currentIndent.length - 4);
1720
+ }
1721
+ log(text) {
1722
+ return this.baseLog(this.currentIndent + text);
1723
+ }
1724
+ }
1725
+
1726
+ function flattenTasks$1(task, baseName = "") {
1727
+ const base = baseName ? `${baseName} > ` : "";
1728
+ if (task.type === "suite") {
1729
+ return task.tasks.flatMap((child) => flattenTasks$1(child, `${base}${task.name}`));
1730
+ } else {
1731
+ return [{
1732
+ ...task,
1733
+ name: `${base}${task.name}`
1734
+ }];
1735
+ }
1736
+ }
1737
+ function removeInvalidXMLCharacters(value, removeDiscouragedChars) {
1738
+ let regex = /([\0-\x08\v\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g;
1739
+ value = String(value || "").replace(regex, "");
1740
+ {
1741
+ regex = new RegExp(
1742
+ /* eslint-disable regexp/prefer-character-class, regexp/no-obscure-range, regexp/no-useless-non-capturing-group */
1743
+ "([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|\\uD83F[\\uDFFE\\uDFFF]|(?:\\uD87F[\\uDF" + "FE\\uDFFF])|\\uD8BF[\\uDFFE\\uDFFF]|\\uD8FF[\\uDFFE\\uDFFF]|(?:\\uD93F[\\uDFFE\\uD" + "FFF])|\\uD97F[\\uDFFE\\uDFFF]|\\uD9BF[\\uDFFE\\uDFFF]|\\uD9FF[\\uDFFE\\uDFFF]" + "|\\uDA3F[\\uDFFE\\uDFFF]|\\uDA7F[\\uDFFE\\uDFFF]|\\uDABF[\\uDFFE\\uDFFF]|(?:\\" + "uDAFF[\\uDFFE\\uDFFF])|\\uDB3F[\\uDFFE\\uDFFF]|\\uDB7F[\\uDFFE\\uDFFF]|(?:\\uDBBF" + "[\\uDFFE\\uDFFF])|\\uDBFF[\\uDFFE\\uDFFF](?:[\\0-\\t\\v\\f\\x0E-\\u2027\\u202A-\\uD7FF\\" + "uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|" + "(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))",
1744
+ "g"
1745
+ /* eslint-enable */
1746
+ );
1747
+ value = value.replace(regex, "");
1748
+ }
1749
+ return value;
1750
+ }
1751
+ function escapeXML(value) {
1752
+ return removeInvalidXMLCharacters(String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/</g, "&lt;").replace(/>/g, "&gt;"));
1753
+ }
1754
+ function executionTime(durationMS) {
1755
+ return (durationMS / 1e3).toLocaleString("en-US", {
1756
+ useGrouping: false,
1757
+ maximumFractionDigits: 10
1758
+ });
1759
+ }
1760
+ function getDuration(task) {
1761
+ const duration = task.result?.duration ?? 0;
1762
+ return executionTime(duration);
1763
+ }
1764
+ class JUnitReporter {
1765
+ ctx;
1766
+ reportFile;
1767
+ baseLog;
1768
+ logger;
1769
+ _timeStart = new Date();
1770
+ fileFd;
1771
+ options;
1772
+ constructor(options) {
1773
+ this.options = { ...options };
1774
+ this.options.includeConsoleOutput ??= true;
1775
+ }
1776
+ async onInit(ctx) {
1777
+ this.ctx = ctx;
1778
+ const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "junit");
1779
+ if (outputFile) {
1780
+ this.reportFile = resolve(this.ctx.config.root, outputFile);
1781
+ const outputDirectory = dirname(this.reportFile);
1782
+ if (!existsSync(outputDirectory)) {
1783
+ await promises.mkdir(outputDirectory, { recursive: true });
1784
+ }
1785
+ const fileFd = await promises.open(this.reportFile, "w+");
1786
+ this.fileFd = fileFd;
1787
+ this.baseLog = async (text) => {
1788
+ if (!this.fileFd) {
1789
+ this.fileFd = await promises.open(this.reportFile, "w+");
1790
+ }
1791
+ await promises.writeFile(this.fileFd, `${text}\n`);
1792
+ };
1793
+ } else {
1794
+ this.baseLog = async (text) => this.ctx.logger.log(text);
1795
+ }
1796
+ this._timeStart = new Date();
1797
+ this.logger = new IndentedLogger(this.baseLog);
1798
+ }
1799
+ async writeElement(name, attrs, children) {
1800
+ const pairs = [];
1801
+ for (const key in attrs) {
1802
+ const attr = attrs[key];
1803
+ if (attr === undefined) {
1804
+ continue;
1805
+ }
1806
+ pairs.push(`${key}="${escapeXML(attr)}"`);
1807
+ }
1808
+ await this.logger.log(`<${name}${pairs.length ? ` ${pairs.join(" ")}` : ""}>`);
1809
+ this.logger.indent();
1810
+ await children.call(this);
1811
+ this.logger.unindent();
1812
+ await this.logger.log(`</${name}>`);
1813
+ }
1814
+ async writeLogs(task, type) {
1815
+ if (task.logs == null || task.logs.length === 0) {
1816
+ return;
1817
+ }
1818
+ const logType = type === "err" ? "stderr" : "stdout";
1819
+ const logs = task.logs.filter((log) => log.type === logType);
1820
+ if (logs.length === 0) {
1821
+ return;
1822
+ }
1823
+ await this.writeElement(`system-${type}`, {}, async () => {
1824
+ for (const log of logs) {
1825
+ await this.baseLog(escapeXML(log.content));
1826
+ }
1827
+ });
1828
+ }
1829
+ async writeTasks(tasks, filename) {
1830
+ for (const task of tasks) {
1831
+ let classname = filename;
1832
+ const templateVars = {
1833
+ filename: task.file.name,
1834
+ filepath: task.file.filepath
1835
+ };
1836
+ if (typeof this.options.classnameTemplate === "function") {
1837
+ classname = this.options.classnameTemplate(templateVars);
1838
+ } else if (typeof this.options.classnameTemplate === "string") {
1839
+ classname = this.options.classnameTemplate.replace(/\{filename\}/g, templateVars.filename).replace(/\{filepath\}/g, templateVars.filepath);
1840
+ } else if (typeof this.options.classname === "string") {
1841
+ classname = this.options.classname;
1842
+ }
1843
+ await this.writeElement("testcase", {
1844
+ classname,
1845
+ file: this.options.addFileAttribute ? filename : undefined,
1846
+ name: task.name,
1847
+ time: getDuration(task)
1848
+ }, async () => {
1849
+ if (this.options.includeConsoleOutput) {
1850
+ await this.writeLogs(task, "out");
1851
+ await this.writeLogs(task, "err");
1852
+ }
1853
+ if (task.mode === "skip" || task.mode === "todo") {
1854
+ await this.logger.log("<skipped/>");
1855
+ }
1856
+ if (task.result?.state === "fail") {
1857
+ const errors = task.result.errors || [];
1858
+ for (const error of errors) {
1859
+ await this.writeElement("failure", {
1860
+ message: error?.message,
1861
+ type: error?.name ?? error?.nameStr
1862
+ }, async () => {
1863
+ if (!error) {
1864
+ return;
1865
+ }
1866
+ const result = capturePrintError(error, this.ctx, {
1867
+ project: this.ctx.getProjectByName(task.file.projectName || ""),
1868
+ task
1869
+ });
1870
+ await this.baseLog(escapeXML(stripVTControlCharacters(result.output.trim())));
1871
+ });
1872
+ }
1873
+ }
1874
+ });
1875
+ }
1876
+ }
1877
+ async onFinished(files = this.ctx.state.getFiles()) {
1878
+ await this.logger.log("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
1879
+ const transformed = files.map((file) => {
1880
+ const tasks = file.tasks.flatMap((task) => flattenTasks$1(task));
1881
+ const stats = tasks.reduce((stats, task) => {
1882
+ return {
1883
+ passed: stats.passed + Number(task.result?.state === "pass"),
1884
+ failures: stats.failures + Number(task.result?.state === "fail"),
1885
+ skipped: stats.skipped + Number(task.mode === "skip" || task.mode === "todo")
1886
+ };
1887
+ }, {
1888
+ passed: 0,
1889
+ failures: 0,
1890
+ skipped: 0
1891
+ });
1892
+ const suites = getSuites(file);
1893
+ for (const suite of suites) {
1894
+ if (suite.result?.errors) {
1895
+ tasks.push(suite);
1896
+ stats.failures += 1;
1897
+ }
1898
+ }
1899
+ if (tasks.length === 0 && file.result?.state === "fail") {
1900
+ stats.failures = 1;
1901
+ tasks.push({
1902
+ id: file.id,
1903
+ type: "test",
1904
+ name: file.name,
1905
+ mode: "run",
1906
+ result: file.result,
1907
+ meta: {},
1908
+ timeout: 0,
1909
+ context: null,
1910
+ suite: null,
1911
+ file: null
1912
+ });
1913
+ }
1914
+ return {
1915
+ ...file,
1916
+ tasks,
1917
+ stats
1918
+ };
1919
+ });
1920
+ const stats = transformed.reduce((stats, file) => {
1921
+ stats.tests += file.tasks.length;
1922
+ stats.failures += file.stats.failures;
1923
+ stats.time += file.result?.duration || 0;
1924
+ return stats;
1925
+ }, {
1926
+ name: this.options.suiteName || "vitest tests",
1927
+ tests: 0,
1928
+ failures: 0,
1929
+ errors: 0,
1930
+ time: 0
1931
+ });
1932
+ await this.writeElement("testsuites", {
1933
+ ...stats,
1934
+ time: executionTime(stats.time)
1935
+ }, async () => {
1936
+ for (const file of transformed) {
1937
+ const filename = relative(this.ctx.config.root, file.filepath);
1938
+ await this.writeElement("testsuite", {
1939
+ name: filename,
1940
+ timestamp: new Date().toISOString(),
1941
+ hostname: hostname(),
1942
+ tests: file.tasks.length,
1943
+ failures: file.stats.failures,
1944
+ errors: 0,
1945
+ skipped: file.stats.skipped,
1946
+ time: getDuration(file)
1947
+ }, async () => {
1948
+ await this.writeTasks(file.tasks, filename);
1949
+ });
1950
+ }
1951
+ });
1952
+ if (this.reportFile) {
1953
+ this.ctx.logger.log(`JUNIT report written to ${this.reportFile}`);
1954
+ }
1955
+ await this.fileFd?.close();
1956
+ this.fileFd = undefined;
1957
+ }
1958
+ }
1959
+
1960
+ function yamlString(str) {
1961
+ return `"${str.replace(/"/g, "\\\"")}"`;
1962
+ }
1963
+ function tapString(str) {
1964
+ return str.replace(/\\/g, "\\\\").replace(/#/g, "\\#").replace(/\n/g, " ");
1965
+ }
1966
+ class TapReporter {
1967
+ ctx;
1968
+ logger;
1969
+ onInit(ctx) {
1970
+ this.ctx = ctx;
1971
+ this.logger = new IndentedLogger(ctx.logger.log.bind(ctx.logger));
1972
+ }
1973
+ static getComment(task) {
1974
+ if (task.mode === "skip") {
1975
+ return " # SKIP";
1976
+ } else if (task.mode === "todo") {
1977
+ return " # TODO";
1978
+ } else if (task.result?.duration != null) {
1979
+ return ` # time=${task.result.duration.toFixed(2)}ms`;
1980
+ } else {
1981
+ return "";
1982
+ }
1983
+ }
1984
+ logErrorDetails(error, stack) {
1985
+ const errorName = error.name || error.nameStr || "Unknown Error";
1986
+ this.logger.log(`name: ${yamlString(String(errorName))}`);
1987
+ this.logger.log(`message: ${yamlString(String(error.message))}`);
1988
+ if (stack) {
1989
+ this.logger.log(`stack: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`);
1990
+ }
1991
+ }
1992
+ logTasks(tasks) {
1993
+ this.logger.log(`1..${tasks.length}`);
1994
+ for (const [i, task] of tasks.entries()) {
1995
+ const id = i + 1;
1996
+ const ok = task.result?.state === "pass" || task.mode === "skip" || task.mode === "todo" ? "ok" : "not ok";
1997
+ const comment = TapReporter.getComment(task);
1998
+ if (task.type === "suite" && task.tasks.length > 0) {
1999
+ this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment} {`);
2000
+ this.logger.indent();
2001
+ this.logTasks(task.tasks);
2002
+ this.logger.unindent();
2003
+ this.logger.log("}");
2004
+ } else {
2005
+ this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`);
2006
+ const project = this.ctx.getProjectByName(task.file.projectName || "");
2007
+ if (task.result?.state === "fail" && task.result.errors) {
2008
+ this.logger.indent();
2009
+ task.result.errors.forEach((error) => {
2010
+ const stacks = task.file.pool === "browser" ? project.browser?.parseErrorStacktrace(error) || [] : parseErrorStacktrace(error, { frameFilter: this.ctx.config.onStackTrace });
2011
+ const stack = stacks[0];
2012
+ this.logger.log("---");
2013
+ this.logger.log("error:");
2014
+ this.logger.indent();
2015
+ this.logErrorDetails(error);
2016
+ this.logger.unindent();
2017
+ if (stack) {
2018
+ this.logger.log(`at: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`);
2019
+ }
2020
+ if (error.showDiff) {
2021
+ this.logger.log(`actual: ${yamlString(error.actual)}`);
2022
+ this.logger.log(`expected: ${yamlString(error.expected)}`);
2023
+ }
2024
+ });
2025
+ this.logger.log("...");
2026
+ this.logger.unindent();
2027
+ }
2028
+ }
2029
+ }
2030
+ }
2031
+ onFinished(files = this.ctx.state.getFiles()) {
2032
+ this.logger.log("TAP version 13");
2033
+ this.logTasks(files);
2034
+ }
2035
+ }
2036
+
2037
+ function flattenTasks(task, baseName = "") {
2038
+ const base = baseName ? `${baseName} > ` : "";
2039
+ if (task.type === "suite" && task.tasks.length > 0) {
2040
+ return task.tasks.flatMap((child) => flattenTasks(child, `${base}${task.name}`));
2041
+ } else {
2042
+ return [{
2043
+ ...task,
2044
+ name: `${base}${task.name}`
2045
+ }];
2046
+ }
2047
+ }
2048
+ class TapFlatReporter extends TapReporter {
2049
+ onInit(ctx) {
2050
+ super.onInit(ctx);
2051
+ }
2052
+ onFinished(files = this.ctx.state.getFiles()) {
2053
+ this.ctx.logger.log("TAP version 13");
2054
+ const flatTasks = files.flatMap((task) => flattenTasks(task));
2055
+ this.logTasks(flatTasks);
2056
+ }
2057
+ }
2058
+
2059
+ class VerboseReporter extends DefaultReporter {
2060
+ verbose = true;
2061
+ renderSucceed = true;
2062
+ printTask(task) {
2063
+ if (this.isTTY) {
2064
+ return super.printTask(task);
2065
+ }
2066
+ if (task.type !== "test" || !task.result?.state || task.result?.state === "run" || task.result?.state === "queued") {
2067
+ return;
2068
+ }
2069
+ let title = ` ${getStateSymbol(task)} `;
2070
+ if (task.file.projectName) {
2071
+ title += formatProjectName(task.file.projectName);
2072
+ }
2073
+ title += getFullName(task, c.dim(" > "));
2074
+ title += super.getDurationPrefix(task);
2075
+ if (this.ctx.config.logHeapUsage && task.result.heap != null) {
2076
+ title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`);
2077
+ }
2078
+ if (task.result?.note) {
2079
+ title += c.dim(c.gray(` [${task.result.note}]`));
2080
+ }
2081
+ this.ctx.logger.log(title);
2082
+ if (task.result.state === "fail") {
2083
+ task.result.errors?.forEach((error) => this.log(c.red(` ${F_RIGHT} ${error?.message}`)));
2084
+ }
2085
+ }
2086
+ printSuite(task) {
2087
+ const indentation = " ".repeat(getIndentation(task));
2088
+ const tests = getTests(task);
2089
+ const state = getStateSymbol(task);
2090
+ this.log(` ${indentation}${state} ${task.name} ${c.dim(`(${tests.length})`)}`);
2091
+ }
2092
+ getTestName(test) {
2093
+ return test.name;
2094
+ }
2095
+ getTestIndentation(test) {
2096
+ return " ".repeat(getIndentation(test));
2097
+ }
2098
+ formatShortError() {
2099
+ return "";
2100
+ }
2101
+ }
2102
+ function getIndentation(suite, level = 1) {
2103
+ if (suite.suite && !("filepath" in suite.suite)) {
2104
+ return getIndentation(suite.suite, level + 1);
2105
+ }
2106
+ return level;
2107
+ }
2108
+
2109
+ function createBenchmarkJsonReport(files) {
2110
+ const report = { files: [] };
2111
+ for (const file of files) {
2112
+ const groups = [];
2113
+ for (const task of getTasks(file)) {
2114
+ if (task?.type === "suite") {
2115
+ const benchmarks = [];
2116
+ for (const t of task.tasks) {
2117
+ const benchmark = t.meta.benchmark && t.result?.benchmark;
2118
+ if (benchmark) {
2119
+ benchmarks.push({
2120
+ id: t.id,
2121
+ ...benchmark,
2122
+ samples: []
2123
+ });
2124
+ }
2125
+ }
2126
+ if (benchmarks.length) {
2127
+ groups.push({
2128
+ fullName: getFullName(task, " > "),
2129
+ benchmarks
2130
+ });
2131
+ }
2132
+ }
2133
+ }
2134
+ report.files.push({
2135
+ filepath: file.filepath,
2136
+ groups
2137
+ });
2138
+ }
2139
+ return report;
2140
+ }
2141
+ function flattenFormattedBenchmarkReport(report) {
2142
+ const flat = {};
2143
+ for (const file of report.files) {
2144
+ for (const group of file.groups) {
2145
+ for (const t of group.benchmarks) {
2146
+ flat[t.id] = t;
2147
+ }
2148
+ }
2149
+ }
2150
+ return flat;
2151
+ }
2152
+
2153
+ const outputMap = new WeakMap();
2154
+ function formatNumber(number) {
2155
+ const res = String(number.toFixed(number < 100 ? 4 : 2)).split(".");
2156
+ return res[0].replace(/(?=(?:\d{3})+$)\B/g, ",") + (res[1] ? `.${res[1]}` : "");
2157
+ }
2158
+ const tableHead = [
2159
+ "name",
2160
+ "hz",
2161
+ "min",
2162
+ "max",
2163
+ "mean",
2164
+ "p75",
2165
+ "p99",
2166
+ "p995",
2167
+ "p999",
2168
+ "rme",
2169
+ "samples"
2170
+ ];
2171
+ function renderBenchmarkItems(result) {
2172
+ return [
2173
+ result.name,
2174
+ formatNumber(result.hz || 0),
2175
+ formatNumber(result.min || 0),
2176
+ formatNumber(result.max || 0),
2177
+ formatNumber(result.mean || 0),
2178
+ formatNumber(result.p75 || 0),
2179
+ formatNumber(result.p99 || 0),
2180
+ formatNumber(result.p995 || 0),
2181
+ formatNumber(result.p999 || 0),
2182
+ `±${(result.rme || 0).toFixed(2)}%`,
2183
+ (result.sampleCount || 0).toString()
2184
+ ];
2185
+ }
2186
+ function computeColumnWidths(results) {
2187
+ const rows = [tableHead, ...results.map((v) => renderBenchmarkItems(v))];
2188
+ return Array.from(tableHead, (_, i) => Math.max(...rows.map((row) => stripVTControlCharacters(row[i]).length)));
2189
+ }
2190
+ function padRow(row, widths) {
2191
+ return row.map((v, i) => i ? v.padStart(widths[i], " ") : v.padEnd(widths[i], " "));
2192
+ }
2193
+ function renderTableHead(widths) {
2194
+ return " ".repeat(3) + padRow(tableHead, widths).map(c.bold).join(" ");
2195
+ }
2196
+ function renderBenchmark(result, widths) {
2197
+ const padded = padRow(renderBenchmarkItems(result), widths);
2198
+ return [
2199
+ padded[0],
2200
+ c.blue(padded[1]),
2201
+ c.cyan(padded[2]),
2202
+ c.cyan(padded[3]),
2203
+ c.cyan(padded[4]),
2204
+ c.cyan(padded[5]),
2205
+ c.cyan(padded[6]),
2206
+ c.cyan(padded[7]),
2207
+ c.cyan(padded[8]),
2208
+ c.dim(padded[9]),
2209
+ c.dim(padded[10])
2210
+ ].join(" ");
2211
+ }
2212
+ function renderTable(options) {
2213
+ const output = [];
2214
+ const benchMap = {};
2215
+ for (const task of options.tasks) {
2216
+ if (task.meta.benchmark && task.result?.benchmark) {
2217
+ benchMap[task.id] = {
2218
+ current: task.result.benchmark,
2219
+ baseline: options.compare?.[task.id]
2220
+ };
2221
+ }
2222
+ }
2223
+ const benchCount = Object.entries(benchMap).length;
2224
+ const columnWidths = computeColumnWidths(Object.values(benchMap).flatMap((v) => [v.current, v.baseline]).filter(notNullish));
2225
+ let idx = 0;
2226
+ const padding = " ".repeat(1 );
2227
+ for (const task of options.tasks) {
2228
+ const duration = task.result?.duration;
2229
+ const bench = benchMap[task.id];
2230
+ let prefix = "";
2231
+ if (idx === 0 && task.meta?.benchmark) {
2232
+ prefix += `${renderTableHead(columnWidths)}\n${padding}`;
2233
+ }
2234
+ prefix += ` ${getStateSymbol(task)} `;
2235
+ let suffix = "";
2236
+ if (task.type === "suite") {
2237
+ suffix += c.dim(` (${getTests(task).length})`);
2238
+ }
2239
+ if (task.mode === "skip" || task.mode === "todo") {
2240
+ suffix += c.dim(c.gray(" [skipped]"));
2241
+ }
2242
+ if (duration != null) {
2243
+ const color = duration > options.slowTestThreshold ? c.yellow : c.green;
2244
+ suffix += color(` ${Math.round(duration)}${c.dim("ms")}`);
2245
+ }
2246
+ if (options.showHeap && task.result?.heap != null) {
2247
+ suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`);
2248
+ }
2249
+ if (bench) {
2250
+ let body = renderBenchmark(bench.current, columnWidths);
2251
+ if (options.compare && bench.baseline) {
2252
+ if (bench.current.hz) {
2253
+ const diff = bench.current.hz / bench.baseline.hz;
2254
+ const diffFixed = diff.toFixed(2);
2255
+ if (diffFixed === "1.0.0") {
2256
+ body += c.gray(` [${diffFixed}x]`);
2257
+ }
2258
+ if (diff > 1) {
2259
+ body += c.blue(` [${diffFixed}x] ⇑`);
2260
+ } else {
2261
+ body += c.red(` [${diffFixed}x] ⇓`);
2262
+ }
2263
+ }
2264
+ output.push(padding + prefix + body + suffix);
2265
+ const bodyBaseline = renderBenchmark(bench.baseline, columnWidths);
2266
+ output.push(`${padding} ${bodyBaseline} ${c.dim("(baseline)")}`);
2267
+ } else {
2268
+ if (bench.current.rank === 1 && benchCount > 1) {
2269
+ body += c.bold(c.green(" fastest"));
2270
+ }
2271
+ if (bench.current.rank === benchCount && benchCount > 2) {
2272
+ body += c.bold(c.gray(" slowest"));
2273
+ }
2274
+ output.push(padding + prefix + body + suffix);
2275
+ }
2276
+ } else {
2277
+ output.push(padding + prefix + task.name + suffix);
2278
+ }
2279
+ if (task.result?.state !== "pass" && outputMap.get(task) != null) {
2280
+ let data = outputMap.get(task);
2281
+ if (typeof data === "string") {
2282
+ data = stripVTControlCharacters(data.trim().split("\n").filter(Boolean).pop());
2283
+ if (data === "") {
2284
+ data = undefined;
2285
+ }
2286
+ }
2287
+ if (data != null) {
2288
+ const out = ` ${" ".repeat(options.level)}${F_RIGHT} ${data}`;
2289
+ output.push(c.gray(truncateString(out, options.columns)));
2290
+ }
2291
+ }
2292
+ idx++;
2293
+ }
2294
+ return output.filter(Boolean).join("\n");
2295
+ }
2296
+
2297
+ class BenchmarkReporter extends DefaultReporter {
2298
+ compare;
2299
+ async onInit(ctx) {
2300
+ super.onInit(ctx);
2301
+ if (this.ctx.config.benchmark?.compare) {
2302
+ const compareFile = pathe.resolve(this.ctx.config.root, this.ctx.config.benchmark?.compare);
2303
+ try {
2304
+ this.compare = flattenFormattedBenchmarkReport(JSON.parse(await fs.promises.readFile(compareFile, "utf-8")));
2305
+ } catch (e) {
2306
+ this.error(`Failed to read '${compareFile}'`, e);
2307
+ }
2308
+ }
2309
+ }
2310
+ onTaskUpdate(packs) {
2311
+ for (const pack of packs) {
2312
+ const task = this.ctx.state.idMap.get(pack[0]);
2313
+ if (task?.type === "suite" && task.result?.state !== "run") {
2314
+ task.tasks.filter((task) => task.result?.benchmark).sort((benchA, benchB) => benchA.result.benchmark.mean - benchB.result.benchmark.mean).forEach((bench, idx) => {
2315
+ bench.result.benchmark.rank = Number(idx) + 1;
2316
+ });
2317
+ }
2318
+ }
2319
+ super.onTaskUpdate(packs);
2320
+ }
2321
+ printTask(task) {
2322
+ if (task?.type !== "suite" || !task.result?.state || task.result?.state === "run" || task.result?.state === "queued") {
2323
+ return;
2324
+ }
2325
+ const benches = task.tasks.filter((t) => t.meta.benchmark);
2326
+ const duration = task.result.duration;
2327
+ if (benches.length > 0 && benches.every((t) => t.result?.state !== "run" && t.result?.state !== "queued")) {
2328
+ let title = `\n ${getStateSymbol(task)} ${formatProjectName(task.file.projectName)}${getFullName(task, c.dim(" > "))}`;
2329
+ if (duration != null && duration > this.ctx.config.slowTestThreshold) {
2330
+ title += c.yellow(` ${Math.round(duration)}${c.dim("ms")}`);
2331
+ }
2332
+ this.log(title);
2333
+ this.log(renderTable({
2334
+ tasks: benches,
2335
+ level: 1,
2336
+ columns: this.ctx.logger.getColumns(),
2337
+ compare: this.compare,
2338
+ showHeap: this.ctx.config.logHeapUsage,
2339
+ slowTestThreshold: this.ctx.config.slowTestThreshold
2340
+ }));
2341
+ }
2342
+ }
2343
+ async onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
2344
+ super.onFinished(files, errors);
2345
+ let outputFile = this.ctx.config.benchmark?.outputJson;
2346
+ if (outputFile) {
2347
+ outputFile = pathe.resolve(this.ctx.config.root, outputFile);
2348
+ const outputDirectory = pathe.dirname(outputFile);
2349
+ if (!fs.existsSync(outputDirectory)) {
2350
+ await fs.promises.mkdir(outputDirectory, { recursive: true });
2351
+ }
2352
+ const output = createBenchmarkJsonReport(files);
2353
+ await fs.promises.writeFile(outputFile, JSON.stringify(output, null, 2));
2354
+ this.log(`Benchmark report written to ${outputFile}`);
2355
+ }
2356
+ }
2357
+ }
2358
+
2359
+ class VerboseBenchmarkReporter extends BenchmarkReporter {
2360
+ verbose = true;
2361
+ }
2362
+
2363
+ const BenchmarkReportsMap = {
2364
+ default: BenchmarkReporter,
2365
+ verbose: VerboseBenchmarkReporter
2366
+ };
2367
+
2368
+ const ReportersMap = {
2369
+ "default": DefaultReporter,
2370
+ "basic": BasicReporter,
2371
+ "blob": BlobReporter,
2372
+ "verbose": VerboseReporter,
2373
+ "dot": DotReporter,
2374
+ "json": JsonReporter,
2375
+ "tap": TapReporter,
2376
+ "tap-flat": TapFlatReporter,
2377
+ "junit": JUnitReporter,
2378
+ "hanging-process": HangingProcessReporter,
2379
+ "github-actions": GithubActionsReporter
2380
+ };
2381
+
2382
+ export { BasicReporter as B, DefaultReporter as D, GithubActionsReporter as G, HangingProcessReporter as H, JsonReporter as J, ReportersMap as R, TapFlatReporter as T, VerboseBenchmarkReporter as V, BenchmarkReporter as a, BenchmarkReportsMap as b, DotReporter as c, JUnitReporter as d, TapReporter as e, VerboseReporter as f, printError as g, generateCodeFrame as h, BlobReporter as i, parse as p, readBlobs as r, stringify as s };