vitest 3.0.0-beta.3 → 3.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -315
- package/config.d.ts +2 -0
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +1 -1
- package/dist/chunks/{RandomSequencer.C6x84bNN.js → RandomSequencer.DB__To1b.js} +35 -6
- package/dist/chunks/{base.CQ2VEtuH.js → base.BJ8KO-VX.js} +2 -2
- package/dist/chunks/{cac.e7qW4xLT.js → cac.BAYqQ2aM.js} +7 -7
- package/dist/chunks/{cli-api.CWDlED-m.js → cli-api.Dhl34Trr.js} +24 -24
- package/dist/chunks/{config.BTPBhmK5.d.ts → config.BRtC-JeT.d.ts} +6 -0
- package/dist/chunks/{console.BYGVloWk.js → console.CN7AiMGV.js} +16 -7
- package/dist/chunks/{execute.2pr0rHgK.js → execute.BMOaRArH.js} +27 -16
- package/dist/chunks/{globals.BFncSRNA.js → globals.C5RQxaV3.js} +2 -2
- package/dist/chunks/{index.BBoOXW-l.js → index.B2M9nD1V.js} +1 -1
- package/dist/chunks/{index.CkWmZCXU.js → index.BQbxGbG9.js} +1 -1
- package/dist/chunks/index.CAueP3cK.js +3205 -0
- package/dist/chunks/{reporters.DCiyjXOg.d.ts → reporters.Dcdq51WE.d.ts} +80 -155
- package/dist/chunks/{resolveConfig.C1d7TK-U.js → resolveConfig.kZFMjKCQ.js} +4 -4
- package/dist/chunks/{runBaseTests.qNWRkgHj.js → runBaseTests.URiUrnWK.js} +8 -6
- package/dist/chunks/{setup-common.Cp_bu5q3.js → setup-common.D0zLenuv.js} +1 -1
- package/dist/chunks/{utils.Coei4Wlj.js → utils.yHKcm4dz.js} +9 -20
- package/dist/chunks/{vi.S4Fq8wSo.js → vi.Da_PT3Vw.js} +554 -272
- package/dist/chunks/{vite.CRSMFy31.d.ts → vite.DzluO1Kj.d.ts} +1 -1
- package/dist/chunks/{vm.DGhTouO3.js → vm.DrFVeTXo.js} +4 -4
- package/dist/chunks/{worker.R-PA7DpW.d.ts → worker.BIVMnzXw.d.ts} +1 -1
- package/dist/chunks/{worker.XbtCXEXv.d.ts → worker.Hz_LAzfd.d.ts} +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/coverage.d.ts +2 -2
- package/dist/coverage.js +2 -2
- package/dist/execute.d.ts +3 -3
- package/dist/execute.js +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +2 -2
- package/dist/node.d.ts +5 -5
- package/dist/node.js +8 -9
- package/dist/reporters.d.ts +2 -2
- package/dist/reporters.js +3 -7
- package/dist/runners.d.ts +1 -1
- package/dist/runners.js +3 -10
- package/dist/workers/forks.js +2 -2
- package/dist/workers/runVmTests.js +7 -5
- package/dist/workers/threads.js +2 -2
- package/dist/workers/vmForks.js +3 -3
- package/dist/workers/vmThreads.js +3 -3
- package/dist/workers.d.ts +3 -3
- package/dist/workers.js +4 -4
- package/package.json +15 -17
- package/dist/chunks/index.CzkCSFCy.js +0 -5455
|
@@ -0,0 +1,3205 @@
|
|
|
1
|
+
import fs, { existsSync, readFileSync, promises } from 'node:fs';
|
|
2
|
+
import { getTests, getTestName, hasFailed, getFullName, getSuites, getTasks } from '@vitest/runner/utils';
|
|
3
|
+
import * as pathe from 'pathe';
|
|
4
|
+
import { extname, relative, normalize, resolve, dirname } from 'pathe';
|
|
5
|
+
import c from 'tinyrainbow';
|
|
6
|
+
import { d as divider, F as F_POINTER, t as truncateString, w as withLabel, f as formatProjectName, a as formatTimeString, g as getStateSymbol, b as taskFail, c as F_RIGHT, e as F_CHECK, 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.yHKcm4dz.js';
|
|
7
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
8
|
+
import { highlight, isPrimitive, inspect, positionToOffset, lineSplitRE, 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 { T as TypeCheckError, R as RandomSequencer, g as getOutputFile, i as isTTY } from './RandomSequencer.DB__To1b.js';
|
|
12
|
+
import { mkdir, writeFile, readdir, stat, readFile } from 'node:fs/promises';
|
|
13
|
+
import { Writable } from 'node:stream';
|
|
14
|
+
import { Console } from 'node:console';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { hostname } from 'node:os';
|
|
17
|
+
|
|
18
|
+
/// <reference types="../types/index.d.ts" />
|
|
19
|
+
|
|
20
|
+
// (c) 2020-present Andrea Giammarchi
|
|
21
|
+
|
|
22
|
+
const {parse: $parse, stringify: $stringify} = JSON;
|
|
23
|
+
const {keys} = Object;
|
|
24
|
+
|
|
25
|
+
const Primitive = String; // it could be Number
|
|
26
|
+
const primitive = 'string'; // it could be 'number'
|
|
27
|
+
|
|
28
|
+
const ignore = {};
|
|
29
|
+
const object = 'object';
|
|
30
|
+
|
|
31
|
+
const noop = (_, value) => value;
|
|
32
|
+
|
|
33
|
+
const primitives = value => (
|
|
34
|
+
value instanceof Primitive ? Primitive(value) : value
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const Primitives = (_, value) => (
|
|
38
|
+
typeof value === primitive ? new Primitive(value) : value
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const revive = (input, parsed, output, $) => {
|
|
42
|
+
const lazy = [];
|
|
43
|
+
for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
|
|
44
|
+
const k = ke[y];
|
|
45
|
+
const value = output[k];
|
|
46
|
+
if (value instanceof Primitive) {
|
|
47
|
+
const tmp = input[value];
|
|
48
|
+
if (typeof tmp === object && !parsed.has(tmp)) {
|
|
49
|
+
parsed.add(tmp);
|
|
50
|
+
output[k] = ignore;
|
|
51
|
+
lazy.push({k, a: [input, parsed, tmp, $]});
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
output[k] = $.call(output, k, tmp);
|
|
55
|
+
}
|
|
56
|
+
else if (output[k] !== ignore)
|
|
57
|
+
output[k] = $.call(output, k, value);
|
|
58
|
+
}
|
|
59
|
+
for (let {length} = lazy, i = 0; i < length; i++) {
|
|
60
|
+
const {k, a} = lazy[i];
|
|
61
|
+
output[k] = $.call(output, k, revive.apply(null, a));
|
|
62
|
+
}
|
|
63
|
+
return output;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const set = (known, input, value) => {
|
|
67
|
+
const index = Primitive(input.push(value) - 1);
|
|
68
|
+
known.set(value, index);
|
|
69
|
+
return index;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Converts a specialized flatted string into a JS value.
|
|
74
|
+
* @param {string} text
|
|
75
|
+
* @param {(this: any, key: string, value: any) => any} [reviver]
|
|
76
|
+
* @returns {any}
|
|
77
|
+
*/
|
|
78
|
+
const parse = (text, reviver) => {
|
|
79
|
+
const input = $parse(text, Primitives).map(primitives);
|
|
80
|
+
const value = input[0];
|
|
81
|
+
const $ = reviver || noop;
|
|
82
|
+
const tmp = typeof value === object && value ?
|
|
83
|
+
revive(input, new Set, value, $) :
|
|
84
|
+
value;
|
|
85
|
+
return $.call({'': tmp}, '', tmp);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Converts a JS value into a specialized flatted string.
|
|
90
|
+
* @param {any} value
|
|
91
|
+
* @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
|
|
92
|
+
* @param {string | number | undefined} [space]
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
const stringify = (value, replacer, space) => {
|
|
96
|
+
const $ = replacer && typeof replacer === object ?
|
|
97
|
+
(k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
|
|
98
|
+
(replacer || noop);
|
|
99
|
+
const known = new Map;
|
|
100
|
+
const input = [];
|
|
101
|
+
const output = [];
|
|
102
|
+
let i = +set(known, input, $.call({'': value}, '', value));
|
|
103
|
+
let firstRun = !i;
|
|
104
|
+
while (i < input.length) {
|
|
105
|
+
firstRun = true;
|
|
106
|
+
output[i] = $stringify(input[i++], replace, space);
|
|
107
|
+
}
|
|
108
|
+
return '[' + output.join(',') + ']';
|
|
109
|
+
function replace(key, value) {
|
|
110
|
+
if (firstRun) {
|
|
111
|
+
firstRun = !firstRun;
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
const after = $.call(this, key, value);
|
|
115
|
+
switch (typeof after) {
|
|
116
|
+
case object:
|
|
117
|
+
if (after === null) return after;
|
|
118
|
+
case primitive:
|
|
119
|
+
return known.get(after) || set(known, input, after);
|
|
120
|
+
}
|
|
121
|
+
return after;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const HIGHLIGHT_SUPPORTED_EXTS = new Set(
|
|
126
|
+
["js", "ts"].flatMap((lang) => [
|
|
127
|
+
`.${lang}`,
|
|
128
|
+
`.m${lang}`,
|
|
129
|
+
`.c${lang}`,
|
|
130
|
+
`.${lang}x`,
|
|
131
|
+
`.m${lang}x`,
|
|
132
|
+
`.c${lang}x`
|
|
133
|
+
])
|
|
134
|
+
);
|
|
135
|
+
function highlightCode(id, source, colors) {
|
|
136
|
+
const ext = extname(id);
|
|
137
|
+
if (!HIGHLIGHT_SUPPORTED_EXTS.has(ext)) {
|
|
138
|
+
return source;
|
|
139
|
+
}
|
|
140
|
+
const isJsx = ext.endsWith("x");
|
|
141
|
+
return highlight(source, { jsx: isJsx, colors: c });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function capturePrintError(error, ctx, options) {
|
|
145
|
+
let output = "";
|
|
146
|
+
const writable = new Writable({
|
|
147
|
+
write(chunk, _encoding, callback) {
|
|
148
|
+
output += String(chunk);
|
|
149
|
+
callback();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
const logger = new Logger(ctx, writable, writable);
|
|
153
|
+
const result = logger.printError(error, {
|
|
154
|
+
showCodeFrame: false,
|
|
155
|
+
...options
|
|
156
|
+
});
|
|
157
|
+
return { nearest: result?.nearest, output };
|
|
158
|
+
}
|
|
159
|
+
function printError(error, project, options) {
|
|
160
|
+
const { showCodeFrame = true, type, printProperties = true } = options;
|
|
161
|
+
const logger = options.logger;
|
|
162
|
+
let e = error;
|
|
163
|
+
if (isPrimitive(e)) {
|
|
164
|
+
e = {
|
|
165
|
+
message: String(error).split(/\n/g)[0],
|
|
166
|
+
stack: String(error)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (!e) {
|
|
170
|
+
const error2 = new Error("unknown error");
|
|
171
|
+
e = {
|
|
172
|
+
message: e ?? error2.message,
|
|
173
|
+
stack: error2.stack
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (!project) {
|
|
177
|
+
printErrorMessage(e, logger);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const stacks = options.parseErrorStacktrace(e);
|
|
181
|
+
const nearest = error instanceof TypeCheckError ? error.stacks[0] : stacks.find((stack) => {
|
|
182
|
+
try {
|
|
183
|
+
return project.server && project.getModuleById(stack.file) && existsSync(stack.file);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
if (type) {
|
|
189
|
+
printErrorType(type, project.ctx);
|
|
190
|
+
}
|
|
191
|
+
printErrorMessage(e, logger);
|
|
192
|
+
if (options.screenshotPaths?.length) {
|
|
193
|
+
const length = options.screenshotPaths.length;
|
|
194
|
+
logger.error(`
|
|
195
|
+
Failure screenshot${length > 1 ? "s" : ""}:`);
|
|
196
|
+
logger.error(options.screenshotPaths.map((p) => ` - ${c.dim(relative(process.cwd(), p))}`).join("\n"));
|
|
197
|
+
if (!e.diff) {
|
|
198
|
+
logger.error();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (e.codeFrame) {
|
|
202
|
+
logger.error(`${e.codeFrame}
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
if ("__vitest_rollup_error__" in e) {
|
|
206
|
+
const err = e.__vitest_rollup_error__;
|
|
207
|
+
logger.error([
|
|
208
|
+
err.plugin && ` Plugin: ${c.magenta(err.plugin)}`,
|
|
209
|
+
err.id && ` File: ${c.cyan(err.id)}${err.loc ? `:${err.loc.line}:${err.loc.column}` : ""}`,
|
|
210
|
+
err.frame && c.yellow(err.frame.split(/\r?\n/g).map((l) => ` `.repeat(2) + l).join(`
|
|
211
|
+
`))
|
|
212
|
+
].filter(Boolean).join("\n"));
|
|
213
|
+
}
|
|
214
|
+
if (e.diff) {
|
|
215
|
+
displayDiff(e.diff, logger.console);
|
|
216
|
+
}
|
|
217
|
+
if (e.frame) {
|
|
218
|
+
logger.error(c.yellow(e.frame));
|
|
219
|
+
} else {
|
|
220
|
+
const errorProperties = printProperties ? getErrorProperties(e) : {};
|
|
221
|
+
printStack(logger, project, stacks, nearest, errorProperties, (s) => {
|
|
222
|
+
if (showCodeFrame && s === nearest && nearest) {
|
|
223
|
+
const sourceCode = readFileSync(nearest.file, "utf-8");
|
|
224
|
+
logger.error(
|
|
225
|
+
generateCodeFrame(
|
|
226
|
+
sourceCode.length > 1e5 ? sourceCode : logger.highlight(nearest.file, sourceCode),
|
|
227
|
+
4,
|
|
228
|
+
s
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const testPath = e.VITEST_TEST_PATH;
|
|
235
|
+
const testName = e.VITEST_TEST_NAME;
|
|
236
|
+
const afterEnvTeardown = e.VITEST_AFTER_ENV_TEARDOWN;
|
|
237
|
+
if (testPath) {
|
|
238
|
+
logger.error(
|
|
239
|
+
c.red(
|
|
240
|
+
`This error originated in "${c.bold(
|
|
241
|
+
testPath
|
|
242
|
+
)}" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.`
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (testName) {
|
|
247
|
+
logger.error(
|
|
248
|
+
c.red(
|
|
249
|
+
`The latest test that might've caused the error is "${c.bold(
|
|
250
|
+
testName
|
|
251
|
+
)}". It might mean one of the following:
|
|
252
|
+
- The error was thrown, while Vitest was running this test.
|
|
253
|
+
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.`
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (afterEnvTeardown) {
|
|
258
|
+
logger.error(
|
|
259
|
+
c.red(
|
|
260
|
+
"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"
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (typeof e.cause === "object" && e.cause && "name" in e.cause) {
|
|
265
|
+
e.cause.name = `Caused by: ${e.cause.name}`;
|
|
266
|
+
printError(e.cause, project, {
|
|
267
|
+
showCodeFrame: false,
|
|
268
|
+
logger: options.logger,
|
|
269
|
+
parseErrorStacktrace: options.parseErrorStacktrace
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
handleImportOutsideModuleError(e.stack || e.stackStr || "", logger);
|
|
273
|
+
return { nearest };
|
|
274
|
+
}
|
|
275
|
+
function printErrorType(type, ctx) {
|
|
276
|
+
ctx.logger.error(`
|
|
277
|
+
${c.red(divider(c.bold(c.inverse(` ${type} `))))}`);
|
|
278
|
+
}
|
|
279
|
+
const skipErrorProperties = /* @__PURE__ */ new Set([
|
|
280
|
+
"nameStr",
|
|
281
|
+
"stack",
|
|
282
|
+
"cause",
|
|
283
|
+
"stacks",
|
|
284
|
+
"stackStr",
|
|
285
|
+
"type",
|
|
286
|
+
"showDiff",
|
|
287
|
+
"ok",
|
|
288
|
+
"operator",
|
|
289
|
+
"diff",
|
|
290
|
+
"codeFrame",
|
|
291
|
+
"actual",
|
|
292
|
+
"expected",
|
|
293
|
+
"diffOptions",
|
|
294
|
+
"VITEST_TEST_NAME",
|
|
295
|
+
"VITEST_TEST_PATH",
|
|
296
|
+
"VITEST_AFTER_ENV_TEARDOWN",
|
|
297
|
+
...Object.getOwnPropertyNames(Error.prototype),
|
|
298
|
+
...Object.getOwnPropertyNames(Object.prototype)
|
|
299
|
+
]);
|
|
300
|
+
function getErrorProperties(e) {
|
|
301
|
+
const errorObject = /* @__PURE__ */ Object.create(null);
|
|
302
|
+
if (e.name === "AssertionError") {
|
|
303
|
+
return errorObject;
|
|
304
|
+
}
|
|
305
|
+
for (const key of Object.getOwnPropertyNames(e)) {
|
|
306
|
+
if (!skipErrorProperties.has(key)) {
|
|
307
|
+
errorObject[key] = e[key];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return errorObject;
|
|
311
|
+
}
|
|
312
|
+
const esmErrors = [
|
|
313
|
+
"Cannot use import statement outside a module",
|
|
314
|
+
"Unexpected token 'export'"
|
|
315
|
+
];
|
|
316
|
+
function handleImportOutsideModuleError(stack, logger) {
|
|
317
|
+
if (!esmErrors.some((e) => stack.includes(e))) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const path = normalize(stack.split("\n")[0].trim());
|
|
321
|
+
let name = path.split("/node_modules/").pop() || "";
|
|
322
|
+
if (name?.startsWith("@")) {
|
|
323
|
+
name = name.split("/").slice(0, 2).join("/");
|
|
324
|
+
} else {
|
|
325
|
+
name = name.split("/")[0];
|
|
326
|
+
}
|
|
327
|
+
if (name) {
|
|
328
|
+
printModuleWarningForPackage(logger, path, name);
|
|
329
|
+
} else {
|
|
330
|
+
printModuleWarningForSourceCode(logger, path);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function printModuleWarningForPackage(logger, path, name) {
|
|
334
|
+
logger.error(
|
|
335
|
+
c.yellow(
|
|
336
|
+
`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(
|
|
337
|
+
`"${name}"`
|
|
338
|
+
)} asking them to ship the file in .mjs extension or add "type": "module" in their package.json.
|
|
339
|
+
|
|
340
|
+
As a temporary workaround you can try to inline the package by updating your config:
|
|
341
|
+
|
|
342
|
+
` + c.gray(c.dim("// vitest.config.js")) + "\n" + c.green(`export default {
|
|
343
|
+
test: {
|
|
344
|
+
server: {
|
|
345
|
+
deps: {
|
|
346
|
+
inline: [
|
|
347
|
+
${c.yellow(c.bold(`"${name}"`))}
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
`)
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
function printModuleWarningForSourceCode(logger, path) {
|
|
358
|
+
logger.error(
|
|
359
|
+
c.yellow(
|
|
360
|
+
`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.`
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
function displayDiff(diff, console) {
|
|
365
|
+
if (diff) {
|
|
366
|
+
console.error(`
|
|
367
|
+
${diff}
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function printErrorMessage(error, logger) {
|
|
372
|
+
const errorName = error.name || error.nameStr || "Unknown Error";
|
|
373
|
+
if (!error.message) {
|
|
374
|
+
logger.error(error);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (error.message.length > 5e3) {
|
|
378
|
+
logger.error(`${c.red(c.bold(errorName))}: ${error.message}`);
|
|
379
|
+
} else {
|
|
380
|
+
logger.error(c.red(`${c.bold(errorName)}: ${error.message}`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function printStack(logger, project, stack, highlight, errorProperties, onStack) {
|
|
384
|
+
for (const frame of stack) {
|
|
385
|
+
const color = frame === highlight ? c.cyan : c.gray;
|
|
386
|
+
const path = relative(project.config.root, frame.file);
|
|
387
|
+
logger.error(
|
|
388
|
+
color(
|
|
389
|
+
` ${c.dim(F_POINTER)} ${[
|
|
390
|
+
frame.method,
|
|
391
|
+
`${path}:${c.dim(`${frame.line}:${frame.column}`)}`
|
|
392
|
+
].filter(Boolean).join(" ")}`
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
onStack?.(frame);
|
|
396
|
+
}
|
|
397
|
+
if (stack.length) {
|
|
398
|
+
logger.error();
|
|
399
|
+
}
|
|
400
|
+
if (hasProperties(errorProperties)) {
|
|
401
|
+
logger.error(c.red(c.dim(divider())));
|
|
402
|
+
const propertiesString = inspect(errorProperties);
|
|
403
|
+
logger.error(c.red(c.bold("Serialized Error:")), c.gray(propertiesString));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function hasProperties(obj) {
|
|
407
|
+
for (const _key in obj) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
function generateCodeFrame(source, indent = 0, loc, range = 2) {
|
|
413
|
+
const start = typeof loc === "object" ? positionToOffset(source, loc.line, loc.column) : loc;
|
|
414
|
+
const end = start;
|
|
415
|
+
const lines = source.split(lineSplitRE);
|
|
416
|
+
const nl = /\r\n/.test(source) ? 2 : 1;
|
|
417
|
+
let count = 0;
|
|
418
|
+
let res = [];
|
|
419
|
+
const columns = process.stdout?.columns || 80;
|
|
420
|
+
for (let i = 0; i < lines.length; i++) {
|
|
421
|
+
count += lines[i].length + nl;
|
|
422
|
+
if (count >= start) {
|
|
423
|
+
for (let j = i - range; j <= i + range || end > count; j++) {
|
|
424
|
+
if (j < 0 || j >= lines.length) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const lineLength = lines[j].length;
|
|
428
|
+
if (stripVTControlCharacters(lines[j]).length > 200) {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
res.push(
|
|
432
|
+
lineNo(j + 1) + truncateString(lines[j].replace(/\t/g, " "), columns - 5 - indent)
|
|
433
|
+
);
|
|
434
|
+
if (j === i) {
|
|
435
|
+
const pad = start - (count - lineLength) + (nl - 1);
|
|
436
|
+
const length = Math.max(
|
|
437
|
+
1,
|
|
438
|
+
end > count ? lineLength - pad : end - start
|
|
439
|
+
);
|
|
440
|
+
res.push(lineNo() + " ".repeat(pad) + c.red("^".repeat(length)));
|
|
441
|
+
} else if (j > i) {
|
|
442
|
+
if (end > count) {
|
|
443
|
+
const length = Math.max(1, Math.min(end - count, lineLength));
|
|
444
|
+
res.push(lineNo() + c.red("^".repeat(length)));
|
|
445
|
+
}
|
|
446
|
+
count += lineLength + 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (indent) {
|
|
453
|
+
res = res.map((line) => " ".repeat(indent) + line);
|
|
454
|
+
}
|
|
455
|
+
return res.join("\n");
|
|
456
|
+
}
|
|
457
|
+
function lineNo(no = "") {
|
|
458
|
+
return c.gray(`${String(no).padStart(3, " ")}| `);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const PAD = " ";
|
|
462
|
+
const ESC$1 = "\x1B[";
|
|
463
|
+
const ERASE_DOWN = `${ESC$1}J`;
|
|
464
|
+
const ERASE_SCROLLBACK = `${ESC$1}3J`;
|
|
465
|
+
const CURSOR_TO_START = `${ESC$1}1;1H`;
|
|
466
|
+
const HIDE_CURSOR = `${ESC$1}?25l`;
|
|
467
|
+
const SHOW_CURSOR = `${ESC$1}?25h`;
|
|
468
|
+
const CLEAR_SCREEN = "\x1Bc";
|
|
469
|
+
class Logger {
|
|
470
|
+
constructor(ctx, outputStream = process.stdout, errorStream = process.stderr) {
|
|
471
|
+
this.ctx = ctx;
|
|
472
|
+
this.outputStream = outputStream;
|
|
473
|
+
this.errorStream = errorStream;
|
|
474
|
+
this.console = new Console({ stdout: outputStream, stderr: errorStream });
|
|
475
|
+
this._highlights.clear();
|
|
476
|
+
this.addCleanupListeners();
|
|
477
|
+
this.registerUnhandledRejection();
|
|
478
|
+
this.outputStream.write(HIDE_CURSOR);
|
|
479
|
+
}
|
|
480
|
+
_clearScreenPending;
|
|
481
|
+
_highlights = /* @__PURE__ */ new Map();
|
|
482
|
+
cleanupListeners = [];
|
|
483
|
+
console;
|
|
484
|
+
log(...args) {
|
|
485
|
+
this._clearScreen();
|
|
486
|
+
this.console.log(...args);
|
|
487
|
+
}
|
|
488
|
+
error(...args) {
|
|
489
|
+
this._clearScreen();
|
|
490
|
+
this.console.error(...args);
|
|
491
|
+
}
|
|
492
|
+
warn(...args) {
|
|
493
|
+
this._clearScreen();
|
|
494
|
+
this.console.warn(...args);
|
|
495
|
+
}
|
|
496
|
+
clearFullScreen(message = "") {
|
|
497
|
+
if (!this.ctx.config.clearScreen) {
|
|
498
|
+
this.console.log(message);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (message) {
|
|
502
|
+
this.console.log(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}${message}`);
|
|
503
|
+
} else {
|
|
504
|
+
this.outputStream.write(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
clearScreen(message, force = false) {
|
|
508
|
+
if (!this.ctx.config.clearScreen) {
|
|
509
|
+
this.console.log(message);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
this._clearScreenPending = message;
|
|
513
|
+
if (force) {
|
|
514
|
+
this._clearScreen();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
_clearScreen() {
|
|
518
|
+
if (this._clearScreenPending == null) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const log = this._clearScreenPending;
|
|
522
|
+
this._clearScreenPending = void 0;
|
|
523
|
+
this.console.log(`${CURSOR_TO_START}${ERASE_DOWN}${log}`);
|
|
524
|
+
}
|
|
525
|
+
printError(err, options = {}) {
|
|
526
|
+
const { fullStack = false, type } = options;
|
|
527
|
+
const project = options.project ?? this.ctx.coreWorkspaceProject ?? this.ctx.projects[0];
|
|
528
|
+
return printError(err, project, {
|
|
529
|
+
type,
|
|
530
|
+
showCodeFrame: options.showCodeFrame ?? true,
|
|
531
|
+
logger: this,
|
|
532
|
+
printProperties: options.verbose,
|
|
533
|
+
screenshotPaths: options.screenshotPaths,
|
|
534
|
+
parseErrorStacktrace: (error) => {
|
|
535
|
+
if (options.task?.file.pool === "browser" && project.browser) {
|
|
536
|
+
return project.browser.parseErrorStacktrace(error, {
|
|
537
|
+
ignoreStackEntries: fullStack ? [] : void 0
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return parseErrorStacktrace(error, {
|
|
541
|
+
frameFilter: project.config.onStackTrace,
|
|
542
|
+
ignoreStackEntries: fullStack ? [] : void 0
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
clearHighlightCache(filename) {
|
|
548
|
+
if (filename) {
|
|
549
|
+
this._highlights.delete(filename);
|
|
550
|
+
} else {
|
|
551
|
+
this._highlights.clear();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
highlight(filename, source) {
|
|
555
|
+
if (this._highlights.has(filename)) {
|
|
556
|
+
return this._highlights.get(filename);
|
|
557
|
+
}
|
|
558
|
+
const code = highlightCode(filename, source);
|
|
559
|
+
this._highlights.set(filename, code);
|
|
560
|
+
return code;
|
|
561
|
+
}
|
|
562
|
+
printNoTestFound(filters) {
|
|
563
|
+
const config = this.ctx.config;
|
|
564
|
+
const comma = c.dim(", ");
|
|
565
|
+
if (filters?.length) {
|
|
566
|
+
this.console.error(c.dim("filter: ") + c.yellow(filters.join(comma)));
|
|
567
|
+
}
|
|
568
|
+
const projectsFilter = toArray(config.project);
|
|
569
|
+
if (projectsFilter.length) {
|
|
570
|
+
this.console.error(
|
|
571
|
+
c.dim("projects: ") + c.yellow(projectsFilter.join(comma))
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
this.ctx.projects.forEach((project) => {
|
|
575
|
+
const config2 = project.config;
|
|
576
|
+
const output = project.isRootProject() || !project.name ? "" : `[${project.name}]`;
|
|
577
|
+
if (output) {
|
|
578
|
+
this.console.error(c.bgCyan(`${output} Config`));
|
|
579
|
+
}
|
|
580
|
+
if (config2.include) {
|
|
581
|
+
this.console.error(
|
|
582
|
+
c.dim("include: ") + c.yellow(config2.include.join(comma))
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if (config2.exclude) {
|
|
586
|
+
this.console.error(
|
|
587
|
+
c.dim("exclude: ") + c.yellow(config2.exclude.join(comma))
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (config2.typecheck.enabled) {
|
|
591
|
+
this.console.error(
|
|
592
|
+
c.dim("typecheck include: ") + c.yellow(config2.typecheck.include.join(comma))
|
|
593
|
+
);
|
|
594
|
+
this.console.error(
|
|
595
|
+
c.dim("typecheck exclude: ") + c.yellow(config2.typecheck.exclude.join(comma))
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
if (config.watch && (config.changed || config.related?.length)) {
|
|
600
|
+
this.log(`No affected ${config.mode} files found
|
|
601
|
+
`);
|
|
602
|
+
} else {
|
|
603
|
+
if (config.passWithNoTests) {
|
|
604
|
+
this.log(`No ${config.mode} files found, exiting with code 0
|
|
605
|
+
`);
|
|
606
|
+
} else {
|
|
607
|
+
this.error(
|
|
608
|
+
c.red(`
|
|
609
|
+
No ${config.mode} files found, exiting with code 1`)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
printBanner() {
|
|
615
|
+
this.log();
|
|
616
|
+
const color = this.ctx.config.watch ? "blue" : "cyan";
|
|
617
|
+
const mode = this.ctx.config.watch ? "DEV" : "RUN";
|
|
618
|
+
this.log(withLabel(color, mode, `v${this.ctx.version} `) + c.gray(this.ctx.config.root));
|
|
619
|
+
if (this.ctx.config.sequence.sequencer === RandomSequencer) {
|
|
620
|
+
this.log(PAD + c.gray(`Running tests with seed "${this.ctx.config.sequence.seed}"`));
|
|
621
|
+
}
|
|
622
|
+
if (this.ctx.config.ui) {
|
|
623
|
+
const host = this.ctx.config.api?.host || "localhost";
|
|
624
|
+
const port = this.ctx.server.config.server.port;
|
|
625
|
+
const base = this.ctx.config.uiBase;
|
|
626
|
+
this.log(PAD + c.dim(c.green(`UI started at http://${host}:${c.bold(port)}${base}`)));
|
|
627
|
+
} else if (this.ctx.config.api?.port) {
|
|
628
|
+
const resolvedUrls = this.ctx.server.resolvedUrls;
|
|
629
|
+
const fallbackUrl = `http://${this.ctx.config.api.host || "localhost"}:${this.ctx.config.api.port}`;
|
|
630
|
+
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] ?? fallbackUrl;
|
|
631
|
+
this.log(PAD + c.dim(c.green(`API started at ${new URL("/", origin)}`)));
|
|
632
|
+
}
|
|
633
|
+
if (this.ctx.coverageProvider) {
|
|
634
|
+
this.log(PAD + c.dim("Coverage enabled with ") + c.yellow(this.ctx.coverageProvider.name));
|
|
635
|
+
}
|
|
636
|
+
if (this.ctx.config.standalone) {
|
|
637
|
+
this.log(c.yellow(`
|
|
638
|
+
Vitest is running in standalone mode. Edit a test file to rerun tests.`));
|
|
639
|
+
} else {
|
|
640
|
+
this.log();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
printBrowserBanner(project) {
|
|
644
|
+
if (!project.browser) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const resolvedUrls = project.browser.vite.resolvedUrls;
|
|
648
|
+
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
|
|
649
|
+
if (!origin) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const output = project.isRootProject() ? "" : formatProjectName(project.name);
|
|
653
|
+
const provider = project.browser.provider.name;
|
|
654
|
+
const providerString = provider === "preview" ? "" : ` by ${c.reset(c.bold(provider))}`;
|
|
655
|
+
this.log(
|
|
656
|
+
c.dim(
|
|
657
|
+
`${output}Browser runner started${providerString} ${c.dim("at")} ${c.blue(new URL("/", origin))}
|
|
658
|
+
`
|
|
659
|
+
)
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
printUnhandledErrors(errors) {
|
|
663
|
+
const errorMessage = c.red(
|
|
664
|
+
c.bold(
|
|
665
|
+
`
|
|
666
|
+
Vitest caught ${errors.length} unhandled error${errors.length > 1 ? "s" : ""} during the test run.
|
|
667
|
+
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.`
|
|
668
|
+
)
|
|
669
|
+
);
|
|
670
|
+
this.error(c.red(divider(c.bold(c.inverse(" Unhandled Errors ")))));
|
|
671
|
+
this.error(errorMessage);
|
|
672
|
+
errors.forEach((err) => {
|
|
673
|
+
this.printError(err, {
|
|
674
|
+
fullStack: true,
|
|
675
|
+
type: err.type || "Unhandled Error"
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
this.error(c.red(divider()));
|
|
679
|
+
}
|
|
680
|
+
printSourceTypeErrors(errors) {
|
|
681
|
+
const errorMessage = c.red(
|
|
682
|
+
c.bold(
|
|
683
|
+
`
|
|
684
|
+
Vitest found ${errors.length} error${errors.length > 1 ? "s" : ""} not related to your test files.`
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
this.log(c.red(divider(c.bold(c.inverse(" Source Errors ")))));
|
|
688
|
+
this.log(errorMessage);
|
|
689
|
+
errors.forEach((err) => {
|
|
690
|
+
this.printError(err, { fullStack: true });
|
|
691
|
+
});
|
|
692
|
+
this.log(c.red(divider()));
|
|
693
|
+
}
|
|
694
|
+
getColumns() {
|
|
695
|
+
return "columns" in this.outputStream ? this.outputStream.columns : 80;
|
|
696
|
+
}
|
|
697
|
+
onTerminalCleanup(listener) {
|
|
698
|
+
this.cleanupListeners.push(listener);
|
|
699
|
+
}
|
|
700
|
+
addCleanupListeners() {
|
|
701
|
+
const cleanup = () => {
|
|
702
|
+
this.cleanupListeners.forEach((fn) => fn());
|
|
703
|
+
this.outputStream.write(SHOW_CURSOR);
|
|
704
|
+
};
|
|
705
|
+
const onExit = (signal, exitCode) => {
|
|
706
|
+
cleanup();
|
|
707
|
+
if (process.exitCode === void 0) {
|
|
708
|
+
process.exitCode = exitCode !== void 0 ? 128 + exitCode : Number(signal);
|
|
709
|
+
}
|
|
710
|
+
process.exit();
|
|
711
|
+
};
|
|
712
|
+
process.once("SIGINT", onExit);
|
|
713
|
+
process.once("SIGTERM", onExit);
|
|
714
|
+
process.once("exit", onExit);
|
|
715
|
+
this.ctx.onClose(() => {
|
|
716
|
+
process.off("SIGINT", onExit);
|
|
717
|
+
process.off("SIGTERM", onExit);
|
|
718
|
+
process.off("exit", onExit);
|
|
719
|
+
cleanup();
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
registerUnhandledRejection() {
|
|
723
|
+
const onUnhandledRejection = (err) => {
|
|
724
|
+
process.exitCode = 1;
|
|
725
|
+
this.printError(err, {
|
|
726
|
+
fullStack: true,
|
|
727
|
+
type: "Unhandled Rejection"
|
|
728
|
+
});
|
|
729
|
+
this.error("\n\n");
|
|
730
|
+
process.exit();
|
|
731
|
+
};
|
|
732
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
733
|
+
this.ctx.onClose(() => {
|
|
734
|
+
process.off("unhandledRejection", onUnhandledRejection);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
class BlobReporter {
|
|
740
|
+
ctx;
|
|
741
|
+
options;
|
|
742
|
+
constructor(options) {
|
|
743
|
+
this.options = options;
|
|
744
|
+
}
|
|
745
|
+
onInit(ctx) {
|
|
746
|
+
if (ctx.config.watch) {
|
|
747
|
+
throw new Error("Blob reporter is not supported in watch mode");
|
|
748
|
+
}
|
|
749
|
+
this.ctx = ctx;
|
|
750
|
+
}
|
|
751
|
+
async onFinished(files = [], errors = [], coverage) {
|
|
752
|
+
let outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "blob");
|
|
753
|
+
if (!outputFile) {
|
|
754
|
+
const shard = this.ctx.config.shard;
|
|
755
|
+
outputFile = shard ? `.vitest-reports/blob-${shard.index}-${shard.count}.json` : ".vitest-reports/blob.json";
|
|
756
|
+
}
|
|
757
|
+
const modules = this.ctx.projects.map(
|
|
758
|
+
(project) => {
|
|
759
|
+
return [
|
|
760
|
+
project.name,
|
|
761
|
+
[...project.vite.moduleGraph.idToModuleMap.entries()].map((mod) => {
|
|
762
|
+
if (!mod[1].file) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
return [mod[0], mod[1].file, mod[1].url];
|
|
766
|
+
}).filter((x) => x != null)
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
const report = stringify([
|
|
771
|
+
this.ctx.version,
|
|
772
|
+
files,
|
|
773
|
+
errors,
|
|
774
|
+
modules,
|
|
775
|
+
coverage
|
|
776
|
+
]);
|
|
777
|
+
const reportFile = resolve(this.ctx.config.root, outputFile);
|
|
778
|
+
const dir = dirname(reportFile);
|
|
779
|
+
if (!existsSync(dir)) {
|
|
780
|
+
await mkdir(dir, { recursive: true });
|
|
781
|
+
}
|
|
782
|
+
await writeFile(reportFile, report, "utf-8");
|
|
783
|
+
this.ctx.logger.log("blob report written to", reportFile);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async function readBlobs(currentVersion, blobsDirectory, projectsArray) {
|
|
787
|
+
const resolvedDir = resolve(process.cwd(), blobsDirectory);
|
|
788
|
+
const blobsFiles = await readdir(resolvedDir);
|
|
789
|
+
const promises = blobsFiles.map(async (filename) => {
|
|
790
|
+
const fullPath = resolve(resolvedDir, filename);
|
|
791
|
+
const stats = await stat(fullPath);
|
|
792
|
+
if (!stats.isFile()) {
|
|
793
|
+
throw new TypeError(
|
|
794
|
+
`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a file`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const content = await readFile(fullPath, "utf-8");
|
|
798
|
+
const [version, files2, errors2, moduleKeys, coverage] = parse(
|
|
799
|
+
content
|
|
800
|
+
);
|
|
801
|
+
if (!version) {
|
|
802
|
+
throw new TypeError(
|
|
803
|
+
`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return { version, files: files2, errors: errors2, moduleKeys, coverage, file: filename };
|
|
807
|
+
});
|
|
808
|
+
const blobs = await Promise.all(promises);
|
|
809
|
+
if (!blobs.length) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
`vitest.mergeReports() requires at least one blob file in "${blobsDirectory}" directory, but none were found`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
const versions = new Set(blobs.map((blob) => blob.version));
|
|
815
|
+
if (versions.size > 1) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
`vitest.mergeReports() requires all blob files to be generated by the same Vitest version, received
|
|
818
|
+
|
|
819
|
+
${blobs.map((b) => `- "${b.file}" uses v${b.version}`).join("\n")}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (!versions.has(currentVersion)) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
`the blobs in "${blobsDirectory}" were generated by a different version of Vitest. Expected v${currentVersion}, but received v${blobs[0].version}`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const projects = Object.fromEntries(
|
|
828
|
+
projectsArray.map((p) => [p.name, p])
|
|
829
|
+
);
|
|
830
|
+
blobs.forEach((blob) => {
|
|
831
|
+
blob.moduleKeys.forEach(([projectName, moduleIds]) => {
|
|
832
|
+
const project = projects[projectName];
|
|
833
|
+
if (!project) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
moduleIds.forEach(([moduleId, file, url]) => {
|
|
837
|
+
const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file);
|
|
838
|
+
moduleNode.url = url;
|
|
839
|
+
moduleNode.id = moduleId;
|
|
840
|
+
project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
const files = blobs.flatMap((blob) => blob.files).sort((f1, f2) => {
|
|
845
|
+
const time1 = f1.result?.startTime || 0;
|
|
846
|
+
const time2 = f2.result?.startTime || 0;
|
|
847
|
+
return time1 - time2;
|
|
848
|
+
});
|
|
849
|
+
const errors = blobs.flatMap((blob) => blob.errors);
|
|
850
|
+
const coverages = blobs.map((blob) => blob.coverage);
|
|
851
|
+
return {
|
|
852
|
+
files,
|
|
853
|
+
errors,
|
|
854
|
+
coverages
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function hasFailedSnapshot(suite) {
|
|
859
|
+
return getTests(suite).some((s) => {
|
|
860
|
+
return s.result?.errors?.some(
|
|
861
|
+
(e) => typeof e?.message === "string" && e.message.match(/Snapshot .* mismatched/)
|
|
862
|
+
);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const BADGE_PADDING = " ";
|
|
867
|
+
class BaseReporter {
|
|
868
|
+
start = 0;
|
|
869
|
+
end = 0;
|
|
870
|
+
watchFilters;
|
|
871
|
+
failedUnwatchedFiles = [];
|
|
872
|
+
isTTY;
|
|
873
|
+
ctx = void 0;
|
|
874
|
+
renderSucceed = false;
|
|
875
|
+
verbose = false;
|
|
876
|
+
_filesInWatchMode = /* @__PURE__ */ new Map();
|
|
877
|
+
_timeStart = formatTimeString(/* @__PURE__ */ new Date());
|
|
878
|
+
constructor(options = {}) {
|
|
879
|
+
this.isTTY = options.isTTY ?? isTTY;
|
|
880
|
+
}
|
|
881
|
+
onInit(ctx) {
|
|
882
|
+
this.ctx = ctx;
|
|
883
|
+
this.ctx.logger.printBanner();
|
|
884
|
+
this.start = performance$1.now();
|
|
885
|
+
}
|
|
886
|
+
log(...messages) {
|
|
887
|
+
this.ctx.logger.log(...messages);
|
|
888
|
+
}
|
|
889
|
+
error(...messages) {
|
|
890
|
+
this.ctx.logger.error(...messages);
|
|
891
|
+
}
|
|
892
|
+
relative(path) {
|
|
893
|
+
return relative(this.ctx.config.root, path);
|
|
894
|
+
}
|
|
895
|
+
onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
|
|
896
|
+
this.end = performance$1.now();
|
|
897
|
+
this.reportSummary(files, errors);
|
|
898
|
+
}
|
|
899
|
+
onTaskUpdate(packs) {
|
|
900
|
+
for (const pack of packs) {
|
|
901
|
+
const task = this.ctx.state.idMap.get(pack[0]);
|
|
902
|
+
if (task) {
|
|
903
|
+
this.printTask(task);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Callback invoked with a single `Task` from `onTaskUpdate`
|
|
909
|
+
*/
|
|
910
|
+
printTask(task) {
|
|
911
|
+
if (!("filepath" in task) || !task.result?.state || task.result?.state === "run" || task.result?.state === "queued") {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const tests = getTests(task);
|
|
915
|
+
const failed = tests.filter((t) => t.result?.state === "fail");
|
|
916
|
+
const skipped = tests.filter((t) => t.mode === "skip" || t.mode === "todo");
|
|
917
|
+
let state = c.dim(`${tests.length} test${tests.length > 1 ? "s" : ""}`);
|
|
918
|
+
if (failed.length) {
|
|
919
|
+
state += c.dim(" | ") + c.red(`${failed.length} failed`);
|
|
920
|
+
}
|
|
921
|
+
if (skipped.length) {
|
|
922
|
+
state += c.dim(" | ") + c.yellow(`${skipped.length} skipped`);
|
|
923
|
+
}
|
|
924
|
+
let suffix = c.dim("(") + state + c.dim(")") + this.getDurationPrefix(task);
|
|
925
|
+
if (this.ctx.config.logHeapUsage && task.result.heap != null) {
|
|
926
|
+
suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`);
|
|
927
|
+
}
|
|
928
|
+
let title = getStateSymbol(task);
|
|
929
|
+
if (task.meta.typecheck) {
|
|
930
|
+
title += ` ${c.bgBlue(c.bold(" TS "))}`;
|
|
931
|
+
}
|
|
932
|
+
if (task.projectName) {
|
|
933
|
+
title += ` ${formatProjectName(task.projectName, "")}`;
|
|
934
|
+
}
|
|
935
|
+
this.log(` ${title} ${task.name} ${suffix}`);
|
|
936
|
+
const anyFailed = tests.some((test) => test.result?.state === "fail");
|
|
937
|
+
for (const test of tests) {
|
|
938
|
+
const { duration, retryCount, repeatCount } = test.result || {};
|
|
939
|
+
let suffix2 = "";
|
|
940
|
+
if (retryCount != null && retryCount > 0) {
|
|
941
|
+
suffix2 += c.yellow(` (retry x${retryCount})`);
|
|
942
|
+
}
|
|
943
|
+
if (repeatCount != null && repeatCount > 0) {
|
|
944
|
+
suffix2 += c.yellow(` (repeat x${repeatCount})`);
|
|
945
|
+
}
|
|
946
|
+
if (test.result?.state === "fail") {
|
|
947
|
+
this.log(c.red(` ${taskFail} ${getTestName(test, c.dim(" > "))}${this.getDurationPrefix(test)}`) + suffix2);
|
|
948
|
+
test.result?.errors?.forEach((e) => {
|
|
949
|
+
this.log(c.red(` ${F_RIGHT} ${e?.message}`));
|
|
950
|
+
});
|
|
951
|
+
} else if (duration && duration > this.ctx.config.slowTestThreshold) {
|
|
952
|
+
this.log(
|
|
953
|
+
` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(" > "))} ${c.yellow(Math.round(duration) + c.dim("ms"))}${suffix2}`
|
|
954
|
+
);
|
|
955
|
+
} else if (this.ctx.config.hideSkippedTests && (test.mode === "skip" || test.result?.state === "skip")) ; else if (test.result?.state === "skip" && test.result.note) {
|
|
956
|
+
this.log(` ${getStateSymbol(test)} ${getTestName(test)}${c.dim(c.gray(` [${test.result.note}]`))}`);
|
|
957
|
+
} else if (this.renderSucceed || anyFailed) {
|
|
958
|
+
this.log(` ${getStateSymbol(test)} ${getTestName(test, c.dim(" > "))}${suffix2}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
getDurationPrefix(task) {
|
|
963
|
+
if (!task.result?.duration) {
|
|
964
|
+
return "";
|
|
965
|
+
}
|
|
966
|
+
const color = task.result.duration > this.ctx.config.slowTestThreshold ? c.yellow : c.gray;
|
|
967
|
+
return color(` ${Math.round(task.result.duration)}${c.dim("ms")}`);
|
|
968
|
+
}
|
|
969
|
+
onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
|
|
970
|
+
const failed = errors.length > 0 || hasFailed(files);
|
|
971
|
+
if (failed) {
|
|
972
|
+
this.log(withLabel("red", "FAIL", "Tests failed. Watching for file changes..."));
|
|
973
|
+
} else if (this.ctx.isCancelling) {
|
|
974
|
+
this.log(withLabel("red", "CANCELLED", "Test run cancelled. Watching for file changes..."));
|
|
975
|
+
} else {
|
|
976
|
+
this.log(withLabel("green", "PASS", "Waiting for file changes..."));
|
|
977
|
+
}
|
|
978
|
+
const hints = [c.dim("press ") + c.bold("h") + c.dim(" to show help")];
|
|
979
|
+
if (hasFailedSnapshot(files)) {
|
|
980
|
+
hints.unshift(c.dim("press ") + c.bold(c.yellow("u")) + c.dim(" to update snapshot"));
|
|
981
|
+
} else {
|
|
982
|
+
hints.push(c.dim("press ") + c.bold("q") + c.dim(" to quit"));
|
|
983
|
+
}
|
|
984
|
+
this.log(BADGE_PADDING + hints.join(c.dim(", ")));
|
|
985
|
+
}
|
|
986
|
+
onWatcherRerun(files, trigger) {
|
|
987
|
+
this.watchFilters = files;
|
|
988
|
+
this.failedUnwatchedFiles = this.ctx.state.getFiles().filter(
|
|
989
|
+
(file) => !files.includes(file.filepath) && hasFailed(file)
|
|
990
|
+
);
|
|
991
|
+
files.forEach((filepath) => {
|
|
992
|
+
let reruns = this._filesInWatchMode.get(filepath) ?? 0;
|
|
993
|
+
this._filesInWatchMode.set(filepath, ++reruns);
|
|
994
|
+
});
|
|
995
|
+
let banner = trigger ? c.dim(`${this.relative(trigger)} `) : "";
|
|
996
|
+
if (files.length === 1) {
|
|
997
|
+
const rerun = this._filesInWatchMode.get(files[0]) ?? 1;
|
|
998
|
+
banner += c.blue(`x${rerun} `);
|
|
999
|
+
}
|
|
1000
|
+
this.ctx.logger.clearFullScreen();
|
|
1001
|
+
this.log(withLabel("blue", "RERUN", banner));
|
|
1002
|
+
if (this.ctx.configOverride.project) {
|
|
1003
|
+
this.log(BADGE_PADDING + c.dim(" Project name: ") + c.blue(toArray(this.ctx.configOverride.project).join(", ")));
|
|
1004
|
+
}
|
|
1005
|
+
if (this.ctx.filenamePattern) {
|
|
1006
|
+
this.log(BADGE_PADDING + c.dim(" Filename pattern: ") + c.blue(this.ctx.filenamePattern.join(", ")));
|
|
1007
|
+
}
|
|
1008
|
+
if (this.ctx.configOverride.testNamePattern) {
|
|
1009
|
+
this.log(BADGE_PADDING + c.dim(" Test name pattern: ") + c.blue(String(this.ctx.configOverride.testNamePattern)));
|
|
1010
|
+
}
|
|
1011
|
+
this.log("");
|
|
1012
|
+
for (const task of this.failedUnwatchedFiles) {
|
|
1013
|
+
this.printTask(task);
|
|
1014
|
+
}
|
|
1015
|
+
this._timeStart = formatTimeString(/* @__PURE__ */ new Date());
|
|
1016
|
+
this.start = performance$1.now();
|
|
1017
|
+
}
|
|
1018
|
+
onUserConsoleLog(log) {
|
|
1019
|
+
if (!this.shouldLog(log)) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const output = log.type === "stdout" ? this.ctx.logger.outputStream : this.ctx.logger.errorStream;
|
|
1023
|
+
const write = (msg) => output.write(msg);
|
|
1024
|
+
let headerText = "unknown test";
|
|
1025
|
+
const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : void 0;
|
|
1026
|
+
if (task) {
|
|
1027
|
+
headerText = getFullName(task, c.dim(" > "));
|
|
1028
|
+
} else if (log.taskId && log.taskId !== "__vitest__unknown_test__") {
|
|
1029
|
+
headerText = log.taskId;
|
|
1030
|
+
}
|
|
1031
|
+
write(c.gray(log.type + c.dim(` | ${headerText}
|
|
1032
|
+
`)) + log.content);
|
|
1033
|
+
if (log.origin) {
|
|
1034
|
+
if (log.browser) {
|
|
1035
|
+
write("\n");
|
|
1036
|
+
}
|
|
1037
|
+
const project = task ? this.ctx.getProjectByName(task.file.projectName || "") : this.ctx.getRootProject();
|
|
1038
|
+
const stack = log.browser ? project.browser?.parseStacktrace(log.origin) || [] : parseStacktrace(log.origin);
|
|
1039
|
+
const highlight = task && stack.find((i) => i.file === task.file.filepath);
|
|
1040
|
+
for (const frame of stack) {
|
|
1041
|
+
const color = frame === highlight ? c.cyan : c.gray;
|
|
1042
|
+
const path = relative(project.config.root, frame.file);
|
|
1043
|
+
const positions = [
|
|
1044
|
+
frame.method,
|
|
1045
|
+
`${path}:${c.dim(`${frame.line}:${frame.column}`)}`
|
|
1046
|
+
].filter(Boolean).join(" ");
|
|
1047
|
+
write(color(` ${c.dim(F_POINTER)} ${positions}
|
|
1048
|
+
`));
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
write("\n");
|
|
1052
|
+
}
|
|
1053
|
+
onTestRemoved(trigger) {
|
|
1054
|
+
this.log(c.yellow("Test removed...") + (trigger ? c.dim(` [ ${this.relative(trigger)} ]
|
|
1055
|
+
`) : ""));
|
|
1056
|
+
}
|
|
1057
|
+
shouldLog(log) {
|
|
1058
|
+
if (this.ctx.config.silent) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
const shouldLog = this.ctx.config.onConsoleLog?.(log.content, log.type);
|
|
1062
|
+
if (shouldLog === false) {
|
|
1063
|
+
return shouldLog;
|
|
1064
|
+
}
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
onServerRestart(reason) {
|
|
1068
|
+
this.log(c.bold(c.magenta(
|
|
1069
|
+
reason === "config" ? "\nRestarting due to config changes..." : "\nRestarting Vitest..."
|
|
1070
|
+
)));
|
|
1071
|
+
}
|
|
1072
|
+
reportSummary(files, errors) {
|
|
1073
|
+
this.printErrorsSummary(files, errors);
|
|
1074
|
+
if (this.ctx.config.mode === "benchmark") {
|
|
1075
|
+
this.reportBenchmarkSummary(files);
|
|
1076
|
+
} else {
|
|
1077
|
+
this.reportTestSummary(files, errors);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
reportTestSummary(files, errors) {
|
|
1081
|
+
this.log();
|
|
1082
|
+
const affectedFiles = [
|
|
1083
|
+
...this.failedUnwatchedFiles,
|
|
1084
|
+
...files
|
|
1085
|
+
];
|
|
1086
|
+
const tests = getTests(affectedFiles);
|
|
1087
|
+
const snapshotOutput = renderSnapshotSummary(
|
|
1088
|
+
this.ctx.config.root,
|
|
1089
|
+
this.ctx.snapshot.summary
|
|
1090
|
+
);
|
|
1091
|
+
for (const [index, snapshot] of snapshotOutput.entries()) {
|
|
1092
|
+
const title = index === 0 ? "Snapshots" : "";
|
|
1093
|
+
this.log(`${padSummaryTitle(title)} ${snapshot}`);
|
|
1094
|
+
}
|
|
1095
|
+
if (snapshotOutput.length > 1) {
|
|
1096
|
+
this.log();
|
|
1097
|
+
}
|
|
1098
|
+
this.log(padSummaryTitle("Test Files"), getStateString$1(affectedFiles));
|
|
1099
|
+
this.log(padSummaryTitle("Tests"), getStateString$1(tests));
|
|
1100
|
+
if (this.ctx.projects.some((c2) => c2.config.typecheck.enabled)) {
|
|
1101
|
+
const failed = tests.filter((t) => t.meta?.typecheck && t.result?.errors?.length);
|
|
1102
|
+
this.log(
|
|
1103
|
+
padSummaryTitle("Type Errors"),
|
|
1104
|
+
failed.length ? c.bold(c.red(`${failed.length} failed`)) : c.dim("no errors")
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
if (errors.length) {
|
|
1108
|
+
this.log(
|
|
1109
|
+
padSummaryTitle("Errors"),
|
|
1110
|
+
c.bold(c.red(`${errors.length} error${errors.length > 1 ? "s" : ""}`))
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
this.log(padSummaryTitle("Start at"), this._timeStart);
|
|
1114
|
+
const collectTime = sum(files, (file) => file.collectDuration);
|
|
1115
|
+
const testsTime = sum(files, (file) => file.result?.duration);
|
|
1116
|
+
const setupTime = sum(files, (file) => file.setupDuration);
|
|
1117
|
+
if (this.watchFilters) {
|
|
1118
|
+
this.log(padSummaryTitle("Duration"), formatTime(collectTime + testsTime + setupTime));
|
|
1119
|
+
} else {
|
|
1120
|
+
const executionTime = this.end - this.start;
|
|
1121
|
+
const environmentTime = sum(files, (file) => file.environmentLoad);
|
|
1122
|
+
const prepareTime = sum(files, (file) => file.prepareDuration);
|
|
1123
|
+
const transformTime = sum(this.ctx.projects, (project) => project.vitenode.getTotalDuration());
|
|
1124
|
+
const typecheck = sum(this.ctx.projects, (project) => project.typechecker?.getResult().time);
|
|
1125
|
+
const timers = [
|
|
1126
|
+
`transform ${formatTime(transformTime)}`,
|
|
1127
|
+
`setup ${formatTime(setupTime)}`,
|
|
1128
|
+
`collect ${formatTime(collectTime)}`,
|
|
1129
|
+
`tests ${formatTime(testsTime)}`,
|
|
1130
|
+
`environment ${formatTime(environmentTime)}`,
|
|
1131
|
+
`prepare ${formatTime(prepareTime)}`,
|
|
1132
|
+
typecheck && `typecheck ${formatTime(typecheck)}`
|
|
1133
|
+
].filter(Boolean).join(", ");
|
|
1134
|
+
this.log(padSummaryTitle("Duration"), formatTime(executionTime) + c.dim(` (${timers})`));
|
|
1135
|
+
}
|
|
1136
|
+
this.log();
|
|
1137
|
+
}
|
|
1138
|
+
printErrorsSummary(files, errors) {
|
|
1139
|
+
const suites = getSuites(files);
|
|
1140
|
+
const tests = getTests(files);
|
|
1141
|
+
const failedSuites = suites.filter((i) => i.result?.errors);
|
|
1142
|
+
const failedTests = tests.filter((i) => i.result?.state === "fail");
|
|
1143
|
+
const failedTotal = countTestErrors(failedSuites) + countTestErrors(failedTests);
|
|
1144
|
+
let current = 1;
|
|
1145
|
+
const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, void 0, 1)))}
|
|
1146
|
+
`);
|
|
1147
|
+
if (failedSuites.length) {
|
|
1148
|
+
this.error(`
|
|
1149
|
+
${errorBanner(`Failed Suites ${failedSuites.length}`)}
|
|
1150
|
+
`);
|
|
1151
|
+
this.printTaskErrors(failedSuites, errorDivider);
|
|
1152
|
+
}
|
|
1153
|
+
if (failedTests.length) {
|
|
1154
|
+
this.error(`
|
|
1155
|
+
${errorBanner(`Failed Tests ${failedTests.length}`)}
|
|
1156
|
+
`);
|
|
1157
|
+
this.printTaskErrors(failedTests, errorDivider);
|
|
1158
|
+
}
|
|
1159
|
+
if (errors.length) {
|
|
1160
|
+
this.ctx.logger.printUnhandledErrors(errors);
|
|
1161
|
+
this.error();
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
reportBenchmarkSummary(files) {
|
|
1165
|
+
const benches = getTests(files);
|
|
1166
|
+
const topBenches = benches.filter((i) => i.result?.benchmark?.rank === 1);
|
|
1167
|
+
this.log(`
|
|
1168
|
+
${withLabel("cyan", "BENCH", "Summary\n")}`);
|
|
1169
|
+
for (const bench of topBenches) {
|
|
1170
|
+
const group = bench.suite || bench.file;
|
|
1171
|
+
if (!group) {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
const groupName = getFullName(group, c.dim(" > "));
|
|
1175
|
+
this.log(` ${formatProjectName(bench.file.projectName)}${bench.name}${c.dim(` - ${groupName}`)}`);
|
|
1176
|
+
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);
|
|
1177
|
+
for (const sibling of siblings) {
|
|
1178
|
+
const number = (sibling.result.benchmark.mean / bench.result.benchmark.mean).toFixed(2);
|
|
1179
|
+
this.log(c.green(` ${number}x `) + c.gray("faster than ") + sibling.name);
|
|
1180
|
+
}
|
|
1181
|
+
this.log("");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
printTaskErrors(tasks, errorDivider) {
|
|
1185
|
+
const errorsQueue = [];
|
|
1186
|
+
for (const task of tasks) {
|
|
1187
|
+
task.result?.errors?.forEach((error) => {
|
|
1188
|
+
let previous;
|
|
1189
|
+
if (error?.stackStr) {
|
|
1190
|
+
previous = errorsQueue.find((i) => {
|
|
1191
|
+
if (i[0]?.stackStr !== error.stackStr) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
const currentProjectName = task?.projectName || task.file?.projectName || "";
|
|
1195
|
+
const projectName = i[1][0]?.projectName || i[1][0].file?.projectName || "";
|
|
1196
|
+
return projectName === currentProjectName;
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
if (previous) {
|
|
1200
|
+
previous[1].push(task);
|
|
1201
|
+
} else {
|
|
1202
|
+
errorsQueue.push([error, [task]]);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
for (const [error, tasks2] of errorsQueue) {
|
|
1207
|
+
for (const task of tasks2) {
|
|
1208
|
+
const filepath = task?.filepath || "";
|
|
1209
|
+
const projectName = task?.projectName || task.file?.projectName || "";
|
|
1210
|
+
let name = getFullName(task, c.dim(" > "));
|
|
1211
|
+
if (filepath) {
|
|
1212
|
+
name += c.dim(` [ ${this.relative(filepath)} ]`);
|
|
1213
|
+
}
|
|
1214
|
+
this.ctx.logger.error(
|
|
1215
|
+
`${c.red(c.bold(c.inverse(" FAIL ")))} ${formatProjectName(projectName)}${name}`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
const screenshotPaths = tasks2.map((t) => t.meta?.failScreenshotPath).filter((screenshot) => screenshot != null);
|
|
1219
|
+
this.ctx.logger.printError(error, {
|
|
1220
|
+
project: this.ctx.getProjectByName(tasks2[0].file.projectName || ""),
|
|
1221
|
+
verbose: this.verbose,
|
|
1222
|
+
screenshotPaths,
|
|
1223
|
+
task: tasks2[0]
|
|
1224
|
+
});
|
|
1225
|
+
errorDivider();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function errorBanner(message) {
|
|
1230
|
+
return c.red(divider(c.bold(c.inverse(` ${message} `))));
|
|
1231
|
+
}
|
|
1232
|
+
function sum(items, cb) {
|
|
1233
|
+
return items.reduce((total, next) => {
|
|
1234
|
+
return total + Math.max(cb(next) || 0, 0);
|
|
1235
|
+
}, 0);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
class BasicReporter extends BaseReporter {
|
|
1239
|
+
constructor() {
|
|
1240
|
+
super();
|
|
1241
|
+
this.isTTY = false;
|
|
1242
|
+
}
|
|
1243
|
+
onInit(ctx) {
|
|
1244
|
+
super.onInit(ctx);
|
|
1245
|
+
ctx.logger.log(c.inverse(c.bold(c.yellow(" DEPRECATED "))), c.yellow(
|
|
1246
|
+
`'basic' reporter is deprecated and will be removed in Vitest v3.
|
|
1247
|
+
Remove 'basic' from 'reporters' option. To match 'basic' reporter 100%, use configuration:
|
|
1248
|
+
${JSON.stringify({ test: { reporters: [["default", { summary: false }]] } }, null, 2)}`
|
|
1249
|
+
));
|
|
1250
|
+
}
|
|
1251
|
+
reportSummary(files, errors) {
|
|
1252
|
+
this.ctx.logger.log();
|
|
1253
|
+
return super.reportSummary(files, errors);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const DEFAULT_RENDER_INTERVAL = 16;
|
|
1258
|
+
const ESC = "\x1B[";
|
|
1259
|
+
const CLEAR_LINE = `${ESC}K`;
|
|
1260
|
+
const MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`;
|
|
1261
|
+
const SYNC_START = `${ESC}?2026h`;
|
|
1262
|
+
const SYNC_END = `${ESC}?2026l`;
|
|
1263
|
+
class WindowRenderer {
|
|
1264
|
+
options;
|
|
1265
|
+
streams;
|
|
1266
|
+
buffer = [];
|
|
1267
|
+
renderInterval = void 0;
|
|
1268
|
+
windowHeight = 0;
|
|
1269
|
+
finished = false;
|
|
1270
|
+
cleanups = [];
|
|
1271
|
+
constructor(options) {
|
|
1272
|
+
this.options = {
|
|
1273
|
+
interval: DEFAULT_RENDER_INTERVAL,
|
|
1274
|
+
...options
|
|
1275
|
+
};
|
|
1276
|
+
this.streams = {
|
|
1277
|
+
output: options.logger.outputStream.write.bind(options.logger.outputStream),
|
|
1278
|
+
error: options.logger.errorStream.write.bind(options.logger.errorStream)
|
|
1279
|
+
};
|
|
1280
|
+
this.cleanups.push(
|
|
1281
|
+
this.interceptStream(process.stdout, "output"),
|
|
1282
|
+
this.interceptStream(process.stderr, "error")
|
|
1283
|
+
);
|
|
1284
|
+
this.options.logger.onTerminalCleanup(() => {
|
|
1285
|
+
this.flushBuffer();
|
|
1286
|
+
this.stop();
|
|
1287
|
+
});
|
|
1288
|
+
this.start();
|
|
1289
|
+
}
|
|
1290
|
+
start() {
|
|
1291
|
+
this.finished = false;
|
|
1292
|
+
this.renderInterval = setInterval(() => this.flushBuffer(), this.options.interval);
|
|
1293
|
+
}
|
|
1294
|
+
stop() {
|
|
1295
|
+
this.cleanups.splice(0).map((fn) => fn());
|
|
1296
|
+
clearInterval(this.renderInterval);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Write all buffered output and stop buffering.
|
|
1300
|
+
* All intercepted writes are forwarded to actual write after this.
|
|
1301
|
+
*/
|
|
1302
|
+
finish() {
|
|
1303
|
+
this.finished = true;
|
|
1304
|
+
this.flushBuffer();
|
|
1305
|
+
clearInterval(this.renderInterval);
|
|
1306
|
+
}
|
|
1307
|
+
flushBuffer() {
|
|
1308
|
+
if (this.buffer.length === 0) {
|
|
1309
|
+
return this.render();
|
|
1310
|
+
}
|
|
1311
|
+
let current;
|
|
1312
|
+
for (const next of this.buffer.splice(0)) {
|
|
1313
|
+
if (!current) {
|
|
1314
|
+
current = next;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (current.type !== next.type) {
|
|
1318
|
+
this.render(current.message, current.type);
|
|
1319
|
+
current = next;
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
current.message += next.message;
|
|
1323
|
+
}
|
|
1324
|
+
if (current) {
|
|
1325
|
+
this.render(current?.message, current?.type);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
render(message, type = "output") {
|
|
1329
|
+
if (this.finished) {
|
|
1330
|
+
this.clearWindow();
|
|
1331
|
+
return this.write(message || "", type);
|
|
1332
|
+
}
|
|
1333
|
+
const windowContent = this.options.getWindow();
|
|
1334
|
+
const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns());
|
|
1335
|
+
let padding = this.windowHeight - rowCount;
|
|
1336
|
+
if (padding > 0 && message) {
|
|
1337
|
+
padding -= getRenderedRowCount([message], this.options.logger.getColumns());
|
|
1338
|
+
}
|
|
1339
|
+
this.write(SYNC_START);
|
|
1340
|
+
this.clearWindow();
|
|
1341
|
+
if (message) {
|
|
1342
|
+
this.write(message, type);
|
|
1343
|
+
}
|
|
1344
|
+
if (padding > 0) {
|
|
1345
|
+
this.write("\n".repeat(padding));
|
|
1346
|
+
}
|
|
1347
|
+
this.write(windowContent.join("\n"));
|
|
1348
|
+
this.write(SYNC_END);
|
|
1349
|
+
this.windowHeight = rowCount + Math.max(0, padding);
|
|
1350
|
+
}
|
|
1351
|
+
clearWindow() {
|
|
1352
|
+
if (this.windowHeight === 0) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
this.write(CLEAR_LINE);
|
|
1356
|
+
for (let i = 1; i < this.windowHeight; i++) {
|
|
1357
|
+
this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`);
|
|
1358
|
+
}
|
|
1359
|
+
this.windowHeight = 0;
|
|
1360
|
+
}
|
|
1361
|
+
interceptStream(stream, type) {
|
|
1362
|
+
const original = stream.write;
|
|
1363
|
+
stream.write = (chunk, _, callback) => {
|
|
1364
|
+
if (chunk) {
|
|
1365
|
+
if (this.finished) {
|
|
1366
|
+
this.write(chunk.toString(), type);
|
|
1367
|
+
} else {
|
|
1368
|
+
this.buffer.push({ type, message: chunk.toString() });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
callback?.();
|
|
1372
|
+
};
|
|
1373
|
+
return function restore() {
|
|
1374
|
+
stream.write = original;
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
write(message, type = "output") {
|
|
1378
|
+
this.streams[type](message);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function getRenderedRowCount(rows, columns) {
|
|
1382
|
+
let count = 0;
|
|
1383
|
+
for (const row of rows) {
|
|
1384
|
+
const text = stripVTControlCharacters(row);
|
|
1385
|
+
count += Math.max(1, Math.ceil(text.length / columns));
|
|
1386
|
+
}
|
|
1387
|
+
return count;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
class TaskParser {
|
|
1391
|
+
ctx;
|
|
1392
|
+
onInit(ctx) {
|
|
1393
|
+
this.ctx = ctx;
|
|
1394
|
+
}
|
|
1395
|
+
onHookStart(_options) {
|
|
1396
|
+
}
|
|
1397
|
+
onHookEnd(_options) {
|
|
1398
|
+
}
|
|
1399
|
+
onTestStart(_test) {
|
|
1400
|
+
}
|
|
1401
|
+
onTestFinished(_test) {
|
|
1402
|
+
}
|
|
1403
|
+
onTestFilePrepare(_file) {
|
|
1404
|
+
}
|
|
1405
|
+
onTestFileFinished(_file) {
|
|
1406
|
+
}
|
|
1407
|
+
onTaskUpdate(packs) {
|
|
1408
|
+
const startingTestFiles = [];
|
|
1409
|
+
const finishedTestFiles = [];
|
|
1410
|
+
const startingTests = [];
|
|
1411
|
+
const finishedTests = [];
|
|
1412
|
+
const startingHooks = [];
|
|
1413
|
+
const endingHooks = [];
|
|
1414
|
+
for (const pack of packs) {
|
|
1415
|
+
const task = this.ctx.state.idMap.get(pack[0]);
|
|
1416
|
+
if (task?.type === "suite" && "filepath" in task && task.result?.state) {
|
|
1417
|
+
if (task?.result?.state === "run" || task?.result?.state === "queued") {
|
|
1418
|
+
startingTestFiles.push(task);
|
|
1419
|
+
} else {
|
|
1420
|
+
for (const test of getTests(task)) {
|
|
1421
|
+
if (!test.result || test.result?.state === "skip") {
|
|
1422
|
+
finishedTests.push(test);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
finishedTestFiles.push(task.file);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (task?.type === "test") {
|
|
1429
|
+
if (task.result?.state === "run" || task.result?.state === "queued") {
|
|
1430
|
+
startingTests.push(task);
|
|
1431
|
+
} else if (task.result?.hooks?.afterEach !== "run") {
|
|
1432
|
+
finishedTests.push(task);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (task?.result?.hooks) {
|
|
1436
|
+
for (const [hook, state] of Object.entries(task.result.hooks)) {
|
|
1437
|
+
if (state === "run" || state === "queued") {
|
|
1438
|
+
startingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type });
|
|
1439
|
+
} else {
|
|
1440
|
+
endingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type });
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
endingHooks.forEach((hook) => this.onHookEnd(hook));
|
|
1446
|
+
finishedTests.forEach((test) => this.onTestFinished(test));
|
|
1447
|
+
finishedTestFiles.forEach((file) => this.onTestFileFinished(file));
|
|
1448
|
+
startingTestFiles.forEach((file) => this.onTestFilePrepare(file));
|
|
1449
|
+
startingTests.forEach((test) => this.onTestStart(test));
|
|
1450
|
+
startingHooks.forEach((hook) => this.onHookStart(hook));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const DURATION_UPDATE_INTERVAL_MS = 100;
|
|
1455
|
+
const FINISHED_TEST_CLEANUP_TIME_MS = 1e3;
|
|
1456
|
+
class SummaryReporter extends TaskParser {
|
|
1457
|
+
options;
|
|
1458
|
+
renderer;
|
|
1459
|
+
suites = emptyCounters();
|
|
1460
|
+
tests = emptyCounters();
|
|
1461
|
+
maxParallelTests = 0;
|
|
1462
|
+
/** Currently running tests, may include finished tests too */
|
|
1463
|
+
runningTests = /* @__PURE__ */ new Map();
|
|
1464
|
+
/** ID of finished `this.runningTests` that are currently being shown */
|
|
1465
|
+
finishedTests = /* @__PURE__ */ new Map();
|
|
1466
|
+
/** IDs of all finished tests */
|
|
1467
|
+
allFinishedTests = /* @__PURE__ */ new Set();
|
|
1468
|
+
startTime = "";
|
|
1469
|
+
currentTime = 0;
|
|
1470
|
+
duration = 0;
|
|
1471
|
+
durationInterval = void 0;
|
|
1472
|
+
onInit(ctx, options = {}) {
|
|
1473
|
+
this.ctx = ctx;
|
|
1474
|
+
this.options = {
|
|
1475
|
+
verbose: false,
|
|
1476
|
+
...options
|
|
1477
|
+
};
|
|
1478
|
+
this.renderer = new WindowRenderer({
|
|
1479
|
+
logger: ctx.logger,
|
|
1480
|
+
getWindow: () => this.createSummary()
|
|
1481
|
+
});
|
|
1482
|
+
this.startTimers();
|
|
1483
|
+
this.ctx.onClose(() => {
|
|
1484
|
+
clearInterval(this.durationInterval);
|
|
1485
|
+
this.renderer.stop();
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
onTestModuleQueued(module) {
|
|
1489
|
+
this.onTestFilePrepare(module.task);
|
|
1490
|
+
}
|
|
1491
|
+
onPathsCollected(paths) {
|
|
1492
|
+
this.suites.total = (paths || []).length;
|
|
1493
|
+
}
|
|
1494
|
+
onWatcherRerun() {
|
|
1495
|
+
this.runningTests.clear();
|
|
1496
|
+
this.finishedTests.clear();
|
|
1497
|
+
this.allFinishedTests.clear();
|
|
1498
|
+
this.suites = emptyCounters();
|
|
1499
|
+
this.tests = emptyCounters();
|
|
1500
|
+
this.startTimers();
|
|
1501
|
+
this.renderer.start();
|
|
1502
|
+
}
|
|
1503
|
+
onFinished() {
|
|
1504
|
+
this.runningTests.clear();
|
|
1505
|
+
this.finishedTests.clear();
|
|
1506
|
+
this.allFinishedTests.clear();
|
|
1507
|
+
this.renderer.finish();
|
|
1508
|
+
clearInterval(this.durationInterval);
|
|
1509
|
+
}
|
|
1510
|
+
onTestFilePrepare(file) {
|
|
1511
|
+
if (this.runningTests.has(file.id)) {
|
|
1512
|
+
const stats = this.runningTests.get(file.id);
|
|
1513
|
+
if (!stats.total) {
|
|
1514
|
+
const total2 = getTests(file).length;
|
|
1515
|
+
this.tests.total += total2;
|
|
1516
|
+
stats.total = total2;
|
|
1517
|
+
}
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (this.allFinishedTests.has(file.id)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const total = getTests(file).length;
|
|
1524
|
+
this.tests.total += total;
|
|
1525
|
+
if (this.finishedTests.size) {
|
|
1526
|
+
const finished = this.finishedTests.keys().next().value;
|
|
1527
|
+
this.removeTestFile(finished);
|
|
1528
|
+
}
|
|
1529
|
+
this.runningTests.set(file.id, {
|
|
1530
|
+
total,
|
|
1531
|
+
completed: 0,
|
|
1532
|
+
filename: file.name,
|
|
1533
|
+
projectName: file.projectName,
|
|
1534
|
+
tests: /* @__PURE__ */ new Map()
|
|
1535
|
+
});
|
|
1536
|
+
this.maxParallelTests = Math.max(this.maxParallelTests, this.runningTests.size);
|
|
1537
|
+
}
|
|
1538
|
+
onHookStart(options) {
|
|
1539
|
+
const stats = this.getHookStats(options);
|
|
1540
|
+
if (!stats) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const hook = {
|
|
1544
|
+
name: options.name,
|
|
1545
|
+
visible: false,
|
|
1546
|
+
startTime: performance.now(),
|
|
1547
|
+
onFinish: () => {
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
stats.hook?.onFinish?.();
|
|
1551
|
+
stats.hook = hook;
|
|
1552
|
+
const timeout = setTimeout(() => {
|
|
1553
|
+
hook.visible = true;
|
|
1554
|
+
}, this.ctx.config.slowTestThreshold).unref();
|
|
1555
|
+
hook.onFinish = () => clearTimeout(timeout);
|
|
1556
|
+
}
|
|
1557
|
+
onHookEnd(options) {
|
|
1558
|
+
const stats = this.getHookStats(options);
|
|
1559
|
+
if (stats?.hook?.name !== options.name) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
stats.hook.onFinish();
|
|
1563
|
+
stats.hook.visible = false;
|
|
1564
|
+
}
|
|
1565
|
+
onTestStart(test) {
|
|
1566
|
+
if (!this.options.verbose) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const stats = this.getTestStats(test);
|
|
1570
|
+
if (!stats || stats.tests.has(test.id)) {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const slowTest = {
|
|
1574
|
+
name: test.name,
|
|
1575
|
+
visible: false,
|
|
1576
|
+
startTime: performance.now(),
|
|
1577
|
+
onFinish: () => {
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
const timeout = setTimeout(() => {
|
|
1581
|
+
slowTest.visible = true;
|
|
1582
|
+
}, this.ctx.config.slowTestThreshold).unref();
|
|
1583
|
+
slowTest.onFinish = () => {
|
|
1584
|
+
slowTest.hook?.onFinish();
|
|
1585
|
+
clearTimeout(timeout);
|
|
1586
|
+
};
|
|
1587
|
+
stats.tests.set(test.id, slowTest);
|
|
1588
|
+
}
|
|
1589
|
+
onTestFinished(test) {
|
|
1590
|
+
const stats = this.getTestStats(test);
|
|
1591
|
+
if (!stats) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
stats.tests.get(test.id)?.onFinish();
|
|
1595
|
+
stats.tests.delete(test.id);
|
|
1596
|
+
stats.completed++;
|
|
1597
|
+
const result = test.result;
|
|
1598
|
+
if (result?.state === "pass") {
|
|
1599
|
+
this.tests.passed++;
|
|
1600
|
+
} else if (result?.state === "fail") {
|
|
1601
|
+
this.tests.failed++;
|
|
1602
|
+
} else if (!result?.state || result?.state === "skip" || result?.state === "todo") {
|
|
1603
|
+
this.tests.skipped++;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
onTestFileFinished(file) {
|
|
1607
|
+
if (this.allFinishedTests.has(file.id)) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
this.allFinishedTests.add(file.id);
|
|
1611
|
+
this.suites.completed++;
|
|
1612
|
+
if (file.result?.state === "pass") {
|
|
1613
|
+
this.suites.passed++;
|
|
1614
|
+
} else if (file.result?.state === "fail") {
|
|
1615
|
+
this.suites.failed++;
|
|
1616
|
+
} else if (file.result?.state === "skip") {
|
|
1617
|
+
this.suites.skipped++;
|
|
1618
|
+
} else if (file.result?.state === "todo") {
|
|
1619
|
+
this.suites.todo++;
|
|
1620
|
+
}
|
|
1621
|
+
const left = this.suites.total - this.suites.completed;
|
|
1622
|
+
if (left > this.maxParallelTests) {
|
|
1623
|
+
this.finishedTests.set(file.id, setTimeout(() => {
|
|
1624
|
+
this.removeTestFile(file.id);
|
|
1625
|
+
}, FINISHED_TEST_CLEANUP_TIME_MS).unref());
|
|
1626
|
+
} else {
|
|
1627
|
+
this.removeTestFile(file.id);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
getTestStats(test) {
|
|
1631
|
+
const file = test.file;
|
|
1632
|
+
let stats = this.runningTests.get(file.id);
|
|
1633
|
+
if (!stats || stats.total === 0) {
|
|
1634
|
+
this.onTestFilePrepare(test.file);
|
|
1635
|
+
stats = this.runningTests.get(file.id);
|
|
1636
|
+
if (!stats) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return stats;
|
|
1641
|
+
}
|
|
1642
|
+
getHookStats({ file, id, type }) {
|
|
1643
|
+
if (!this.options.verbose) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const stats = this.runningTests.get(file.id);
|
|
1647
|
+
if (!stats) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
return type === "suite" ? stats : stats?.tests.get(id);
|
|
1651
|
+
}
|
|
1652
|
+
createSummary() {
|
|
1653
|
+
const summary = [""];
|
|
1654
|
+
for (const testFile of Array.from(this.runningTests.values()).sort(sortRunningTests)) {
|
|
1655
|
+
summary.push(
|
|
1656
|
+
c.bold(c.yellow(` ${F_POINTER} `)) + formatProjectName(testFile.projectName) + testFile.filename + c.dim(!testFile.completed && !testFile.total ? " [queued]" : ` ${testFile.completed}/${testFile.total}`)
|
|
1657
|
+
);
|
|
1658
|
+
const slowTasks = [
|
|
1659
|
+
testFile.hook,
|
|
1660
|
+
...Array.from(testFile.tests.values())
|
|
1661
|
+
].filter((t) => t != null && t.visible);
|
|
1662
|
+
for (const [index, task] of slowTasks.entries()) {
|
|
1663
|
+
const elapsed = this.currentTime - task.startTime;
|
|
1664
|
+
const icon = index === slowTasks.length - 1 ? F_TREE_NODE_END : F_TREE_NODE_MIDDLE;
|
|
1665
|
+
summary.push(
|
|
1666
|
+
c.bold(c.yellow(` ${icon} `)) + task.name + c.bold(c.yellow(` ${formatTime(Math.max(0, elapsed))}`))
|
|
1667
|
+
);
|
|
1668
|
+
if (task.hook?.visible) {
|
|
1669
|
+
summary.push(c.bold(c.yellow(` ${F_TREE_NODE_END} `)) + task.hook.name);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (this.runningTests.size > 0) {
|
|
1674
|
+
summary.push("");
|
|
1675
|
+
}
|
|
1676
|
+
summary.push(padSummaryTitle("Test Files") + getStateString(this.suites));
|
|
1677
|
+
summary.push(padSummaryTitle("Tests") + getStateString(this.tests));
|
|
1678
|
+
summary.push(padSummaryTitle("Start at") + this.startTime);
|
|
1679
|
+
summary.push(padSummaryTitle("Duration") + formatTime(this.duration));
|
|
1680
|
+
summary.push("");
|
|
1681
|
+
return summary;
|
|
1682
|
+
}
|
|
1683
|
+
startTimers() {
|
|
1684
|
+
const start = performance.now();
|
|
1685
|
+
this.startTime = formatTimeString(/* @__PURE__ */ new Date());
|
|
1686
|
+
this.durationInterval = setInterval(() => {
|
|
1687
|
+
this.currentTime = performance.now();
|
|
1688
|
+
this.duration = this.currentTime - start;
|
|
1689
|
+
}, DURATION_UPDATE_INTERVAL_MS).unref();
|
|
1690
|
+
}
|
|
1691
|
+
removeTestFile(id) {
|
|
1692
|
+
if (!id) {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const testFile = this.runningTests.get(id);
|
|
1696
|
+
testFile?.hook?.onFinish();
|
|
1697
|
+
testFile?.tests?.forEach((test) => test.onFinish());
|
|
1698
|
+
this.runningTests.delete(id);
|
|
1699
|
+
clearTimeout(this.finishedTests.get(id));
|
|
1700
|
+
this.finishedTests.delete(id);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function emptyCounters() {
|
|
1704
|
+
return { completed: 0, passed: 0, failed: 0, skipped: 0, todo: 0, total: 0 };
|
|
1705
|
+
}
|
|
1706
|
+
function getStateString(entry) {
|
|
1707
|
+
return [
|
|
1708
|
+
entry.failed ? c.bold(c.red(`${entry.failed} failed`)) : null,
|
|
1709
|
+
c.bold(c.green(`${entry.passed} passed`)),
|
|
1710
|
+
entry.skipped ? c.yellow(`${entry.skipped} skipped`) : null,
|
|
1711
|
+
entry.todo ? c.gray(`${entry.todo} todo`) : null
|
|
1712
|
+
].filter(Boolean).join(c.dim(" | ")) + c.gray(` (${entry.total})`);
|
|
1713
|
+
}
|
|
1714
|
+
function sortRunningTests(a, b) {
|
|
1715
|
+
if ((a.projectName || "") > (b.projectName || "")) {
|
|
1716
|
+
return 1;
|
|
1717
|
+
}
|
|
1718
|
+
if ((a.projectName || "") < (b.projectName || "")) {
|
|
1719
|
+
return -1;
|
|
1720
|
+
}
|
|
1721
|
+
return a.filename.localeCompare(b.filename);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
class DefaultReporter extends BaseReporter {
|
|
1725
|
+
options;
|
|
1726
|
+
summary;
|
|
1727
|
+
constructor(options = {}) {
|
|
1728
|
+
super(options);
|
|
1729
|
+
this.options = {
|
|
1730
|
+
summary: true,
|
|
1731
|
+
...options
|
|
1732
|
+
};
|
|
1733
|
+
if (!this.isTTY) {
|
|
1734
|
+
this.options.summary = false;
|
|
1735
|
+
}
|
|
1736
|
+
if (this.options.summary) {
|
|
1737
|
+
this.summary = new SummaryReporter();
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
onTestModuleQueued(file) {
|
|
1741
|
+
this.summary?.onTestModuleQueued(file);
|
|
1742
|
+
}
|
|
1743
|
+
onInit(ctx) {
|
|
1744
|
+
super.onInit(ctx);
|
|
1745
|
+
this.summary?.onInit(ctx, { verbose: this.verbose });
|
|
1746
|
+
}
|
|
1747
|
+
onPathsCollected(paths = []) {
|
|
1748
|
+
if (this.isTTY) {
|
|
1749
|
+
if (this.renderSucceed === void 0) {
|
|
1750
|
+
this.renderSucceed = !!this.renderSucceed;
|
|
1751
|
+
}
|
|
1752
|
+
if (this.renderSucceed !== true) {
|
|
1753
|
+
this.renderSucceed = paths.length <= 1;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
this.summary?.onPathsCollected(paths);
|
|
1757
|
+
}
|
|
1758
|
+
onTaskUpdate(packs) {
|
|
1759
|
+
this.summary?.onTaskUpdate(packs);
|
|
1760
|
+
super.onTaskUpdate(packs);
|
|
1761
|
+
}
|
|
1762
|
+
onWatcherRerun(files, trigger) {
|
|
1763
|
+
this.summary?.onWatcherRerun();
|
|
1764
|
+
super.onWatcherRerun(files, trigger);
|
|
1765
|
+
}
|
|
1766
|
+
onFinished(files, errors) {
|
|
1767
|
+
this.summary?.onFinished();
|
|
1768
|
+
super.onFinished(files, errors);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
class DotReporter extends BaseReporter {
|
|
1773
|
+
summary;
|
|
1774
|
+
onInit(ctx) {
|
|
1775
|
+
super.onInit(ctx);
|
|
1776
|
+
if (this.isTTY) {
|
|
1777
|
+
this.summary = new DotSummary();
|
|
1778
|
+
this.summary.onInit(ctx);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
onTaskUpdate(packs) {
|
|
1782
|
+
this.summary?.onTaskUpdate(packs);
|
|
1783
|
+
if (!this.isTTY) {
|
|
1784
|
+
super.onTaskUpdate(packs);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
onWatcherRerun(files, trigger) {
|
|
1788
|
+
this.summary?.onWatcherRerun();
|
|
1789
|
+
super.onWatcherRerun(files, trigger);
|
|
1790
|
+
}
|
|
1791
|
+
onFinished(files, errors) {
|
|
1792
|
+
this.summary?.onFinished();
|
|
1793
|
+
super.onFinished(files, errors);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
class DotSummary extends TaskParser {
|
|
1797
|
+
renderer;
|
|
1798
|
+
tests = /* @__PURE__ */ new Map();
|
|
1799
|
+
finishedTests = /* @__PURE__ */ new Set();
|
|
1800
|
+
onInit(ctx) {
|
|
1801
|
+
this.ctx = ctx;
|
|
1802
|
+
this.renderer = new WindowRenderer({
|
|
1803
|
+
logger: ctx.logger,
|
|
1804
|
+
getWindow: () => this.createSummary()
|
|
1805
|
+
});
|
|
1806
|
+
this.ctx.onClose(() => this.renderer.stop());
|
|
1807
|
+
}
|
|
1808
|
+
onWatcherRerun() {
|
|
1809
|
+
this.tests.clear();
|
|
1810
|
+
this.renderer.start();
|
|
1811
|
+
}
|
|
1812
|
+
onFinished() {
|
|
1813
|
+
const finalLog = formatTests(Array.from(this.tests.values()));
|
|
1814
|
+
this.ctx.logger.log(finalLog);
|
|
1815
|
+
this.tests.clear();
|
|
1816
|
+
this.renderer.finish();
|
|
1817
|
+
}
|
|
1818
|
+
onTestFilePrepare(file) {
|
|
1819
|
+
for (const test of getTests(file)) {
|
|
1820
|
+
this.onTestStart(test);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
onTestStart(test) {
|
|
1824
|
+
if (this.finishedTests.has(test.id)) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
this.tests.set(test.id, test.mode || "run");
|
|
1828
|
+
}
|
|
1829
|
+
onTestFinished(test) {
|
|
1830
|
+
if (this.finishedTests.has(test.id)) {
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
this.finishedTests.add(test.id);
|
|
1834
|
+
this.tests.set(test.id, test.result?.state || "skip");
|
|
1835
|
+
}
|
|
1836
|
+
onTestFileFinished() {
|
|
1837
|
+
const columns = this.ctx.logger.getColumns();
|
|
1838
|
+
if (this.tests.size < columns) {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const finishedTests = Array.from(this.tests).filter((entry) => entry[1] !== "run");
|
|
1842
|
+
if (finishedTests.length < columns) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const states = [];
|
|
1846
|
+
let count = 0;
|
|
1847
|
+
for (const [id, state] of finishedTests) {
|
|
1848
|
+
if (count++ >= columns) {
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
this.tests.delete(id);
|
|
1852
|
+
states.push(state);
|
|
1853
|
+
}
|
|
1854
|
+
this.ctx.logger.log(formatTests(states));
|
|
1855
|
+
}
|
|
1856
|
+
createSummary() {
|
|
1857
|
+
return [
|
|
1858
|
+
formatTests(Array.from(this.tests.values())),
|
|
1859
|
+
""
|
|
1860
|
+
];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
const pass = { char: "\xB7", color: c.green };
|
|
1864
|
+
const fail = { char: "x", color: c.red };
|
|
1865
|
+
const pending = { char: "*", color: c.yellow };
|
|
1866
|
+
const skip = { char: "-", color: (char) => c.dim(c.gray(char)) };
|
|
1867
|
+
function getIcon(state) {
|
|
1868
|
+
switch (state) {
|
|
1869
|
+
case "pass":
|
|
1870
|
+
return pass;
|
|
1871
|
+
case "fail":
|
|
1872
|
+
return fail;
|
|
1873
|
+
case "skip":
|
|
1874
|
+
case "todo":
|
|
1875
|
+
return skip;
|
|
1876
|
+
default:
|
|
1877
|
+
return pending;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function formatTests(states) {
|
|
1881
|
+
let currentIcon = pending;
|
|
1882
|
+
let count = 0;
|
|
1883
|
+
let output = "";
|
|
1884
|
+
for (const state of states) {
|
|
1885
|
+
const icon = getIcon(state);
|
|
1886
|
+
if (currentIcon === icon) {
|
|
1887
|
+
count++;
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
output += currentIcon.color(currentIcon.char.repeat(count));
|
|
1891
|
+
count = 1;
|
|
1892
|
+
currentIcon = icon;
|
|
1893
|
+
}
|
|
1894
|
+
output += currentIcon.color(currentIcon.char.repeat(count));
|
|
1895
|
+
return output;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
class GithubActionsReporter {
|
|
1899
|
+
ctx = void 0;
|
|
1900
|
+
onInit(ctx) {
|
|
1901
|
+
this.ctx = ctx;
|
|
1902
|
+
}
|
|
1903
|
+
onFinished(files = [], errors = []) {
|
|
1904
|
+
const projectErrors = new Array();
|
|
1905
|
+
for (const error of errors) {
|
|
1906
|
+
projectErrors.push({
|
|
1907
|
+
project: this.ctx.getRootProject(),
|
|
1908
|
+
title: "Unhandled error",
|
|
1909
|
+
error
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
for (const file of files) {
|
|
1913
|
+
const tasks = getTasks(file);
|
|
1914
|
+
const project = this.ctx.getProjectByName(file.projectName || "");
|
|
1915
|
+
for (const task of tasks) {
|
|
1916
|
+
if (task.result?.state !== "fail") {
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
const title = getFullName(task, " > ");
|
|
1920
|
+
for (const error of task.result?.errors ?? []) {
|
|
1921
|
+
projectErrors.push({
|
|
1922
|
+
project,
|
|
1923
|
+
title,
|
|
1924
|
+
error,
|
|
1925
|
+
file
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
for (const { project, title, error, file } of projectErrors) {
|
|
1931
|
+
const result = capturePrintError(error, this.ctx, { project, task: file });
|
|
1932
|
+
const stack = result?.nearest;
|
|
1933
|
+
if (!stack) {
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
const formatted = formatMessage({
|
|
1937
|
+
command: "error",
|
|
1938
|
+
properties: {
|
|
1939
|
+
file: stack.file,
|
|
1940
|
+
title,
|
|
1941
|
+
line: String(stack.line),
|
|
1942
|
+
column: String(stack.column)
|
|
1943
|
+
},
|
|
1944
|
+
message: stripVTControlCharacters(result.output)
|
|
1945
|
+
});
|
|
1946
|
+
this.ctx.logger.log(`
|
|
1947
|
+
${formatted}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function formatMessage({
|
|
1952
|
+
command,
|
|
1953
|
+
properties,
|
|
1954
|
+
message
|
|
1955
|
+
}) {
|
|
1956
|
+
let result = `::${command}`;
|
|
1957
|
+
Object.entries(properties).forEach(([k, v], i) => {
|
|
1958
|
+
result += i === 0 ? " " : ",";
|
|
1959
|
+
result += `${k}=${escapeProperty(v)}`;
|
|
1960
|
+
});
|
|
1961
|
+
result += `::${escapeData(message)}`;
|
|
1962
|
+
return result;
|
|
1963
|
+
}
|
|
1964
|
+
function escapeData(s) {
|
|
1965
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
1966
|
+
}
|
|
1967
|
+
function escapeProperty(s) {
|
|
1968
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
class HangingProcessReporter {
|
|
1972
|
+
whyRunning;
|
|
1973
|
+
onInit() {
|
|
1974
|
+
const _require = createRequire(import.meta.url);
|
|
1975
|
+
this.whyRunning = _require("why-is-node-running");
|
|
1976
|
+
}
|
|
1977
|
+
onProcessTimeout() {
|
|
1978
|
+
this.whyRunning?.();
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const StatusMap = {
|
|
1983
|
+
fail: "failed",
|
|
1984
|
+
only: "pending",
|
|
1985
|
+
pass: "passed",
|
|
1986
|
+
run: "pending",
|
|
1987
|
+
skip: "skipped",
|
|
1988
|
+
todo: "todo",
|
|
1989
|
+
queued: "pending"
|
|
1990
|
+
};
|
|
1991
|
+
class JsonReporter {
|
|
1992
|
+
start = 0;
|
|
1993
|
+
ctx;
|
|
1994
|
+
options;
|
|
1995
|
+
constructor(options) {
|
|
1996
|
+
this.options = options;
|
|
1997
|
+
}
|
|
1998
|
+
onInit(ctx) {
|
|
1999
|
+
this.ctx = ctx;
|
|
2000
|
+
this.start = Date.now();
|
|
2001
|
+
}
|
|
2002
|
+
async logTasks(files, coverageMap) {
|
|
2003
|
+
const suites = getSuites(files);
|
|
2004
|
+
const numTotalTestSuites = suites.length;
|
|
2005
|
+
const tests = getTests(files);
|
|
2006
|
+
const numTotalTests = tests.length;
|
|
2007
|
+
const numFailedTestSuites = suites.filter((s) => s.result?.state === "fail").length;
|
|
2008
|
+
const numPendingTestSuites = suites.filter(
|
|
2009
|
+
(s) => s.result?.state === "run" || s.result?.state === "queued" || s.mode === "todo"
|
|
2010
|
+
).length;
|
|
2011
|
+
const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites - numPendingTestSuites;
|
|
2012
|
+
const numFailedTests = tests.filter(
|
|
2013
|
+
(t) => t.result?.state === "fail"
|
|
2014
|
+
).length;
|
|
2015
|
+
const numPassedTests = tests.filter((t) => t.result?.state === "pass").length;
|
|
2016
|
+
const numPendingTests = tests.filter(
|
|
2017
|
+
(t) => t.result?.state === "run" || t.result?.state === "queued" || t.mode === "skip" || t.result?.state === "skip"
|
|
2018
|
+
).length;
|
|
2019
|
+
const numTodoTests = tests.filter((t) => t.mode === "todo").length;
|
|
2020
|
+
const testResults = [];
|
|
2021
|
+
const success = numFailedTestSuites === 0 && numFailedTests === 0;
|
|
2022
|
+
for (const file of files) {
|
|
2023
|
+
const tests2 = getTests([file]);
|
|
2024
|
+
let startTime = tests2.reduce(
|
|
2025
|
+
(prev, next) => Math.min(prev, next.result?.startTime ?? Number.POSITIVE_INFINITY),
|
|
2026
|
+
Number.POSITIVE_INFINITY
|
|
2027
|
+
);
|
|
2028
|
+
if (startTime === Number.POSITIVE_INFINITY) {
|
|
2029
|
+
startTime = this.start;
|
|
2030
|
+
}
|
|
2031
|
+
const endTime = tests2.reduce(
|
|
2032
|
+
(prev, next) => Math.max(
|
|
2033
|
+
prev,
|
|
2034
|
+
(next.result?.startTime ?? 0) + (next.result?.duration ?? 0)
|
|
2035
|
+
),
|
|
2036
|
+
startTime
|
|
2037
|
+
);
|
|
2038
|
+
const assertionResults = tests2.map((t) => {
|
|
2039
|
+
const ancestorTitles = [];
|
|
2040
|
+
let iter = t.suite;
|
|
2041
|
+
while (iter) {
|
|
2042
|
+
ancestorTitles.push(iter.name);
|
|
2043
|
+
iter = iter.suite;
|
|
2044
|
+
}
|
|
2045
|
+
ancestorTitles.reverse();
|
|
2046
|
+
return {
|
|
2047
|
+
ancestorTitles,
|
|
2048
|
+
fullName: t.name ? [...ancestorTitles, t.name].join(" ") : ancestorTitles.join(" "),
|
|
2049
|
+
status: StatusMap[t.result?.state || t.mode] || "skipped",
|
|
2050
|
+
title: t.name,
|
|
2051
|
+
duration: t.result?.duration,
|
|
2052
|
+
failureMessages: t.result?.errors?.map((e) => e.stack || e.message) || [],
|
|
2053
|
+
location: t.location,
|
|
2054
|
+
meta: t.meta
|
|
2055
|
+
};
|
|
2056
|
+
});
|
|
2057
|
+
if (tests2.some((t) => t.result?.state === "run" || t.result?.state === "queued")) {
|
|
2058
|
+
this.ctx.logger.warn(
|
|
2059
|
+
"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"
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
const hasFailedTests = tests2.some((t) => t.result?.state === "fail");
|
|
2063
|
+
testResults.push({
|
|
2064
|
+
assertionResults,
|
|
2065
|
+
startTime,
|
|
2066
|
+
endTime,
|
|
2067
|
+
status: file.result?.state === "fail" || hasFailedTests ? "failed" : "passed",
|
|
2068
|
+
message: file.result?.errors?.[0]?.message ?? "",
|
|
2069
|
+
name: file.filepath
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
const result = {
|
|
2073
|
+
numTotalTestSuites,
|
|
2074
|
+
numPassedTestSuites,
|
|
2075
|
+
numFailedTestSuites,
|
|
2076
|
+
numPendingTestSuites,
|
|
2077
|
+
numTotalTests,
|
|
2078
|
+
numPassedTests,
|
|
2079
|
+
numFailedTests,
|
|
2080
|
+
numPendingTests,
|
|
2081
|
+
numTodoTests,
|
|
2082
|
+
snapshot: this.ctx.snapshot.summary,
|
|
2083
|
+
startTime: this.start,
|
|
2084
|
+
success,
|
|
2085
|
+
testResults,
|
|
2086
|
+
coverageMap
|
|
2087
|
+
};
|
|
2088
|
+
await this.writeReport(JSON.stringify(result));
|
|
2089
|
+
}
|
|
2090
|
+
async onFinished(files = this.ctx.state.getFiles(), _errors = [], coverageMap) {
|
|
2091
|
+
await this.logTasks(files, coverageMap);
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Writes the report to an output file if specified in the config,
|
|
2095
|
+
* or logs it to the console otherwise.
|
|
2096
|
+
* @param report
|
|
2097
|
+
*/
|
|
2098
|
+
async writeReport(report) {
|
|
2099
|
+
const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "json");
|
|
2100
|
+
if (outputFile) {
|
|
2101
|
+
const reportFile = resolve(this.ctx.config.root, outputFile);
|
|
2102
|
+
const outputDirectory = dirname(reportFile);
|
|
2103
|
+
if (!existsSync(outputDirectory)) {
|
|
2104
|
+
await promises.mkdir(outputDirectory, { recursive: true });
|
|
2105
|
+
}
|
|
2106
|
+
await promises.writeFile(reportFile, report, "utf-8");
|
|
2107
|
+
this.ctx.logger.log(`JSON report written to ${reportFile}`);
|
|
2108
|
+
} else {
|
|
2109
|
+
this.ctx.logger.log(report);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
class IndentedLogger {
|
|
2115
|
+
constructor(baseLog) {
|
|
2116
|
+
this.baseLog = baseLog;
|
|
2117
|
+
}
|
|
2118
|
+
currentIndent = "";
|
|
2119
|
+
indent() {
|
|
2120
|
+
this.currentIndent += " ";
|
|
2121
|
+
}
|
|
2122
|
+
unindent() {
|
|
2123
|
+
this.currentIndent = this.currentIndent.substring(
|
|
2124
|
+
0,
|
|
2125
|
+
this.currentIndent.length - 4
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
log(text) {
|
|
2129
|
+
return this.baseLog(this.currentIndent + text);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function flattenTasks$1(task, baseName = "") {
|
|
2134
|
+
const base = baseName ? `${baseName} > ` : "";
|
|
2135
|
+
if (task.type === "suite") {
|
|
2136
|
+
return task.tasks.flatMap(
|
|
2137
|
+
(child) => flattenTasks$1(child, `${base}${task.name}`)
|
|
2138
|
+
);
|
|
2139
|
+
} else {
|
|
2140
|
+
return [
|
|
2141
|
+
{
|
|
2142
|
+
...task,
|
|
2143
|
+
name: `${base}${task.name}`
|
|
2144
|
+
}
|
|
2145
|
+
];
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function removeInvalidXMLCharacters(value, removeDiscouragedChars) {
|
|
2149
|
+
let regex = /([\0-\x08\v\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g;
|
|
2150
|
+
value = String(value || "").replace(regex, "");
|
|
2151
|
+
{
|
|
2152
|
+
regex = new RegExp(
|
|
2153
|
+
/* eslint-disable regexp/prefer-character-class, regexp/no-obscure-range, regexp/no-useless-non-capturing-group */
|
|
2154
|
+
"([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|\\uD83F[\\uDFFE\\uDFFF]|(?:\\uD87F[\\uDFFE\\uDFFF])|\\uD8BF[\\uDFFE\\uDFFF]|\\uD8FF[\\uDFFE\\uDFFF]|(?:\\uD93F[\\uDFFE\\uDFFF])|\\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]))",
|
|
2155
|
+
"g"
|
|
2156
|
+
/* eslint-enable */
|
|
2157
|
+
);
|
|
2158
|
+
value = value.replace(regex, "");
|
|
2159
|
+
}
|
|
2160
|
+
return value;
|
|
2161
|
+
}
|
|
2162
|
+
function escapeXML(value) {
|
|
2163
|
+
return removeInvalidXMLCharacters(
|
|
2164
|
+
String(value).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"));
|
|
2165
|
+
}
|
|
2166
|
+
function executionTime(durationMS) {
|
|
2167
|
+
return (durationMS / 1e3).toLocaleString("en-US", {
|
|
2168
|
+
useGrouping: false,
|
|
2169
|
+
maximumFractionDigits: 10
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
function getDuration(task) {
|
|
2173
|
+
const duration = task.result?.duration ?? 0;
|
|
2174
|
+
return executionTime(duration);
|
|
2175
|
+
}
|
|
2176
|
+
class JUnitReporter {
|
|
2177
|
+
ctx;
|
|
2178
|
+
reportFile;
|
|
2179
|
+
baseLog;
|
|
2180
|
+
logger;
|
|
2181
|
+
_timeStart = /* @__PURE__ */ new Date();
|
|
2182
|
+
fileFd;
|
|
2183
|
+
options;
|
|
2184
|
+
constructor(options) {
|
|
2185
|
+
this.options = { ...options };
|
|
2186
|
+
this.options.includeConsoleOutput ??= true;
|
|
2187
|
+
}
|
|
2188
|
+
async onInit(ctx) {
|
|
2189
|
+
this.ctx = ctx;
|
|
2190
|
+
const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "junit");
|
|
2191
|
+
if (outputFile) {
|
|
2192
|
+
this.reportFile = resolve(this.ctx.config.root, outputFile);
|
|
2193
|
+
const outputDirectory = dirname(this.reportFile);
|
|
2194
|
+
if (!existsSync(outputDirectory)) {
|
|
2195
|
+
await promises.mkdir(outputDirectory, { recursive: true });
|
|
2196
|
+
}
|
|
2197
|
+
const fileFd = await promises.open(this.reportFile, "w+");
|
|
2198
|
+
this.fileFd = fileFd;
|
|
2199
|
+
this.baseLog = async (text) => {
|
|
2200
|
+
if (!this.fileFd) {
|
|
2201
|
+
this.fileFd = await promises.open(this.reportFile, "w+");
|
|
2202
|
+
}
|
|
2203
|
+
await promises.writeFile(this.fileFd, `${text}
|
|
2204
|
+
`);
|
|
2205
|
+
};
|
|
2206
|
+
} else {
|
|
2207
|
+
this.baseLog = async (text) => this.ctx.logger.log(text);
|
|
2208
|
+
}
|
|
2209
|
+
this._timeStart = /* @__PURE__ */ new Date();
|
|
2210
|
+
this.logger = new IndentedLogger(this.baseLog);
|
|
2211
|
+
}
|
|
2212
|
+
async writeElement(name, attrs, children) {
|
|
2213
|
+
const pairs = [];
|
|
2214
|
+
for (const key in attrs) {
|
|
2215
|
+
const attr = attrs[key];
|
|
2216
|
+
if (attr === void 0) {
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
pairs.push(`${key}="${escapeXML(attr)}"`);
|
|
2220
|
+
}
|
|
2221
|
+
await this.logger.log(
|
|
2222
|
+
`<${name}${pairs.length ? ` ${pairs.join(" ")}` : ""}>`
|
|
2223
|
+
);
|
|
2224
|
+
this.logger.indent();
|
|
2225
|
+
await children.call(this);
|
|
2226
|
+
this.logger.unindent();
|
|
2227
|
+
await this.logger.log(`</${name}>`);
|
|
2228
|
+
}
|
|
2229
|
+
async writeLogs(task, type) {
|
|
2230
|
+
if (task.logs == null || task.logs.length === 0) {
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const logType = type === "err" ? "stderr" : "stdout";
|
|
2234
|
+
const logs = task.logs.filter((log) => log.type === logType);
|
|
2235
|
+
if (logs.length === 0) {
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
await this.writeElement(`system-${type}`, {}, async () => {
|
|
2239
|
+
for (const log of logs) {
|
|
2240
|
+
await this.baseLog(escapeXML(log.content));
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
async writeTasks(tasks, filename) {
|
|
2245
|
+
for (const task of tasks) {
|
|
2246
|
+
let classname = filename;
|
|
2247
|
+
const templateVars = {
|
|
2248
|
+
filename: task.file.name,
|
|
2249
|
+
filepath: task.file.filepath
|
|
2250
|
+
};
|
|
2251
|
+
if (typeof this.options.classnameTemplate === "function") {
|
|
2252
|
+
classname = this.options.classnameTemplate(templateVars);
|
|
2253
|
+
} else if (typeof this.options.classnameTemplate === "string") {
|
|
2254
|
+
classname = this.options.classnameTemplate.replace(/\{filename\}/g, templateVars.filename).replace(/\{filepath\}/g, templateVars.filepath);
|
|
2255
|
+
} else if (typeof this.options.classname === "string") {
|
|
2256
|
+
classname = this.options.classname;
|
|
2257
|
+
}
|
|
2258
|
+
await this.writeElement(
|
|
2259
|
+
"testcase",
|
|
2260
|
+
{
|
|
2261
|
+
classname,
|
|
2262
|
+
file: this.options.addFileAttribute ? filename : void 0,
|
|
2263
|
+
name: task.name,
|
|
2264
|
+
time: getDuration(task)
|
|
2265
|
+
},
|
|
2266
|
+
async () => {
|
|
2267
|
+
if (this.options.includeConsoleOutput) {
|
|
2268
|
+
await this.writeLogs(task, "out");
|
|
2269
|
+
await this.writeLogs(task, "err");
|
|
2270
|
+
}
|
|
2271
|
+
if (task.mode === "skip" || task.mode === "todo") {
|
|
2272
|
+
await this.logger.log("<skipped/>");
|
|
2273
|
+
}
|
|
2274
|
+
if (task.result?.state === "fail") {
|
|
2275
|
+
const errors = task.result.errors || [];
|
|
2276
|
+
for (const error of errors) {
|
|
2277
|
+
await this.writeElement(
|
|
2278
|
+
"failure",
|
|
2279
|
+
{
|
|
2280
|
+
message: error?.message,
|
|
2281
|
+
type: error?.name ?? error?.nameStr
|
|
2282
|
+
},
|
|
2283
|
+
async () => {
|
|
2284
|
+
if (!error) {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const result = capturePrintError(
|
|
2288
|
+
error,
|
|
2289
|
+
this.ctx,
|
|
2290
|
+
{ project: this.ctx.getProjectByName(task.file.projectName || ""), task }
|
|
2291
|
+
);
|
|
2292
|
+
await this.baseLog(
|
|
2293
|
+
escapeXML(stripVTControlCharacters(result.output.trim()))
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
async onFinished(files = this.ctx.state.getFiles()) {
|
|
2304
|
+
await this.logger.log('<?xml version="1.0" encoding="UTF-8" ?>');
|
|
2305
|
+
const transformed = files.map((file) => {
|
|
2306
|
+
const tasks = file.tasks.flatMap((task) => flattenTasks$1(task));
|
|
2307
|
+
const stats2 = tasks.reduce(
|
|
2308
|
+
(stats3, task) => {
|
|
2309
|
+
return {
|
|
2310
|
+
passed: stats3.passed + Number(task.result?.state === "pass"),
|
|
2311
|
+
failures: stats3.failures + Number(task.result?.state === "fail"),
|
|
2312
|
+
skipped: stats3.skipped + Number(task.mode === "skip" || task.mode === "todo")
|
|
2313
|
+
};
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
passed: 0,
|
|
2317
|
+
failures: 0,
|
|
2318
|
+
skipped: 0
|
|
2319
|
+
}
|
|
2320
|
+
);
|
|
2321
|
+
const suites = getSuites(file);
|
|
2322
|
+
for (const suite of suites) {
|
|
2323
|
+
if (suite.result?.errors) {
|
|
2324
|
+
tasks.push(suite);
|
|
2325
|
+
stats2.failures += 1;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (tasks.length === 0 && file.result?.state === "fail") {
|
|
2329
|
+
stats2.failures = 1;
|
|
2330
|
+
tasks.push({
|
|
2331
|
+
id: file.id,
|
|
2332
|
+
type: "test",
|
|
2333
|
+
name: file.name,
|
|
2334
|
+
mode: "run",
|
|
2335
|
+
result: file.result,
|
|
2336
|
+
meta: {},
|
|
2337
|
+
// NOTE: not used in JUnitReporter
|
|
2338
|
+
context: null,
|
|
2339
|
+
suite: null,
|
|
2340
|
+
file: null
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
return {
|
|
2344
|
+
...file,
|
|
2345
|
+
tasks,
|
|
2346
|
+
stats: stats2
|
|
2347
|
+
};
|
|
2348
|
+
});
|
|
2349
|
+
const stats = transformed.reduce(
|
|
2350
|
+
(stats2, file) => {
|
|
2351
|
+
stats2.tests += file.tasks.length;
|
|
2352
|
+
stats2.failures += file.stats.failures;
|
|
2353
|
+
stats2.time += file.result?.duration || 0;
|
|
2354
|
+
return stats2;
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
name: this.options.suiteName || "vitest tests",
|
|
2358
|
+
tests: 0,
|
|
2359
|
+
failures: 0,
|
|
2360
|
+
errors: 0,
|
|
2361
|
+
// we cannot detect those
|
|
2362
|
+
time: 0
|
|
2363
|
+
}
|
|
2364
|
+
);
|
|
2365
|
+
await this.writeElement("testsuites", { ...stats, time: executionTime(stats.time) }, async () => {
|
|
2366
|
+
for (const file of transformed) {
|
|
2367
|
+
const filename = relative(this.ctx.config.root, file.filepath);
|
|
2368
|
+
await this.writeElement(
|
|
2369
|
+
"testsuite",
|
|
2370
|
+
{
|
|
2371
|
+
name: filename,
|
|
2372
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2373
|
+
hostname: hostname(),
|
|
2374
|
+
tests: file.tasks.length,
|
|
2375
|
+
failures: file.stats.failures,
|
|
2376
|
+
errors: 0,
|
|
2377
|
+
// An errored test is one that had an unanticipated problem. We cannot detect those.
|
|
2378
|
+
skipped: file.stats.skipped,
|
|
2379
|
+
time: getDuration(file)
|
|
2380
|
+
},
|
|
2381
|
+
async () => {
|
|
2382
|
+
await this.writeTasks(file.tasks, filename);
|
|
2383
|
+
}
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
if (this.reportFile) {
|
|
2388
|
+
this.ctx.logger.log(`JUNIT report written to ${this.reportFile}`);
|
|
2389
|
+
}
|
|
2390
|
+
await this.fileFd?.close();
|
|
2391
|
+
this.fileFd = void 0;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
class ReportedTaskImplementation {
|
|
2396
|
+
/**
|
|
2397
|
+
* Task instance.
|
|
2398
|
+
* @internal
|
|
2399
|
+
*/
|
|
2400
|
+
task;
|
|
2401
|
+
/**
|
|
2402
|
+
* The project associated with the test or suite.
|
|
2403
|
+
*/
|
|
2404
|
+
project;
|
|
2405
|
+
/**
|
|
2406
|
+
* Unique identifier.
|
|
2407
|
+
* This ID is deterministic and will be the same for the same test across multiple runs.
|
|
2408
|
+
* The ID is based on the project name, module url and test order.
|
|
2409
|
+
*/
|
|
2410
|
+
id;
|
|
2411
|
+
/**
|
|
2412
|
+
* Location in the module where the test or suite is defined.
|
|
2413
|
+
*/
|
|
2414
|
+
location;
|
|
2415
|
+
/** @internal */
|
|
2416
|
+
constructor(task, project) {
|
|
2417
|
+
this.task = task;
|
|
2418
|
+
this.project = project;
|
|
2419
|
+
this.id = task.id;
|
|
2420
|
+
this.location = task.location;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Checks if the test did not fail the suite.
|
|
2424
|
+
* If the test is not finished yet or was skipped, it will return `true`.
|
|
2425
|
+
*/
|
|
2426
|
+
ok() {
|
|
2427
|
+
const result = this.task.result;
|
|
2428
|
+
return !result || result.state !== "fail";
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Creates a new reported task instance and stores it in the project's state for future use.
|
|
2432
|
+
* @internal
|
|
2433
|
+
*/
|
|
2434
|
+
static register(task, project) {
|
|
2435
|
+
const state = new this(task, project);
|
|
2436
|
+
storeTask(project, task, state);
|
|
2437
|
+
return state;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
class TestCase extends ReportedTaskImplementation {
|
|
2441
|
+
#fullName;
|
|
2442
|
+
type = "test";
|
|
2443
|
+
/**
|
|
2444
|
+
* Direct reference to the test module where the test or suite is defined.
|
|
2445
|
+
*/
|
|
2446
|
+
module;
|
|
2447
|
+
/**
|
|
2448
|
+
* Name of the test.
|
|
2449
|
+
*/
|
|
2450
|
+
name;
|
|
2451
|
+
/**
|
|
2452
|
+
* Options that the test was initiated with.
|
|
2453
|
+
*/
|
|
2454
|
+
options;
|
|
2455
|
+
/**
|
|
2456
|
+
* Parent suite. If the test was called directly inside the module, the parent will be the module itself.
|
|
2457
|
+
*/
|
|
2458
|
+
parent;
|
|
2459
|
+
/** @internal */
|
|
2460
|
+
constructor(task, project) {
|
|
2461
|
+
super(task, project);
|
|
2462
|
+
this.name = task.name;
|
|
2463
|
+
this.module = getReportedTask(project, task.file);
|
|
2464
|
+
const suite = this.task.suite;
|
|
2465
|
+
if (suite) {
|
|
2466
|
+
this.parent = getReportedTask(project, suite);
|
|
2467
|
+
} else {
|
|
2468
|
+
this.parent = this.module;
|
|
2469
|
+
}
|
|
2470
|
+
this.options = buildOptions(task);
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Full name of the test including all parent suites separated with `>`.
|
|
2474
|
+
*/
|
|
2475
|
+
get fullName() {
|
|
2476
|
+
if (this.#fullName === void 0) {
|
|
2477
|
+
if (this.parent.type !== "module") {
|
|
2478
|
+
this.#fullName = `${this.parent.fullName} > ${this.name}`;
|
|
2479
|
+
} else {
|
|
2480
|
+
this.#fullName = this.name;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
return this.#fullName;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Test results. Will be `undefined` if test is skipped, not finished yet or was just collected.
|
|
2487
|
+
*/
|
|
2488
|
+
result() {
|
|
2489
|
+
const result = this.task.result;
|
|
2490
|
+
if (!result || result.state === "run" || result.state === "queued") {
|
|
2491
|
+
return void 0;
|
|
2492
|
+
}
|
|
2493
|
+
const state = result.state === "fail" ? "failed" : result.state === "pass" ? "passed" : "skipped";
|
|
2494
|
+
if (state === "skipped") {
|
|
2495
|
+
return {
|
|
2496
|
+
state,
|
|
2497
|
+
note: result.note,
|
|
2498
|
+
errors: void 0
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
if (state === "passed") {
|
|
2502
|
+
return {
|
|
2503
|
+
state,
|
|
2504
|
+
errors: result.errors
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
return {
|
|
2508
|
+
state,
|
|
2509
|
+
errors: result.errors || []
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Checks if the test was skipped during collection or dynamically with `ctx.skip()`.
|
|
2514
|
+
*/
|
|
2515
|
+
skipped() {
|
|
2516
|
+
const mode = this.task.result?.state || this.task.mode;
|
|
2517
|
+
return mode === "skip" || mode === "todo";
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Custom metadata that was attached to the test during its execution.
|
|
2521
|
+
*/
|
|
2522
|
+
meta() {
|
|
2523
|
+
return this.task.meta;
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Useful information about the test like duration, memory usage, etc.
|
|
2527
|
+
* Diagnostic is only available after the test has finished.
|
|
2528
|
+
*/
|
|
2529
|
+
diagnostic() {
|
|
2530
|
+
const result = this.task.result;
|
|
2531
|
+
if (!result || result.state === "run" || result.state === "queued" || !result.startTime) {
|
|
2532
|
+
return void 0;
|
|
2533
|
+
}
|
|
2534
|
+
const duration = result.duration || 0;
|
|
2535
|
+
const slow = duration > this.project.globalConfig.slowTestThreshold;
|
|
2536
|
+
return {
|
|
2537
|
+
slow,
|
|
2538
|
+
heap: result.heap,
|
|
2539
|
+
duration,
|
|
2540
|
+
startTime: result.startTime,
|
|
2541
|
+
retryCount: result.retryCount ?? 0,
|
|
2542
|
+
repeatCount: result.repeatCount ?? 0,
|
|
2543
|
+
flaky: !!result.retryCount && result.state === "pass" && result.retryCount > 0
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
class TestCollection {
|
|
2548
|
+
#task;
|
|
2549
|
+
#project;
|
|
2550
|
+
constructor(task, project) {
|
|
2551
|
+
this.#task = task;
|
|
2552
|
+
this.#project = project;
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Returns the test or suite at a specific index.
|
|
2556
|
+
*/
|
|
2557
|
+
at(index) {
|
|
2558
|
+
if (index < 0) {
|
|
2559
|
+
index = this.size + index;
|
|
2560
|
+
}
|
|
2561
|
+
return getReportedTask(this.#project, this.#task.tasks[index]);
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* The number of tests and suites in the collection.
|
|
2565
|
+
*/
|
|
2566
|
+
get size() {
|
|
2567
|
+
return this.#task.tasks.length;
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* Returns the collection in array form for easier manipulation.
|
|
2571
|
+
*/
|
|
2572
|
+
array() {
|
|
2573
|
+
return Array.from(this);
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Filters all tests that are part of this collection and its children.
|
|
2577
|
+
*/
|
|
2578
|
+
*allTests(state) {
|
|
2579
|
+
for (const child of this) {
|
|
2580
|
+
if (child.type === "suite") {
|
|
2581
|
+
yield* child.children.allTests(state);
|
|
2582
|
+
} else if (state) {
|
|
2583
|
+
const testState = getTestState(child);
|
|
2584
|
+
if (state === testState) {
|
|
2585
|
+
yield child;
|
|
2586
|
+
}
|
|
2587
|
+
} else {
|
|
2588
|
+
yield child;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Filters only the tests that are part of this collection.
|
|
2594
|
+
*/
|
|
2595
|
+
*tests(state) {
|
|
2596
|
+
for (const child of this) {
|
|
2597
|
+
if (child.type !== "test") {
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
if (state) {
|
|
2601
|
+
const testState = getTestState(child);
|
|
2602
|
+
if (state === testState) {
|
|
2603
|
+
yield child;
|
|
2604
|
+
}
|
|
2605
|
+
} else {
|
|
2606
|
+
yield child;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Filters only the suites that are part of this collection.
|
|
2612
|
+
*/
|
|
2613
|
+
*suites() {
|
|
2614
|
+
for (const child of this) {
|
|
2615
|
+
if (child.type === "suite") {
|
|
2616
|
+
yield child;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Filters all suites that are part of this collection and its children.
|
|
2622
|
+
*/
|
|
2623
|
+
*allSuites() {
|
|
2624
|
+
for (const child of this) {
|
|
2625
|
+
if (child.type === "suite") {
|
|
2626
|
+
yield child;
|
|
2627
|
+
yield* child.children.allSuites();
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
*[Symbol.iterator]() {
|
|
2632
|
+
for (const task of this.#task.tasks) {
|
|
2633
|
+
yield getReportedTask(this.#project, task);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
class SuiteImplementation extends ReportedTaskImplementation {
|
|
2638
|
+
/**
|
|
2639
|
+
* Collection of suites and tests that are part of this suite.
|
|
2640
|
+
*/
|
|
2641
|
+
children;
|
|
2642
|
+
/** @internal */
|
|
2643
|
+
constructor(task, project) {
|
|
2644
|
+
super(task, project);
|
|
2645
|
+
this.children = new TestCollection(task, project);
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Checks if the suite was skipped during collection.
|
|
2649
|
+
*/
|
|
2650
|
+
skipped() {
|
|
2651
|
+
const mode = this.task.mode;
|
|
2652
|
+
return mode === "skip" || mode === "todo";
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Errors that happened outside of the test run during collection, like syntax errors.
|
|
2656
|
+
*/
|
|
2657
|
+
errors() {
|
|
2658
|
+
return this.task.result?.errors || [];
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
class TestSuite extends SuiteImplementation {
|
|
2662
|
+
#fullName;
|
|
2663
|
+
type = "suite";
|
|
2664
|
+
/**
|
|
2665
|
+
* Name of the test or the suite.
|
|
2666
|
+
*/
|
|
2667
|
+
name;
|
|
2668
|
+
/**
|
|
2669
|
+
* Direct reference to the test module where the test or suite is defined.
|
|
2670
|
+
*/
|
|
2671
|
+
module;
|
|
2672
|
+
/**
|
|
2673
|
+
* Parent suite. If suite was called directly inside the module, the parent will be the module itself.
|
|
2674
|
+
*/
|
|
2675
|
+
parent;
|
|
2676
|
+
/**
|
|
2677
|
+
* Options that suite was initiated with.
|
|
2678
|
+
*/
|
|
2679
|
+
options;
|
|
2680
|
+
/** @internal */
|
|
2681
|
+
constructor(task, project) {
|
|
2682
|
+
super(task, project);
|
|
2683
|
+
this.name = task.name;
|
|
2684
|
+
this.module = getReportedTask(project, task.file);
|
|
2685
|
+
const suite = this.task.suite;
|
|
2686
|
+
if (suite) {
|
|
2687
|
+
this.parent = getReportedTask(project, suite);
|
|
2688
|
+
} else {
|
|
2689
|
+
this.parent = this.module;
|
|
2690
|
+
}
|
|
2691
|
+
this.options = buildOptions(task);
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Full name of the suite including all parent suites separated with `>`.
|
|
2695
|
+
*/
|
|
2696
|
+
get fullName() {
|
|
2697
|
+
if (this.#fullName === void 0) {
|
|
2698
|
+
if (this.parent.type !== "module") {
|
|
2699
|
+
this.#fullName = `${this.parent.fullName} > ${this.name}`;
|
|
2700
|
+
} else {
|
|
2701
|
+
this.#fullName = this.name;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return this.#fullName;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
class TestModule extends SuiteImplementation {
|
|
2708
|
+
type = "module";
|
|
2709
|
+
/**
|
|
2710
|
+
* This is usually an absolute UNIX file path.
|
|
2711
|
+
* It can be a virtual id if the file is not on the disk.
|
|
2712
|
+
* This value corresponds to Vite's `ModuleGraph` id.
|
|
2713
|
+
*/
|
|
2714
|
+
moduleId;
|
|
2715
|
+
/** @internal */
|
|
2716
|
+
constructor(task, project) {
|
|
2717
|
+
super(task, project);
|
|
2718
|
+
this.moduleId = task.filepath;
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Useful information about the module like duration, memory usage, etc.
|
|
2722
|
+
* If the module was not executed yet, all diagnostic values will return `0`.
|
|
2723
|
+
*/
|
|
2724
|
+
diagnostic() {
|
|
2725
|
+
const setupDuration = this.task.setupDuration || 0;
|
|
2726
|
+
const collectDuration = this.task.collectDuration || 0;
|
|
2727
|
+
const prepareDuration = this.task.prepareDuration || 0;
|
|
2728
|
+
const environmentSetupDuration = this.task.environmentLoad || 0;
|
|
2729
|
+
const duration = this.task.result?.duration || 0;
|
|
2730
|
+
return {
|
|
2731
|
+
environmentSetupDuration,
|
|
2732
|
+
prepareDuration,
|
|
2733
|
+
collectDuration,
|
|
2734
|
+
setupDuration,
|
|
2735
|
+
duration
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
function buildOptions(task) {
|
|
2740
|
+
return {
|
|
2741
|
+
each: task.each,
|
|
2742
|
+
concurrent: task.concurrent,
|
|
2743
|
+
shuffle: task.shuffle,
|
|
2744
|
+
retry: task.retry,
|
|
2745
|
+
repeats: task.repeats,
|
|
2746
|
+
mode: task.mode
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
function getTestState(test) {
|
|
2750
|
+
if (test.skipped()) {
|
|
2751
|
+
return "skipped";
|
|
2752
|
+
}
|
|
2753
|
+
const result = test.result();
|
|
2754
|
+
return result ? result.state : "running";
|
|
2755
|
+
}
|
|
2756
|
+
function storeTask(project, runnerTask, reportedTask) {
|
|
2757
|
+
project.vitest.state.reportedTasksMap.set(runnerTask, reportedTask);
|
|
2758
|
+
}
|
|
2759
|
+
function getReportedTask(project, runnerTask) {
|
|
2760
|
+
const reportedTask = project.vitest.state.getReportedEntity(runnerTask);
|
|
2761
|
+
if (!reportedTask) {
|
|
2762
|
+
throw new Error(
|
|
2763
|
+
`Task instance was not found for ${runnerTask.type} "${runnerTask.name}"`
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2766
|
+
return reportedTask;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
function yamlString(str) {
|
|
2770
|
+
return `"${str.replace(/"/g, '\\"')}"`;
|
|
2771
|
+
}
|
|
2772
|
+
function tapString(str) {
|
|
2773
|
+
return str.replace(/\\/g, "\\\\").replace(/#/g, "\\#").replace(/\n/g, " ");
|
|
2774
|
+
}
|
|
2775
|
+
class TapReporter {
|
|
2776
|
+
ctx;
|
|
2777
|
+
logger;
|
|
2778
|
+
onInit(ctx) {
|
|
2779
|
+
this.ctx = ctx;
|
|
2780
|
+
this.logger = new IndentedLogger(ctx.logger.log.bind(ctx.logger));
|
|
2781
|
+
}
|
|
2782
|
+
static getComment(task) {
|
|
2783
|
+
if (task.mode === "skip") {
|
|
2784
|
+
return " # SKIP";
|
|
2785
|
+
} else if (task.mode === "todo") {
|
|
2786
|
+
return " # TODO";
|
|
2787
|
+
} else if (task.result?.duration != null) {
|
|
2788
|
+
return ` # time=${task.result.duration.toFixed(2)}ms`;
|
|
2789
|
+
} else {
|
|
2790
|
+
return "";
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
logErrorDetails(error, stack) {
|
|
2794
|
+
const errorName = error.name || error.nameStr || "Unknown Error";
|
|
2795
|
+
this.logger.log(`name: ${yamlString(String(errorName))}`);
|
|
2796
|
+
this.logger.log(`message: ${yamlString(String(error.message))}`);
|
|
2797
|
+
if (stack) {
|
|
2798
|
+
this.logger.log(
|
|
2799
|
+
`stack: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
logTasks(tasks) {
|
|
2804
|
+
this.logger.log(`1..${tasks.length}`);
|
|
2805
|
+
for (const [i, task] of tasks.entries()) {
|
|
2806
|
+
const id = i + 1;
|
|
2807
|
+
const ok = task.result?.state === "pass" || task.mode === "skip" || task.mode === "todo" ? "ok" : "not ok";
|
|
2808
|
+
const comment = TapReporter.getComment(task);
|
|
2809
|
+
if (task.type === "suite" && task.tasks.length > 0) {
|
|
2810
|
+
this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment} {`);
|
|
2811
|
+
this.logger.indent();
|
|
2812
|
+
this.logTasks(task.tasks);
|
|
2813
|
+
this.logger.unindent();
|
|
2814
|
+
this.logger.log("}");
|
|
2815
|
+
} else {
|
|
2816
|
+
this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`);
|
|
2817
|
+
const project = this.ctx.getProjectByName(task.file.projectName || "");
|
|
2818
|
+
if (task.result?.state === "fail" && task.result.errors) {
|
|
2819
|
+
this.logger.indent();
|
|
2820
|
+
task.result.errors.forEach((error) => {
|
|
2821
|
+
const stacks = task.file.pool === "browser" ? project.browser?.parseErrorStacktrace(error) || [] : parseErrorStacktrace(error, {
|
|
2822
|
+
frameFilter: this.ctx.config.onStackTrace
|
|
2823
|
+
});
|
|
2824
|
+
const stack = stacks[0];
|
|
2825
|
+
this.logger.log("---");
|
|
2826
|
+
this.logger.log("error:");
|
|
2827
|
+
this.logger.indent();
|
|
2828
|
+
this.logErrorDetails(error);
|
|
2829
|
+
this.logger.unindent();
|
|
2830
|
+
if (stack) {
|
|
2831
|
+
this.logger.log(
|
|
2832
|
+
`at: ${yamlString(
|
|
2833
|
+
`${stack.file}:${stack.line}:${stack.column}`
|
|
2834
|
+
)}`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
if (error.showDiff) {
|
|
2838
|
+
this.logger.log(`actual: ${yamlString(error.actual)}`);
|
|
2839
|
+
this.logger.log(`expected: ${yamlString(error.expected)}`);
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
this.logger.log("...");
|
|
2843
|
+
this.logger.unindent();
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
onFinished(files = this.ctx.state.getFiles()) {
|
|
2849
|
+
this.logger.log("TAP version 13");
|
|
2850
|
+
this.logTasks(files);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function flattenTasks(task, baseName = "") {
|
|
2855
|
+
const base = baseName ? `${baseName} > ` : "";
|
|
2856
|
+
if (task.type === "suite" && task.tasks.length > 0) {
|
|
2857
|
+
return task.tasks.flatMap(
|
|
2858
|
+
(child) => flattenTasks(child, `${base}${task.name}`)
|
|
2859
|
+
);
|
|
2860
|
+
} else {
|
|
2861
|
+
return [
|
|
2862
|
+
{
|
|
2863
|
+
...task,
|
|
2864
|
+
name: `${base}${task.name}`
|
|
2865
|
+
}
|
|
2866
|
+
];
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
class TapFlatReporter extends TapReporter {
|
|
2870
|
+
onInit(ctx) {
|
|
2871
|
+
super.onInit(ctx);
|
|
2872
|
+
}
|
|
2873
|
+
onFinished(files = this.ctx.state.getFiles()) {
|
|
2874
|
+
this.ctx.logger.log("TAP version 13");
|
|
2875
|
+
const flatTasks = files.flatMap((task) => flattenTasks(task));
|
|
2876
|
+
this.logTasks(flatTasks);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
class VerboseReporter extends DefaultReporter {
|
|
2881
|
+
verbose = true;
|
|
2882
|
+
renderSucceed = true;
|
|
2883
|
+
printTask(task) {
|
|
2884
|
+
if (this.isTTY) {
|
|
2885
|
+
return super.printTask(task);
|
|
2886
|
+
}
|
|
2887
|
+
if (task.type !== "test" || !task.result?.state || task.result?.state === "run" || task.result?.state === "queued") {
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const duration = task.result.duration;
|
|
2891
|
+
let title = ` ${getStateSymbol(task)} `;
|
|
2892
|
+
if (task.file.projectName) {
|
|
2893
|
+
title += formatProjectName(task.file.projectName);
|
|
2894
|
+
}
|
|
2895
|
+
title += getFullName(task, c.dim(" > "));
|
|
2896
|
+
if (duration != null && duration > this.ctx.config.slowTestThreshold) {
|
|
2897
|
+
title += c.yellow(` ${Math.round(duration)}${c.dim("ms")}`);
|
|
2898
|
+
}
|
|
2899
|
+
if (this.ctx.config.logHeapUsage && task.result.heap != null) {
|
|
2900
|
+
title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`);
|
|
2901
|
+
}
|
|
2902
|
+
if (task.result?.note) {
|
|
2903
|
+
title += c.dim(c.gray(` [${task.result.note}]`));
|
|
2904
|
+
}
|
|
2905
|
+
this.ctx.logger.log(title);
|
|
2906
|
+
if (task.result.state === "fail") {
|
|
2907
|
+
task.result.errors?.forEach((error) => this.log(c.red(` ${F_RIGHT} ${error?.message}`)));
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
function createBenchmarkJsonReport(files) {
|
|
2913
|
+
const report = { files: [] };
|
|
2914
|
+
for (const file of files) {
|
|
2915
|
+
const groups = [];
|
|
2916
|
+
for (const task of getTasks(file)) {
|
|
2917
|
+
if (task?.type === "suite") {
|
|
2918
|
+
const benchmarks = [];
|
|
2919
|
+
for (const t of task.tasks) {
|
|
2920
|
+
const benchmark = t.meta.benchmark && t.result?.benchmark;
|
|
2921
|
+
if (benchmark) {
|
|
2922
|
+
benchmarks.push({ id: t.id, ...benchmark, samples: [] });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
if (benchmarks.length) {
|
|
2926
|
+
groups.push({
|
|
2927
|
+
fullName: getFullName(task, " > "),
|
|
2928
|
+
benchmarks
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
report.files.push({
|
|
2934
|
+
filepath: file.filepath,
|
|
2935
|
+
groups
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
return report;
|
|
2939
|
+
}
|
|
2940
|
+
function flattenFormattedBenchmarkReport(report) {
|
|
2941
|
+
const flat = {};
|
|
2942
|
+
for (const file of report.files) {
|
|
2943
|
+
for (const group of file.groups) {
|
|
2944
|
+
for (const t of group.benchmarks) {
|
|
2945
|
+
flat[t.id] = t;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return flat;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
const outputMap = /* @__PURE__ */ new WeakMap();
|
|
2953
|
+
function formatNumber(number) {
|
|
2954
|
+
const res = String(number.toFixed(number < 100 ? 4 : 2)).split(".");
|
|
2955
|
+
return res[0].replace(/(?=(?:\d{3})+$)\B/g, ",") + (res[1] ? `.${res[1]}` : "");
|
|
2956
|
+
}
|
|
2957
|
+
const tableHead = [
|
|
2958
|
+
"name",
|
|
2959
|
+
"hz",
|
|
2960
|
+
"min",
|
|
2961
|
+
"max",
|
|
2962
|
+
"mean",
|
|
2963
|
+
"p75",
|
|
2964
|
+
"p99",
|
|
2965
|
+
"p995",
|
|
2966
|
+
"p999",
|
|
2967
|
+
"rme",
|
|
2968
|
+
"samples"
|
|
2969
|
+
];
|
|
2970
|
+
function renderBenchmarkItems(result) {
|
|
2971
|
+
return [
|
|
2972
|
+
result.name,
|
|
2973
|
+
formatNumber(result.hz || 0),
|
|
2974
|
+
formatNumber(result.min || 0),
|
|
2975
|
+
formatNumber(result.max || 0),
|
|
2976
|
+
formatNumber(result.mean || 0),
|
|
2977
|
+
formatNumber(result.p75 || 0),
|
|
2978
|
+
formatNumber(result.p99 || 0),
|
|
2979
|
+
formatNumber(result.p995 || 0),
|
|
2980
|
+
formatNumber(result.p999 || 0),
|
|
2981
|
+
`\xB1${(result.rme || 0).toFixed(2)}%`,
|
|
2982
|
+
(result.sampleCount || 0).toString()
|
|
2983
|
+
];
|
|
2984
|
+
}
|
|
2985
|
+
function computeColumnWidths(results) {
|
|
2986
|
+
const rows = [tableHead, ...results.map((v) => renderBenchmarkItems(v))];
|
|
2987
|
+
return Array.from(tableHead, (_, i) => Math.max(...rows.map((row) => stripVTControlCharacters(row[i]).length)));
|
|
2988
|
+
}
|
|
2989
|
+
function padRow(row, widths) {
|
|
2990
|
+
return row.map(
|
|
2991
|
+
(v, i) => i ? v.padStart(widths[i], " ") : v.padEnd(widths[i], " ")
|
|
2992
|
+
// name
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
function renderTableHead(widths) {
|
|
2996
|
+
return " ".repeat(3) + padRow(tableHead, widths).map(c.bold).join(" ");
|
|
2997
|
+
}
|
|
2998
|
+
function renderBenchmark(result, widths) {
|
|
2999
|
+
const padded = padRow(renderBenchmarkItems(result), widths);
|
|
3000
|
+
return [
|
|
3001
|
+
padded[0],
|
|
3002
|
+
// name
|
|
3003
|
+
c.blue(padded[1]),
|
|
3004
|
+
// hz
|
|
3005
|
+
c.cyan(padded[2]),
|
|
3006
|
+
// min
|
|
3007
|
+
c.cyan(padded[3]),
|
|
3008
|
+
// max
|
|
3009
|
+
c.cyan(padded[4]),
|
|
3010
|
+
// mean
|
|
3011
|
+
c.cyan(padded[5]),
|
|
3012
|
+
// p75
|
|
3013
|
+
c.cyan(padded[6]),
|
|
3014
|
+
// p99
|
|
3015
|
+
c.cyan(padded[7]),
|
|
3016
|
+
// p995
|
|
3017
|
+
c.cyan(padded[8]),
|
|
3018
|
+
// p999
|
|
3019
|
+
c.dim(padded[9]),
|
|
3020
|
+
// rem
|
|
3021
|
+
c.dim(padded[10])
|
|
3022
|
+
// sample
|
|
3023
|
+
].join(" ");
|
|
3024
|
+
}
|
|
3025
|
+
function renderTable(options) {
|
|
3026
|
+
const output = [];
|
|
3027
|
+
const benchMap = {};
|
|
3028
|
+
for (const task of options.tasks) {
|
|
3029
|
+
if (task.meta.benchmark && task.result?.benchmark) {
|
|
3030
|
+
benchMap[task.id] = {
|
|
3031
|
+
current: task.result.benchmark,
|
|
3032
|
+
baseline: options.compare?.[task.id]
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
const benchCount = Object.entries(benchMap).length;
|
|
3037
|
+
const columnWidths = computeColumnWidths(
|
|
3038
|
+
Object.values(benchMap).flatMap((v) => [v.current, v.baseline]).filter(notNullish)
|
|
3039
|
+
);
|
|
3040
|
+
let idx = 0;
|
|
3041
|
+
const padding = " ".repeat(1 );
|
|
3042
|
+
for (const task of options.tasks) {
|
|
3043
|
+
const duration = task.result?.duration;
|
|
3044
|
+
const bench = benchMap[task.id];
|
|
3045
|
+
let prefix = "";
|
|
3046
|
+
if (idx === 0 && task.meta?.benchmark) {
|
|
3047
|
+
prefix += `${renderTableHead(columnWidths)}
|
|
3048
|
+
${padding}`;
|
|
3049
|
+
}
|
|
3050
|
+
prefix += ` ${getStateSymbol(task)} `;
|
|
3051
|
+
let suffix = "";
|
|
3052
|
+
if (task.type === "suite") {
|
|
3053
|
+
suffix += c.dim(` (${getTests(task).length})`);
|
|
3054
|
+
}
|
|
3055
|
+
if (task.mode === "skip" || task.mode === "todo") {
|
|
3056
|
+
suffix += c.dim(c.gray(" [skipped]"));
|
|
3057
|
+
}
|
|
3058
|
+
if (duration != null && duration > options.slowTestThreshold) {
|
|
3059
|
+
suffix += c.yellow(` ${Math.round(duration)}${c.dim("ms")}`);
|
|
3060
|
+
}
|
|
3061
|
+
if (options.showHeap && task.result?.heap != null) {
|
|
3062
|
+
suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`);
|
|
3063
|
+
}
|
|
3064
|
+
if (bench) {
|
|
3065
|
+
let body = renderBenchmark(bench.current, columnWidths);
|
|
3066
|
+
if (options.compare && bench.baseline) {
|
|
3067
|
+
if (bench.current.hz) {
|
|
3068
|
+
const diff = bench.current.hz / bench.baseline.hz;
|
|
3069
|
+
const diffFixed = diff.toFixed(2);
|
|
3070
|
+
if (diffFixed === "1.0.0") {
|
|
3071
|
+
body += c.gray(` [${diffFixed}x]`);
|
|
3072
|
+
}
|
|
3073
|
+
if (diff > 1) {
|
|
3074
|
+
body += c.blue(` [${diffFixed}x] \u21D1`);
|
|
3075
|
+
} else {
|
|
3076
|
+
body += c.red(` [${diffFixed}x] \u21D3`);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
output.push(padding + prefix + body + suffix);
|
|
3080
|
+
const bodyBaseline = renderBenchmark(bench.baseline, columnWidths);
|
|
3081
|
+
output.push(`${padding} ${bodyBaseline} ${c.dim("(baseline)")}`);
|
|
3082
|
+
} else {
|
|
3083
|
+
if (bench.current.rank === 1 && benchCount > 1) {
|
|
3084
|
+
body += c.bold(c.green(" fastest"));
|
|
3085
|
+
}
|
|
3086
|
+
if (bench.current.rank === benchCount && benchCount > 2) {
|
|
3087
|
+
body += c.bold(c.gray(" slowest"));
|
|
3088
|
+
}
|
|
3089
|
+
output.push(padding + prefix + body + suffix);
|
|
3090
|
+
}
|
|
3091
|
+
} else {
|
|
3092
|
+
output.push(padding + prefix + task.name + suffix);
|
|
3093
|
+
}
|
|
3094
|
+
if (task.result?.state !== "pass" && outputMap.get(task) != null) {
|
|
3095
|
+
let data = outputMap.get(task);
|
|
3096
|
+
if (typeof data === "string") {
|
|
3097
|
+
data = stripVTControlCharacters(data.trim().split("\n").filter(Boolean).pop());
|
|
3098
|
+
if (data === "") {
|
|
3099
|
+
data = void 0;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if (data != null) {
|
|
3103
|
+
const out = ` ${" ".repeat(options.level)}${F_RIGHT} ${data}`;
|
|
3104
|
+
output.push(c.gray(truncateString(out, options.columns)));
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
idx++;
|
|
3108
|
+
}
|
|
3109
|
+
return output.filter(Boolean).join("\n");
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
class BenchmarkReporter extends DefaultReporter {
|
|
3113
|
+
compare;
|
|
3114
|
+
async onInit(ctx) {
|
|
3115
|
+
super.onInit(ctx);
|
|
3116
|
+
if (this.ctx.config.benchmark?.compare) {
|
|
3117
|
+
const compareFile = pathe.resolve(
|
|
3118
|
+
this.ctx.config.root,
|
|
3119
|
+
this.ctx.config.benchmark?.compare
|
|
3120
|
+
);
|
|
3121
|
+
try {
|
|
3122
|
+
this.compare = flattenFormattedBenchmarkReport(
|
|
3123
|
+
JSON.parse(await fs.promises.readFile(compareFile, "utf-8"))
|
|
3124
|
+
);
|
|
3125
|
+
} catch (e) {
|
|
3126
|
+
this.error(`Failed to read '${compareFile}'`, e);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
onTaskUpdate(packs) {
|
|
3131
|
+
for (const pack of packs) {
|
|
3132
|
+
const task = this.ctx.state.idMap.get(pack[0]);
|
|
3133
|
+
if (task?.type === "suite" && task.result?.state !== "run") {
|
|
3134
|
+
task.tasks.filter((task2) => task2.result?.benchmark).sort((benchA, benchB) => benchA.result.benchmark.mean - benchB.result.benchmark.mean).forEach((bench, idx) => {
|
|
3135
|
+
bench.result.benchmark.rank = Number(idx) + 1;
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
super.onTaskUpdate(packs);
|
|
3140
|
+
}
|
|
3141
|
+
printTask(task) {
|
|
3142
|
+
if (task?.type !== "suite" || !task.result?.state || task.result?.state === "run" || task.result?.state === "queued") {
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const benches = task.tasks.filter((t) => t.meta.benchmark);
|
|
3146
|
+
const duration = task.result.duration;
|
|
3147
|
+
if (benches.length > 0 && benches.every((t) => t.result?.state !== "run" && t.result?.state !== "queued")) {
|
|
3148
|
+
let title = `
|
|
3149
|
+
${getStateSymbol(task)} ${formatProjectName(task.file.projectName)}${getFullName(task, c.dim(" > "))}`;
|
|
3150
|
+
if (duration != null && duration > this.ctx.config.slowTestThreshold) {
|
|
3151
|
+
title += c.yellow(` ${Math.round(duration)}${c.dim("ms")}`);
|
|
3152
|
+
}
|
|
3153
|
+
this.log(title);
|
|
3154
|
+
this.log(renderTable({
|
|
3155
|
+
tasks: benches,
|
|
3156
|
+
level: 1,
|
|
3157
|
+
shallow: true,
|
|
3158
|
+
columns: this.ctx.logger.getColumns(),
|
|
3159
|
+
compare: this.compare,
|
|
3160
|
+
showHeap: this.ctx.config.logHeapUsage,
|
|
3161
|
+
slowTestThreshold: this.ctx.config.slowTestThreshold
|
|
3162
|
+
}));
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
async onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
|
|
3166
|
+
super.onFinished(files, errors);
|
|
3167
|
+
let outputFile = this.ctx.config.benchmark?.outputJson;
|
|
3168
|
+
if (outputFile) {
|
|
3169
|
+
outputFile = pathe.resolve(this.ctx.config.root, outputFile);
|
|
3170
|
+
const outputDirectory = pathe.dirname(outputFile);
|
|
3171
|
+
if (!fs.existsSync(outputDirectory)) {
|
|
3172
|
+
await fs.promises.mkdir(outputDirectory, { recursive: true });
|
|
3173
|
+
}
|
|
3174
|
+
const output = createBenchmarkJsonReport(files);
|
|
3175
|
+
await fs.promises.writeFile(outputFile, JSON.stringify(output, null, 2));
|
|
3176
|
+
this.log(`Benchmark report written to ${outputFile}`);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
class VerboseBenchmarkReporter extends BenchmarkReporter {
|
|
3182
|
+
verbose = true;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
const BenchmarkReportsMap = {
|
|
3186
|
+
default: BenchmarkReporter,
|
|
3187
|
+
verbose: VerboseBenchmarkReporter
|
|
3188
|
+
};
|
|
3189
|
+
|
|
3190
|
+
const TestFile = TestModule;
|
|
3191
|
+
const ReportersMap = {
|
|
3192
|
+
"default": DefaultReporter,
|
|
3193
|
+
"basic": BasicReporter,
|
|
3194
|
+
"blob": BlobReporter,
|
|
3195
|
+
"verbose": VerboseReporter,
|
|
3196
|
+
"dot": DotReporter,
|
|
3197
|
+
"json": JsonReporter,
|
|
3198
|
+
"tap": TapReporter,
|
|
3199
|
+
"tap-flat": TapFlatReporter,
|
|
3200
|
+
"junit": JUnitReporter,
|
|
3201
|
+
"hanging-process": HangingProcessReporter,
|
|
3202
|
+
"github-actions": GithubActionsReporter
|
|
3203
|
+
};
|
|
3204
|
+
|
|
3205
|
+
export { BasicReporter as B, DefaultReporter as D, GithubActionsReporter as G, HangingProcessReporter as H, JsonReporter as J, Logger as L, ReportersMap as R, TapFlatReporter as T, VerboseReporter as V, DotReporter as a, JUnitReporter as b, TapReporter as c, TestFile as d, TestCase as e, TestModule as f, TestSuite as g, BenchmarkReportsMap as h, generateCodeFrame as i, BlobReporter as j, parse as p, readBlobs as r, stringify as s };
|