supercov 4.1.1 → 4.2.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/agent-loop.md +5 -0
- package/docs/cli.md +18 -1
- package/package.json +9 -9
- package/runtime/javascript/playwright.mjs +30 -3
- package/runtime/javascript/register.mjs +56 -12
- package/runtime/javascript/runtime.mjs +10 -2
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
|
@@ -354,7 +354,24 @@ npx supercov diff <older-run> <newer-run>
|
|
|
354
354
|
|
|
355
355
|
`diff` reports gains and losses. Use it after adding a test to prove that the
|
|
356
356
|
expected behavior became covered without an unexplained regression elsewhere.
|
|
357
|
-
Neither input run is changed.
|
|
357
|
+
Neither input run is changed. Under the totals it lists what was lost, marked
|
|
358
|
+
`-`, and then what was gained, marked `+`:
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
gained: 1 lines, 1 branches, 0 MC/DC conditions
|
|
362
|
+
lost: 1 lines, 1 branches, 0 MC/DC conditions
|
|
363
|
+
- line src/fee.js:5
|
|
364
|
+
- branch src/fee.js:2 false
|
|
365
|
+
+ line src/fee.js:3
|
|
366
|
+
+ branch src/fee.js:2 true
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Two runs of the same code can differ a little: what an end-to-end suite
|
|
370
|
+
reaches depends on timing, on the order of tests, and on state outside the
|
|
371
|
+
code. When both runs measured the same source, `diff` says so under the
|
|
372
|
+
totals, and a loss it lists is not a change in the code. Run again before
|
|
373
|
+
acting on one. In `--json`, `inputs.sourceChanged` and `inputs.testsChanged`
|
|
374
|
+
say what differed between the two runs.
|
|
358
375
|
|
|
359
376
|
The same filters can focus a comparison:
|
|
360
377
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.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.2.1",
|
|
123
|
+
"@supercov/cli-darwin-x64": "4.2.1",
|
|
124
|
+
"@supercov/cli-linux-arm64-gnu": "4.2.1",
|
|
125
|
+
"@supercov/cli-linux-arm64-musl": "4.2.1",
|
|
126
|
+
"@supercov/cli-linux-x64-gnu": "4.2.1",
|
|
127
|
+
"@supercov/cli-linux-x64-musl": "4.2.1",
|
|
128
|
+
"@supercov/cli-win32-arm64": "4.2.1",
|
|
129
|
+
"@supercov/cli-win32-x64": "4.2.1"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
132
132
|
"@playwright/test": ">=1.55.0",
|
|
@@ -198,6 +198,7 @@ class CoveragePhaseController {
|
|
|
198
198
|
cdpSessions = new Map();
|
|
199
199
|
newDocumentScriptIds = new Map();
|
|
200
200
|
pendingRegistrations = new Set();
|
|
201
|
+
pageRegistrations = new WeakMap();
|
|
201
202
|
scriptUpdate = Promise.resolve();
|
|
202
203
|
proxyCache = new WeakMap();
|
|
203
204
|
runtimeSnapshots;
|
|
@@ -381,8 +382,27 @@ class CoveragePhaseController {
|
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
}
|
|
384
|
-
|
|
385
|
-
|
|
385
|
+
/**
|
|
386
|
+
* A page is registered once, and whoever asks while that is under way
|
|
387
|
+
* waits for it. Playwright announces a page to the context's listener
|
|
388
|
+
* before the fixture that hands it to the test has it, so the listener
|
|
389
|
+
* started the registration and the fixture, finding the page known,
|
|
390
|
+
* returned at once: the test's first `page.goto` began before the page had
|
|
391
|
+
* the session a phase is installed through. The document it loaded did not
|
|
392
|
+
* know its phase, and its hits were left to be placed by their time.
|
|
393
|
+
*/
|
|
394
|
+
registerPage(page) {
|
|
395
|
+
if (this.disposed)
|
|
396
|
+
return Promise.resolve();
|
|
397
|
+
let registration = this.pageRegistrations.get(page);
|
|
398
|
+
if (!registration) {
|
|
399
|
+
registration = this.registerNewPage(page);
|
|
400
|
+
this.pageRegistrations.set(page, registration);
|
|
401
|
+
}
|
|
402
|
+
return registration;
|
|
403
|
+
}
|
|
404
|
+
async registerNewPage(page) {
|
|
405
|
+
if (this.pages.has(page))
|
|
386
406
|
return;
|
|
387
407
|
timingCount("registerPage");
|
|
388
408
|
this.pages.add(page);
|
|
@@ -393,8 +413,15 @@ class CoveragePhaseController {
|
|
|
393
413
|
adopted: !this.contexts.has(context) && tracked && !headersKnown,
|
|
394
414
|
});
|
|
395
415
|
const cdp = await page.context().newCDPSession(page).catch(() => undefined);
|
|
396
|
-
if (cdp)
|
|
416
|
+
if (cdp) {
|
|
417
|
+
// Chromium runs a session's new-document scripts only once the
|
|
418
|
+
// session has enabled the Page domain. Without it the script that
|
|
419
|
+
// tells a document its phase was accepted and never ran: a page
|
|
420
|
+
// learned its phase only from the next action, after it had
|
|
421
|
+
// loaded, and everything it ran while loading was placed by time.
|
|
422
|
+
await cdp.send("Page.enable").catch(() => undefined);
|
|
397
423
|
this.cdpSessions.set(page, cdp);
|
|
424
|
+
}
|
|
398
425
|
const phaseId = this.requestPhaseId();
|
|
399
426
|
if (phaseId)
|
|
400
427
|
await this.activatePage(page, phaseId);
|
|
@@ -452,6 +452,16 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
452
452
|
return failureOutput.names ? namedForItself(file, contents) : contents;
|
|
453
453
|
}
|
|
454
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
|
+
}
|
|
455
465
|
fs.promises.readFile = async function supercovReadFile(file, ...rest) {
|
|
456
466
|
const contents = await Reflect.apply(readPromise, this, [file, ...rest]);
|
|
457
467
|
return failureOutput.names && rewrittenUnder(copies, file) !== undefined && (typeof contents === "string" || Buffer.isBuffer(contents))
|
|
@@ -469,35 +479,69 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
469
479
|
path.relative = supercovRelative;
|
|
470
480
|
syncBuiltinESMExports();
|
|
471
481
|
};
|
|
472
|
-
// Jest itself prints from here on.
|
|
473
|
-
//
|
|
474
|
-
//
|
|
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.
|
|
475
488
|
if (isJestEntrypoint)
|
|
476
489
|
process.__SUPERCOV_FAILURE_OUTPUT__("jest");
|
|
477
|
-
else
|
|
478
|
-
|
|
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
|
+
}
|
|
479
502
|
// Next.js looks for lockfiles from its directory upwards, takes the outermost
|
|
480
503
|
// as its workspace root, and warns when it finds more than one. The copy
|
|
481
504
|
// holds the project's lockfile and lies inside the project, so every build
|
|
482
505
|
// under Supercov warned twice that the root "may not be correct", naming the
|
|
483
506
|
// 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
|
|
485
|
-
//
|
|
486
|
-
|
|
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
|
+
{
|
|
487
515
|
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
488
516
|
const copies = [...new Set([copy, (() => {
|
|
489
517
|
try {
|
|
490
|
-
return realpathSync(copy)
|
|
518
|
+
return `${realpathSync(copy)}${sep}`;
|
|
491
519
|
}
|
|
492
520
|
catch {
|
|
493
521
|
return copy;
|
|
494
522
|
}
|
|
495
523
|
})()])];
|
|
496
|
-
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, " ");
|
|
497
538
|
const compile = Module.prototype._compile;
|
|
498
539
|
Module.prototype._compile = function _compile(content, filename, ...rest) {
|
|
499
|
-
if (typeof content === "string" &&
|
|
500
|
-
|
|
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)}`;
|
|
501
545
|
}
|
|
502
546
|
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
503
547
|
};
|
|
@@ -926,8 +926,16 @@ function withNodeAssertionPhase(operation, source, callback) {
|
|
|
926
926
|
const lexical = typeof source === "string";
|
|
927
927
|
const context = currentRequestContext();
|
|
928
928
|
const scope = context.scope;
|
|
929
|
-
|
|
930
|
-
|
|
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
|
+
}
|
|
931
939
|
// A lexically instrumented occurrence is an authored assertion site in its
|
|
932
940
|
// own right, even when it runs inside another assertion's callback: the
|
|
933
941
|
// validator passed to assert.throws is the ordinary case, and its inner
|