supercov 4.10.1 → 4.11.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 +27 -0
- package/package.json +10 -10
- package/runtime/javascript/atomic.mjs +20 -4
- package/runtime/javascript/bootstrap.cjs +12 -5
- package/runtime/javascript/launchSupervisor.mjs +17 -1
- package/runtime/javascript/playwright.mjs +10 -0
- package/runtime/javascript/runtime.mjs +25 -9
package/docs/supported-suites.md
CHANGED
|
@@ -452,6 +452,33 @@ is not the one Supercov instruments.
|
|
|
452
452
|
npx supercov -- npx playwright test
|
|
453
453
|
```
|
|
454
454
|
|
|
455
|
+
### A server Docker Compose builds
|
|
456
|
+
|
|
457
|
+
A Playwright suite whose server `docker compose up` builds (`build:` in the
|
|
458
|
+
compose file) measures it in any of these languages: Node, Python, Ruby, Go,
|
|
459
|
+
Java or Kotlin. The image is built from Supercov's copy, so it holds the
|
|
460
|
+
copy's code. Supercov's `docker` on the suite's PATH gives each service
|
|
461
|
+
built from the copy the run's settings, and mounts the project's `.supercov`
|
|
462
|
+
into it at the same path, so the server writes its coverage where the run
|
|
463
|
+
reads it. Each request or window is credited to its test as for a server
|
|
464
|
+
started directly. Supercov builds those services before starting them, so
|
|
465
|
+
an image left over from an earlier run is never used. When the command ends
|
|
466
|
+
it stops the containers it started; it never removes them.
|
|
467
|
+
|
|
468
|
+
- Services that pull an image (`image:` with no `build:`) run code nobody
|
|
469
|
+
instrumented, and are left as they are.
|
|
470
|
+
- A Python or Ruby image may keep the code at a path of its own
|
|
471
|
+
(`COPY backend/ /srv/`): the runtime matches the image's files to the
|
|
472
|
+
copy's by their paths' common ending, and only when several of them match.
|
|
473
|
+
- A Go image needs Supercov's runtime package, `_supercov_runtime/` at the
|
|
474
|
+
module root: copy the whole module (`COPY . .`), not chosen directories.
|
|
475
|
+
- `docker run` and a `docker build` the command makes itself are not
|
|
476
|
+
covered; neither is Windows.
|
|
477
|
+
|
|
478
|
+
```sh
|
|
479
|
+
npx supercov -- npx playwright test
|
|
480
|
+
```
|
|
481
|
+
|
|
455
482
|
## Java and Kotlin
|
|
456
483
|
|
|
457
484
|
| Runner | Attribution | Current requirement |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.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/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/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.11.1",
|
|
124
|
+
"@supercov/cli-darwin-x64": "4.11.1",
|
|
125
|
+
"@supercov/cli-linux-arm64-gnu": "4.11.1",
|
|
126
|
+
"@supercov/cli-linux-arm64-musl": "4.11.1",
|
|
127
|
+
"@supercov/cli-linux-x64-gnu": "4.11.1",
|
|
128
|
+
"@supercov/cli-linux-x64-musl": "4.11.1",
|
|
129
|
+
"@supercov/cli-win32-arm64": "4.11.1",
|
|
130
|
+
"@supercov/cli-win32-x64": "4.11.1"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"@playwright/test": ">=1.55.0",
|
|
@@ -2,6 +2,22 @@ import { appendFileSync, existsSync, mkdirSync, openSync, closeSync, fsyncSync,
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
|
+
|
|
6
|
+
// In a container Supercov's `docker` launcher mounts the run's directory
|
|
7
|
+
// into, its user (root) owns what it creates there; a directory made with
|
|
8
|
+
// the usual mask would keep the run, outside, from removing what goes in it.
|
|
9
|
+
function makeDirectory(directory) {
|
|
10
|
+
if (process.env.SUPERCOV_CONTAINER !== "1")
|
|
11
|
+
return mkdirSync(directory, { recursive: true });
|
|
12
|
+
const previous = process.umask(0);
|
|
13
|
+
try {
|
|
14
|
+
return mkdirSync(directory, { recursive: true, mode: 0o777 });
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
process.umask(previous);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
function fsyncDirectory(path) {
|
|
6
22
|
try {
|
|
7
23
|
const directory = openSync(path, "r");
|
|
@@ -18,7 +34,7 @@ function fsyncDirectory(path) {
|
|
|
18
34
|
}
|
|
19
35
|
/** Write a complete sibling file and atomically replace the destination. */
|
|
20
36
|
export function atomicWriteFileSync(path, data, options) {
|
|
21
|
-
|
|
37
|
+
makeDirectory(dirname(path));
|
|
22
38
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
23
39
|
let descriptor;
|
|
24
40
|
try {
|
|
@@ -41,7 +57,7 @@ export function atomicWriteFileSync(path, data, options) {
|
|
|
41
57
|
}
|
|
42
58
|
/** Append one recoverable JSONL record and make the completed line durable. */
|
|
43
59
|
export function appendJsonLineDurableSync(path, data) {
|
|
44
|
-
|
|
60
|
+
makeDirectory(dirname(path));
|
|
45
61
|
const existed = existsSync(path);
|
|
46
62
|
const descriptor = openSync(path, "a", 0o600);
|
|
47
63
|
try {
|
|
@@ -56,12 +72,12 @@ export function appendJsonLineDurableSync(path, data) {
|
|
|
56
72
|
}
|
|
57
73
|
/** Append a complete local record; process exit closes it before publication. */
|
|
58
74
|
export function appendJsonLineSync(path, data) {
|
|
59
|
-
|
|
75
|
+
makeDirectory(dirname(path));
|
|
60
76
|
appendFileSync(path, data.endsWith("\n") ? data : `${data}\n`, { mode: 0o600 });
|
|
61
77
|
}
|
|
62
78
|
/** Atomically publish a fully prepared file or directory and persist its entry. */
|
|
63
79
|
export function atomicRenameSync(source, destination) {
|
|
64
|
-
|
|
80
|
+
makeDirectory(dirname(destination));
|
|
65
81
|
renameSync(source, destination);
|
|
66
82
|
fsyncDirectory(dirname(destination));
|
|
67
83
|
}
|
|
@@ -19,21 +19,28 @@ else {
|
|
|
19
19
|
// <workspace>/.supercov/node_modules/bootstrap.cjs
|
|
20
20
|
const root = path.resolve(__dirname, "..", "..");
|
|
21
21
|
const settings = JSON.parse(fs.readFileSync(path.join(__dirname, "run.json"), "utf8"));
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// An image built from the workspace holds a copy of it, but its
|
|
23
|
+
// evidence belongs where the run reads it: when the run's own
|
|
24
|
+
// directory is mounted here at its path (Supercov's `docker` launcher
|
|
25
|
+
// mounts it), the host's paths are kept, and a copy that goes away
|
|
26
|
+
// with its container is not written to.
|
|
27
|
+
const hostVisible = settings.root !== root && fs.existsSync(path.join(settings.root, ".supercov"));
|
|
28
|
+
const here = hostVisible ? settings.root : root;
|
|
29
|
+
const moved = (value) => typeof value === "string" && settings.root !== here
|
|
30
|
+
? value.split(settings.root).join(here)
|
|
24
31
|
: value;
|
|
25
32
|
// Settings carried over from the host name the host's paths, which do
|
|
26
33
|
// not exist here; replace them all rather than keep a mixture.
|
|
27
|
-
const foreign = process.env.SUPERCOV_PROJECT_ROOT !==
|
|
34
|
+
const foreign = process.env.SUPERCOV_PROJECT_ROOT !== here;
|
|
28
35
|
for (const [name, value] of Object.entries(settings.environment)) {
|
|
29
36
|
if (foreign || process.env[name] === undefined)
|
|
30
37
|
process.env[name] = moved(value);
|
|
31
38
|
}
|
|
32
|
-
process.env.SUPERCOV_PROJECT_ROOT =
|
|
39
|
+
process.env.SUPERCOV_PROJECT_ROOT = here;
|
|
33
40
|
process.env.SUPERCOV_EXECUTION_LOG_SHARD ??= `bootstrap-${process.pid}-${Date.now()}`;
|
|
34
41
|
require("./runtime.mjs");
|
|
35
42
|
// Counted in the run's summary: coverage from here belongs to no test.
|
|
36
|
-
require("./launchSupervisor.mjs").recordExecution({ event: "guest-process", guestRoot:
|
|
43
|
+
require("./launchSupervisor.mjs").recordExecution({ event: "guest-process", guestRoot: here });
|
|
37
44
|
}
|
|
38
45
|
catch {
|
|
39
46
|
// Without its settings or its runtime nothing can be recorded here,
|
|
@@ -4,6 +4,22 @@ import { appendFileSync, mkdirSync, statSync } from "node:fs";
|
|
|
4
4
|
import Module, { syncBuiltinESMExports } from "node:module";
|
|
5
5
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// In a container Supercov's `docker` launcher mounts the run's directory
|
|
9
|
+
// into, its user (root) owns what it creates there; a directory made with
|
|
10
|
+
// the usual mask would keep the run, outside, from removing what goes in it.
|
|
11
|
+
function makeDirectory(directory) {
|
|
12
|
+
if (process.env.SUPERCOV_CONTAINER !== "1")
|
|
13
|
+
return mkdirSync(directory, { recursive: true });
|
|
14
|
+
const previous = process.umask(0);
|
|
15
|
+
try {
|
|
16
|
+
return mkdirSync(directory, { recursive: true, mode: 0o777 });
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
process.umask(previous);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
const HOST_PATH_KEYS = ["hostPath", "source", "src", "localPath"];
|
|
8
24
|
const GUEST_PATH_KEYS = [
|
|
9
25
|
"guestPath",
|
|
@@ -83,7 +99,7 @@ function record(value) {
|
|
|
83
99
|
ppid: process.ppid,
|
|
84
100
|
...value,
|
|
85
101
|
})}\n`;
|
|
86
|
-
|
|
102
|
+
makeDirectory(dirname(path));
|
|
87
103
|
appendFileSync(path, line);
|
|
88
104
|
executionLog = { token: executionLog.token, path, size: executionLog.size + Buffer.byteLength(line) };
|
|
89
105
|
}
|
|
@@ -719,6 +719,16 @@ class CoveragePhaseController {
|
|
|
719
719
|
ArrayBuffer.isView(result))
|
|
720
720
|
return result;
|
|
721
721
|
const candidate = result;
|
|
722
|
+
// A native dialog is Playwright's own object, returned as it is. Its
|
|
723
|
+
// `type()` was taken for a locator's action: it returned a promise
|
|
724
|
+
// where Playwright returns a string, and started a phase whose
|
|
725
|
+
// activation waits on the page, which runs nothing while the dialog
|
|
726
|
+
// is open, so the dialog could never be answered and the page never
|
|
727
|
+
// closed. Nothing a dialog's methods do is the application's work.
|
|
728
|
+
if (typeof candidate["accept"] === "function" &&
|
|
729
|
+
typeof candidate["dismiss"] === "function" &&
|
|
730
|
+
typeof candidate["defaultValue"] === "function")
|
|
731
|
+
return result;
|
|
722
732
|
if (typeof candidate["contexts"] === "function" &&
|
|
723
733
|
typeof candidate["newContext"] === "function")
|
|
724
734
|
trackBrowser(result);
|
|
@@ -450,12 +450,7 @@ function persistBrowser() {
|
|
|
450
450
|
return;
|
|
451
451
|
persistBrowserListenersInstalled = true;
|
|
452
452
|
try {
|
|
453
|
-
|
|
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
|
-
});
|
|
453
|
+
addEventListener("beforeunload", handOverBrowser);
|
|
459
454
|
addEventListener("pagehide", persistBrowserNow);
|
|
460
455
|
addEventListener("visibilitychange", () => {
|
|
461
456
|
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
@@ -526,7 +521,7 @@ function flushBufferedServerEvidence(scope) {
|
|
|
526
521
|
if (!fs || buffered.records.size === 0)
|
|
527
522
|
return void 0;
|
|
528
523
|
try {
|
|
529
|
-
fs
|
|
524
|
+
makeEvidenceDirectory(fs, buffered.directory);
|
|
530
525
|
fs.appendFileSync(buffered.path, [...buffered.records.values()].map((record) => JSON.stringify(record)).join("\n") + "\n");
|
|
531
526
|
return buffered.path;
|
|
532
527
|
} catch (e) {
|
|
@@ -616,11 +611,32 @@ function flushBackgroundAppends(fs) {
|
|
|
616
611
|
}
|
|
617
612
|
}
|
|
618
613
|
}
|
|
614
|
+
// In a container Supercov's `docker` launcher mounts the run's directory
|
|
615
|
+
// into, its user (root) owns what it creates there, and the run, outside,
|
|
616
|
+
// could not remove what went in it. There each directory made is opened to
|
|
617
|
+
// all.
|
|
618
|
+
function makeEvidenceDirectory(fs, directory) {
|
|
619
|
+
const first = fs.mkdirSync(directory, { recursive: true });
|
|
620
|
+
if (!first || typeof process === "undefined" || !process.env || process.env.SUPERCOV_CONTAINER !== "1")
|
|
621
|
+
return;
|
|
622
|
+
let current = first;
|
|
623
|
+
const opened = [current];
|
|
624
|
+
for (const part of directory.slice(first.length).split(/[\\/]/).filter(Boolean)) {
|
|
625
|
+
current = `${current}/${part}`;
|
|
626
|
+
opened.push(current);
|
|
627
|
+
}
|
|
628
|
+
for (const made of opened) {
|
|
629
|
+
try {
|
|
630
|
+
fs.chmodSync(made, 0o777);
|
|
631
|
+
} catch (e) {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
619
635
|
function writeBackgroundLines(fs, runId, payload) {
|
|
620
636
|
var _a8;
|
|
621
637
|
const directory = backgroundEvidenceDirectory(runId);
|
|
622
638
|
if (!state.createdEvidenceDirectories.has(directory)) {
|
|
623
|
-
fs
|
|
639
|
+
makeEvidenceDirectory(fs, directory);
|
|
624
640
|
state.createdEvidenceDirectories.add(directory);
|
|
625
641
|
}
|
|
626
642
|
let path = state.backgroundWriters.get(runId);
|
|
@@ -750,7 +766,7 @@ function flushServerAppends() {
|
|
|
750
766
|
if (!fs)
|
|
751
767
|
throw new Error("node:fs is unavailable");
|
|
752
768
|
if (!state.createdEvidenceDirectories.has(entry.directory)) {
|
|
753
|
-
fs
|
|
769
|
+
makeEvidenceDirectory(fs, entry.directory);
|
|
754
770
|
state.createdEvidenceDirectories.add(entry.directory);
|
|
755
771
|
}
|
|
756
772
|
fs.appendFileSync(path, entry.lines.join(""));
|