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.
@@ -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.1.1",
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.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"
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
- async registerPage(page) {
385
- if (this.pages.has(page) || this.disposed)
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. 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.
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 if (process.env.JEST_WORKER_ID)
478
- process.__SUPERCOV_FAILURE_OUTPUT__();
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 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)) {
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 declared = "function warnDuplicatedLockFiles(lockFiles) {";
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" && /[\\/]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)));`);
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
- if (!scope)
930
- return callback();
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