supercov 4.1.0 → 4.2.0
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/agent-loop.md
CHANGED
|
@@ -49,6 +49,11 @@ and file paths in place of the examples. For example,
|
|
|
49
49
|
Rust projects can use `cargo test`, Python projects `pytest`, and Ruby projects
|
|
50
50
|
`bundle exec rspec`. Keep the baseline and verification commands identical.
|
|
51
51
|
|
|
52
|
+
A loss in the comparison is not always a regression. An end-to-end suite
|
|
53
|
+
reaches slightly different code from one run to the next, and adding a test
|
|
54
|
+
changes no source: when `diff` notes that both runs measured the same source,
|
|
55
|
+
rerun before treating a loss as real.
|
|
56
|
+
|
|
52
57
|
The `line` query is useful before writing a test because it shows which tests
|
|
53
58
|
already reach that line. Extending a nearby test is often better than adding a
|
|
54
59
|
duplicate.
|
package/docs/cli.md
CHANGED
|
@@ -356,6 +356,13 @@ npx supercov diff <older-run> <newer-run>
|
|
|
356
356
|
expected behavior became covered without an unexplained regression elsewhere.
|
|
357
357
|
Neither input run is changed.
|
|
358
358
|
|
|
359
|
+
Two runs of the same code can differ a little: what an end-to-end suite
|
|
360
|
+
reaches depends on timing, on the order of tests, and on state outside the
|
|
361
|
+
code. When both runs measured the same source, `diff` says so under the
|
|
362
|
+
totals, and a loss it lists is not a change in the code. Run again before
|
|
363
|
+
acting on one. In `--json`, `inputs.sourceChanged` and `inputs.testsChanged`
|
|
364
|
+
say what differed between the two runs.
|
|
365
|
+
|
|
359
366
|
The same filters can focus a comparison:
|
|
360
367
|
|
|
361
368
|
```sh supercov
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
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.2.0",
|
|
123
|
+
"@supercov/cli-darwin-x64": "4.2.0",
|
|
124
|
+
"@supercov/cli-linux-arm64-gnu": "4.2.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-musl": "4.2.0",
|
|
126
|
+
"@supercov/cli-linux-x64-gnu": "4.2.0",
|
|
127
|
+
"@supercov/cli-linux-x64-musl": "4.2.0",
|
|
128
|
+
"@supercov/cli-win32-arm64": "4.2.0",
|
|
129
|
+
"@supercov/cli-win32-x64": "4.2.0"
|
|
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";
|
|
@@ -224,28 +224,324 @@ else if (/\/node_modules\/next\/dist\/compiled\/jest-worker\/processChild\.js$/.
|
|
|
224
224
|
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
225
225
|
};
|
|
226
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
|
+
// graceful-fs, which Jest reads through, copies `fs` when it loads, and a
|
|
456
|
+
// tool that runs Jest in its own process has usually loaded it by then.
|
|
457
|
+
for (const [file, cached] of Object.entries(Module._cache)) {
|
|
458
|
+
if (/[\\/]graceful-fs[\\/]graceful-fs\.js$/.test(file) && typeof cached?.exports?.readFileSync === "function") {
|
|
459
|
+
try {
|
|
460
|
+
cached.exports.readFileSync = supercovReadFileSync;
|
|
461
|
+
}
|
|
462
|
+
catch { }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
fs.promises.readFile = async function supercovReadFile(file, ...rest) {
|
|
466
|
+
const contents = await Reflect.apply(readPromise, this, [file, ...rest]);
|
|
467
|
+
return failureOutput.names && rewrittenUnder(copies, file) !== undefined && (typeof contents === "string" || Buffer.isBuffer(contents))
|
|
468
|
+
? namedForItself(file, contents)
|
|
469
|
+
: contents;
|
|
470
|
+
};
|
|
471
|
+
function supercovRelative(from, to) {
|
|
472
|
+
if (!failureOutput.prints)
|
|
473
|
+
return Reflect.apply(relativePath, this, [from, to]);
|
|
474
|
+
const local = rewrittenUnder(copies, to) === undefined ? rewrittenUnder(projects, to) : undefined;
|
|
475
|
+
return local !== undefined && calledByPrinter(supercovRelative)
|
|
476
|
+
? Reflect.apply(relativePath, this, [from, copies[0] + local.split("/").join(sep)])
|
|
477
|
+
: Reflect.apply(relativePath, this, [from, to]);
|
|
478
|
+
}
|
|
479
|
+
path.relative = supercovRelative;
|
|
480
|
+
syncBuiltinESMExports();
|
|
481
|
+
};
|
|
482
|
+
// Jest itself prints from here on. Any other process is Jest's from the
|
|
483
|
+
// moment it loads Jest: a worker of its, or a tool that runs Jest in its own
|
|
484
|
+
// process (`require("jest").run()`, as react-scripts and Vue's CLI do), where
|
|
485
|
+
// a failure in a single test file still printed the line Supercov had
|
|
486
|
+
// wrapped. A worker of another tool's (a bundler minifies in the same kind
|
|
487
|
+
// of process) never loads it.
|
|
488
|
+
if (isJestEntrypoint)
|
|
489
|
+
process.__SUPERCOV_FAILURE_OUTPUT__("jest");
|
|
490
|
+
else {
|
|
491
|
+
const jest = /(?:^|[\\/])(?:jest|jest-cli|@jest[\\/]core|jest-runner)(?:[\\/]|$)/;
|
|
492
|
+
const load = Module._load;
|
|
493
|
+
let loaded = false;
|
|
494
|
+
Module._load = function supercovJestLoad(request, ...rest) {
|
|
495
|
+
if (!loaded && typeof request === "string" && request.includes("jest") && jest.test(request)) {
|
|
496
|
+
loaded = true;
|
|
497
|
+
process.__SUPERCOV_FAILURE_OUTPUT__("jest");
|
|
498
|
+
}
|
|
499
|
+
return Reflect.apply(load, this, [request, ...rest]);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
227
502
|
// Next.js looks for lockfiles from its directory upwards, takes the outermost
|
|
228
503
|
// as its workspace root, and warns when it finds more than one. The copy
|
|
229
504
|
// holds the project's lockfile and lies inside the project, so every build
|
|
230
505
|
// under Supercov warned twice that the root "may not be correct", naming the
|
|
231
506
|
// copy's lockfile. The root Next picks is the project, which is right: it
|
|
232
|
-
// holds the copy and the dependencies. The copy's lockfile is
|
|
233
|
-
//
|
|
234
|
-
|
|
507
|
+
// holds the copy and the dependencies. The copy's lockfile is taken out of
|
|
508
|
+
// the warning, which is not printed when no other is left; one the project
|
|
509
|
+
// really has twice still warns.
|
|
510
|
+
//
|
|
511
|
+
// The warning is recognised by its text where Next prints it, in whatever
|
|
512
|
+
// process loads Next: started by its own command, by a server of the
|
|
513
|
+
// project's, or from a script. Next 15.4 to 16.4 print it through `warnOnce`.
|
|
514
|
+
{
|
|
235
515
|
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
236
516
|
const copies = [...new Set([copy, (() => {
|
|
237
517
|
try {
|
|
238
|
-
return realpathSync(copy)
|
|
518
|
+
return `${realpathSync(copy)}${sep}`;
|
|
239
519
|
}
|
|
240
520
|
catch {
|
|
241
521
|
return copy;
|
|
242
522
|
}
|
|
243
523
|
})()])];
|
|
244
|
-
const
|
|
524
|
+
const quieted = `
|
|
525
|
+
;try { typeof warnOnce === "function" && (warnOnce = ((print) => function (...message) {
|
|
526
|
+
const text = message[0];
|
|
527
|
+
if (typeof text !== "string" || !text.includes("multiple lockfiles"))
|
|
528
|
+
return print.apply(this, message);
|
|
529
|
+
const copies = ${JSON.stringify(copies)};
|
|
530
|
+
const lines = text.split("\\n");
|
|
531
|
+
const listed = (line) => /^\\s+\\* \\S/.test(line);
|
|
532
|
+
const kept = lines.filter((line) => !listed(line) || !copies.some((copy) => line.trim().slice(2).startsWith(copy)));
|
|
533
|
+
if (!kept.some(listed))
|
|
534
|
+
return undefined;
|
|
535
|
+
return print.apply(this, [kept.join("\\n"), ...message.slice(1)]);
|
|
536
|
+
})(warnOnce)); } catch {}
|
|
537
|
+
`.replace(/\n\s*/g, " ");
|
|
245
538
|
const compile = Module.prototype._compile;
|
|
246
539
|
Module.prototype._compile = function _compile(content, filename, ...rest) {
|
|
247
|
-
if (typeof content === "string" &&
|
|
248
|
-
|
|
540
|
+
if (typeof content === "string" && typeof filename === "string" && filename.endsWith("log.js") &&
|
|
541
|
+
/[\\/]next[\\/]dist[\\/]build[\\/]output[\\/]log\.js$/.test(filename)) {
|
|
542
|
+
// After the last statement, before the source map's line.
|
|
543
|
+
const map = content.lastIndexOf("\n//# sourceMappingURL=");
|
|
544
|
+
content = map < 0 ? `${content}${quieted}` : `${content.slice(0, map)}${quieted}${content.slice(map)}`;
|
|
249
545
|
}
|
|
250
546
|
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
251
547
|
};
|
|
@@ -905,8 +905,12 @@ function cleanInstrumentationStack(error, lexical = false) {
|
|
|
905
905
|
const frame = /^(\s*at (?:async )?)(?:(.*?) \()?(.*?:\d+:\d+)\)?$/;
|
|
906
906
|
const added = lexical && first > 1 ? frame.exec(lines[first - 1]) : null;
|
|
907
907
|
const written = added && last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
|
|
908
|
-
|
|
909
|
-
|
|
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]})`;
|
|
910
914
|
visible = [...lines.slice(0, first - 1), merged, ...lines.slice(last + 2)];
|
|
911
915
|
} else {
|
|
912
916
|
visible = lines.filter((line, index) => !hidden[index]);
|
|
@@ -922,8 +926,16 @@ function withNodeAssertionPhase(operation, source, callback) {
|
|
|
922
926
|
const lexical = typeof source === "string";
|
|
923
927
|
const context = currentRequestContext();
|
|
924
928
|
const scope = context.scope;
|
|
925
|
-
|
|
926
|
-
|
|
929
|
+
// No test is known to be running: a runner Supercov has no adapter in, such
|
|
930
|
+
// as Jest started from another tool's process. The assertion still fails
|
|
931
|
+
// with the frames its author would see.
|
|
932
|
+
if (!scope) {
|
|
933
|
+
try {
|
|
934
|
+
return callback();
|
|
935
|
+
} catch (error) {
|
|
936
|
+
throw cleanInstrumentationStack(error, lexical);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
927
939
|
// A lexically instrumented occurrence is an authored assertion site in its
|
|
928
940
|
// own right, even when it runs inside another assertion's callback: the
|
|
929
941
|
// validator passed to assert.throws is the ordinary case, and its inner
|
|
@@ -1,112 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { syncBuiltinESMExports } from "node:module";
|
|
3
|
-
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
4
2
|
import { fileURLToPath } from "node:url";
|
|
5
3
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
6
4
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
7
|
-
// The files of the copy Supercov rewrote, by path inside it, and where each
|
|
8
|
-
// is kept as its author wrote it or as a tool of the command changed it.
|
|
9
|
-
const copyRoots = (() => {
|
|
10
|
-
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
11
|
-
try {
|
|
12
|
-
return [...new Set([copy, fs.realpathSync(copy)])];
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return [copy];
|
|
16
|
-
}
|
|
17
|
-
})();
|
|
18
|
-
let rewritten;
|
|
19
|
-
function rewrittenFile(path) {
|
|
20
|
-
if (rewritten === undefined) {
|
|
21
|
-
try {
|
|
22
|
-
rewritten = new Set(JSON.parse(fs.readFileSync(new URL("./authored-sources.json", import.meta.url), "utf8")));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
rewritten = new Set();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (typeof path !== "string" || !isAbsolute(path))
|
|
29
|
-
return undefined;
|
|
30
|
-
for (const root of copyRoots) {
|
|
31
|
-
const local = relative(root, path);
|
|
32
|
-
if (local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local)) {
|
|
33
|
-
const file = local.split(sep).join("/");
|
|
34
|
-
return rewritten.has(file) ? file : undefined;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
const SOURCE_MAP = "\n//# sourceMappingURL=data:application/json;base64,";
|
|
40
|
-
/**
|
|
41
|
-
* A rewritten file's source map names the project's own file, by absolute
|
|
42
|
-
* path, so a Node stack trace reads as the project's. Vitest prints a frame
|
|
43
|
-
* relative to its root, which is the copy: `../../../../lib/crypto.ts:3:11`
|
|
44
|
-
* where it prints `lib/crypto.ts:3:11` without Supercov, and a file outside
|
|
45
|
-
* its module graph gets no code under the frame. Vite is handed the same
|
|
46
|
-
* code with a map that names the file itself.
|
|
47
|
-
*/
|
|
48
|
-
export function supercovSourceMaps() {
|
|
49
|
-
const { readFile } = fs.promises;
|
|
50
|
-
return {
|
|
51
|
-
name: "supercov:source-maps",
|
|
52
|
-
enforce: "pre",
|
|
53
|
-
async load(id) {
|
|
54
|
-
if (rewrittenFile(id) === undefined)
|
|
55
|
-
return null;
|
|
56
|
-
try {
|
|
57
|
-
const code = await readFile(id, "utf8");
|
|
58
|
-
const found = code.lastIndexOf(SOURCE_MAP);
|
|
59
|
-
if (found < 0)
|
|
60
|
-
return null;
|
|
61
|
-
const encoded = code.slice(found + SOURCE_MAP.length).trim();
|
|
62
|
-
if (/\s/.test(encoded))
|
|
63
|
-
return null;
|
|
64
|
-
const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
65
|
-
map.sources = [basename(id)];
|
|
66
|
-
return { code: code.slice(0, found + 1), map };
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
// Vitest reads a file a failure names: to print the code around the nearest
|
|
75
|
-
// frame, and (Vitest 4) to look for a source map in it. In the copy that is
|
|
76
|
-
// the rewritten file. An assertion wrapped into one long line printed no code
|
|
77
|
-
// at all, any other line printed Supercov's, and positions that were already
|
|
78
|
-
// the author's were mapped a second time through the file's own map: a test
|
|
79
|
-
// file's frame at `:12:35` read `:12:0`. What Vitest itself reads is the text
|
|
80
|
-
// the file was rewritten from. What runs is not read here: Vite loads it.
|
|
81
|
-
let readsAuthored = false;
|
|
82
|
-
function readAuthoredFromVitest() {
|
|
83
|
-
if (readsAuthored)
|
|
84
|
-
return;
|
|
85
|
-
readsAuthored = true;
|
|
86
|
-
const { readFileSync, existsSync } = fs;
|
|
87
|
-
const kept = (name, file) => fileURLToPath(new URL(`./${name}/${file}`, import.meta.url));
|
|
88
|
-
const calledByVitest = () => {
|
|
89
|
-
const holder = {};
|
|
90
|
-
const limit = Error.stackTraceLimit;
|
|
91
|
-
try {
|
|
92
|
-
Error.stackTraceLimit = 1;
|
|
93
|
-
Error.captureStackTrace(holder, supercovReadFileSync);
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
Error.stackTraceLimit = limit;
|
|
97
|
-
}
|
|
98
|
-
return /[\\/]vitest[\\/]dist[\\/]/.test(String(holder.stack).split("\n")[1] ?? "");
|
|
99
|
-
};
|
|
100
|
-
function supercovReadFileSync(path, ...rest) {
|
|
101
|
-
const file = rewrittenFile(path);
|
|
102
|
-
if (file === undefined || !calledByVitest())
|
|
103
|
-
return Reflect.apply(readFileSync, this, [path, ...rest]);
|
|
104
|
-
const changed = kept(".changed", file);
|
|
105
|
-
return Reflect.apply(readFileSync, this, [existsSync(changed) ? changed : kept(".authored", file), ...rest]);
|
|
106
|
-
}
|
|
107
|
-
fs.readFileSync = supercovReadFileSync;
|
|
108
|
-
syncBuiltinESMExports();
|
|
109
|
-
}
|
|
110
5
|
function sourcePath(moduleId) {
|
|
111
6
|
const absolute = moduleId.startsWith("file:")
|
|
112
7
|
? fileURLToPath(moduleId)
|
|
@@ -134,7 +29,8 @@ export default class SupercovVitestReporter {
|
|
|
134
29
|
}
|
|
135
30
|
onInit(vitest) {
|
|
136
31
|
this.configureProjects?.(vitest.projects);
|
|
137
|
-
|
|
32
|
+
// Failures print the project's paths and code: see register.mjs.
|
|
33
|
+
process.__SUPERCOV_FAILURE_OUTPUT__?.("vitest");
|
|
138
34
|
}
|
|
139
35
|
onBrowserInit(project) {
|
|
140
36
|
this.configureProjects?.([project]);
|