supercov 4.17.2 → 4.19.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.
@@ -122,9 +122,12 @@ The file query explained the gap:
122
122
  ```text
123
123
  LINE STATUS SOURCE
124
124
  2 PARTIAL signedIn && !expired
125
- Unobserved: `!expired` was never false; needs `signedIn` true, `!expired` false, decision false
125
+ Unobserved: no witness pair shows `!expired` independently changing the decision result
126
126
  ```
127
127
 
128
+ Current releases name the missing value in that line: `` `!expired` was never
129
+ false; needs `signedIn` true, `!expired` false, decision false ``.
130
+
128
131
  Both return paths had run, but neither test checked an expired session. MC/DC
129
132
  checks whether each condition has been shown to affect the decision
130
133
  independently. Here, `signedIn` had; `!expired` had not.
package/docs/cli.md CHANGED
@@ -28,6 +28,7 @@ npx supercov --help
28
28
  | Open an interactive report | `npx supercov report` |
29
29
  | Find the tests a change affects | `npx supercov runs latest tests affected` |
30
30
  | Combine shards | `npx supercov merge <id> <id> [...]` |
31
+ | Add coverage measured elsewhere, by line | `npx supercov import lcov.info --test <name>` |
31
32
  | Remove stored runs | `npx supercov runs clean` |
32
33
  | Assess code quality with Jev | `npx supercov quality` |
33
34
  | See only files with findings | `npx supercov quality gaps` |
@@ -204,6 +205,11 @@ In `--json`, every file of `files` and `gaps` carries `totals` and `covered`
204
205
  for each metric beside what is missing, so a percentage can be worked out for
205
206
  any file or set of files.
206
207
 
208
+ A page holds at most what fits in one reply, so a `--limit` larger than that
209
+ returns fewer entries. Such a page says so: `pageCut` in `--json` gives
210
+ `asked`, `returned` and `nextOffset`, and stderr names the `--offset` the rest
211
+ start at.
212
+
207
213
  `line` reads the run once for every location it is given, so asking for
208
214
  twenty lines in one call costs about what one does. `--from-file` takes the
209
215
  locations from a file, one per line, skipping blank lines and `#` comments.
@@ -214,6 +220,14 @@ entry per location in order, each location once. Locations that do not fit one
214
220
  reply are listed in `remaining`, and `pagination.nextOffset` says where the next
215
221
  reply starts: ask again with `--offset`, as for any page.
216
222
 
