supercov 4.0.1 → 4.1.1
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/docs/cli.md
CHANGED
|
@@ -147,6 +147,7 @@ npx supercov runs <run-id> [query] [options]
|
|
|
147
147
|
| `decision <id \| path:line>` | Understand missing boolean outcomes and MC/DC witnesses |
|
|
148
148
|
| `line <path:line>` | See one line's state, obligations, and covering tests |
|
|
149
149
|
| `test <id \| name>` | See the coverage attributed to one test |
|
|
150
|
+
| `tests without-evidence` | List the tests that made assertions and recorded no coverage, which the summary counts in a warning |
|
|
150
151
|
| `kinds` | Group coverage by test level, such as unit or E2E |
|
|
151
152
|
| `runners` | Group coverage by test runner |
|
|
152
153
|
| `scope` | See what is source and what is not, by directory and reason; `--files` lists every file |
|
|
@@ -175,12 +176,16 @@ npx supercov runs latest files --group dir --depth 2 --metric branches
|
|
|
175
176
|
```
|
|
176
177
|
|
|
177
178
|
```
|
|
178
|
-
Directory Files Lines
|
|
179
|
-
app
|
|
180
|
-
lib 1 10
|
|
181
|
-
|
|
179
|
+
Directory Files Lines e2e unit no test All
|
|
180
|
+
app 5 23 86.96% 0.00% 8.70% 95.65%
|
|
181
|
+
lib 1 10 90.00% 40.00% 0.00% 90.00%
|
|
182
|
+
. 2 8 75.00% 0.00% 25.00% 100.00%
|
|
183
|
+
components 1 2 100.00% 0.00% 0.00% 100.00%
|
|
182
184
|
```
|
|
183
185
|
|
|
186
|
+
`no test` is what ran only while no test was running, such as a server
|
|
187
|
+
starting before the first test. The column is there when a directory has any.
|
|
188
|
+
|
|
184
189
|
In `--json`, every file of `files` and `gaps` carries `totals` and `covered`
|
|
185
190
|
for each metric beside what is missing, so a percentage can be worked out for
|
|
186
191
|
any file or set of files.
|
package/docs/coverage-model.md
CHANGED
|
@@ -106,6 +106,14 @@ By test kind
|
|
|
106
106
|
no test lines 17.14% branches 0.00% MC/DC 0.00%
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
`files --group dir` has the same figure as a `no test` column wherever a
|
|
110
|
+
directory has any, so a directory's `All` never stands above its kinds
|
|
111
|
+
unexplained.
|
|
112
|
+
|
|
113
|
+
A test that made assertions and has no covered line to its name is counted in
|
|
114
|
+
a warning on the summary. `npx supercov runs latest tests without-evidence`
|
|
115
|
+
lists every such test, with its file, kind and outcome.
|
|
116
|
+
|
|
109
117
|
See [Supported suites](supported-suites.md) for the attribution available from
|
|
110
118
|
each runner.
|
|
111
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -119,14 +119,14 @@
|
|
|
119
119
|
"test:windows-tls": "node scripts/windows-tls-test.mjs"
|
|
120
120
|
},
|
|
121
121
|
"optionalDependencies": {
|
|
122
|
-
"@supercov/cli-darwin-arm64": "4.
|
|
123
|
-
"@supercov/cli-darwin-x64": "4.
|
|
124
|
-
"@supercov/cli-linux-arm64-gnu": "4.
|
|
125
|
-
"@supercov/cli-linux-arm64-musl": "4.
|
|
126
|
-
"@supercov/cli-linux-x64-gnu": "4.
|
|
127
|
-
"@supercov/cli-linux-x64-musl": "4.
|
|
128
|
-
"@supercov/cli-win32-arm64": "4.
|
|
129
|
-
"@supercov/cli-win32-x64": "4.
|
|
122
|
+
"@supercov/cli-darwin-arm64": "4.1.1",
|
|
123
|
+
"@supercov/cli-darwin-x64": "4.1.1",
|
|
124
|
+
"@supercov/cli-linux-arm64-gnu": "4.1.1",
|
|
125
|
+
"@supercov/cli-linux-arm64-musl": "4.1.1",
|
|
126
|
+
"@supercov/cli-linux-x64-gnu": "4.1.1",
|
|
127
|
+
"@supercov/cli-linux-x64-musl": "4.1.1",
|
|
128
|
+
"@supercov/cli-win32-arm64": "4.1.1",
|
|
129
|
+
"@supercov/cli-win32-x64": "4.1.1"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
132
132
|
"@playwright/test": ">=1.55.0",
|
|
@@ -9,4 +9,9 @@ const runtime = globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIR
|
|
|
9
9
|
require("node:vm").runInThisContext("globalThis.__SUPERCOV_DIRECT_RUNTIME__");
|
|
10
10
|
if (!runtime) throw new Error("[supercov] Jest runtime was not initialized by the preload");
|
|
11
11
|
globalThis.__SUPERCOV_DIRECT_RUNTIME__ = runtime;
|
|
12
|
+
// A process that turns out to run Jest (a wrapper that calls it in place)
|
|
13
|
+
// prints a failure's paths as the project's too. See register.mjs.
|
|
14
|
+
const failureOutput = process.__SUPERCOV_FAILURE_OUTPUT__ ??
|
|
15
|
+
require("node:vm").runInThisContext("process.__SUPERCOV_FAILURE_OUTPUT__");
|
|
16
|
+
if (typeof failureOutput === "function") failureOutput("jest");
|
|
12
17
|
module.exports = runtime;
|
|
@@ -8,7 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import Module, { register, syncBuiltinESMExports } from "node:module";
|
|
10
10
|
import fs, { closeSync, openSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
11
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
11
|
+
import path, { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervisor.mjs";
|
|
14
14
|
import { __supercovBindCapabilityWrapper } from "./capability.mjs";
|
|
@@ -85,6 +85,34 @@ if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1" ||
|
|
|
85
85
|
// and may evaluate before any ESM instrumented file imports it.
|
|
86
86
|
globalThis.__supercovRuntime ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
87
87
|
}
|
|
88
|
+
// Next.js runs a middleware and a route with `runtime = "edge"` in a VM
|
|
89
|
+
// context of its own, the Edge Runtime's, and that context's global is not
|
|
90
|
+
// this one. An instrumented file found no runtime there: every request to an
|
|
91
|
+
// application with a middleware.ts failed on "Cannot read properties of
|
|
92
|
+
// undefined (reading 'mcdcBegin')", and `next build` failed collecting page
|
|
93
|
+
// data for an edge route. Such a context reads this process's runtime, so
|
|
94
|
+
// what runs there is measured like the rest of the server.
|
|
95
|
+
const virtualMachine = Module._load("node:vm", undefined, false);
|
|
96
|
+
const createContext = virtualMachine.createContext;
|
|
97
|
+
virtualMachine.createContext = function createContextWithRuntime(...parameters) {
|
|
98
|
+
const context = Reflect.apply(createContext, this, parameters);
|
|
99
|
+
if (parameters[1]?.name === "Edge Runtime") {
|
|
100
|
+
try {
|
|
101
|
+
Object.defineProperty(context, "__SUPERCOV_DIRECT_RUNTIME__", {
|
|
102
|
+
configurable: true,
|
|
103
|
+
enumerable: false,
|
|
104
|
+
get: () => globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIRECT_RUNTIME__,
|
|
105
|
+
set(value) {
|
|
106
|
+
Object.defineProperty(context, "__SUPERCOV_DIRECT_RUNTIME__", {
|
|
107
|
+
configurable: true, enumerable: false, writable: true, value,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
}
|
|
114
|
+
return context;
|
|
115
|
+
};
|
|
88
116
|
// Workers are independent Node processes and an explicit `execArgv: []`
|
|
89
117
|
// otherwise strips the preload that supplies the isolated runtime. Preserve
|
|
90
118
|
// every user option while adding exactly one Supercov import.
|
|
@@ -196,6 +224,284 @@ else if (/\/node_modules\/next\/dist\/compiled\/jest-worker\/processChild\.js$/.
|
|
|
196
224
|
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
197
225
|
};
|
|
198
226
|
}
|
|
227
|
+
// A runner prints a failure with the files it names read from disk, and with
|
|
228
|
+
// their paths relative to its root. Both are the copy's: the code under a
|
|
229
|
+
// frame was Supercov's, and a file whose map names the project's own (so that
|
|
230
|
+
// a Node stack trace reads as the project's) printed as
|
|
231
|
+
// `../../../../src/crypto.js:10:11`. A runner's own printing reads a
|
|
232
|
+
// rewritten file as the text it was rewritten from, and names it as it lies
|
|
233
|
+
// in the copy. What runs is read as it is.
|
|
234
|
+
//
|
|
235
|
+
// Jest keeps the function it reads with from before any test loads, so its
|
|
236
|
+
// processes are set up here, when they start. Its setup file and Vitest's
|
|
237
|
+
// reporter ask again, which is what a process that only turns out to run a
|
|
238
|
+
// suite gets.
|
|
239
|
+
const failureOutput = { readers: [], installed: false };
|
|
240
|
+
process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
241
|
+
const reader = {
|
|
242
|
+
// Its message formatter reads the file under a frame and makes each
|
|
243
|
+
// frame's path relative to the root directory. So does the reporter
|
|
244
|
+
// that writes the tracefile coverage services read, where a file was
|
|
245
|
+
// `SF:../../../../src/crypto.js`.
|
|
246
|
+
jest: { prints: /[\\/](?:jest-message-util|istanbul-reports)[\\/]/, names: false, composes: true },
|
|
247
|
+
// Its own code reads the file under a frame, and Vitest 4 also looks in
|
|
248
|
+
// it for a source map, mapping positions a second time. Vite, which
|
|
249
|
+
// loads what runs, gets a map that names the file itself: frames then
|
|
250
|
+
// name the copy's file, and coverage reports it by its path in the
|
|
251
|
+
// project's layout.
|
|
252
|
+
vitest: { prints: /[\\/]vitest[\\/]dist[\\/]/, names: true },
|
|
253
|
+
}[runner];
|
|
254
|
+
// Asked with no runner, the reads are in place and change nothing until
|
|
255
|
+
// one is named: a process that may yet run a suite.
|
|
256
|
+
if (reader && !failureOutput.readers.includes(reader.prints.source)) {
|
|
257
|
+
failureOutput.readers.push(reader.prints.source);
|
|
258
|
+
failureOutput.prints = new RegExp(failureOutput.readers.join("|"));
|
|
259
|
+
failureOutput.names || (failureOutput.names = reader.names);
|
|
260
|
+
failureOutput.composes || (failureOutput.composes = reader.composes === true);
|
|
261
|
+
}
|
|
262
|
+
if (failureOutput.installed)
|
|
263
|
+
return;
|
|
264
|
+
failureOutput.installed = true;
|
|
265
|
+
let rewritten;
|
|
266
|
+
const copies = (() => {
|
|
267
|
+
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
268
|
+
try {
|
|
269
|
+
return [...new Set([copy, `${realpathSync(copy)}${sep}`])];
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return [copy];
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
// The copy lies in the project at .supercov/workspaces/workspace/<name>.
|
|
276
|
+
const projects = copies
|
|
277
|
+
.filter((copy) => /[\\/]\.supercov[\\/]workspaces[\\/]workspace[\\/][^\\/]+[\\/]$/.test(copy))
|
|
278
|
+
.map((copy) => resolve(copy, "../../../..") + sep);
|
|
279
|
+
// The path inside the copy of a rewritten file that lies under one of `roots`.
|
|
280
|
+
const rewrittenUnder = (roots, file) => {
|
|
281
|
+
if (typeof file !== "string")
|
|
282
|
+
return undefined;
|
|
283
|
+
if (rewritten === undefined) {
|
|
284
|
+
try {
|
|
285
|
+
rewritten = new Set(JSON.parse(readFileSync(new URL("./authored-sources.json", import.meta.url), "utf8")));
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
rewritten = new Set();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const root of roots) {
|
|
292
|
+
if (file.startsWith(root)) {
|
|
293
|
+
const local = file.slice(root.length).split(sep).join("/");
|
|
294
|
+
return rewritten.has(local) ? local : undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
};
|
|
299
|
+
const kept = (name, file) => fileURLToPath(new URL(`./${name}/${file}`, import.meta.url));
|
|
300
|
+
const calledByPrinter = (above) => {
|
|
301
|
+
const holder = {};
|
|
302
|
+
const limit = Error.stackTraceLimit;
|
|
303
|
+
try {
|
|
304
|
+
Error.stackTraceLimit = 1;
|
|
305
|
+
Error.captureStackTrace(holder, above);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
Error.stackTraceLimit = limit;
|
|
309
|
+
}
|
|
310
|
+
return failureOutput.prints.test(String(holder.stack).split("\n")[1] ?? "");
|
|
311
|
+
};
|
|
312
|
+
const mapComment = "\n//# sourceMappingURL=data:application/json;base64,";
|
|
313
|
+
const namedForItself = (file, contents) => {
|
|
314
|
+
const text = typeof contents === "string" ? contents : contents.toString("utf8");
|
|
315
|
+
const found = text.lastIndexOf(mapComment);
|
|
316
|
+
if (found < 0)
|
|
317
|
+
return contents;
|
|
318
|
+
const encoded = text.slice(found + mapComment.length).trimEnd();
|
|
319
|
+
if (/\s/.test(encoded))
|
|
320
|
+
return contents;
|
|
321
|
+
try {
|
|
322
|
+
const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
323
|
+
map.sources = [basename(file)];
|
|
324
|
+
const renamed = `${text.slice(0, found + mapComment.length)}${Buffer.from(JSON.stringify(map)).toString("base64")}\n`;
|
|
325
|
+
return typeof contents === "string" ? renamed : Buffer.from(renamed, "utf8");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return contents;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
// A transformer that compiles what it is given and reads no map in it
|
|
332
|
+
// (ts-jest) leaves Jest a map from its output to the rewritten file, so a
|
|
333
|
+
// frame read `src/crypto.ts:51:10` for line 10 of what the author wrote.
|
|
334
|
+
// Such a map is taken through the rewritten file's own before Jest reads
|
|
335
|
+
// it. One that already carries the author's text went through it in the
|
|
336
|
+
// transformer, as Babel's and SWC's do.
|
|
337
|
+
const digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
338
|
+
const decoded = (mappings) => {
|
|
339
|
+
const lines = [];
|
|
340
|
+
const state = [0, 0, 0, 0, 0];
|
|
341
|
+
for (const line of mappings.split(";")) {
|
|
342
|
+
const segments = [];
|
|
343
|
+
state[0] = 0;
|
|
344
|
+
for (const text of line.split(",")) {
|
|
345
|
+
if (text === "")
|
|
346
|
+
continue;
|
|
347
|
+
const fields = [];
|
|
348
|
+
let value = 0, shift = 0;
|
|
349
|
+
for (const digit of text) {
|
|
350
|
+
const bits = digits.indexOf(digit);
|
|
351
|
+
value += (bits & 31) << shift;
|
|
352
|
+
if (bits & 32) {
|
|
353
|
+
shift += 5;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
fields.push(value & 1 ? -(value >> 1) : value >> 1);
|
|
357
|
+
value = shift = 0;
|
|
358
|
+
}
|
|
359
|
+
segments.push(fields.map((field, index) => (state[index] += field)));
|
|
360
|
+
}
|
|
361
|
+
lines.push(segments);
|
|
362
|
+
}
|
|
363
|
+
return lines;
|
|
364
|
+
};
|
|
365
|
+
const encoded = (lines) => {
|
|
366
|
+
const state = [0, 0, 0, 0, 0];
|
|
367
|
+
const number = (value) => {
|
|
368
|
+
let rest = value < 0 ? (-value << 1) | 1 : value << 1;
|
|
369
|
+
let text = "";
|
|
370
|
+
do {
|
|
371
|
+
const bits = rest & 31;
|
|
372
|
+
rest >>>= 5;
|
|
373
|
+
text += digits[rest > 0 ? bits | 32 : bits];
|
|
374
|
+
} while (rest > 0);
|
|
375
|
+
return text;
|
|
376
|
+
};
|
|
377
|
+
return lines.map((segments) => {
|
|
378
|
+
state[0] = 0;
|
|
379
|
+
return segments.map((fields) => fields.map((field, index) => {
|
|
380
|
+
const text = number(field - state[index]);
|
|
381
|
+
state[index] = field;
|
|
382
|
+
return text;
|
|
383
|
+
}).join("")).join(",");
|
|
384
|
+
}).join(";");
|
|
385
|
+
};
|
|
386
|
+
const ownMap = (file) => {
|
|
387
|
+
const text = readSync(file, "utf8");
|
|
388
|
+
const found = text.lastIndexOf(mapComment);
|
|
389
|
+
return found < 0 ? undefined : JSON.parse(Buffer.from(text.slice(found + mapComment.length).trimEnd(), "base64").toString("utf8"));
|
|
390
|
+
};
|
|
391
|
+
const composed = (text) => {
|
|
392
|
+
let map;
|
|
393
|
+
try {
|
|
394
|
+
map = JSON.parse(text);
|
|
395
|
+
if (map?.version !== 3 || typeof map.mappings !== "string" || !Array.isArray(map.sources))
|
|
396
|
+
return text;
|
|
397
|
+
const through = map.sources.map((source, index) => {
|
|
398
|
+
if (typeof source !== "string" || rewrittenUnder(copies, source) === undefined)
|
|
399
|
+
return undefined;
|
|
400
|
+
const own = ownMap(source);
|
|
401
|
+
if (!own || own.sourcesContent?.[0] === undefined || map.sourcesContent?.[index] === own.sourcesContent[0])
|
|
402
|
+
return undefined;
|
|
403
|
+
return { own, lines: decoded(own.mappings) };
|
|
404
|
+
});
|
|
405
|
+
if (!through.some(Boolean))
|
|
406
|
+
return text;
|
|
407
|
+
const lines = decoded(map.mappings).map((segments) => segments.map((fields) => {
|
|
408
|
+
const inner = fields.length >= 4 ? through[fields[1]] : undefined;
|
|
409
|
+
if (!inner)
|
|
410
|
+
return fields;
|
|
411
|
+
// The segment of the rewritten file's line that starts at or
|
|
412
|
+
// before the position; one that maps nothing is Supercov's.
|
|
413
|
+
const found = (inner.lines[fields[2]] ?? []).filter((segment) => segment[0] <= fields[3]).pop();
|
|
414
|
+
if (!found || found.length < 4)
|
|
415
|
+
return [fields[0]];
|
|
416
|
+
return [fields[0], fields[1], found[2], found[3], ...fields.slice(4)];
|
|
417
|
+
}));
|
|
418
|
+
through.forEach((inner, index) => {
|
|
419
|
+
if (!inner)
|
|
420
|
+
return;
|
|
421
|
+
const source = inner.own.sources?.[0];
|
|
422
|
+
if (typeof source === "string" && isAbsolute(source))
|
|
423
|
+
map.sources[index] = source;
|
|
424
|
+
map.sourcesContent ?? (map.sourcesContent = []);
|
|
425
|
+
map.sourcesContent[index] = inner.own.sourcesContent[0];
|
|
426
|
+
});
|
|
427
|
+
map.mappings = encoded(lines);
|
|
428
|
+
return JSON.stringify(map);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return text;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const { readFileSync: readSync, existsSync: exists } = fs;
|
|
435
|
+
const { readFile: readPromise } = fs.promises;
|
|
436
|
+
const { relative: relativePath } = path;
|
|
437
|
+
function supercovReadFileSync(file, ...rest) {
|
|
438
|
+
if (!failureOutput.prints)
|
|
439
|
+
return Reflect.apply(readSync, this, [file, ...rest]);
|
|
440
|
+
const local = rewrittenUnder(copies, file);
|
|
441
|
+
if (local === undefined) {
|
|
442
|
+
const contents = Reflect.apply(readSync, this, [file, ...rest]);
|
|
443
|
+
return failureOutput.composes && typeof contents === "string" && typeof file === "string" && file.endsWith(".map")
|
|
444
|
+
? composed(contents)
|
|
445
|
+
: contents;
|
|
446
|
+
}
|
|
447
|
+
if (calledByPrinter(supercovReadFileSync)) {
|
|
448
|
+
const changed = kept(".changed", local);
|
|
449
|
+
return Reflect.apply(readSync, this, [exists(changed) ? changed : kept(".authored", local), ...rest]);
|
|
450
|
+
}
|
|
451
|
+
const contents = Reflect.apply(readSync, this, [file, ...rest]);
|
|
452
|
+
return failureOutput.names ? namedForItself(file, contents) : contents;
|
|
453
|
+
}
|
|
454
|
+
fs.readFileSync = supercovReadFileSync;
|
|
455
|
+
fs.promises.readFile = async function supercovReadFile(file, ...rest) {
|
|
456
|
+
const contents = await Reflect.apply(readPromise, this, [file, ...rest]);
|
|
457
|
+
return failureOutput.names && rewrittenUnder(copies, file) !== undefined && (typeof contents === "string" || Buffer.isBuffer(contents))
|
|
458
|
+
? namedForItself(file, contents)
|
|
459
|
+
: contents;
|
|
460
|
+
};
|
|
461
|
+
function supercovRelative(from, to) {
|
|
462
|
+
if (!failureOutput.prints)
|
|
463
|
+
return Reflect.apply(relativePath, this, [from, to]);
|
|
464
|
+
const local = rewrittenUnder(copies, to) === undefined ? rewrittenUnder(projects, to) : undefined;
|
|
465
|
+
return local !== undefined && calledByPrinter(supercovRelative)
|
|
466
|
+
? Reflect.apply(relativePath, this, [from, copies[0] + local.split("/").join(sep)])
|
|
467
|
+
: Reflect.apply(relativePath, this, [from, to]);
|
|
468
|
+
}
|
|
469
|
+
path.relative = supercovRelative;
|
|
470
|
+
syncBuiltinESMExports();
|
|
471
|
+
};
|
|
472
|
+
// Jest itself prints from here on. A worker may be Jest's or another tool's
|
|
473
|
+
// (a bundler minifies in the same kind of process): Jest's setup file names
|
|
474
|
+
// the runner when a test file loads there.
|
|
475
|
+
if (isJestEntrypoint)
|
|
476
|
+
process.__SUPERCOV_FAILURE_OUTPUT__("jest");
|
|
477
|
+
else if (process.env.JEST_WORKER_ID)
|
|
478
|
+
process.__SUPERCOV_FAILURE_OUTPUT__();
|
|
479
|
+
// Next.js looks for lockfiles from its directory upwards, takes the outermost
|
|
480
|
+
// as its workspace root, and warns when it finds more than one. The copy
|
|
481
|
+
// holds the project's lockfile and lies inside the project, so every build
|
|
482
|
+
// under Supercov warned twice that the root "may not be correct", naming the
|
|
483
|
+
// copy's lockfile. The root Next picks is the project, which is right: it
|
|
484
|
+
// holds the copy and the dependencies. The copy's lockfile is left out of
|
|
485
|
+
// what the warning counts, and one the project really has twice still warns.
|
|
486
|
+
if (/\/node_modules\/(?:\.bin\/next$|next\/dist\/)/.test(entrypoint)) {
|
|
487
|
+
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
488
|
+
const copies = [...new Set([copy, (() => {
|
|
489
|
+
try {
|
|
490
|
+
return realpathSync(copy);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return copy;
|
|
494
|
+
}
|
|
495
|
+
})()])];
|
|
496
|
+
const declared = "function warnDuplicatedLockFiles(lockFiles) {";
|
|
497
|
+
const compile = Module.prototype._compile;
|
|
498
|
+
Module.prototype._compile = function _compile(content, filename, ...rest) {
|
|
499
|
+
if (typeof content === "string" && /[\\/]next[\\/]dist[\\/]lib[\\/]find-root\.js$/.test(filename)) {
|
|
500
|
+
content = content.replace(declared, `${declared} lockFiles = lockFiles.filter((file, index) => index === lockFiles.length - 1 || !${JSON.stringify(copies)}.some((copy) => file.startsWith(copy)));`);
|
|
501
|
+
}
|
|
502
|
+
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
503
|
+
};
|
|
504
|
+
}
|
|
199
505
|
function installAuthoredSourceView() {
|
|
200
506
|
let authored;
|
|
201
507
|
try {
|
|
@@ -886,21 +886,44 @@ function finishAssertionPhase(phase, error) {
|
|
|
886
886
|
phase.error = error instanceof Error ? error.message : String(error);
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
-
|
|
889
|
+
const INSTRUMENTATION_FRAME = /[\\/]\.supercov[\\/](?:node_modules[\\/])?(?:playwright|nodeTest|vitest|runtime|launchSupervisor|nodeAssert|nodeAssertStrict|nodeAssertAdapter|register|resolve-loader)\.(?:js|mjs)(?::|\))/u;
|
|
890
|
+
// A lexical wrapper runs the assertion in a function of its own, so its
|
|
891
|
+
// stack had two frames where the author wrote one: the function Supercov
|
|
892
|
+
// added, at the assertion, and below the runtime's frames the function the
|
|
893
|
+
// author wrote, at the wrapper. Vitest printed both, `:12:35` and `:12:1`.
|
|
894
|
+
// They are one frame again: the author's function, where the assertion is.
|
|
895
|
+
function cleanInstrumentationStack(error, lexical = false) {
|
|
890
896
|
if (!error || typeof error !== "object" || typeof error.stack !== "string")
|
|
891
897
|
return error;
|
|
892
898
|
const lines = error.stack.split("\n");
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
+
const hidden = lines.map((line, index) => index > 0 && INSTRUMENTATION_FRAME.test(line));
|
|
900
|
+
const first = hidden.indexOf(true);
|
|
901
|
+
if (first < 0)
|
|
902
|
+
return error;
|
|
903
|
+
const last = hidden.lastIndexOf(true);
|
|
904
|
+
let visible;
|
|
905
|
+
const frame = /^(\s*at (?:async )?)(?:(.*?) \()?(.*?:\d+:\d+)\)?$/;
|
|
906
|
+
const added = lexical && first > 1 ? frame.exec(lines[first - 1]) : null;
|
|
907
|
+
const written = added && last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
|
|
908
|
+
const file = (location) => location.replace(/:\d+:\d+$/, "");
|
|
909
|
+
// Both are frames of the author's file, or the error is another's. A name
|
|
910
|
+
// on the added one comes from a source map (Jest names a frame by what is
|
|
911
|
+
// called at its position), and it is the name the author's line would have.
|
|
912
|
+
if (added && written && file(added[3]) === file(written[3])) {
|
|
913
|
+
const merged = added[2] !== void 0 || written[2] === void 0 ? lines[first - 1] : `${written[1]}${written[2]} (${added[3]})`;
|
|
914
|
+
visible = [...lines.slice(0, first - 1), merged, ...lines.slice(last + 2)];
|
|
915
|
+
} else {
|
|
916
|
+
visible = lines.filter((line, index) => !hidden[index]);
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
error.stack = visible.join("\n");
|
|
920
|
+
} catch (e) {
|
|
899
921
|
}
|
|
900
922
|
return error;
|
|
901
923
|
}
|
|
902
924
|
function withNodeAssertionPhase(operation, source, callback) {
|
|
903
925
|
var _a8;
|
|
926
|
+
const lexical = typeof source === "string";
|
|
904
927
|
const context = currentRequestContext();
|
|
905
928
|
const scope = context.scope;
|
|
906
929
|
if (!scope)
|
|
@@ -944,13 +967,13 @@ function withNodeAssertionPhase(operation, source, callback) {
|
|
|
944
967
|
return value;
|
|
945
968
|
}, (error) => {
|
|
946
969
|
finishAssertionPhase(phase, error);
|
|
947
|
-
throw cleanInstrumentationStack(error);
|
|
970
|
+
throw cleanInstrumentationStack(error, lexical);
|
|
948
971
|
});
|
|
949
972
|
finishAssertionPhase(phase);
|
|
950
973
|
return result;
|
|
951
974
|
} catch (error) {
|
|
952
975
|
finishAssertionPhase(phase, error);
|
|
953
|
-
throw cleanInstrumentationStack(error);
|
|
976
|
+
throw cleanInstrumentationStack(error, lexical);
|
|
954
977
|
}
|
|
955
978
|
}
|
|
956
979
|
// Callee binding preserves receiver, getter/evaluation order, spreads and the
|
|
@@ -29,6 +29,8 @@ export default class SupercovVitestReporter {
|
|
|
29
29
|
}
|
|
30
30
|
onInit(vitest) {
|
|
31
31
|
this.configureProjects?.(vitest.projects);
|
|
32
|
+
// Failures print the project's paths and code: see register.mjs.
|
|
33
|
+
process.__SUPERCOV_FAILURE_OUTPUT__?.("vitest");
|
|
32
34
|
}
|
|
33
35
|
onBrowserInit(project) {
|
|
34
36
|
this.configureProjects?.([project]);
|