supercov 4.11.1 → 4.12.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
|
@@ -349,6 +349,19 @@ same run:
|
|
|
349
349
|
|
|
350
350
|
## Export for other tools
|
|
351
351
|
|
|
352
|
+
Every obligation a run left uncovered, one JSON object per line:
|
|
353
|
+
|
|
354
|
+
```sh supercov-example
|
|
355
|
+
npx supercov runs latest gaps --format jsonl --metric mcdc --output gaps.jsonl
|
|
356
|
+
npx supercov runs latest gaps --format jsonl > gaps.jsonl
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Each line holds the file, the line, the source of that line and what `runs
|
|
360
|
+
<id> file` says of the obligation: for an MC/DC condition its decision, the
|
|
361
|
+
condition and the vectors observed. `--metric`, `--filter`, `--kind` and
|
|
362
|
+
`--runner` narrow it as they narrow `gaps`, and nothing is paged, so a tool or
|
|
363
|
+
an agent reads a run's every gap in one pass instead of a page at a time.
|
|
364
|
+
|
|
352
365
|
```sh supercov-example
|
|
353
366
|
npx supercov runs report --format lcov --output coverage/lcov.info
|
|
354
367
|
npx supercov runs report --format cobertura --output coverage/cobertura.xml
|
|
@@ -468,6 +481,20 @@ The same filters can focus a comparison:
|
|
|
468
481
|
npx supercov diff <older-run> <newer-run> --kind e2e
|
|
469
482
|
```
|
|
470
483
|
|
|
484
|
+
To see what a new test covered that the suite did not, measure it alone and
|
|
485
|
+
compare it with the suite's run, keeping only what it gained:
|
|
486
|
+
|
|
487
|
+
```sh supercov-example
|
|
488
|
+
npx supercov -- <the new test alone>
|
|
489
|
+
npx supercov diff <suite-run> latest --gained
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
A test run alone covers far less than the suite, so a plain `diff` of the two
|
|
493
|
+
lists nearly everything as lost. `--gained` lists only the lines, branches and
|
|
494
|
+
MC/DC conditions the newer run covers and the older does not, which is how to
|
|
495
|
+
check that a test reached the conditions it was written for without measuring
|
|
496
|
+
the whole suite. In `--json`, `gainedOnly` is `true` and `lost` is empty.
|
|
497
|
+
|
|
471
498
|
## Generate a portable HTML report
|
|
472
499
|
|
|
473
500
|
```sh
|
|
@@ -533,6 +560,16 @@ A source file
|
|
|
533
560
|
added since the run is outside every test's record; the working-tree check
|
|
534
561
|
says so, and the suite should run in full.
|
|
535
562
|
|
|
563
|
+
A server the suite starts is recorded too: a mock server `webServer` starts,
|
|
564
|
+
or the application's own. Each Node process of a Playwright run notes the
|
|
565
|
+
project files it loads and reads, and the tests it serves, by the test a
|
|
566
|
+
request names or, for one that names none (a webhook), the test running
|
|
567
|
+
then. A change to a file such a process loaded or read, a mock's code, a
|
|
568
|
+
helper it imports or the data it reads, affects the tests it served and no
|
|
569
|
+
others. A mock keeps its state between tests, so a mock's files count whole,
|
|
570
|
+
not route by route; a file the application's server loaded, a preload of its
|
|
571
|
+
own, affects every test it served.
|
|
572
|
+
|
|
536
573
|
### Which affected tests check the change
|
|
537
574
|
|
|
538
575
|
When the run has an [assertion assessment](assertions.md), `tests affected`
|
|
@@ -655,6 +692,19 @@ A run left out is named with the files and the functions that changed. The
|
|
|
655
692
|
merged run lists what differed and why it does not reach the earlier runs:
|
|
656
693
|
`merged across:` in `runs <id>`, and `mergedAcross` in JSON.
|
|
657
694
|
|
|
695
|
+
A changed file that a server the run's tests reached loaded or read (see
|
|
696
|
+
[Find the tests a change affects](#find-the-tests-a-change-affects)) takes
|
|
697
|
+
only those tests from the run: measure them again, and the merge keeps the
|
|
698
|
+
rest of the run and leaves their old records out:
|
|
699
|
+
|
|
700
|
+
```sh supercov-example
|
|
701
|
+
npx supercov runs <suite-run> tests affected --names # after changing a mock
|
|
702
|
+
npx supercov -- <those tests alone>
|
|
703
|
+
npx supercov merge
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Until they are measured again, the merge refuses and names them.
|
|
707
|
+
|
|
658
708
|
When you know a change Supercov counts does not alter what the earlier tests
|
|
659
709
|
ran, say so:
|
|
660
710
|
|
|
@@ -666,6 +716,22 @@ The product code, the dependencies, and what is measured must still be the
|
|
|
666
716
|
same, and a run whose own test files changed is still left out. What was
|
|
667
717
|
accepted is listed in `merged across:` as accepted.
|
|
668
718
|
|
|
719
|
+
Runs measured on different machines merge the same way. Each machine
|
|
720
|
+
measures its part of the suite on the same commit and toolchain, and one job
|
|
721
|
+
merges them after copying each run's directory from `.supercov/runs/` into its
|
|
722
|
+
own:
|
|
723
|
+
|
|
724
|
+
```yaml
|
|
725
|
+
# Each of two jobs:
|
|
726
|
+
- run: npx supercov -- <test command for shard ${{ matrix.shard }}>
|
|
727
|
+
- uses: actions/upload-artifact@v4
|
|
728
|
+
with: { name: "supercov-${{ matrix.shard }}", path: .supercov/runs, include-hidden-files: true }
|
|
729
|
+
# Then one job:
|
|
730
|
+
- uses: actions/download-artifact@v4
|
|
731
|
+
with: { pattern: "supercov-*", path: .supercov/runs, merge-multiple: true }
|
|
732
|
+
- run: npx supercov merge
|
|
733
|
+
```
|
|
734
|
+
|
|
669
735
|
`assertions assess` assesses a merged run across every suite merged into it,
|
|
670
736
|
reusing the answers saved when its parts were assessed. `tests affected` of a
|
|
671
737
|
merged run is answered from the runs it was merged from, which hold the record
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.1",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"eval:skill": "claude plugin eval plugins/supercov --scaffold --trust-plugin --no-publish --model claude-sonnet-5 --judge-model claude-haiku-4-5 --threshold 0.66 --tag behavior trigger --allow-tools Bash Edit Write --max-cost-usd 15",
|
|
84
84
|
"eval:skill-triggers": "claude plugin eval plugins/supercov --scaffold --trust-plugin --no-publish --model claude-sonnet-5 --judge-model claude-haiku-4-5 --threshold 0.66 --tag trigger --ablation none --max-cost-usd 5",
|
|
85
85
|
"test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && npm run test:runtime && npm run test:rust-assets && npm run test:engine-integrations",
|
|
86
|
-
"test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-jest-integration.mjs && node scripts/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.mjs && node scripts/leaving-page.mjs && node scripts/background-by-time.mjs && node scripts/python-server-playwright.mjs && node scripts/ruby-server-playwright.mjs && node scripts/go-server-playwright.mjs && node scripts/jvm-server-playwright.mjs && node scripts/docker-server-playwright.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/rust-top-packages-integration.mjs && node scripts/rust-generic-esbuild-integration.mjs && node scripts/rust-generic-tsc-integration.mjs && node scripts/rust-generic-build-matrix.mjs && node scripts/rust-vite-playwright-integration.mjs",
|
|
86
|
+
"test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-jest-integration.mjs && node scripts/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/mock-reach.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.mjs && node scripts/leaving-page.mjs && node scripts/shared-page.mjs && node scripts/background-by-time.mjs && node scripts/python-server-playwright.mjs && node scripts/ruby-server-playwright.mjs && node scripts/go-server-playwright.mjs && node scripts/jvm-server-playwright.mjs && node scripts/docker-server-playwright.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/rust-top-packages-integration.mjs && node scripts/rust-generic-esbuild-integration.mjs && node scripts/rust-generic-tsc-integration.mjs && node scripts/rust-generic-build-matrix.mjs && node scripts/rust-vite-playwright-integration.mjs",
|
|
87
87
|
"test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
|
|
88
88
|
"test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
89
89
|
"test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
|
|
@@ -120,14 +120,14 @@
|
|
|
120
120
|
"test:windows-tls": "node scripts/windows-tls-test.mjs"
|
|
121
121
|
},
|
|
122
122
|
"optionalDependencies": {
|
|
123
|
-
"@supercov/cli-darwin-arm64": "4.
|
|
124
|
-
"@supercov/cli-darwin-x64": "4.
|
|
125
|
-
"@supercov/cli-linux-arm64-gnu": "4.
|
|
126
|
-
"@supercov/cli-linux-arm64-musl": "4.
|
|
127
|
-
"@supercov/cli-linux-x64-gnu": "4.
|
|
128
|
-
"@supercov/cli-linux-x64-musl": "4.
|
|
129
|
-
"@supercov/cli-win32-arm64": "4.
|
|
130
|
-
"@supercov/cli-win32-x64": "4.
|
|
123
|
+
"@supercov/cli-darwin-arm64": "4.12.1",
|
|
124
|
+
"@supercov/cli-darwin-x64": "4.12.1",
|
|
125
|
+
"@supercov/cli-linux-arm64-gnu": "4.12.1",
|
|
126
|
+
"@supercov/cli-linux-arm64-musl": "4.12.1",
|
|
127
|
+
"@supercov/cli-linux-x64-gnu": "4.12.1",
|
|
128
|
+
"@supercov/cli-linux-x64-musl": "4.12.1",
|
|
129
|
+
"@supercov/cli-win32-arm64": "4.12.1",
|
|
130
|
+
"@supercov/cli-win32-x64": "4.12.1"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"@playwright/test": ">=1.55.0",
|
|
@@ -391,6 +391,42 @@ class CoveragePhaseController {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Start the pages an earlier test left open (a page a serial describe
|
|
396
|
+
* opens in `beforeAll`, a worker-scoped one) with no evidence, and return
|
|
397
|
+
* what they held. Their documents kept every hit since they loaded, and
|
|
398
|
+
* each later test was credited with what the tests before it ran. What
|
|
399
|
+
* they held is the earlier tests' (already recorded) and work between
|
|
400
|
+
* tests, such as a `beforeAll` signing in: no test's. A document that
|
|
401
|
+
* already has one of this test's phases is left alone: a reset that
|
|
402
|
+
* arrives late must not drop what this test ran.
|
|
403
|
+
*/
|
|
404
|
+
async takeCarriedEvidence() {
|
|
405
|
+
await Promise.all([...this.pendingRegistrations]);
|
|
406
|
+
const prefix = `${this.scope.attemptId}:`;
|
|
407
|
+
const take = (target) => bounded(target
|
|
408
|
+
.evaluate((prefix) => {
|
|
409
|
+
const coverageGlobal = globalThis;
|
|
410
|
+
if (String(coverageGlobal.__SUPERCOV_PHASE_ID__ ?? "").startsWith(prefix))
|
|
411
|
+
return undefined;
|
|
412
|
+
const getSnapshot = coverageGlobal.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
413
|
+
const reset = coverageGlobal.__SUPERCOV_RESET__;
|
|
414
|
+
if (!getSnapshot || !reset)
|
|
415
|
+
return undefined;
|
|
416
|
+
const snapshot = JSON.stringify(getSnapshot());
|
|
417
|
+
reset();
|
|
418
|
+
return snapshot;
|
|
419
|
+
}, prefix)
|
|
420
|
+
.then((text) => (typeof text === "string" ? JSON.parse(text) : undefined))
|
|
421
|
+
.catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
|
|
422
|
+
const taken = await Promise.all(this.allPages().flatMap((page) => [
|
|
423
|
+
...page.frames().map(take),
|
|
424
|
+
...page.workers().map(take),
|
|
425
|
+
]));
|
|
426
|
+
const snapshots = taken.filter((snapshot) => snapshot && snapshot !== WORKER_TIMED_OUT &&
|
|
427
|
+
(snapshot.hits?.length || snapshot.events?.length || snapshot.decisions?.length));
|
|
428
|
+
return snapshots.length > 0 ? mergeSnapshots(snapshots) : undefined;
|
|
429
|
+
}
|
|
394
430
|
/**
|
|
395
431
|
* A page is registered once, and whoever asks while that is under way
|
|
396
432
|
* waits for it. Playwright announces a page to the context's listener
|
|
@@ -1458,6 +1494,25 @@ function mergeCollectedPhases(controllerPhases, fallbackPhases) {
|
|
|
1458
1494
|
});
|
|
1459
1495
|
return [...controllerPhases, ...remaining];
|
|
1460
1496
|
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Pool runners may cycle-restore a VM immediately after Playwright exits,
|
|
1499
|
+
* which can discard or overwrite the normal artifact copy. Each record is
|
|
1500
|
+
* written to one uniquely named, one-shot evidence file in the runner's shared
|
|
1501
|
+
* directory as well. No test streams into this path.
|
|
1502
|
+
*/
|
|
1503
|
+
function appendEvidenceLine(serialized) {
|
|
1504
|
+
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"] ??
|
|
1505
|
+
(GENERATED_EVIDENCE_DIRECTORY.startsWith("__")
|
|
1506
|
+
? undefined
|
|
1507
|
+
: GENERATED_EVIDENCE_DIRECTORY);
|
|
1508
|
+
if (!evidenceDirectory)
|
|
1509
|
+
return;
|
|
1510
|
+
const resolvedDirectory = resolve(process.cwd(), evidenceDirectory);
|
|
1511
|
+
const append = process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1"
|
|
1512
|
+
? appendJsonLineDurableSync
|
|
1513
|
+
: appendJsonLineSync;
|
|
1514
|
+
append(resolve(resolvedDirectory, `playwright-worker-${evidenceWriterIdentity()}-${process.pid}.mcdc.jsonl`), serialized);
|
|
1515
|
+
}
|
|
1461
1516
|
const instrumentedFixtures = {
|
|
1462
1517
|
page: async ({ page }, use, testInfo) => {
|
|
1463
1518
|
const scope = executionScope(testInfo);
|
|
@@ -1507,6 +1562,17 @@ const instrumentedFixtures = {
|
|
|
1507
1562
|
activeController = controller;
|
|
1508
1563
|
directRuntime()?.activateCoverageScope(scope);
|
|
1509
1564
|
await controller.adoptTrackedContexts().catch(() => undefined);
|
|
1565
|
+
const carried = await controller.takeCarriedEvidence().catch(() => undefined);
|
|
1566
|
+
if (carried) {
|
|
1567
|
+
appendEvidenceLine(`${JSON.stringify({
|
|
1568
|
+
testId: `__supercov_background__:playwright:${scope.attemptId}`,
|
|
1569
|
+
test: "Playwright work between tests",
|
|
1570
|
+
status: "unknown",
|
|
1571
|
+
provenance: { runner: "background", kind: "background", source: "explicit" },
|
|
1572
|
+
role: "background",
|
|
1573
|
+
browser: [carried],
|
|
1574
|
+
})}\n`);
|
|
1575
|
+
}
|
|
1510
1576
|
const serverOutput = serverEvidencePath(scope);
|
|
1511
1577
|
mkdirSync(serverEvidenceDirectory(scope), { recursive: true });
|
|
1512
1578
|
rmSync(serverOutput, { force: true });
|
|
@@ -1543,22 +1609,7 @@ const instrumentedFixtures = {
|
|
|
1543
1609
|
browser,
|
|
1544
1610
|
server,
|
|
1545
1611
|
};
|
|
1546
|
-
|
|
1547
|
-
// Pool runners may cycle-restore a VM immediately after Playwright
|
|
1548
|
-
// exits, which can discard or overwrite the normal artifact copy.
|
|
1549
|
-
// Write one uniquely named, one-shot evidence file to the runner's
|
|
1550
|
-
// shared directory as well. No test streams into this path.
|
|
1551
|
-
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"] ??
|
|
1552
|
-
(GENERATED_EVIDENCE_DIRECTORY.startsWith("__")
|
|
1553
|
-
? undefined
|
|
1554
|
-
: GENERATED_EVIDENCE_DIRECTORY);
|
|
1555
|
-
if (evidenceDirectory) {
|
|
1556
|
-
const resolvedDirectory = resolve(process.cwd(), evidenceDirectory);
|
|
1557
|
-
const append = process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1"
|
|
1558
|
-
? appendJsonLineDurableSync
|
|
1559
|
-
: appendJsonLineSync;
|
|
1560
|
-
append(resolve(resolvedDirectory, `playwright-worker-${evidenceWriterIdentity()}-${process.pid}.mcdc.jsonl`), serialized);
|
|
1561
|
-
}
|
|
1612
|
+
appendEvidenceLine(`${JSON.stringify(payload)}\n`);
|
|
1562
1613
|
}
|
|
1563
1614
|
finally {
|
|
1564
1615
|
await timed("controller.dispose", () => controller.dispose());
|
|
@@ -847,8 +847,64 @@ function skipCoverageThresholds(tool) {
|
|
|
847
847
|
// instead, where such a file is read as an ES module and the hook never
|
|
848
848
|
// sees it; the loader is told so it can hand those files back.
|
|
849
849
|
const typescriptHandler = Module._extensions[".ts"];
|
|
850
|
+
// The tests a process serves reach the project files it loaded and read: a
|
|
851
|
+
// mock server a suite starts, the app server. Where SUPERCOV_SUPPORT_DIR is
|
|
852
|
+
// set this process notes them in a file of its own, which the loader adds
|
|
853
|
+
// its modules to and the runtime the tests the process served; a change to
|
|
854
|
+
// one of those files then affects those tests alone (`tests affected`,
|
|
855
|
+
// `merge`).
|
|
856
|
+
const supportDirectory = process.env.SUPERCOV_SUPPORT_DIR;
|
|
857
|
+
let supportFile;
|
|
858
|
+
if (supportDirectory) {
|
|
859
|
+
supportFile = path.join(supportDirectory, `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.jsonl`);
|
|
860
|
+
// This process's own, not a parent's it inherited.
|
|
861
|
+
process.env.__SUPERCOV_SUPPORT_FILE = supportFile;
|
|
862
|
+
try {
|
|
863
|
+
fs.mkdirSync(supportDirectory, { recursive: true });
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
// Then nothing is noted, and every test counts as before.
|
|
867
|
+
}
|
|
868
|
+
const projectRoot = process.env.SUPERCOV_PROJECT_ROOT;
|
|
869
|
+
const read = new Set();
|
|
870
|
+
const noteRead = (target) => {
|
|
871
|
+
try {
|
|
872
|
+
const file = typeof target === "string"
|
|
873
|
+
? resolve(target)
|
|
874
|
+
: target instanceof URL && target.protocol === "file:"
|
|
875
|
+
? fileURLToPath(target)
|
|
876
|
+
: undefined;
|
|
877
|
+
// The copy lives under `.supercov/`: what is the project's is
|
|
878
|
+
// decided by the project root.
|
|
879
|
+
const inside = file && projectRoot && file.startsWith(projectRoot + sep) ? file.slice(projectRoot.length + 1) : undefined;
|
|
880
|
+
if (!inside || inside.startsWith(`.supercov${sep}`) || inside.split(sep).includes("node_modules") || read.has(file))
|
|
881
|
+
return;
|
|
882
|
+
read.add(file);
|
|
883
|
+
fs.appendFileSync(supportFile, `${JSON.stringify({ read: file })}\n`);
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
// A read not noted counts for every test, as before.
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
for (const name of ["readFileSync", "readFile"]) {
|
|
890
|
+
const original = fs[name];
|
|
891
|
+
fs[name] = function supercovNotedRead(target, ...rest) {
|
|
892
|
+
noteRead(target);
|
|
893
|
+
return original.call(this, target, ...rest);
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const readPromised = fs.promises.readFile;
|
|
897
|
+
fs.promises.readFile = function supercovNotedRead(target, ...rest) {
|
|
898
|
+
noteRead(target);
|
|
899
|
+
return readPromised.call(this, target, ...rest);
|
|
900
|
+
};
|
|
901
|
+
syncBuiltinESMExports();
|
|
902
|
+
}
|
|
850
903
|
register(new URL("./resolve-loader.mjs", import.meta.url), {
|
|
851
|
-
data: {
|
|
904
|
+
data: {
|
|
905
|
+
typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS",
|
|
906
|
+
supportFile,
|
|
907
|
+
},
|
|
852
908
|
});
|
|
853
909
|
if (process.env.SUPERCOV_DEBUG === "1") {
|
|
854
910
|
console.error("[supercov] preload", { entrypoint });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
const GENERATED_TARGET = "__SUPERCOV_PLAYWRIGHT_MODULE__";
|
|
@@ -27,8 +27,40 @@ function belongsToProject(parentURL) {
|
|
|
27
27
|
return (parentURL.startsWith(projectURL) && !parentURL.startsWith(generatedURL));
|
|
28
28
|
}
|
|
29
29
|
let typescriptRequireHook = false;
|
|
30
|
+
// Where this process notes the project modules it loads (`register.mjs`
|
|
31
|
+
// names it): a mock server a suite starts is reached by the tests it serves,
|
|
32
|
+
// and a change to a file it loaded is a change for them alone.
|
|
33
|
+
let supportFile;
|
|
34
|
+
const supportModules = new Set();
|
|
35
|
+
// A project file, not a dependency nor Supercov's own: the copy itself lives
|
|
36
|
+
// under `.supercov/`, so the project root decides.
|
|
37
|
+
function projectFile(file) {
|
|
38
|
+
if (!PROJECT_ROOT || !file.startsWith(PROJECT_ROOT))
|
|
39
|
+
return false;
|
|
40
|
+
const relative = file.slice(PROJECT_ROOT.length).replace(/^[\\/]/, "");
|
|
41
|
+
return relative !== "" && !/^\.supercov[\\/]/.test(relative) && !/(?:^|[\\/])node_modules[\\/]/.test(relative);
|
|
42
|
+
}
|
|
43
|
+
function noteLoaded(url) {
|
|
44
|
+
if (!supportFile || !url.startsWith("file:") || supportModules.has(url))
|
|
45
|
+
return;
|
|
46
|
+
supportModules.add(url);
|
|
47
|
+
try {
|
|
48
|
+
if (!projectFile(fileURLToPath(url)))
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
appendFileSync(supportFile, `${JSON.stringify({ loaded: url })}\n`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Not recorded: the file counts for every test, as before.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
30
61
|
export function initialize(data) {
|
|
31
62
|
typescriptRequireHook = data?.typescriptRequireHook === true;
|
|
63
|
+
supportFile = typeof data?.supportFile === "string" ? data.supportFile : undefined;
|
|
32
64
|
}
|
|
33
65
|
const packageTypes = new Map();
|
|
34
66
|
function packageType(directory) {
|
|
@@ -144,6 +176,7 @@ function mapToCopy(source, filename) {
|
|
|
144
176
|
}
|
|
145
177
|
export async function load(url, context, nextLoad) {
|
|
146
178
|
const result = await nextLoad(url, context);
|
|
179
|
+
noteLoaded(url);
|
|
147
180
|
if (!NODE_COVERAGE_FILTERS || result.source == null || !url.startsWith("file:"))
|
|
148
181
|
return result;
|
|
149
182
|
const text = typeof result.source === "string" ? result.source : Buffer.from(result.source).toString("utf8");
|
|
@@ -451,6 +451,12 @@ function persistBrowser() {
|
|
|
451
451
|
persistBrowserListenersInstalled = true;
|
|
452
452
|
try {
|
|
453
453
|
addEventListener("beforeunload", handOverBrowser);
|
|
454
|
+
// An iframe the page removes (a client-side navigation away from the
|
|
455
|
+
// page that held it, an embed closed) gets no `beforeunload`: what it ran
|
|
456
|
+
// went with it, though the test clicked in it. Its `pagehide` reaches the
|
|
457
|
+
// collector, unlike a top-level document's.
|
|
458
|
+
if (typeof window !== "undefined" && window !== window.top)
|
|
459
|
+
addEventListener("pagehide", handOverBrowser);
|
|
454
460
|
addEventListener("pagehide", persistBrowserNow);
|
|
455
461
|
addEventListener("visibilitychange", () => {
|
|
456
462
|
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
@@ -1425,6 +1431,55 @@ emitterContextMaps.set(runtimeInstance, emitterCoverageContexts);
|
|
|
1425
1431
|
if (runtimeGlobal.__SUPERCOV_EMITTER_PATCHED_INSTANCES__ == null)
|
|
1426
1432
|
runtimeGlobal.__SUPERCOV_EMITTER_PATCHED_INSTANCES__ = new Set();
|
|
1427
1433
|
const patchedEmitterInstances = runtimeGlobal.__SUPERCOV_EMITTER_PATCHED_INSTANCES__;
|
|
1434
|
+
// Which tests this process served, and when it served a request that named
|
|
1435
|
+
// none, in the file `register.mjs` named for it: the project files it loaded
|
|
1436
|
+
// and read are reached by those tests, and a change to one is a change for
|
|
1437
|
+
// them alone (`tests affected`, `merge`). Its CommonJS modules are noted
|
|
1438
|
+
// here, once each, as requests come; the loader notes its ES modules.
|
|
1439
|
+
var supportNoted = { served: new Set(), modules: new Set(), lastUnscoped: 0 };
|
|
1440
|
+
function noteSupport(scope) {
|
|
1441
|
+
const file = typeof process !== "undefined" && process.env ? process.env.__SUPERCOV_SUPPORT_FILE : void 0;
|
|
1442
|
+
if (!file)
|
|
1443
|
+
return;
|
|
1444
|
+
const fs = getFs();
|
|
1445
|
+
if (!fs)
|
|
1446
|
+
return;
|
|
1447
|
+
const lines = [];
|
|
1448
|
+
if (scope) {
|
|
1449
|
+
if (!supportNoted.served.has(scope.testId)) {
|
|
1450
|
+
supportNoted.served.add(scope.testId);
|
|
1451
|
+
lines.push({ served: scope.testId });
|
|
1452
|
+
}
|
|
1453
|
+
} else {
|
|
1454
|
+
const now = Date.now();
|
|
1455
|
+
if (now - supportNoted.lastUnscoped >= 5) {
|
|
1456
|
+
supportNoted.lastUnscoped = now;
|
|
1457
|
+
lines.push({ unscoped: now });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
const moduleBuiltin = typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("node:module") : void 0;
|
|
1462
|
+
const cache = moduleBuiltin && moduleBuiltin._cache;
|
|
1463
|
+
const root = process.env.SUPERCOV_PROJECT_ROOT;
|
|
1464
|
+
if (cache && root) {
|
|
1465
|
+
for (const name of Object.keys(cache)) {
|
|
1466
|
+
// The copy lives under `.supercov/`: the project root decides.
|
|
1467
|
+
const inside = name.indexOf(root) === 0 ? name.slice(root.length).replace(/^[\\/]/, "") : "";
|
|
1468
|
+
if (supportNoted.modules.has(name) || inside === "" || /^\.supercov[\\/]/.test(inside) || /(?:^|[\\/])node_modules[\\/]/.test(inside))
|
|
1469
|
+
continue;
|
|
1470
|
+
supportNoted.modules.add(name);
|
|
1471
|
+
lines.push({ loaded: name });
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
} catch (e) {
|
|
1475
|
+
}
|
|
1476
|
+
if (lines.length === 0)
|
|
1477
|
+
return;
|
|
1478
|
+
try {
|
|
1479
|
+
fs.appendFileSync(file, lines.map((line) => JSON.stringify(line)).join("\n") + "\n");
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1428
1483
|
function installNodeRequestPropagation() {
|
|
1429
1484
|
if (isBrowser || patchedEmitterInstances.has(runtimeInstance) || typeof process === "undefined") return;
|
|
1430
1485
|
const builtin = (name) => typeof process.getBuiltinModule === "function" ? process.getBuiltinModule(name) : void 0;
|
|
@@ -1443,6 +1498,7 @@ function installNodeRequestPropagation() {
|
|
|
1443
1498
|
// HTTP headers establish ownership even when Express/SDK listeners and
|
|
1444
1499
|
// the server process were created by a shared before hook.
|
|
1445
1500
|
const incoming = requestCoverageContext(args[0]);
|
|
1501
|
+
noteSupport(incoming && incoming.scope);
|
|
1446
1502
|
if (incoming) context = incoming.scope ? incoming : context != null ? context : currentRequestContext();
|
|
1447
1503
|
if (incoming && incoming.scope)
|
|
1448
1504
|
noteServedAttempt(incoming.scope);
|