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.
- package/docs/agent-loop.md +4 -1
- package/docs/cli.md +56 -0
- package/docs/supported-suites.md +164 -22
- package/package.json +10 -10
- package/runtime/javascript/jest.cjs +3 -0
- package/runtime/javascript/playwright.mjs +17 -0
- package/runtime/javascript/runtime.mjs +165 -0
package/docs/agent-loop.md
CHANGED
|
@@ -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:
|
|
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
|
package/docs/supported-suites.md
CHANGED
|
@@ -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
|
|
173
|
-
|
|
174
|
-
is not
|
|
175
|
-
`target_*`
|
|
176
|
-
|
|
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
|
|
456
|
-
|
|
457
|
-
A Playwright suite whose server
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
-
|
|
476
|
-
covered
|
|
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.
|
|
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.
|
|
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.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,
|