supercov 4.14.0 → 4.15.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/cli.md +10 -6
- package/package.json +10 -10
- package/runtime/javascript/playwright.mjs +25 -2
- package/runtime/javascript/runtime.mjs +45 -20
package/docs/cli.md
CHANGED
|
@@ -210,8 +210,9 @@ locations from a file, one per line, skipping blank lines and `#` comments.
|
|
|
210
210
|
Each line of several lists its state, its gaps and its first three covering
|
|
211
211
|
tests, with `totalTests` and `totalPhases` for the rest; asked alone, a line
|
|
212
212
|
lists every test and phase. In `--json` the reply is `coverage.lines`, one
|
|
213
|
-
entry per location in order
|
|
214
|
-
`remaining`,
|
|
213
|
+
entry per location in order, each location once. Locations that do not fit one
|
|
214
|
+
reply are listed in `remaining`, and `pagination.nextOffset` says where the next
|
|
215
|
+
reply starts: ask again with `--offset`, as for any page.
|
|
215
216
|
|
|
216
217
|
```sh supercov
|
|
217
218
|
npx supercov runs latest line app/routes/checkout.ts:42 app/routes/checkout.ts:57
|
|
@@ -458,7 +459,7 @@ one as a public artifact.
|
|
|
458
459
|
| `--filter all \| passed \| failed` | Recalculate the view from all, successful, or failed attempts |
|
|
459
460
|
| `--kind <kind>` | Restrict to a test level such as `unit`, `integration`, or `e2e` |
|
|
460
461
|
| `--runner <runner>` | Restrict to one runner |
|
|
461
|
-
| `--tag <tag>` | Restrict to tests with this tag; repeat it for tests with any of several |
|
|
462
|
+
| `--tag <tag>` | Restrict to tests with this tag; repeat it for tests with any of several, and start a value with `!` to leave those tests out |
|
|
462
463
|
| `--title <text>` | Restrict to tests whose name contains this text |
|
|
463
464
|
| `--test-file <path>` | Restrict to tests in this file, or in this directory |
|
|
464
465
|
| `--metric all \| lines \| statements \| functions \| branches \| mcdc` | Choose a metric for `files`, `gaps`, `diff`, or `minimize` |
|
|
@@ -473,12 +474,14 @@ Collection output includes a copyable command for the next page.
|
|
|
473
474
|
pick, from a run already measured: the API tests of a Playwright suite, the
|
|
474
475
|
tests of one spec file, everything named `checkout`. Each takes several values
|
|
475
476
|
for tests matching any of them, and when several are given a test must match
|
|
476
|
-
each.
|
|
477
|
+
each. A value starting with `!` leaves those tests out, so the browser tests of
|
|
478
|
+
a suite whose other tests are tagged read as the tests with none of the tags.
|
|
477
479
|
|
|
478
480
|
```sh supercov
|
|
479
481
|
npx supercov runs latest --tag @api
|
|
480
482
|
npx supercov runs latest gaps --test-file tests/e2e/checkout.spec.ts
|
|
481
483
|
npx supercov runs latest files --title checkout --tag smoke
|
|
484
|
+
npx supercov runs latest --tag '!api' --tag '!cron' --tag '!webhook'
|
|
482
485
|
```
|
|
483
486
|
|
|
484
487
|
A tag is what the runner gives a test: a Playwright tag (`{ tag: '@api' }`, or
|
|
@@ -488,8 +491,9 @@ JUnit `@Tag` or a TestNG group. Tags are written with or without the `@` and in
|
|
|
488
491
|
any case. `runs <id> test <name>` lists a test's tags.
|
|
489
492
|
|
|
490
493
|
The first query with a filter works out its coverage from the run's evidence
|
|
491
|
-
and keeps it beside the run (
|
|
492
|
-
|
|
494
|
+
and keeps it beside the run (12 s for a run of about 3,000 tests); asking again
|
|
495
|
+
reads it back in a fraction of a second. Nothing is added to the test run
|
|
496
|
+
itself.
|
|
493
497
|
|
|
494
498
|
For a large file, group and rank its decisions:
|
|
495
499
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.0",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"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",
|
|
83
83
|
"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",
|
|
84
84
|
"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",
|
|
85
|
-
"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/context-headers.mjs && node scripts/js-syntax-zoo.mjs && node scripts/ruby-syntax-zoo.mjs && node scripts/rust-syntax-zoo.mjs && node scripts/python-syntax-zoo.mjs && node scripts/go-syntax-zoo.mjs && node scripts/java-syntax-zoo.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",
|
|
85
|
+
"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/context-headers.mjs && node scripts/context-cookies.mjs && node scripts/hidden-request-emit.mjs && node scripts/js-syntax-zoo.mjs && node scripts/ruby-syntax-zoo.mjs && node scripts/rust-syntax-zoo.mjs && node scripts/python-syntax-zoo.mjs && node scripts/go-syntax-zoo.mjs && node scripts/java-syntax-zoo.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
86
|
"test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
|
|
87
87
|
"test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
88
88
|
"test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
|
|
@@ -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.15.0",
|
|
123
|
+
"@supercov/cli-darwin-x64": "4.15.0",
|
|
124
|
+
"@supercov/cli-linux-arm64-gnu": "4.15.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-musl": "4.15.0",
|
|
126
|
+
"@supercov/cli-linux-x64-gnu": "4.15.0",
|
|
127
|
+
"@supercov/cli-linux-x64-musl": "4.15.0",
|
|
128
|
+
"@supercov/cli-win32-arm64": "4.15.0",
|
|
129
|
+
"@supercov/cli-win32-x64": "4.15.0"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
132
132
|
"@playwright/test": ">=1.55.0",
|
|
@@ -8,11 +8,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
10
|
import childProcess from "node:child_process";
|
|
11
|
-
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import http from "node:http";
|
|
13
13
|
import https from "node:https";
|
|
14
14
|
import { syncBuiltinESMExports } from "node:module";
|
|
15
|
-
import { relative, resolve, sep } from "node:path";
|
|
15
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import * as standardPlaywright from "@playwright/test";
|
|
18
18
|
import * as coverageRuntime from "./runtime.mjs";
|
|
@@ -493,6 +493,7 @@ class CoveragePhaseController {
|
|
|
493
493
|
if (this.contexts.has(context) || this.disposed)
|
|
494
494
|
return;
|
|
495
495
|
this.contexts.add(context);
|
|
496
|
+
patchContextPrototype(context);
|
|
496
497
|
if (adopted)
|
|
497
498
|
this.adoptedContexts.add(context);
|
|
498
499
|
await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
|
|
@@ -1077,7 +1078,29 @@ function patchBrowserPrototype(browser) {
|
|
|
1077
1078
|
* a customer context opened mid-test is usually closed in a `finally`; either
|
|
1078
1079
|
* way the page is gone by the time the test's snapshots are collected.
|
|
1079
1080
|
*/
|
|
1081
|
+
/** The cookies Supercov gives a context to carry the test's scope. */
|
|
1082
|
+
const OWN_COOKIES = new Set([COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE]);
|
|
1083
|
+
const suiteCookies = (cookies) => (Array.isArray(cookies) ? cookies.filter((cookie) => !OWN_COOKIES.has(cookie?.name)) : cookies);
|
|
1080
1084
|
function patchContextPrototype(context) {
|
|
1085
|
+
// A test reads and saves the cookies it and its app set, never
|
|
1086
|
+
// Supercov's: an assertion on `cookies()` failed only under measurement,
|
|
1087
|
+
// and `storageState({ path })` wrote the scope into the suite's saved
|
|
1088
|
+
// login. A `clearCookies()` takes the scope too, and the test's next
|
|
1089
|
+
// action puts it back, as every action does.
|
|
1090
|
+
patchOnce(context, "cookies", (original) => async function (...args) {
|
|
1091
|
+
return suiteCookies(await original.apply(this, args));
|
|
1092
|
+
});
|
|
1093
|
+
patchOnce(context, "storageState", (original) => async function (options, ...rest) {
|
|
1094
|
+
const { path, ...others } = options && typeof options === "object" ? options : {};
|
|
1095
|
+
const state = await original.call(this, others, ...rest);
|
|
1096
|
+
const own = state && typeof state === "object" ? { ...state, cookies: suiteCookies(state.cookies) } : state;
|
|
1097
|
+
if (path) {
|
|
1098
|
+
// As Playwright writes it.
|
|
1099
|
+
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
1100
|
+
writeFileSync(path, JSON.stringify(own, undefined, 2));
|
|
1101
|
+
}
|
|
1102
|
+
return own;
|
|
1103
|
+
});
|
|
1081
1104
|
patchOnce(context, "close", (original) => async function (...args) {
|
|
1082
1105
|
for (const page of this.pages()) {
|
|
1083
1106
|
await controllerOwning(page)?.snapshotBeforeClose(page).catch(() => undefined);
|
|
@@ -1491,30 +1491,55 @@ function installNodeRequestPropagation() {
|
|
|
1491
1491
|
const SecureServer = https && https.Server;
|
|
1492
1492
|
if (!EventEmitter) return;
|
|
1493
1493
|
const original = EventEmitter.prototype.emit;
|
|
1494
|
+
// The test a request is for, worked out once, from whichever sees it first.
|
|
1495
|
+
const requestContexts = new WeakMap();
|
|
1496
|
+
const owningRequest = (server, request, response) => {
|
|
1497
|
+
if (requestContexts.has(request))
|
|
1498
|
+
return requestContexts.get(request);
|
|
1499
|
+
let context = emitterCoverageContexts.get(server);
|
|
1500
|
+
// HTTP headers establish ownership even when Express/SDK listeners and
|
|
1501
|
+
// the server process were created by a shared before hook.
|
|
1502
|
+
const incoming = requestCoverageContext(request);
|
|
1503
|
+
noteSupport(incoming && incoming.scope);
|
|
1504
|
+
if (incoming) context = incoming.scope ? incoming : context != null ? context : currentRequestContext();
|
|
1505
|
+
if (incoming && incoming.scope)
|
|
1506
|
+
noteServedAttempt(incoming.scope);
|
|
1507
|
+
// A request that names no test, to a server no test owns, is numbered:
|
|
1508
|
+
// the report credits its work to the test running when it came.
|
|
1509
|
+
if (incoming && serverPhaseStorage && !(context && (context.scope || context.phaseId)))
|
|
1510
|
+
context = { request: nextUnscopedRequest() };
|
|
1511
|
+
// Node emits a request's body and its response's events outside the
|
|
1512
|
+
// handler's context, so a handler that reads the body -- `req.on('end')`
|
|
1513
|
+
// -- ran as no test's. The request and response carry it with them.
|
|
1514
|
+
if (incoming && context) {
|
|
1515
|
+
for (const emitter of [request, response]) {
|
|
1516
|
+
if (emitter && typeof emitter === "object" && typeof emitter.emit === "function")
|
|
1517
|
+
emitterCoverageContexts.set(emitter, context);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (request && typeof request === "object")
|
|
1521
|
+
requestContexts.set(request, incoming ? context : void 0);
|
|
1522
|
+
return incoming ? context : void 0;
|
|
1523
|
+
};
|
|
1524
|
+
// Node publishes every request here before it emits it. An `emit` of the
|
|
1525
|
+
// server's own, defined before this patch by a preload that called the
|
|
1526
|
+
// emit it found, hid every request from the patch below, and what ran on
|
|
1527
|
+
// the request's events -- a page rendered as the response went out --
|
|
1528
|
+
// was credited to no test. Nothing hides this channel.
|
|
1529
|
+
try {
|
|
1530
|
+
const channels = builtin("node:diagnostics_channel");
|
|
1531
|
+
if (channels && typeof channels.subscribe === "function")
|
|
1532
|
+
channels.subscribe("http.server.request.start", (message) => {
|
|
1533
|
+
if (message && message.request)
|
|
1534
|
+
owningRequest(message.server, message.request, message.response);
|
|
1535
|
+
});
|
|
1536
|
+
} catch {}
|
|
1494
1537
|
EventEmitter.prototype.emit = function(event, ...args) {
|
|
1495
1538
|
let context = emitterCoverageContexts.get(this);
|
|
1496
1539
|
if ((event === "request" || event === "upgrade") &&
|
|
1497
1540
|
((Server && this instanceof Server) || (SecureServer && this instanceof SecureServer))) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
const incoming = requestCoverageContext(args[0]);
|
|
1501
|
-
noteSupport(incoming && incoming.scope);
|
|
1502
|
-
if (incoming) context = incoming.scope ? incoming : context != null ? context : currentRequestContext();
|
|
1503
|
-
if (incoming && incoming.scope)
|
|
1504
|
-
noteServedAttempt(incoming.scope);
|
|
1505
|
-
// A request that names no test, to a server no test owns, is numbered:
|
|
1506
|
-
// the report credits its work to the test running when it came.
|
|
1507
|
-
if (incoming && serverPhaseStorage && !(context && (context.scope || context.phaseId)))
|
|
1508
|
-
context = { request: nextUnscopedRequest() };
|
|
1509
|
-
// Node emits a request's body and its response's events outside the
|
|
1510
|
-
// handler's context, so a handler that reads the body -- `req.on('end')`
|
|
1511
|
-
// -- ran as no test's. The request and response carry it with them.
|
|
1512
|
-
if (incoming && context) {
|
|
1513
|
-
for (const emitter of args.slice(0, 2)) {
|
|
1514
|
-
if (emitter && typeof emitter === "object" && typeof emitter.emit === "function")
|
|
1515
|
-
emitterCoverageContexts.set(emitter, context);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1541
|
+
const owner = owningRequest(this, args[0], args[1]);
|
|
1542
|
+
if (owner) context = owner;
|
|
1518
1543
|
}
|
|
1519
1544
|
const invoke = () => Reflect.apply(original, this, [event, ...args]);
|
|
1520
1545
|
try {
|