supercov 4.9.0 → 4.10.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/supported-suites.md +13 -1
- package/docs/workspace-isolation.md +10 -0
- package/package.json +10 -10
- package/runtime/javascript/playwright.mjs +86 -18
- package/runtime/javascript/runtime.mjs +34 -25
package/docs/supported-suites.md
CHANGED
|
@@ -429,13 +429,25 @@ server is built from that copy. Go has no per-request context Supercov can
|
|
|
429
429
|
carry a test's name in to every probe, so the server sweeps what ran every
|
|
430
430
|
5 ms, and each sweep is credited to the browser test that was running then,
|
|
431
431
|
if only one was, and said to be. Work done while two tests ran at once, as
|
|
432
|
-
with parallel workers, is the run's and no test's
|
|
432
|
+
with parallel workers, is the run's and no test's; the run says how many tests
|
|
433
|
+
that left without their server work, and `--workers=1` gives each test its
|
|
434
|
+
own. The server's coverage is a
|
|
433
435
|
Go run of its own, published as the run or beside the browser run, as for a
|
|
434
436
|
[Python server](#a-python-server-under-browser-tests).
|
|
435
437
|
|
|
436
438
|
A server binary built before the run is not the one Supercov instruments:
|
|
437
439
|
start it with `go run`, or build it in the test command.
|
|
438
440
|
|
|
441
|
+
### A Java or Kotlin server under browser tests
|
|
442
|
+
|
|
443
|
+
A Playwright suite whose server is built by its own command (`./gradlew
|
|
444
|
+
bootRun`, `mvn spring-boot:run`, or `javac` and `java`) measures it the way it
|
|
445
|
+
measures a [Go server](#a-go-server-under-browser-tests): the copy's Java and
|
|
446
|
+
Kotlin are rewritten with probes, the runtime goes in each module's main
|
|
447
|
+
source set, and what the server ran is swept every 5 ms and credited by time
|
|
448
|
+
to the browser test running then, if only one was. A jar built before the run
|
|
449
|
+
is not the one Supercov instruments.
|
|
450
|
+
|
|
439
451
|
```sh
|
|
440
452
|
npx supercov -- npx playwright test
|
|
441
453
|
```
|
|
@@ -85,6 +85,16 @@ When a run fails in a project that has a `build` script the command does not
|
|
|
85
85
|
reach, it says this. `SUPERCOV_KEEP_WORKSPACE=1` leaves the instrumented copy
|
|
86
86
|
in `.supercov/workspaces/` after a run, to inspect.
|
|
87
87
|
|
|
88
|
+
A package inside a larger git repository (`web/` beside `cmd/`, or
|
|
89
|
+
`packages/app` in a monorepo) is copied alone, but the copy sits at the
|
|
90
|
+
package's path in the repository, with the repository's other files and
|
|
91
|
+
directories linked beside it from your checkout. A suite that reaches outside
|
|
92
|
+
the package, a `webServer` running `go run ../cmd/testserver` or a tsconfig
|
|
93
|
+
extending `../../tsconfig.base.json`, finds them where it finds them without
|
|
94
|
+
Supercov. They are your checkout's own files, not copies, and they are not
|
|
95
|
+
measured; the repository's `.git` and lockfiles are never linked. To measure code outside the
|
|
96
|
+
package too, run Supercov from the repository root.
|
|
97
|
+
|
|
88
98
|
## Files Supercov creates
|
|
89
99
|
|
|
90
100
|
| Location | What it is for |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.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/background-by-time.mjs && node scripts/python-server-playwright.mjs && node scripts/ruby-server-playwright.mjs && node scripts/go-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/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/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.10.1",
|
|
124
|
+
"@supercov/cli-darwin-x64": "4.10.1",
|
|
125
|
+
"@supercov/cli-linux-arm64-gnu": "4.10.1",
|
|
126
|
+
"@supercov/cli-linux-arm64-musl": "4.10.1",
|
|
127
|
+
"@supercov/cli-linux-x64-gnu": "4.10.1",
|
|
128
|
+
"@supercov/cli-linux-x64-musl": "4.10.1",
|
|
129
|
+
"@supercov/cli-win32-arm64": "4.10.1",
|
|
130
|
+
"@supercov/cli-win32-x64": "4.10.1"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"@playwright/test": ">=1.55.0",
|
|
@@ -216,6 +216,9 @@ class CoveragePhaseController {
|
|
|
216
216
|
// fixture override tears down before the collector's own fixture, and a
|
|
217
217
|
// closed page cannot be evaluated, so the evidence is taken on the way out.
|
|
218
218
|
earlySnapshots = [];
|
|
219
|
+
// Evidence documents gave as they left (`__supercovLeaving`), merged with
|
|
220
|
+
// the rest when there is any.
|
|
221
|
+
leftSnapshots = [];
|
|
219
222
|
snapshottedPages = new WeakSet();
|
|
220
223
|
snapshottedWorkers = new WeakSet();
|
|
221
224
|
// Workers that did not answer an evaluation in time. They are not waited
|
|
@@ -279,8 +282,10 @@ class CoveragePhaseController {
|
|
|
279
282
|
if (snapshot)
|
|
280
283
|
snapshots.push(snapshot);
|
|
281
284
|
}
|
|
282
|
-
this.runtimeSnapshots =
|
|
283
|
-
|
|
285
|
+
this.runtimeSnapshots = this.leftSnapshots.length > 0
|
|
286
|
+
? [mergeSnapshots([...this.leftSnapshots, ...snapshots])]
|
|
287
|
+
: snapshots;
|
|
288
|
+
return this.runtimeSnapshots;
|
|
284
289
|
}
|
|
285
290
|
/**
|
|
286
291
|
* A worker's evidence, or nothing when it cannot be read. A worker that
|
|
@@ -291,20 +296,15 @@ class CoveragePhaseController {
|
|
|
291
296
|
if (this.workersWithoutRuntime.has(worker)) {
|
|
292
297
|
// Asked once more, without waiting: it may have loaded an
|
|
293
298
|
// instrumented script since it was registered.
|
|
294
|
-
const answer = await bounded(worker
|
|
295
|
-
.evaluate(() => globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__?.())
|
|
296
|
-
.catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
|
|
299
|
+
const answer = await bounded(readSnapshot(worker).catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
|
|
297
300
|
if (answer && answer !== WORKER_TIMED_OUT) {
|
|
298
301
|
this.snapshottedWorkers.add(worker);
|
|
299
302
|
return answer;
|
|
300
303
|
}
|
|
301
304
|
return undefined;
|
|
302
305
|
}
|
|
303
|
-
const answer = await bounded(worker
|
|
304
|
-
.
|
|
305
|
-
const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
306
|
-
return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
|
|
307
|
-
})
|
|
306
|
+
const answer = await bounded(readSnapshot(worker)
|
|
307
|
+
.then((snapshot) => snapshot ?? { decisions: [], hits: [], events: [] })
|
|
308
308
|
.catch(() => undefined), WORKER_SNAPSHOT_WAIT_MS);
|
|
309
309
|
if (answer === WORKER_TIMED_OUT) {
|
|
310
310
|
this.unresponsiveWorkers.add(worker);
|
|
@@ -335,11 +335,11 @@ class CoveragePhaseController {
|
|
|
335
335
|
.evaluate(({ token, url }) => {
|
|
336
336
|
const entries = globalThis.__SUPERCOV_WORKER_EVIDENCE__?.() ?? [];
|
|
337
337
|
const byToken = token ? entries.find((entry) => entry.token === token) : undefined;
|
|
338
|
-
if (byToken)
|
|
339
|
-
return byToken;
|
|
340
338
|
const byUrl = entries.filter((entry) => entry.url === url);
|
|
341
|
-
|
|
339
|
+
const found = byToken ?? (byUrl.length === 1 ? byUrl[0] : undefined);
|
|
340
|
+
return found ? JSON.stringify(found) : undefined;
|
|
342
341
|
}, { token, url })
|
|
342
|
+
.then((text) => (typeof text === "string" ? JSON.parse(text) : undefined))
|
|
343
343
|
.catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
|
|
344
344
|
if (pushed && pushed !== WORKER_TIMED_OUT)
|
|
345
345
|
return pushed;
|
|
@@ -349,11 +349,8 @@ class CoveragePhaseController {
|
|
|
349
349
|
async snapshotPage(page) {
|
|
350
350
|
const snapshots = [];
|
|
351
351
|
for (const frame of page.frames()) {
|
|
352
|
-
snapshots.push(await frame
|
|
353
|
-
.
|
|
354
|
-
const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
355
|
-
return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
|
|
356
|
-
})
|
|
352
|
+
snapshots.push(await readSnapshot(frame)
|
|
353
|
+
.then((snapshot) => snapshot ?? { decisions: [], hits: [], events: [] })
|
|
357
354
|
.catch(() => ({
|
|
358
355
|
decisions: [],
|
|
359
356
|
hits: [],
|
|
@@ -432,6 +429,20 @@ class CoveragePhaseController {
|
|
|
432
429
|
// learned its phase only from the next action, after it had
|
|
433
430
|
// loaded, and everything it ran while loading was placed by time.
|
|
434
431
|
await cdp.send("Page.enable").catch(() => undefined);
|
|
432
|
+
// A document that leaves hands its evidence over here: the
|
|
433
|
+
// next one reads what it stored only on the same origin.
|
|
434
|
+
cdp.on("Runtime.bindingCalled", ({ name, payload }) => {
|
|
435
|
+
if (name !== LEAVING_BINDING || this.runtimeSnapshots)
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
this.leftSnapshots.push(JSON.parse(payload));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Not evidence.
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
await cdp.send("Runtime.enable").catch(() => undefined);
|
|
445
|
+
await cdp.send("Runtime.addBinding", { name: LEAVING_BINDING }).catch(() => undefined);
|
|
435
446
|
this.cdpSessions.set(page, cdp);
|
|
436
447
|
}
|
|
437
448
|
const phaseId = this.requestPhaseId();
|
|
@@ -883,6 +894,63 @@ class CoveragePhaseController {
|
|
|
883
894
|
await this.scriptUpdate.catch(() => undefined);
|
|
884
895
|
}
|
|
885
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* A page's or worker's evidence. It holds an event for every hit of every
|
|
899
|
+
* phase, tens of thousands of objects on a large frontend, and Playwright
|
|
900
|
+
* returns an object value by value: reading it took half a second a page at
|
|
901
|
+
* the end of every test (20,000 events: 470 ms, against 34 ms as one JSON
|
|
902
|
+
* string), and doubled a 76-test suite's time.
|
|
903
|
+
*/
|
|
904
|
+
const LEAVING_BINDING = "__supercovLeaving";
|
|
905
|
+
/**
|
|
906
|
+
* One snapshot holding everything in `snapshots`, each observation once: a
|
|
907
|
+
* document that left gave its evidence, and the next one on its origin
|
|
908
|
+
* restored the same evidence and added to it.
|
|
909
|
+
*/
|
|
910
|
+
function mergeSnapshots(snapshots) {
|
|
911
|
+
const hits = new Set();
|
|
912
|
+
const events = new Map();
|
|
913
|
+
const decisions = new Map();
|
|
914
|
+
const logicals = new Map();
|
|
915
|
+
const vectorsOf = (map, key, initial) => {
|
|
916
|
+
let entry = map.get(key);
|
|
917
|
+
if (!entry) {
|
|
918
|
+
entry = { ...initial, seen: new Map() };
|
|
919
|
+
map.set(key, entry);
|
|
920
|
+
}
|
|
921
|
+
return entry;
|
|
922
|
+
};
|
|
923
|
+
for (const snapshot of snapshots) {
|
|
924
|
+
for (const id of snapshot?.hits ?? [])
|
|
925
|
+
hits.add(id);
|
|
926
|
+
for (const event of snapshot?.events ?? [])
|
|
927
|
+
events.set(JSON.stringify(event), event);
|
|
928
|
+
for (const decision of snapshot?.decisions ?? []) {
|
|
929
|
+
const entry = vectorsOf(decisions, decision.meta?.id, { meta: decision.meta });
|
|
930
|
+
for (const vector of decision.vectors ?? [])
|
|
931
|
+
entry.seen.set(JSON.stringify(vector), vector);
|
|
932
|
+
}
|
|
933
|
+
for (const logical of snapshot?.logicals ?? []) {
|
|
934
|
+
const entry = vectorsOf(logicals, logical.id, { id: logical.id });
|
|
935
|
+
for (const vector of logical.vectors ?? [])
|
|
936
|
+
entry.seen.set(JSON.stringify(vector), vector);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
decisions: [...decisions.values()].map(({ seen, meta }) => ({ meta, vectors: [...seen.values()] })),
|
|
941
|
+
hits: [...hits],
|
|
942
|
+
events: [...events.values()],
|
|
943
|
+
logicals: [...logicals.values()].map(({ seen, id }) => ({ id, vectors: [...seen.values()] })),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function readSnapshot(target) {
|
|
947
|
+
return target
|
|
948
|
+
.evaluate(() => {
|
|
949
|
+
const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
950
|
+
return getSnapshot ? JSON.stringify(getSnapshot()) : undefined;
|
|
951
|
+
})
|
|
952
|
+
.then((text) => (typeof text === "string" ? JSON.parse(text) : undefined));
|
|
953
|
+
}
|
|
886
954
|
let activeController;
|
|
887
955
|
let bridgedAssertionDepth = 0;
|
|
888
956
|
const controllers = new Map();
|
|
@@ -25,9 +25,6 @@ var __spreadProps = (a, b) => __spreadValues(a, b);
|
|
|
25
25
|
// `globalThis.Buffer = undefined` to check it copes -- and the runtime runs
|
|
26
26
|
// inside those tests; it must not fail where the application does not.
|
|
27
27
|
var capturedBuffer = typeof Buffer === "undefined" ? void 0 : Buffer;
|
|
28
|
-
// Likewise the timer: a test that installs fake timers would otherwise hold
|
|
29
|
-
// the browser's evidence back until it advanced its clock.
|
|
30
|
-
var capturedSetTimeout = setTimeout;
|
|
31
28
|
|
|
32
29
|
// dist/transport.js
|
|
33
30
|
var COVERAGE_SCOPE_HEADER = "x-supercov-scope";
|
|
@@ -419,7 +416,6 @@ function pushWorkerEvidence() {
|
|
|
419
416
|
}
|
|
420
417
|
runtimeGlobal.__SUPERCOV_WORKER_EVIDENCE__ = () => Array.from(received, ([token, pushed]) => ({ token, url: pushed.url, current: pushed.current, snapshot: pushed.snapshot }));
|
|
421
418
|
})();
|
|
422
|
-
var persistBrowserScheduled = false;
|
|
423
419
|
var persistBrowserListenersInstalled = false;
|
|
424
420
|
function persistBrowserNow() {
|
|
425
421
|
if (!isBrowser)
|
|
@@ -429,31 +425,44 @@ function persistBrowserNow() {
|
|
|
429
425
|
} catch (e) {
|
|
430
426
|
}
|
|
431
427
|
}
|
|
428
|
+
// The Playwright collector's binding takes the evidence of a document that
|
|
429
|
+
// is about to leave. localStorage gives it to the next document only on the
|
|
430
|
+
// same origin: a click that sent the page to another one (an admin's
|
|
431
|
+
// masquerade link, a sign-in redirect) lost every hit it made. Called from
|
|
432
|
+
// `pagehide` the binding never reached the collector; from `beforeunload`
|
|
433
|
+
// it does.
|
|
434
|
+
function handOverBrowser() {
|
|
435
|
+
try {
|
|
436
|
+
if (typeof runtimeGlobal.__supercovLeaving === "function")
|
|
437
|
+
runtimeGlobal.__supercovLeaving(JSON.stringify(coverageSnapshot()));
|
|
438
|
+
} catch (e) {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
432
441
|
function persistBrowser() {
|
|
433
442
|
if (!isBrowser)
|
|
434
443
|
return;
|
|
435
|
-
// Persistence exists so evidence survives navigation,
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
addEventListener("pagehide", persistBrowserNow);
|
|
443
|
-
addEventListener("visibilitychange", () => {
|
|
444
|
-
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
445
|
-
persistBrowserNow();
|
|
446
|
-
});
|
|
447
|
-
} catch (e) {
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
if (persistBrowserScheduled)
|
|
444
|
+
// Persistence exists so evidence survives navigation, so it is written
|
|
445
|
+
// when the page leaves. Written after every macrotask that recorded
|
|
446
|
+
// anything, the whole snapshot was serialized again and again: a large
|
|
447
|
+
// instrumented frontend doubled a Playwright suite's time. A page the
|
|
448
|
+
// test closes is read by the collector before it closes.
|
|
449
|
+
if (persistBrowserListenersInstalled)
|
|
451
450
|
return;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
451
|
+
persistBrowserListenersInstalled = true;
|
|
452
|
+
try {
|
|
453
|
+
// `beforeunload` fires inside the assignment that starts the navigation,
|
|
454
|
+
// and the rest of that handler runs after it, before the document goes.
|
|
455
|
+
addEventListener("beforeunload", () => {
|
|
456
|
+
handOverBrowser();
|
|
457
|
+
Promise.resolve().then(handOverBrowser);
|
|
458
|
+
});
|
|
459
|
+
addEventListener("pagehide", persistBrowserNow);
|
|
460
|
+
addEventListener("visibilitychange", () => {
|
|
461
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
462
|
+
persistBrowserNow();
|
|
463
|
+
});
|
|
464
|
+
} catch (e) {
|
|
465
|
+
}
|
|
457
466
|
}
|
|
458
467
|
function attemptKey(scope) {
|
|
459
468
|
return `${scope.runId}\0${scope.workerId}\0${scope.attemptId}`;
|