223
+ A location naming no file of the run, or more than one, is listed under **Not
224
+ answered** (`failed` in `--json`, each with its `location`, `code` and
225
+ `message`) and the others are still answered; the call fails only when none
226
+ is. A line whose file changed since the run is answered from the working tree
227
+ with a warning naming that file; a change elsewhere, a test's included, does
228
+ not make the answer stale. A line holding no statement, branch or function
229
+ start is **NOT MEASURED** and says so.
230
+
217
231
  ```sh supercov
218
232
  npx supercov runs latest line app/routes/checkout.ts:42 app/routes/checkout.ts:57
219
233
  npx supercov runs latest line --from-file lines.txt --json
@@ -321,6 +335,11 @@ changed file that looks like product source but is absent from the run is named
321
335
  separately, because treating it as zero uncovered lines would report success for
322
336
  code nothing ran.
323
337
 
338
+ A run whose suite failed still shows the change's coverage, but `patch` exits 2
339
+ and `--json` gives `result: "error"` with the reasons: a gate over a failed
340
+ suite cannot pass. A run whose source has changed since is refused, because
341
+ its lines no longer map onto the checkout.
342
+
324
343
  `--kind`, `--runner`, `--tag`, `--title` and `--test-file` ask whether some of
325
344
  the tests reach the change: the E2E tests, or the tests of one file.
326
345
 
@@ -819,6 +838,43 @@ reusing the answers saved when its parts were assessed. `tests affected` of a
819
838
  merged run is answered from the runs it was merged from, which hold the record
820
839
  of what each test ran; a test in several of them is affected where any says so.
821
840
 
841
+ ## Add coverage measured elsewhere
842
+
843
+ Some code runs where Supercov cannot measure it: a deployed worker, a
844
+ production container, a binary another tool built. If that environment can
845
+ write an LCOV tracefile, add its lines to a run:
846
+
847
+ ```sh
848
+ npx supercov import lcov.info --test "edge in production"
849
+ npx supercov import lcov.info --background --run <run-id>
850
+ ```
851
+
852
+ The import publishes a new run made from the latest one (or the one `--run`
853
+ names), and leaves that run as it was. The lines are credited to a test of
854
+ their own, of kind `imported` with runner `lines`, or to the run's background.
855
+
856
+ This evidence is line-only. A line that ran credits every statement on it, and
857
+ a function that was entered credits the function. Branches, decisions and MC/DC
858
+ come only from Supercov's own measurement. A file the run did not measure joins
859
+ it with one statement per recorded line, and is marked as measured only by the
860
+ import.
861
+
862
+ Paths are matched to the project: an absolute path under the project, or the
863
+ one measured file a path ends with, such as `/app/src/lib.rs` for
864
+ `src/lib.rs`. Files outside the project are left out and listed. If evidence
865
+ names a line past the end of a file, it was recorded from other source, and
866
+ the import refuses it.
867
+
868
+ Supercov runs no other coverage tool. A profile another tool recorded is
869
+ exported as LCOV by that tool first. For an LLVM profile from a binary built
870
+ with `-C instrument-coverage`, with `rustup component add llvm-tools`:
871
+
872
+ ```sh
873
+ llvm-profdata merge -sparse default.profraw -o edge.profdata
874
+ llvm-cov export -format=lcov -instr-profile edge.profdata target/debug/edge > edge.info
875
+ npx supercov import edge.info --test "edge binary"
876
+ ```
877
+
822
878
  ## Clean local data
823
879
 
824
880
  ```sh supercov
@@ -144,6 +144,12 @@ signal exactly as it would unmeasured, and one with its own handler keeps
144
144
  it. `SIGKILL` cannot be caught by anything and is the one stop that loses
145
145
  whatever was still buffered.
146
146
 
147
+ A program that hands its process over to another with `exec` keeps what it
148
+ ran: a cache front end that restores a key and then `process.execve`s the
149
+ runner's Node, or a Ruby script that calls `exec`, writes its evidence before
150
+ the new program starts. Python and Rust write as they run, so an `exec`
151
+ loses nothing there either.
152
+
147
153
  ## Rust
148
154
 
149
155
  | Runner | Attribution | Current requirement |
@@ -169,11 +175,14 @@ files under a `tests`, `examples` or `benches` directory, files named
169
175
  `#[bench]`. Its assertions still mark which evidence a passing assertion
170
176
  witnessed.
171
177
 
172
- Neither is code the compiler never builds for the target: a `#[cfg(windows)]`
173
- function on Linux, or one behind `#[cfg(not(target_os = "macos"))]` on a Mac,
174
- is not counted. Only predicates the target settles (`unix`, `windows`,
175
- `target_*`) are decided; an item behind a feature or a custom cfg is counted
176
- either way.
178
+ Neither is code the compiler never builds: a `#[cfg(windows)]` function on
179
+ Linux, an item or statement behind a feature the build leaves off, or one
180
+ behind a build script's `--cfg` that is not set. The target settles `unix`,
181
+ `windows` and `target_*` before the run; every other predicate is settled by
182
+ the build itself, as each crate's probes record what `cfg!` says of its gated
183
+ code. Code no build of the run compiled is left out and the run names it
184
+ (`N item(s) behind a #[cfg] no build compiled are outside the denominator`);
185
+ code of a crate no test reached stays counted, and the run says that too.
177
186
 
178
187
  Use the repository's normal flags after the wrapped command:
179
188
 
@@ -186,6 +195,127 @@ npx supercov -- cargo nextest run --workspace
186
195
  explanation instead of silently falling back to plausible but inaccurate
187
196
  attribution.
188
197
 
