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.
@@ -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.10.1",
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.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"
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
- mkdirSync(dirname(path), { recursive: true });
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
- mkdirSync(dirname(path), { recursive: true });
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
- mkdirSync(dirname(path), { recursive: true });
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
- mkdirSync(dirname(destination), { recursive: true });
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
- const moved = (value) => typeof value === "string" && settings.root !== root
23
- ? value.split(settings.root).join(root)
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 !== 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 = 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: root });
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
- mkdirSync(dirname(path), { recursive: true });
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
- // `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
- });
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.mkdirSync(buffered.directory, { recursive: true });
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.mkdirSync(directory, { recursive: true });
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.mkdirSync(entry.directory, { recursive: true });
769
+ makeEvidenceDirectory(fs, entry.directory);
754
770
  state.createdEvidenceDirectories.add(entry.directory);
755
771
  }
756
772
  fs.appendFileSync(path, entry.lines.join(""));