198
+ ### Rust compiled to wasm in JavaScript tests
199
+
200
+ A crate wasm-bindgen exposes to JavaScript (a `cdylib` that depends on
201
+ `wasm-bindgen`) is measured when JavaScript tests load it: a Node suite
202
+ (Vitest, Jest, node:test, Mocha) through the `nodejs` or bundler glue, or a
203
+ Playwright suite whose page loads the `web` glue. Build the wasm in the test
204
+ command, as the suite already does when it tests the crate it ships
205
+ (`wasm-pack build && vitest`, or `cargo build --target wasm32-unknown-unknown`
206
+ and `wasm-bindgen`), so the module the tests load is built from Supercov's
207
+ copy:
208
+
209
+ ```sh
210
+ npx supercov -- npm test
211
+ npx supercov -- npx playwright test
212
+ ```
213
+
214
+ The JavaScript runtime remembers each wasm instance, in the test process and
215
+ in each page, and reads what the module ran at every test's end, so each test
216
+ is credited exactly with its own lines and decisions. The crate's coverage is
217
+ a Rust run beside the JavaScript run. What the copy built (`target/`, and the
218
+ generated `pkg/` with its instrumented wasm) stays in the copy.
219
+
220
+ ### A Rust Worker under browser tests
221
+
222
+ A Playwright suite that starts a Cloudflare Worker written in Rust with
223
+ `wrangler dev`, built by `worker-build`, measures the Worker:
224
+
225
+ ```sh
226
+ npx supercov -- npx playwright test
227
+ ```
228
+
229
+ The crate is rewritten with probes in the copy the suite runs in, and
230
+ `wrangler dev` builds it there as usual. The copy's wrangler configuration
231
+ (`wrangler.toml`, `wrangler.json` or `wrangler.jsonc`) points `main` at a
232
+ small entry of Supercov's that remembers the Worker's wasm instances and then
233
+ is your entry, and gives `[dev]` an inspector port unless you set one; your
234
+ own configuration and command are not changed. While the suite runs,
235
+ Supercov reads what the Worker ran through that inspector every 5 ms, and
236
+ each read is credited to the browser test that was running then, if only one
237
+ was, as for a [Go server](#a-go-server-under-browser-tests). The Worker's
238
+ coverage is a Rust run of its own, published as the run or beside the browser
239
+ run. Requests made before the first test, such as the readiness check
240
+ `webServer` waits on, are the run's and no test's.
241
+
242
+ The build runs with `--cap-lints=warn`, so a crate that denies warnings still
243
+ builds with the probes in it.
244
+
245
+ ### A Rust Worker that `cargo test` starts
246
+
247
+ Integration tests that start `wrangler dev` themselves -- to test a Worker in
248
+ Cloudflare's own runtime -- measure the Worker too:
249
+
250
+ ```sh
251
+ npx supercov -- cargo test
252
+ npx supercov -- cargo nextest run
253
+ ```
254
+
255
+ The Worker crate is instrumented with the rest of the workspace, and `wrangler
256
+ dev` builds it in the copy as usual. The tests find a `wrangler` of
257
+ Supercov's first on `PATH`: for `wrangler dev` it picks an inspector port,
258
+ starts a reader of the Worker through it, and then runs your `wrangler` with
259
+ the test's own arguments, process and signals. What the Worker ran is
260
+ credited to the test that started it, exactly, as its own coverage. The
261
+ Worker crate counts though `cargo test` never compiles it, once a test has
262
+ run it. This needs a `wrangler` on `PATH` when the run starts; without one,
263
+ nothing is put in front of it. A run whose tests start no Worker still compiles the crate under
264
+ `cargo test --workspace`; its files then say they are a Rust Worker no test
265
+ started, in `gaps` and the file view, rather than reading as code the tests
266
+ missed.
267
+
268
+ `wrangler dev --remote` runs the Worker on Cloudflare's edge, and wrangler
269
+ opens no inspector into it there, so nothing can read what it ran. A test
270
+ that starts it that way runs as usual; the Worker's files then say they ran
271
+ remotely and could not be read. Start the Worker with `wrangler dev` to
272
+ measure it.
273
+
274
+ The Worker built with probes stays in the copy: its `build/` output is never
275
+ returned to the project, and neither is the copy's configuration, which
276
+ points `main` at Supercov's entry. wrangler's `.wrangler/` state is not a
277
+ build input, so writing it does not make the run stale.
278
+
279
+ ### Rust tests compiled to wasm
280
+
281
+ `cargo test --target <wasm target>` runs each test through the target runner
282
+ Cargo is configured with, and so does Supercov, one test per process:
283
+
284
+ ```sh
285
+ npx supercov -- cargo test --target wasm32-unknown-unknown
286
+ npx supercov -- cargo test --target wasm32-wasip1
287
+ ```
288
+
289
+ - `wasm32-unknown-unknown` with `wasm-bindgen-test-runner` (what
290
+ `wasm-pack test --node` sets up) runs each test module in Node. Supercov
291
+ gives Node a hook that writes what the module ran, which lives in its
292
+ memory, to the run as the test's process ends.
293
+ - `wasm32-wasip1` with `wasmtime` or `wasmer` as the runner: Supercov opens
294
+ the test's evidence directory to the module (`--dir`) and names it in the
295
+ module's environment (`--env`), and the module writes what it runs to a
296
+ file there as it runs.
297
+
298
+ Each test is credited exactly, assertions included, as for a native test.
299
+
300
+ `wasm-pack test` works the same way. Supercov runs wasm-pack once to learn
301
+ the `cargo test` it would run (the runner, the browser driver), then runs
302
+ that command test by test:
303
+
304
+ ```sh
305
+ npx supercov -- wasm-pack test --node
306
+ npx supercov -- wasm-pack test --headless --chrome
307
+ ```
308
+
309
+ - In headless Chrome, Supercov stands between the runner and `chromedriver`,
310
+ and reads what each test's module ran from the page before the runner
311
+ closes it.
312
+ - Tests run in Firefox, Safari or Edge are run and reported, but not
313
+ measured yet; the run says so.
314
+ - `wasm-pack test` is measured on Linux and macOS. On Windows, run the
315
+ `cargo test --target wasm32-unknown-unknown` it runs.
316
+ - Deno is not supported. `wasm-bindgen-test-runner`'s Deno mode
317
+ (`WASM_BINDGEN_USE_DENO=1`) does not run on Deno 2.
318
+
189
319
  ## Python
190
320
 
191
321
  | Runner | Attribution | Current requirement |
@@ -452,28 +582,40 @@ is not the one Supercov instruments.
452
582
  npx supercov -- npx playwright test
453
583
  ```
454
584
 
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.
585
+ ### A server in a container
586
+
587
+ A Playwright suite whose server runs in a container it builds measures it in
588
+ any of these languages: Node, Python, Ruby, Go, Java or Kotlin, whichever
589
+ client starts it: `docker compose up` with `build:` in the compose file,
590
+ `docker build` and `docker run` in `webServer`, testcontainers building from
591
+ a Dockerfile in a test, or a Docker SDK. The suite's `DOCKER_HOST` points at
592
+ a relay of Supercov's that passes every request to your Docker daemon and
593
+ gives each container whose image holds the copy's code the run's settings,
594
+ with the project's `.supercov` mounted at the same path, so the server
595
+ writes its coverage where the run reads it. Each request or window is
596
+ credited to its test as for a server started directly. When the command
597
+ ends, the run stops the containers it measured and the rest of their compose
598
+ projects; it never removes them.
599
+
600
+ An image holds the copy's code when it was built during the run, when an
601
+ earlier run recorded it as built from the same source (a build that finds
602
+ every layer cached returns it unchanged), or when it is a compose service
603
+ whose build context is in the copy: Supercov builds that service from the
604
+ copy before its container starts, so an image left over from an earlier run
605
+ is never used. An image built from an earlier copy, by a command outside the
606
+ run, is left as it is, and the run says to build it in the test command.
607
+
608
+ - Images pulled from a registry (`image:` with no `build:`, `postgres:16`)
609
+ run code nobody instrumented, and are left as they are.
610
+ - testcontainers' reaper, Ryuk, is pointed at the daemon's own socket
611
+ (`TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE`) unless you set one.
470
612
  - A Python or Ruby image may keep the code at a path of its own
471
613
  (`COPY backend/ /srv/`): the runtime matches the image's files to the
472
614
  copy's by their paths' common ending, and only when several of them match.
473
615
  - A Go image needs Supercov's runtime package, `_supercov_runtime/` at the
474
616
  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.
617
+ - A daemon reached over TCP or SSH (`DOCKER_HOST=tcp://...`), and Windows,
618
+ are not covered: there the containers run as written.
477
619
 
478
620
  ```sh
479
621
  npx supercov -- npx playwright test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.17.2",
3
+ "version": "4.19.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/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",
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/import-line-evidence.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/docker-clients-playwright.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/exec-handover.mjs && node scripts/rust-region-container.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 && node scripts/workspace-crash-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.17.2",
123
- "@supercov/cli-darwin-x64": "4.17.2",
124
- "@supercov/cli-linux-arm64-gnu": "4.17.2",
125
- "@supercov/cli-linux-arm64-musl": "4.17.2",
126
- "@supercov/cli-linux-x64-gnu": "4.17.2",
127
- "@supercov/cli-linux-x64-musl": "4.17.2",
128
- "@supercov/cli-win32-arm64": "4.17.2",
129
- "@supercov/cli-win32-x64": "4.17.2"
122
+ "@supercov/cli-darwin-arm64": "4.19.0",
123
+ "@supercov/cli-darwin-x64": "4.19.0",
124
+ "@supercov/cli-linux-arm64-gnu": "4.19.0",
125
+ "@supercov/cli-linux-arm64-musl": "4.19.0",
126
+ "@supercov/cli-linux-x64-gnu": "4.19.0",
127
+ "@supercov/cli-linux-x64-musl": "4.19.0",
128
+ "@supercov/cli-win32-arm64": "4.19.0",
129
+ "@supercov/cli-win32-x64": "4.19.0"
130
130
  },
131
131
  "peerDependencies": {
132
132
  "@playwright/test": ">=1.55.0",
@@ -21,6 +21,9 @@ const runtime =
21
21
  globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??
22
22
  (typeof process !== "undefined" ? process.__SUPERCOV_DIRECT_RUNTIME__ : undefined);
23
23
  const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
24
+ // This file's context has a `WebAssembly` of its own: the instances a Rust
25
+ // crate compiled to wasm creates here are remembered with the runtime's.
26
+ runtime?.registerWasmRealm?.(globalThis);
24
27
 
25
28
  function digest(value) {
26
29
  return createHash("sha256").update(value).digest("hex").slice(0, 24);
@@ -355,6 +355,13 @@ class CoveragePhaseController {
355
355
  hits: [],
356
356
  events: [],
357
357
  })));
358
+ if (process.env.SUPERCOV_WASM_REGIONS === "1") {
359
+ // What the frame's wasm ran since the last take is this test's.
360
+ const regions = await frame
361
+ .evaluate(() => globalThis.__SUPERCOV_DIRECT_RUNTIME__?.takeWasmRegions?.() ?? [])
362
+ .catch(() => []);
363
+ directRuntime()?.appendWasmRecord?.(this.scope.attemptId, regions);
364
+ }
358
365
  }
359
366
  return snapshots;
360
367
  }
@@ -496,6 +503,10 @@ class CoveragePhaseController {
496
503
  patchContextPrototype(context);
497
504
  if (adopted)
498
505
  this.adoptedContexts.add(context);
506
+ // A Rust crate compiled to wasm the run measures: pages remember
507
+ // their wasm instances, so each test's work can be taken from them.
508
+ if (process.env.SUPERCOV_WASM_REGIONS === "1")
509
+ await context.addInitScript({ content: "globalThis.__SUPERCOV_WASM_REGIONS__ = true;" }).catch(() => undefined);
499
510
  await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
500
511
  // A context that outlives its test (a worker-scoped browser fixture)
501
512
  // is registered again by every later test, so this script accumulates
@@ -1588,10 +1599,15 @@ const instrumentedFixtures = {
1588
1599
  const serverOutput = serverEvidencePath(scope);
1589
1600
  mkdirSync(serverEvidenceDirectory(scope), { recursive: true });
1590
1601
  rmSync(serverOutput, { force: true });
1602
+ // The attempt's first and last moment: a server's work that names
1603
+ // no test is credited by time to the one test running then, and a
1604
+ // test is running for all of this, not only during its actions.
1605
+ const startedAtMs = Date.now();
1591
1606
  try {
1592
1607
  await use();
1593
1608
  }
1594
1609
  finally {
1610
+ const endedAtMs = Date.now();
1595
1611
  try {
1596
1612
  const browser = await controller.collectRuntimeSnapshots();
1597
1613
  const server = readServerRecords(scope);
@@ -1621,6 +1637,7 @@ const instrumentedFixtures = {
1621
1637
  ...(controller.workerEvidenceMissing ? { attribution: "partial" } : {}),
1622
1638
  browser,
1623
1639
  server,
1640
+ span: [startedAtMs, endedAtMs],
1624
1641
  };
1625
1642
  appendEvidenceLine(`${JSON.stringify(payload)}\n`);
1626
1643
  }
@@ -504,6 +504,7 @@ function nextUnscopedRequest() {
504
504
  function beginBufferedServerEvidence(scope) {
505
505
  if (isBrowser)
506
506
  return;
507
+ switchWasmOwner(scope);
507
508
  const key = attemptKey(scope);
508
509
  state.bufferedAttempts.add(key);
509
510
  if (!state.serverBuffers.has(key))
@@ -517,6 +518,7 @@ function beginBufferedServerEvidence(scope) {
517
518
  function flushBufferedServerEvidence(scope) {
518
519
  if (isBrowser)
519
520
  return void 0;
521
+ switchWasmOwner(void 0);
520
522
  const key = attemptKey(scope);
521
523
  state.bufferedAttempts.delete(key);
522
524
  const buffered = state.serverBuffers.get(key);
@@ -727,6 +729,19 @@ function flushOnTermination(flush) {
727
729
  process.on("exit", drain);
728
730
  } catch (e) {
729
731
  }
732
+ // `process.execve` replaces the process with another program: no "exit"
733
+ // runs, and what this one buffered would go with it. A cache front end
734
+ // that hands itself over to the runner's Node lost every line it ran.
735
+ try {
736
+ const execve = process.execve;
737
+ if (typeof execve === "function") {
738
+ process.execve = function supercovExecve() {
739
+ drain();
740
+ return execve.apply(this, arguments);
741
+ };
742
+ }
743
+ } catch (e) {
744
+ }
730
745
  for (const signal of ["SIGTERM", "SIGINT", "SIGHUP", "SIGBREAK"]) {
731
746
  try {
732
747
  process.on(signal, () => {
@@ -740,10 +755,153 @@ function flushOnTermination(flush) {
740
755
  }
741
756
  }
742
757
  }
758
+ // Rust compiled to wasm and measured by the Rust frontend keeps what it ran
759
+ // in the module's memory, behind `__supercov_region_<crate>`. When the run
760
+ // measures such a crate (SUPERCOV_WASM_REGIONS, or the page flag the
761
+ // Playwright fixture sets), every instance the process or page creates is
762
+ // remembered, and its regions are taken -- copied and cleared -- at each
763
+ // test's edges: what was taken belongs to the test that just ended, or to no
764
+ // test. The Rust run reads the takes and credits each to its test.
765
+ var WASM_REGION_PREFIX = "__supercov_region_";
766
+ function wasmRegionsWanted() {
767
+ if (runtimeGlobal.__SUPERCOV_WASM_REGIONS__ === true)
768
+ return true;
769
+ return typeof process !== "undefined" && process.env != null && process.env.SUPERCOV_WASM_REGIONS === "1";
770
+ }
771
+ function installWasmRegistry() {
772
+ if (runtimeGlobal.__SUPERCOV_WASM_REGISTRY__ || typeof WebAssembly === "undefined" || !wasmRegionsWanted())
773
+ return;
774
+ runtimeGlobal.__SUPERCOV_WASM_REGISTRY__ = true;
775
+ if (!runtimeGlobal.__supercovWasm)
776
+ runtimeGlobal.__supercovWasm = [];
777
+ registerWasmRealm(runtimeGlobal);
778
+ }
779
+ /** Remember the instances another realm creates too: Jest runs each test file in a context with a `WebAssembly` of its own. */
780
+ function registerWasmRealm(realm) {
781
+ if (!runtimeGlobal.__SUPERCOV_WASM_REGISTRY__ || !realm || !realm.WebAssembly)
782
+ return;
783
+ // By the `WebAssembly` itself: a flag on the realm's global would show
784
+ // through in a context that exposes the outer globals, as Jest's does.
785
+ if (!runtimeGlobal.__supercovWasmRealms)
786
+ runtimeGlobal.__supercovWasmRealms = new WeakSet();
787
+ const wasm = realm.WebAssembly;
788
+ if (runtimeGlobal.__supercovWasmRealms.has(wasm))
789
+ return;
790
+ runtimeGlobal.__supercovWasmRealms.add(wasm);
791
+ const instances = runtimeGlobal.__supercovWasm;
792
+ const Instance = wasm.Instance;
793
+ function Recorded(module, imports) {
794
+ const instance = new Instance(module, imports);
795
+ instances.push(instance);
796
+ return instance;
797
+ }
798
+ Recorded.prototype = Instance.prototype;
799
+ try {
800
+ wasm.Instance = Recorded;
801
+ } catch (e) {
802
+ }
803
+ ["instantiate", "instantiateStreaming"].forEach(function(name) {
804
+ const original = wasm[name];
805
+ if (typeof original !== "function")
806
+ return;
807
+ try {
808
+ wasm[name] = function() {
809
+ return original.apply(wasm, arguments).then(function(result) {
810
+ instances.push(result instanceof Instance ? result : result.instance);
811
+ return result;
812
+ });
813
+ };
814
+ } catch (e) {
815
+ }
816
+ });
817
+ }
818
+ installWasmRegistry();
819
+ function wasmBase64(bytes) {
820
+ if (typeof Buffer !== "undefined")
821
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
822
+ let text = "";
823
+ for (let index = 0; index < bytes.length; index += 32768)
824
+ text += String.fromCharCode.apply(null, bytes.subarray(index, Math.min(bytes.length, index + 32768)));
825
+ return btoa(text);
826
+ }
827
+ /** Each instrumented crate's region: its header and the pages a probe touched since the last take, then cleared. A region with nothing new is left out, but for the first take, which carries what `cfg!` said of the crate's gates. */
828
+ function takeWasmRegions() {
829
+ const out = [];
830
+ const instances = runtimeGlobal.__supercovWasm;
831
+ if (!instances)
832
+ return out;
833
+ if (!runtimeGlobal.__supercovWasmTaken)
834
+ runtimeGlobal.__supercovWasmTaken = new WeakSet();
835
+ const seen = runtimeGlobal.__supercovWasmTaken;
836
+ for (const instance of instances) {
837
+ const exports = instance && instance.exports;
838
+ if (!exports || !exports.memory)
839
+ continue;
840
+ for (const name of Object.keys(exports)) {
841
+ if (name.indexOf(WASM_REGION_PREFIX) !== 0)
842
+ continue;
843
+ try {
844
+ const crate = name.slice(WASM_REGION_PREFIX.length);
845
+ const packed = BigInt.asUintN(64, BigInt(exports[name]()));
846
+ const at = Number(packed >> BigInt(32));
847
+ const length = Number(packed & BigInt(4294967295));
848
+ if (!length)
849
+ continue;
850
+ const view = new DataView(exports.memory.buffer, at, length);
851
+ const first = !seen.has(exports);
852
+ if (!first && view.getUint32(40, true) === 0 && view.getUint32(44, true) === 0)
853
+ continue;
854
+ seen.add(exports);
855
+ const bytes = new Uint8Array(exports.memory.buffer, at, length);
856
+ const bodyAt = view.getUint32(32, true);
857
+ const segments = [[0, wasmBase64(bytes.subarray(0, bodyAt))]];
858
+ for (let page = 0; bodyAt + page * 4096 < length; page++) {
859
+ if (bytes[128 + (page >> 3)] & 1 << (page & 7)) {
860
+ const from = bodyAt + page * 4096;
861
+ segments.push([from, wasmBase64(bytes.subarray(from, Math.min(length, from + 4096)))]);
862
+ }
863
+ }
864
+ out.push([crate, length, segments]);
865
+ const clear = exports["__supercov_clear_" + crate];
866
+ if (typeof clear === "function")
867
+ clear();
868
+ } catch (e) {
869
+ }
870
+ }
871
+ }
872
+ return out;
873
+ }
874
+ /** Write a take, owned by an attempt or by no test, where the Rust run reads it. */
875
+ function appendWasmRecord(attemptId, regions) {
876
+ if (isBrowser || !regions || regions.length === 0)
877
+ return;
878
+ const fs = getFs();
879
+ if (!fs)
880
+ return;
881
+ try {
882
+ const directory = configuredServerEvidenceRoot() + "/wasm";
883
+ makeEvidenceDirectory(fs, directory);
884
+ if (!state.wasmFile)
885
+ state.wasmFile = directory + "/" + process.pid + "-" + Math.random().toString(36).slice(2) + ".jsonl";
886
+ fs.appendFileSync(state.wasmFile, JSON.stringify({ owner: attemptId || null, regions }) + "\n");
887
+ } catch (e) {
888
+ }
889
+ }
890
+ /** The test whose wasm work is being recorded changes: what was recorded until now is the previous owner's. */
891
+ function switchWasmOwner(scope) {
892
+ if (!runtimeGlobal.__SUPERCOV_WASM_REGISTRY__ || isBrowser)
893
+ return;
894
+ const owner = state.wasmOwner || null;
895
+ state.wasmOwner = scope && scope.attemptId ? scope.attemptId : null;
896
+ appendWasmRecord(owner, takeWasmRegions());
897
+ }
743
898
  if (!isBrowser) {
744
899
  const flushers = (_a7 = runtimeGlobal.__SUPERCOV_BUFFER_FLUSHERS__) != null ? _a7 : /* @__PURE__ */ new Set();
745
900
  runtimeGlobal.__SUPERCOV_BUFFER_FLUSHERS__ = flushers;
746
901
  flushers.add(flushAllBufferedServerEvidence);
902
+ flushers.add(function() {
903
+ switchWasmOwner(void 0);
904
+ });
747
905
  if (!runtimeGlobal.__SUPERCOV_BUFFER_EXIT_INSTALLED__) {
748
906
  flushOnTermination(() => {
749
907
  var _a8;
@@ -913,6 +1071,7 @@ function currentRequestContext() {
913
1071
  return runtimeGlobal.__SUPERCOV_ACTIVE_SCOPE__ ? { scope: runtimeGlobal.__SUPERCOV_ACTIVE_SCOPE__ } : {};
914
1072
  }
915
1073
  function activateCoverageScope(scope) {
1074
+ switchWasmOwner(scope);
916
1075
  if (scope)
917
1076
  runtimeGlobal.__SUPERCOV_ACTIVE_SCOPE__ = scope;
918
1077
  else
@@ -2155,6 +2314,9 @@ const directRuntimeApi = {
2155
2314
  selectEndV2,
2156
2315
  selectNextV2,
2157
2316
  takeNodeAssertionPhases,
2317
+ takeWasmRegions,
2318
+ appendWasmRecord,
2319
+ registerWasmRealm,
2158
2320
  tryBegin,
2159
2321
  tryCatch,
2160
2322
  tryEnd,
@@ -2221,6 +2383,9 @@ export {
2221
2383
  selectEndV2,
2222
2384
  selectNextV2,
2223
2385
  takeNodeAssertionPhases,
2386
+ takeWasmRegions,
2387
+ appendWasmRecord,
2388
+ registerWasmRealm,
2224
2389
  tryBegin,
2225
2390
  tryCatch,
2226
2391
  tryEnd,