supercov 4.3.0 → 4.5.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/README.md +24 -1
- package/docs/assertions.md +25 -5
- package/docs/cli.md +102 -1
- package/docs/coverage-model.md +15 -0
- package/docs/supported-suites.md +1 -1
- package/docs/troubleshooting.md +7 -0
- package/docs/workspace-isolation.md +46 -2
- package/package.json +17 -16
- package/runtime/javascript/ava.mjs +33 -1
- package/runtime/javascript/mocha.mjs +46 -0
- package/runtime/javascript/nodeTest.mjs +5 -9
- package/runtime/javascript/register.mjs +74 -5
- package/runtime/javascript/runnerEvidence.mjs +77 -2
- package/runtime/javascript/runtime.mjs +81 -18
- package/runtime/javascript/tap.mjs +61 -1
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ Java and Kotlin have no registry of their own here, so a JVM project takes the b
|
|
|
282
282
|
| PHP | Coming soon | — | — |
|
|
283
283
|
| C | Coming soon | — | — |
|
|
284
284
|
|
|
285
|
-
The `npx` path requires Node.js 22 or newer; the other installs need no Node. Rust support currently uses Rust 1.95; cargo-nextest 0.9.138 and
|
|
285
|
+
The `npx` path requires Node.js 22 or newer; the other installs need no Node. Rust support currently uses Rust 1.95; cargo-nextest 0.9.138 and newer are supported. Python support requires CPython 3.9 or newer and measures pytest and unittest runs. Ruby support requires Ruby 3.3 or newer (3.4 or newer for full measurement) and measures RSpec, Minitest, test-unit and Cucumber runs. Go support requires Go 1.22 or newer and measures `go test`. Java and Kotlin support requires a JDK 17 or newer with Maven or Gradle, and measures every JUnit Platform engine -- JUnit 5, JUnit 4 through Vintage, Kotest and Spock -- as well as TestNG.
|
|
286
286
|
|
|
287
287
|
## Supported operating systems and architectures
|
|
288
288
|
|
|
@@ -344,6 +344,29 @@ npx supercov report
|
|
|
344
344
|
|
|
345
345
|
Collections accept `--limit` and `--offset` and print a copyable next-page command. Machine-readable output is available with `--json` when an integration needs it.
|
|
346
346
|
|
|
347
|
+
## Comment on pull requests
|
|
348
|
+
|
|
349
|
+
In GitHub Actions, one step runs your tests and tells the pull request which
|
|
350
|
+
changed lines no test ran:
|
|
351
|
+
|
|
352
|
+
```yaml
|
|
353
|
+
permissions:
|
|
354
|
+
contents: read
|
|
355
|
+
pull-requests: write
|
|
356
|
+
|
|
357
|
+
steps:
|
|
358
|
+
- uses: actions/checkout@v6
|
|
359
|
+
- run: npm ci
|
|
360
|
+
- uses: supercorp-ai/supercov@v4
|
|
361
|
+
with:
|
|
362
|
+
command: npm test
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
It works with every supported language, keeps one comment up to date, marks the
|
|
366
|
+
lines in the diff, and can fail the job below a floor (`min-changed-lines: 90`).
|
|
367
|
+
No account, no upload token and no server: the report never leaves the runner.
|
|
368
|
+
See [Comment on a pull request](docs/cli.md#comment-on-a-pull-request).
|
|
369
|
+
|
|
347
370
|
## Local, private, and zero-edit
|
|
348
371
|
|
|
349
372
|
Everything Supercov writes lives under one hidden `.supercov/` directory: run evidence in `.supercov/runs/<run-id>/` and the isolated build cache in `.supercov/workspaces/`. It ignores itself in Git, so there is nothing to add to your `.gitignore`.
|
package/docs/assertions.md
CHANGED
|
@@ -83,9 +83,11 @@ npx supercov runs latest assertions
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
`assertions assess` works it out and saves the result to the run. For each test,
|
|
86
|
-
Supercov renders what the test ran -- its code
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
Supercov renders what the test ran -- its code and the source it executed --
|
|
87
|
+
and asks [Jev](https://typesafe.ai) whether the test would fail for each
|
|
88
|
+
statement's change. In JavaScript, TypeScript and Python the source shown is
|
|
89
|
+
the function each asked statement is in; in the other languages it is all the
|
|
90
|
+
test ran in the statement's file, with the helper files the test imports. It prints an estimate first; `--dry-run` sends nothing.
|
|
89
91
|
The estimate gives the first round and the most the pass can send, which is
|
|
90
92
|
what it sends when no test is judged to catch anything.
|
|
91
93
|
It needs a TypeSafe AI API key in `TYPESAFE_API_KEY`.
|
|
@@ -127,7 +129,25 @@ ones most likely written for the statement: tests whose name shares words with
|
|
|
127
129
|
the statement's line (`corsOrigin: a false value` for
|
|
128
130
|
`return corsOrigin(origin)`), and among those the tests that ran the least
|
|
129
131
|
other code. A statement many tests run can still be caught by one that was
|
|
130
|
-
never asked; the statement view (`assertions <file>:<line>`) says how many were
|
|
132
|
+
never asked; the statement view (`assertions <file>:<line>`) says how many were,
|
|
133
|
+
and names the test files none of whose tests were asked, which is where to look
|
|
134
|
+
before writing a test that may already exist.
|
|
135
|
+
|
|
136
|
+
An asserted statement is a judgment too: some test was judged to fail if it
|
|
137
|
+
changed, and the pass stops at the first that is. How strong that answer was
|
|
138
|
+
says how far to trust it. Checked by applying each change and running the
|
|
139
|
+
tests, an asserted verdict was wrong 22 times in 1,169 on an answer of 75% or
|
|
140
|
+
more, and 33 times in 711 on a weaker one. The overview says how many of the
|
|
141
|
+
asserted rest on a weaker answer, and `--weak` lists them:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx supercov runs latest assertions --weak # the files by statements asserted on a weak answer
|
|
145
|
+
npx supercov runs latest assertions src/adapters.ts --weak # those statements, the least certain first
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
What reads as covered and is not is most often a deferred effect: a flag set
|
|
149
|
+
or an entry removed whose only consequence is what happens on a later event,
|
|
150
|
+
run by a test that asserts on the immediate reply.
|
|
131
151
|
|
|
132
152
|
## Which tests check a change
|
|
133
153
|
|
|
@@ -142,7 +162,7 @@ Answers are kept in `.supercov/assertions/`. Each is stored under the exact text
|
|
|
142
162
|
Jev saw for that statement and test, and reused only when a new run would ask
|
|
143
163
|
the same question with the same text. Nothing decides whether a change could
|
|
144
164
|
matter: a change to the statement, to code around it that the test ran, to the
|
|
145
|
-
test
|
|
165
|
+
test, or to which tests run it, asks again. A dependency or
|
|
146
166
|
configuration change asks everything again. After a typical commit only a few
|
|
147
167
|
dozen questions are asked; the rest are reused.
|
|
148
168
|
|
package/docs/cli.md
CHANGED
|
@@ -22,6 +22,7 @@ npx supercov --help
|
|
|
22
22
|
| Find what is not asserted in one file | `npx supercov runs latest assertions <path>` |
|
|
23
23
|
| See each test's answer for one statement | `npx supercov runs latest assertions <path>:<line>` |
|
|
24
24
|
| See what one test is judged to catch | `npx supercov runs latest assertions --test <name>` |
|
|
25
|
+
| List what is asserted on a weak answer | `npx supercov runs latest assertions --weak` |
|
|
25
26
|
| Read matching current source code | `npx supercov runs latest source <path>` |
|
|
26
27
|
| Compare two runs | `npx supercov diff <older> <newer>` |
|
|
27
28
|
| Open an interactive report | `npx supercov report` |
|
|
@@ -278,6 +279,62 @@ code nothing ran.
|
|
|
278
279
|
adjacent misses into one range and capping the total (`--max-annotations`). It
|
|
279
280
|
needs no token and posts no comment.
|
|
280
281
|
|
|
282
|
+
## Comment on a pull request
|
|
283
|
+
|
|
284
|
+
In GitHub Actions, one step runs the tests, says in a comment which changed
|
|
285
|
+
lines no test ran, and marks them in the diff:
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
on: pull_request
|
|
289
|
+
|
|
290
|
+
permissions:
|
|
291
|
+
contents: read
|
|
292
|
+
pull-requests: write
|
|
293
|
+
|
|
294
|
+
jobs:
|
|
295
|
+
test:
|
|
296
|
+
runs-on: ubuntu-latest
|
|
297
|
+
steps:
|
|
298
|
+
- uses: actions/checkout@v6
|
|
299
|
+
- run: npm ci
|
|
300
|
+
- uses: supercorp-ai/supercov@v4
|
|
301
|
+
with:
|
|
302
|
+
command: npm test
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`command` is the test command in any supported language: `pytest`,
|
|
306
|
+
`cargo test`, `go test ./...`, `bundle exec rspec`, `mvn test`. The step takes
|
|
307
|
+
the place of the one that ran the tests, and fails when they fail. It needs no
|
|
308
|
+
account and no server: the comment is written with the workflow's own token,
|
|
309
|
+
and everything else stays on the runner.
|
|
310
|
+
|
|
311
|
+
The comment gives the coverage of the changed lines, the run's line, branch and
|
|
312
|
+
MC/DC coverage, and each changed file with the lines no test ran, linked to the
|
|
313
|
+
commit. A later push rewrites the same comment. The job summary holds the same
|
|
314
|
+
report, which is what a pull request from a fork gets: its token cannot write a
|
|
315
|
+
comment.
|
|
316
|
+
|
|
317
|
+
| Input | Default | |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| `command` | required | The test command. One command; put several in a script. |
|
|
320
|
+
| `working-directory` | `.` | Where to run it. |
|
|
321
|
+
| `min-changed-lines` | none | Fail below this percentage of changed lines covered. |
|
|
322
|
+
| `min-lines`, `min-branches`, `min-mcdc` | none | Fail below this percentage for the whole run. |
|
|
323
|
+
| `title` | `Coverage` | The comment's heading. Give each job its own when several comment on one pull request. |
|
|
324
|
+
| `base` | the pull request's base | What the change is compared with. |
|
|
325
|
+
| `comment`, `annotate` | `true` | Turn the comment or the marks in the diff off. |
|
|
326
|
+
| `version` | the action's release | The Supercov release to run. |
|
|
327
|
+
| `binary` | none | A `supercov` already on the runner, used instead of downloading one. |
|
|
328
|
+
|
|
329
|
+
The default checkout fetches one commit; the step fetches the history between
|
|
330
|
+
it and the base itself. It puts `supercov` on the `PATH` and sets the outputs
|
|
331
|
+
`run-id`, `lines`, `branches` and `changed-lines`, so later steps can read the
|
|
332
|
+
same run:
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
- run: supercov runs report --format html --output coverage/report
|
|
336
|
+
```
|
|
337
|
+
|
|
281
338
|
## Export for other tools
|
|
282
339
|
|
|
283
340
|
```sh supercov-example
|
|
@@ -299,7 +356,13 @@ one to fill a field.
|
|
|
299
356
|
|
|
300
357
|
MC/DC conditions are not exported as ordinary branches. A consumer would then
|
|
301
358
|
show condition obligations as branch coverage, which is a different
|
|
302
|
-
measurement
|
|
359
|
+
measurement. The LCOV export writes them as LCOV 2's own records, `MCDC:` for
|
|
360
|
+
each condition with `MCF:` and `MCH:` for a file, so `lcov --mcdc-coverage` and
|
|
361
|
+
`genhtml --mcdc-coverage` show a per-file MC/DC figure from the export alone.
|
|
362
|
+
LCOV counts a condition twice, once for each way it can turn the outcome, and
|
|
363
|
+
Supercov's covered condition is both: hit over found is the run's MC/DC. A
|
|
364
|
+
reader of the older format passes the records over. Cobertura has no such
|
|
365
|
+
record; there the evidence stays in the JSON view and the HTML report. A report
|
|
303
366
|
from a failed or stale run is still written, with a warning on stderr — only
|
|
304
367
|
`check` refuses to pass on one.
|
|
305
368
|
|
|
@@ -367,6 +430,19 @@ lost: 1 lines, 1 branches, 0 MC/DC conditions
|
|
|
367
430
|
+ branch src/fee.js:2 true
|
|
368
431
|
```
|
|
369
432
|
|
|
433
|
+
When the difference is in more than one file, the files come first, the one
|
|
434
|
+
that lost most at the top, so a component that did not render in one run reads
|
|
435
|
+
as one row and not as forty:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
in 2 files, most lost first:
|
|
439
|
+
src/panel.js: lost 6 lines, 1 branches
|
|
440
|
+
src/fee.js: lost 1 lines, 1 branches; gained 1 lines, 1 branches
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
The counts are of the whole difference, whatever page of it is shown. In
|
|
444
|
+
`--json`, `files` and `byFile`.
|
|
445
|
+
|
|
370
446
|
Two runs of the same code can differ a little: what an end-to-end suite
|
|
371
447
|
reaches depends on timing, on the order of tests, and on state outside the
|
|
372
448
|
code. When both runs measured the same source, `diff` says so under the
|
|
@@ -502,12 +578,37 @@ assessment, each commit cost under a cent.
|
|
|
502
578
|
|
|
503
579
|
```sh supercov
|
|
504
580
|
npx supercov merge <shard-a> <shard-b> <shard-c>
|
|
581
|
+
npx supercov merge
|
|
505
582
|
```
|
|
506
583
|
|
|
507
584
|
Merge creates a new run. The inputs must describe the same source,
|
|
508
585
|
configuration, toolchain, schema, and coverage denominator. Supercov rejects an
|
|
509
586
|
incompatible merge rather than publishing a misleading aggregate.
|
|
510
587
|
|
|
588
|
+
With no run named, `merge` takes the runs recorded on the source as it is now:
|
|
589
|
+
the newest of each command, leaving out runs that are themselves a merge. It
|
|
590
|
+
prints the runs it took. This is what a script uses after measuring its
|
|
591
|
+
suites, one after another or side by side, without knowing what each run was
|
|
592
|
+
called. Asked again with nothing new, it names the merge already made.
|
|
593
|
+
|
|
594
|
+
A test that is in two of the runs is one test in the merge. If it failed in
|
|
595
|
+
either, it is failed, which is why a command run again replaces its earlier
|
|
596
|
+
run and is not merged with it.
|
|
597
|
+
|
|
598
|
+
The runs may differ in their tests when the later ones have only gained test
|
|
599
|
+
files. Measuring a new test alone and adding it to the runs before it is
|
|
600
|
+
then the whole suite's figure, without the whole suite's time:
|
|
601
|
+
|
|
602
|
+
```sh supercov-example
|
|
603
|
+
npx supercov -- npm test # the suite, once
|
|
604
|
+
npx supercov -- npx vitest run test/new.test.ts # a test added since, alone
|
|
605
|
+
npx supercov merge
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
A run is left out, and named, when a file of its own tests has changed since
|
|
609
|
+
it ran, or a file tests load that is not itself a test's: what it recorded is
|
|
610
|
+
then of other tests. Measure those again to count them.
|
|
611
|
+
|
|
511
612
|
`assertions assess` assesses a merged run across every suite merged into it,
|
|
512
613
|
reusing the answers saved when its parts were assessed. `tests affected` is
|
|
513
614
|
asked of the runs that were merged, which hold the record of what each test
|
package/docs/coverage-model.md
CHANGED
|
@@ -176,6 +176,21 @@ without a conventional source directory is measured as a whole. Functions passed
|
|
|
176
176
|
to compile-time style macros (`stylex.create(...)`) are left as written because
|
|
177
177
|
the bundler consumes them at build time; nothing about them runs.
|
|
178
178
|
|
|
179
|
+
In a Python or Ruby project every source file outside test directories,
|
|
180
|
+
virtual environments and build output is measured, except what lies beside
|
|
181
|
+
the package at the top of the project: `docs/`, `examples/`, benchmark
|
|
182
|
+
directories (`bench/`, `benchmarks/`) and `fuzz/`. A test runs none of it, and
|
|
183
|
+
counted in, a package covered to 99% reads 69%. Name one of them in
|
|
184
|
+
`SUPERCOV_SOURCE_ROOTS` and it is source.
|
|
185
|
+
|
|
186
|
+
In a Go module every `.go` file the toolchain compiles is measured: not
|
|
187
|
+
`vendor/` or `testdata/`, not a nested module, and not a file or directory
|
|
188
|
+
whose name begins with `_` or `.`, which `go build ./...` walks past. The
|
|
189
|
+
programs under a top-level `examples/` are left out too. A Rust run measures
|
|
190
|
+
what the build compiled, so a bench or an example `cargo test` does not build
|
|
191
|
+
is listed as not compiled. A Maven or Gradle project is measured by its source
|
|
192
|
+
sets: `src/main` is source, `src/test` is tests.
|
|
193
|
+
|
|
179
194
|
Inside the roots, tests, fixtures, tool scripts and configuration files are
|
|
180
195
|
still recognised by their path and left out. A root you name settles what it
|
|
181
196
|
names: with `SUPERCOV_SOURCE_ROOTS=src,scripts` the `scripts/` directory is
|
package/docs/supported-suites.md
CHANGED
|
@@ -150,7 +150,7 @@ whatever was still buffered.
|
|
|
150
150
|
| --- | --- | --- |
|
|
151
151
|
| Cargo's standard libtest runner | Exact test, attempt, and passing-assertion identity | Rust 1.95; run with `npx supercov -- cargo test` |
|
|
152
152
|
| rustdoc doctests | Exact doctest identity; every doctest runs in a process of its own | Rust 1.95; part of `npx supercov -- cargo test` |
|
|
153
|
-
| cargo-nextest | Exact test, attempt, retry, and binary identity | cargo-nextest 0.9.138 or 0.9.140 |
|
|
153
|
+
| cargo-nextest | Exact test, attempt, retry, and binary identity | cargo-nextest 0.9.138 or newer (tested with 0.9.140 and 0.9.148) |
|
|
154
154
|
|
|
155
155
|
Supercov preserves Cargo's test selection, scheduling, fail-fast behavior,
|
|
156
156
|
environment and exit status. Doctests run with their own identities; nextest
|
package/docs/troubleshooting.md
CHANGED
|
@@ -157,6 +157,13 @@ npx supercov -- sh -c "npm run build && npm test"
|
|
|
157
157
|
A `pretest` script, or a Playwright `webServer` that builds, does the same. The
|
|
158
158
|
build then compiles the instrumented copy, and what the tests run is measured.
|
|
159
159
|
|
|
160
|
+
A build made in an earlier step does not reach the copy either. A CI job that
|
|
161
|
+
ran `npm run build` and then `supercov -- node --test`, as a gate written for
|
|
162
|
+
Supercov 3 would, built the project and tested a copy without `dist/`: the
|
|
163
|
+
tests that need it fail, and the run still reports a coverage figure for the
|
|
164
|
+
rest. The message at the end of such a run says the copy starts without build
|
|
165
|
+
output and gives the command with the build in it.
|
|
166
|
+
|
|
160
167
|
## A test that reads your source fails under Supercov
|
|
161
168
|
|
|
162
169
|
A test that opens your source files and asserts on their text sees the probes,
|
|
@@ -114,12 +114,56 @@ partial output.
|
|
|
114
114
|
|
|
115
115
|
## Interrupted and overlapping commands
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
Runs of one project can be started at the same moment. Each takes a copy of
|
|
118
|
+
the project to itself: a run alone always has the first,
|
|
119
|
+
`.supercov/workspaces/workspace/<name>`, and one started while that is in use
|
|
120
|
+
has `workspace-2`, the next `workspace-3`. None waits and none is refused:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
npx supercov -- npm run test:unit &
|
|
124
|
+
npx supercov -- npm run test:integration &
|
|
125
|
+
npx supercov -- npm run test:e2e &
|
|
126
|
+
wait
|
|
127
|
+
npx supercov merge
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`merge` with no run named adds up the runs recorded on the source as it is
|
|
131
|
+
now, so a script does not have to know what each was called.
|
|
132
|
+
|
|
133
|
+
A copy past the first is kept for a day after its last run, so that runs that
|
|
134
|
+
overlap again start from what they left, and is removed after that. Rust runs
|
|
135
|
+
build into one directory, as two `cargo test` do, and Cargo makes the second
|
|
136
|
+
wait while the first compiles.
|
|
137
|
+
|
|
138
|
+
Suites can also be started from one command. They are then one run with
|
|
139
|
+
nothing to merge, and each test keeps the kind its own process was given:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
npx supercov -- sh -c '
|
|
143
|
+
SUPERCOV_TEST_KIND=integration npm run test:integration & integration=$!
|
|
144
|
+
SUPERCOV_TEST_KIND=e2e npm run test:e2e & e2e=$!
|
|
145
|
+
wait $integration; first=$?
|
|
146
|
+
wait $e2e && exit $first
|
|
147
|
+
'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`wait` with no argument ends with 0 whatever the suites did, which would turn a
|
|
151
|
+
failing suite green; waiting for each by its id keeps the status.
|
|
152
|
+
|
|
153
|
+
`runs clean` wants the store to itself. While a run is active it ends with
|
|
154
|
+
status 2 and names the run; a run started while it works is told so, and no
|
|
155
|
+
test of it runs.
|
|
119
156
|
|
|
120
157
|
After interruption or host restart, the next command recovers unpublished
|
|
121
158
|
staging state. Completed runs remain immutable.
|
|
122
159
|
|
|
160
|
+
Ctrl-C ends the test command and the run: nothing is recorded, and the exit
|
|
161
|
+
status is 130. A test command that reads the keyboard (a debugger at a
|
|
162
|
+
breakpoint, a watch mode, a question before an install) is given the terminal
|
|
163
|
+
when it first reads, on macOS and Linux. From then on Ctrl-C and Ctrl-Z go to
|
|
164
|
+
it, as they do without Supercov, and a command that Ctrl-C ended is still a
|
|
165
|
+
run that was interrupted.
|
|
166
|
+
|
|
123
167
|
## Clean up local data
|
|
124
168
|
|
|
125
169
|
Preview cleanup before removing anything:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"test:react": "cargo build -p supercov && node scripts/rust-vitest-projects-integration.mjs && npm --prefix examples/react-verification ci && npm --prefix examples/react-verification run demo && node scripts/react-hydration-compatibility.mjs",
|
|
71
71
|
"test:release-tooling": "node --test tests/release/*.test.mjs",
|
|
72
72
|
"test:runtime": "node --test tests/runtime/*.test.mjs",
|
|
73
|
+
"test:action": "cargo build -p supercov && node --test tests/action/*.test.mjs",
|
|
73
74
|
"test:fixture": "cargo build -p supercov && node scripts/rust-fixture-matrix.mjs",
|
|
74
75
|
"test:packed-npx": "cargo build --release -p supercov && node scripts/packed-npx-integration.mjs",
|
|
75
76
|
"test:isolation": "cargo build -p supercov && node scripts/isolation-integration.mjs",
|
|
@@ -82,7 +83,7 @@
|
|
|
82
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",
|
|
83
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",
|
|
84
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",
|
|
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/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.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/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.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
87
|
"test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
|
|
87
88
|
"test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
88
89
|
"test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
"test:test262": "cargo build --release -p supercov && node scripts/test262-equivalence.mjs",
|
|
96
97
|
"benchmark:check": "cargo build --release -p supercov && node scripts/rust-transform-benchmark.mjs",
|
|
97
98
|
"benchmark:python": "cargo build -p supercov && node scripts/python-benchmark.mjs",
|
|
98
|
-
"check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:release-tooling && npm run test:runtime && npm run test:rust-assets && npm run test:agent-commands && npm run test:assertions && npm run test:sweep-target && npm run test:skip-guard && npm run test:windows-tls && node scripts/package-preflight.mjs && node scripts/verify-binstall-metadata.mjs --offline",
|
|
99
|
+
"check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:release-tooling && npm run test:runtime && npm run test:action && npm run test:rust-assets && npm run test:agent-commands && npm run test:assertions && npm run test:sweep-target && npm run test:skip-guard && npm run test:windows-tls && node scripts/package-preflight.mjs && node scripts/verify-binstall-metadata.mjs --offline",
|
|
99
100
|
"release:check": "cargo clean && node scripts/sweep-target.mjs && npm run check && npm run test:engine && npm run test:go && npm run test:jvm && npm run test:launcher && npm run test:fixture && npm run test:watchdog && npm run test:engine-contract && npm run test:agent && npm run test:child-attribution && npm run test:host-loader && npm run test:python && npm run test:ruby-coverage && npm run test:rust-public-cargo && npm run test:packed-npx && npm run test:clang-mcdc && npm run benchmark:check",
|
|
100
101
|
"release:bump": "node scripts/bump-version.mjs",
|
|
101
102
|
"sweep": "node scripts/sweep-target.mjs",
|
|
@@ -104,11 +105,11 @@
|
|
|
104
105
|
"prepublishOnly": "npm run release:check",
|
|
105
106
|
"test:child-attribution": "cargo build -p supercov && node scripts/rust-child-attribution-integration.mjs",
|
|
106
107
|
"test:host-loader": "cargo build -p supercov && node scripts/rust-host-loader-integration.mjs",
|
|
107
|
-
"test:python": "cargo build -p supercov && node scripts/python-integration.mjs",
|
|
108
|
-
"test:ruby-coverage": "cargo build -p supercov && node scripts/ruby-coverage-integration.mjs",
|
|
109
|
-
"test:go": "cargo test --no-fail-fast -p supercov-engine --test go_frontend --test go_run",
|
|
110
|
-
"test:jvm": "cargo test --no-fail-fast -p supercov-engine --test jvm_frontend --test jvm_run -- --include-ignored --test-threads=1",
|
|
111
|
-
"test:rust-public-cargo": "cargo build -p supercov && node scripts/rust-public-cargo-integration.mjs",
|
|
108
|
+
"test:python": "cargo build -p supercov && node scripts/python-integration.mjs && node scripts/failure-output.mjs python && node scripts/side-by-side.mjs python",
|
|
109
|
+
"test:ruby-coverage": "cargo build -p supercov && node scripts/ruby-coverage-integration.mjs && node scripts/failure-output.mjs ruby && node scripts/side-by-side.mjs ruby",
|
|
110
|
+
"test:go": "cargo test --no-fail-fast -p supercov-engine --test go_frontend --test go_run && cargo build -p supercov && node scripts/failure-output.mjs go && node scripts/side-by-side.mjs go",
|
|
111
|
+
"test:jvm": "cargo test --no-fail-fast -p supercov-engine --test jvm_frontend --test jvm_run -- --include-ignored --test-threads=1 && cargo build -p supercov && node scripts/side-by-side.mjs jvm",
|
|
112
|
+
"test:rust-public-cargo": "cargo build -p supercov && node scripts/rust-public-cargo-integration.mjs && node scripts/failure-output.mjs rust && node scripts/side-by-side.mjs rust",
|
|
112
113
|
"test:assertions": "cargo build -p supercov && node scripts/assertions-integration.mjs",
|
|
113
114
|
"docs:sync": "node scripts/sync-docs.mjs",
|
|
114
115
|
"docs:check": "node scripts/sync-docs.mjs --check",
|
|
@@ -119,14 +120,14 @@
|
|
|
119
120
|
"test:windows-tls": "node scripts/windows-tls-test.mjs"
|
|
120
121
|
},
|
|
121
122
|
"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.
|
|
123
|
+
"@supercov/cli-darwin-arm64": "4.5.0",
|
|
124
|
+
"@supercov/cli-darwin-x64": "4.5.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-gnu": "4.5.0",
|
|
126
|
+
"@supercov/cli-linux-arm64-musl": "4.5.0",
|
|
127
|
+
"@supercov/cli-linux-x64-gnu": "4.5.0",
|
|
128
|
+
"@supercov/cli-linux-x64-musl": "4.5.0",
|
|
129
|
+
"@supercov/cli-win32-arm64": "4.5.0",
|
|
130
|
+
"@supercov/cli-win32-x64": "4.5.0"
|
|
130
131
|
},
|
|
131
132
|
"peerDependencies": {
|
|
132
133
|
"@playwright/test": ">=1.55.0",
|
|
@@ -11,7 +11,7 @@ import { realpathSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
13
|
import { beginBufferedServerEvidence, flushBufferedServerEvidence, takeNodeAssertionPhases, withCoverageCarrier, } from "./runtime.mjs";
|
|
14
|
-
import { runnerExecutionScope, writeRunnerEvidence } from "./runnerEvidence.mjs";
|
|
14
|
+
import { runnerExecutionScope, stackAsWritten, writeRunnerEvidence } from "./runnerEvidence.mjs";
|
|
15
15
|
|
|
16
16
|
// The installed ava package the entrypoint belongs to.
|
|
17
17
|
function avaRoot(entrypoint) {
|
|
@@ -26,6 +26,24 @@ function avaRoot(entrypoint) {
|
|
|
26
26
|
return at < 0 ? undefined : real.slice(0, at + "/node_modules/ava".length);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function asWritten(failure, depth = 0) {
|
|
30
|
+
if (!failure || typeof failure !== "object" || depth > 4)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
// AVA prints the stack of an error a test threw as it is, and
|
|
34
|
+
// shortens a failed assertion's by its own working directory, which
|
|
35
|
+
// is the copy. A thrown error reaches here as an assertion's cause.
|
|
36
|
+
const shortened = failure.type === "ava" && !(failure.originalError?.cause instanceof Error);
|
|
37
|
+
if (typeof failure.stack === "string")
|
|
38
|
+
failure.stack = stackAsWritten(failure.stack, shortened);
|
|
39
|
+
for (const inner of Array.isArray(failure.errors) ? failure.errors : [])
|
|
40
|
+
asWritten(inner, depth + 1);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A stack that cannot be written is printed as it is.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
export async function adaptAva(entrypoint) {
|
|
30
48
|
const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
|
|
31
49
|
const root = avaRoot(entrypoint);
|
|
@@ -66,6 +84,20 @@ export async function adaptAva(entrypoint) {
|
|
|
66
84
|
});
|
|
67
85
|
};
|
|
68
86
|
Runner.prototype.runTest.supercovAva = true;
|
|
87
|
+
// AVA prints a failure's stack as it is. The test ran in the copy, so a
|
|
88
|
+
// frame of the test file named it there, and a frame of a rewritten
|
|
89
|
+
// module was named by its path where Node names a module by its URL:
|
|
90
|
+
// `at seal (/home/me/shop/src/crypto.js:3:11)` over
|
|
91
|
+
// `at file:///home/me/shop/.supercov/workspaces/…/test/crypto.test.js:5:18`.
|
|
92
|
+
// A failure is told to the reporter as a state change; its stack is put
|
|
93
|
+
// right on the way.
|
|
94
|
+
const emit = Runner.prototype.emit;
|
|
95
|
+
if (typeof emit === "function")
|
|
96
|
+
Runner.prototype.emit = function supercovAvaEmit(name, data, ...rest) {
|
|
97
|
+
if (name === "stateChange" && data?.err)
|
|
98
|
+
asWritten(data.err);
|
|
99
|
+
return Reflect.apply(emit, this, [name, data, ...rest]);
|
|
100
|
+
};
|
|
69
101
|
// A skipped or todo test never reaches runTest; AVA announces it.
|
|
70
102
|
const start = Runner.prototype.start;
|
|
71
103
|
if (typeof start === "function")
|
|
@@ -64,6 +64,52 @@ export async function adaptMocha(entrypoint) {
|
|
|
64
64
|
return Reflect.apply(run, this, args);
|
|
65
65
|
};
|
|
66
66
|
Runner.prototype.run.supercovMocha = true;
|
|
67
|
+
// Mocha takes its working directory off the front of each frame and
|
|
68
|
+
// prints the rest. Under Supercov that is the copy, while an application
|
|
69
|
+
// frame names the project's own file, so it printed whole:
|
|
70
|
+
// `at seal (/home/me/shop/src/crypto.js:3:11)` for `src/crypto.js:3:11`.
|
|
71
|
+
// And Supercov's hooks into asynchronous work put a frame of Node's under
|
|
72
|
+
// every stack that starts in a timer. A failure reads as it does without
|
|
73
|
+
// Supercov before Mocha or a reporter sees it.
|
|
74
|
+
const fail = Runner.prototype.fail;
|
|
75
|
+
if (typeof fail === "function") {
|
|
76
|
+
const place = asWritten();
|
|
77
|
+
Runner.prototype.fail = function supercovMochaFail(runnable, error, ...rest) {
|
|
78
|
+
place(error);
|
|
79
|
+
return Reflect.apply(fail, this, [runnable, error, ...rest]);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const HOOK_FRAME = /^\s*at process\.(?:callbackTrampoline|topLevelDomainCallback) \(node:(?:internal\/async_hooks|domain):\d+:\d+\)$/;
|
|
85
|
+
|
|
86
|
+
function asWritten() {
|
|
87
|
+
const real = (directory) => {
|
|
88
|
+
try {
|
|
89
|
+
return directory ? realpathSync(directory) : undefined;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const project = real(process.env.SUPERCOV_SOURCE_PROJECT_ROOT);
|
|
96
|
+
const copy = real(process.env.SUPERCOV_PROJECT_ROOT);
|
|
97
|
+
return (error) => {
|
|
98
|
+
if (!error || typeof error.stack !== "string")
|
|
99
|
+
return;
|
|
100
|
+
const lines = error.stack.split("\n").filter((line) => !HOOK_FRAME.test(line));
|
|
101
|
+
const placed = project && copy && project !== copy
|
|
102
|
+
? lines.map((line) => /^\s*at /.test(line) && line.includes(project + sep) && !line.includes(copy + sep)
|
|
103
|
+
? line.replace(`(${project}${sep}`, `(${copy}${sep}`).replace(` at ${project}${sep}`, ` at ${copy}${sep}`)
|
|
104
|
+
: line)
|
|
105
|
+
: lines;
|
|
106
|
+
try {
|
|
107
|
+
error.stack = placed.join("\n");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// A stack that cannot be written is printed as it is.
|
|
111
|
+
}
|
|
112
|
+
};
|
|
67
113
|
}
|
|
68
114
|
|
|
69
115
|
function observe(runner, runtime, write) {
|
|
@@ -2,8 +2,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import * as native from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import vm from "node:vm";
|
|
5
|
-
import { beginBufferedServerEvidence, flushBufferedServerEvidence, takeNodeAssertionPhases, withCoverageCarrier, } from "./runtime.mjs";
|
|
6
|
-
import { callerLocation, runnerExecutionScope, runnerTestId, writeRunnerEvidence, } from "./runnerEvidence.mjs";
|
|
5
|
+
import { beginBufferedServerEvidence, flushBufferedServerEvidence, takeNodeAssertionPhases, withCoverageCarrier, withStackRoom, } from "./runtime.mjs";
|
|
6
|
+
import { callerLocation, runnerExecutionScope, runnerTestId, stackAsWritten, writeRunnerEvidence, } from "./runnerEvidence.mjs";
|
|
7
7
|
function callbackIndex(args) {
|
|
8
8
|
for (let index = args.length - 1; index >= 0; index -= 1)
|
|
9
9
|
if (typeof args[index] === "function")
|
|
@@ -65,11 +65,7 @@ function restoreUserError(error, depth = 0) {
|
|
|
65
65
|
: text;
|
|
66
66
|
try {
|
|
67
67
|
if (typeof error.stack === "string")
|
|
68
|
-
error.stack =
|
|
69
|
-
.split("\n")
|
|
70
|
-
.filter((line) => !(line.trimStart().startsWith("at ") &&
|
|
71
|
-
line.includes("/.supercov/")))
|
|
72
|
-
.join("\n"));
|
|
68
|
+
error.stack = stackAsWritten(error.stack);
|
|
73
69
|
if (typeof error.message === "string")
|
|
74
70
|
error.message = remap(error.message);
|
|
75
71
|
}
|
|
@@ -227,9 +223,9 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
227
223
|
finishBody();
|
|
228
224
|
done?.(error);
|
|
229
225
|
};
|
|
230
|
-
return withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy, callbackDone]));
|
|
226
|
+
return withStackRoom(() => withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy, callbackDone])));
|
|
231
227
|
}
|
|
232
|
-
const result = withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy]));
|
|
228
|
+
const result = withStackRoom(() => withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy])));
|
|
233
229
|
if (result && typeof result.then === "function")
|
|
234
230
|
return Promise.resolve(result).then((value) => {
|
|
235
231
|
finishBody();
|
|
@@ -250,6 +250,14 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
250
250
|
// name the copy's file, and coverage reports it by its path in the
|
|
251
251
|
// project's layout.
|
|
252
252
|
vitest: { prints: /[\\/]vitest[\\/]dist[\\/]/, names: true },
|
|
253
|
+
// A test's process makes each frame's path relative to where it
|
|
254
|
+
// runs (@tapjs/stack), and the process that reports reads the file
|
|
255
|
+
// under a frame by that path to print it (prismjs-terminal, for
|
|
256
|
+
// @tapjs/reporter): `../../../../src/crypto.js` over the right
|
|
257
|
+
// lines, or `src/crypto.js` over Supercov's.
|
|
258
|
+
// libtap, which tap 16 and older print with, reads the file itself,
|
|
259
|
+
// and their reporter reads it through cardinal.
|
|
260
|
+
tap: { prints: /[\\/](?:@tapjs[\\/](?:stack|reporter)|prismjs-terminal|libtap|cardinal)[\\/]/, names: false },
|
|
253
261
|
}[runner];
|
|
254
262
|
// Asked with no runner, the reads are in place and change nothing until
|
|
255
263
|
// one is named: a process that may yet run a suite.
|
|
@@ -272,9 +280,10 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
272
280
|
return [copy];
|
|
273
281
|
}
|
|
274
282
|
})();
|
|
275
|
-
// The copy lies in the project at .supercov/workspaces/workspace/<name
|
|
283
|
+
// The copy lies in the project at .supercov/workspaces/workspace/<name>,
|
|
284
|
+
// or at workspace-2 where it is one of several runs at once.
|
|
276
285
|
const projects = copies
|
|
277
|
-
.filter((copy) => /[\\/]\.supercov[\\/]workspaces[\\/]workspace[\\/][^\\/]+[\\/]$/.test(copy))
|
|
286
|
+
.filter((copy) => /[\\/]\.supercov[\\/]workspaces[\\/]workspace(?:-\d+)?[\\/][^\\/]+[\\/]$/.test(copy))
|
|
278
287
|
.map((copy) => resolve(copy, "../../../..") + sep);
|
|
279
288
|
// The path inside the copy of a rewritten file that lies under one of `roots`.
|
|
280
289
|
const rewrittenUnder = (roots, file) => {
|
|
@@ -288,9 +297,11 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
288
297
|
rewritten = new Set();
|
|
289
298
|
}
|
|
290
299
|
}
|
|
300
|
+
// ts-node names a file with forward slashes on Windows too.
|
|
301
|
+
const named = sep === "\\" ? file.split("/").join(sep) : file;
|
|
291
302
|
for (const root of roots) {
|
|
292
|
-
if (
|
|
293
|
-
const local =
|
|
303
|
+
if (named.startsWith(root)) {
|
|
304
|
+
const local = named.slice(root.length).split(sep).join("/");
|
|
294
305
|
return rewritten.has(local) ? local : undefined;
|
|
295
306
|
}
|
|
296
307
|
}
|
|
@@ -437,7 +448,8 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
437
448
|
function supercovReadFileSync(file, ...rest) {
|
|
438
449
|
if (!failureOutput.prints)
|
|
439
450
|
return Reflect.apply(readSync, this, [file, ...rest]);
|
|
440
|
-
|
|
451
|
+
// A printer may name the file from where it runs.
|
|
452
|
+
const local = rewrittenUnder(copies, typeof file === "string" && !isAbsolute(file) ? resolve(file) : file);
|
|
441
453
|
if (local === undefined) {
|
|
442
454
|
const contents = Reflect.apply(readSync, this, [file, ...rest]);
|
|
443
455
|
return failureOutput.composes && typeof contents === "string" && typeof file === "string" && file.endsWith(".map")
|
|
@@ -478,7 +490,61 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
478
490
|
}
|
|
479
491
|
path.relative = supercovRelative;
|
|
480
492
|
syncBuiltinESMExports();
|
|
493
|
+
// ts-node compiles the text it is given, which is the rewritten file,
|
|
494
|
+
// and its source map ends there: TypeScript reads no map in its input.
|
|
495
|
+
// A frame under Mocha read `src/crypto.ts:37:10` for line 8, and a test
|
|
496
|
+
// file's `:15:172` for column 12. ts-node keeps what it compiled in
|
|
497
|
+
// memory and has source-map-support read it from there, so that is
|
|
498
|
+
// where the map is carried on to the lines the author wrote: a handler
|
|
499
|
+
// of Supercov's in the list the library keeps on the global object,
|
|
500
|
+
// asked before its default.
|
|
501
|
+
const shared = Symbol.for("source-map-support/sharedData");
|
|
502
|
+
function supercovRetrieveSourceMap(source) {
|
|
503
|
+
try {
|
|
504
|
+
const file = typeof source === "string" && source.startsWith("file://") ? fileURLToPath(source) : source;
|
|
505
|
+
if (rewrittenUnder(copies, file) === undefined)
|
|
506
|
+
return null;
|
|
507
|
+
let code;
|
|
508
|
+
for (const read of globalThis[shared]?.retrieveFileHandlers ?? []) {
|
|
509
|
+
code = read(file);
|
|
510
|
+
if (code)
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
const found = typeof code === "string" ? code.lastIndexOf("//# sourceMappingURL=data:application/json") : -1;
|
|
514
|
+
if (found < 0)
|
|
515
|
+
return null;
|
|
516
|
+
const text = Buffer.from(code.slice(code.indexOf(",", found) + 1).trim(), "base64").toString("utf8");
|
|
517
|
+
const through = composed(text);
|
|
518
|
+
return through === text ? null : { url: source, map: through };
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
failureOutput.adopt = () => {
|
|
525
|
+
const handlers = globalThis[shared]?.retrieveMapHandlers;
|
|
526
|
+
if (Array.isArray(handlers) && !handlers.includes(supercovRetrieveSourceMap))
|
|
527
|
+
handlers.unshift(supercovRetrieveSourceMap);
|
|
528
|
+
};
|
|
529
|
+
failureOutput.adopt();
|
|
481
530
|
};
|
|
531
|
+
// source-map-support is ts-node's, loaded when ts-node is: by a runner's own
|
|
532
|
+
// `--require`, after this file, or by Node's, before it.
|
|
533
|
+
{
|
|
534
|
+
const adopt = () => {
|
|
535
|
+
process.__SUPERCOV_FAILURE_OUTPUT__();
|
|
536
|
+
failureOutput.adopt?.();
|
|
537
|
+
};
|
|
538
|
+
if (globalThis[Symbol.for("source-map-support/sharedData")])
|
|
539
|
+
adopt();
|
|
540
|
+
const load = Module._load;
|
|
541
|
+
Module._load = function supercovSourceMapSupportLoad(request, ...rest) {
|
|
542
|
+
const loaded = Reflect.apply(load, this, [request, ...rest]);
|
|
543
|
+
if (typeof request === "string" && request.includes("source-map-support"))
|
|
544
|
+
adopt();
|
|
545
|
+
return loaded;
|
|
546
|
+
};
|
|
547
|
+
}
|
|
482
548
|
// Jest itself prints from here on. Any other process is Jest's from the
|
|
483
549
|
// moment it loads Jest: a worker of its, or a tool that runs Jest in its own
|
|
484
550
|
// process (`require("jest").run()`, as react-scripts and Vue's CLI do), where
|
|
@@ -487,6 +553,9 @@ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
|
|
|
487
553
|
// of process) never loads it.
|
|
488
554
|
if (isJestEntrypoint)
|
|
489
555
|
process.__SUPERCOV_FAILURE_OUTPUT__("jest");
|
|
556
|
+
// tap prints from the process its command starts, and from each test's own.
|
|
557
|
+
else if (process.env.TAP_CHILD_ID || /\/node_modules\/(?:\.bin\/tap$|tap\/|@tapjs\/run\/)/.test(entrypoint))
|
|
558
|
+
process.__SUPERCOV_FAILURE_OUTPUT__("tap");
|
|
490
559
|
else {
|
|
491
560
|
const jest = /(?:^|[\\/])(?:jest|jest-cli|@jest[\\/]core|jest-runner)(?:[\\/]|$)/;
|
|
492
561
|
const load = Module._load;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { relative, resolve, sep } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
6
6
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
7
7
|
import { serverEvidencePath } from "./transport.mjs";
|
|
@@ -133,3 +133,78 @@ export function writeRunnerEvidence(identity, status, scope, evidenceDirectoryOv
|
|
|
133
133
|
};
|
|
134
134
|
appendEvidenceRecord(resolve(process.cwd(), evidenceDirectory), identity.runner.replace(/[^A-Za-z0-9_-]/g, "_"), payload);
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
// A failing test's stack as its author would have seen it, for the runners
|
|
138
|
+
// that print a stack as it is (node:test, AVA).
|
|
139
|
+
//
|
|
140
|
+
// - A frame of Supercov's is one in a file of its own, straight under a
|
|
141
|
+
// `.supercov` directory or its `node_modules`. The copy lies under a
|
|
142
|
+
// `.supercov` directory too, and every frame in it was once dropped for
|
|
143
|
+
// that: a failing node:test test printed no frame of its own file.
|
|
144
|
+
// `AsyncLocalStorage.run` is Node's frame for running a test under
|
|
145
|
+
// Supercov, and goes with the frames around it.
|
|
146
|
+
// - The test ran in the copy, and a frame of a test file named it there.
|
|
147
|
+
// - Node names a module's file by its URL and a CommonJS file by its path.
|
|
148
|
+
// A rewritten file is named through its source map, by path either way:
|
|
149
|
+
// `at seal (/home/me/shop/src/crypto.js:3:11)` for `file:///home/…`.
|
|
150
|
+
// - What is left is cut to the length the author's limit allows; the
|
|
151
|
+
// runtime made room for its own frames while they were on the stack.
|
|
152
|
+
const OWN_FRAME = /[\\/]\.supercov[\\/](?:node_modules[\\/])?[^\\/:]+\.[cm]?js:\d+/u;
|
|
153
|
+
const CARRIER_FRAME = /^\s*at AsyncLocalStorage\.run \(node:internal\//u;
|
|
154
|
+
const FRAME_PATH = /^(\s*at (?:.*? \()?)((?:\/|[A-Za-z]:[\\/])[^()]*?)(:\d+:\d+\)?)$/u;
|
|
155
|
+
const packageTypes = new Map();
|
|
156
|
+
function loadedAsModule(file) {
|
|
157
|
+
if (/\.m[jt]sx?$/u.test(file))
|
|
158
|
+
return true;
|
|
159
|
+
if (/\.c[jt]sx?$/u.test(file))
|
|
160
|
+
return false;
|
|
161
|
+
const visited = [];
|
|
162
|
+
let directory = dirname(file);
|
|
163
|
+
let type;
|
|
164
|
+
for (;;) {
|
|
165
|
+
if (packageTypes.has(directory)) {
|
|
166
|
+
type = packageTypes.get(directory);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
visited.push(directory);
|
|
170
|
+
try {
|
|
171
|
+
type = JSON.parse(readFileSync(resolve(directory, "package.json"), "utf8")).type === "module";
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// No manifest here, or not one that parses: the next directory up.
|
|
176
|
+
}
|
|
177
|
+
const parent = dirname(directory);
|
|
178
|
+
if (parent === directory) {
|
|
179
|
+
type = false;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
directory = parent;
|
|
183
|
+
}
|
|
184
|
+
for (const seen of visited)
|
|
185
|
+
packageTypes.set(seen, type);
|
|
186
|
+
return type;
|
|
187
|
+
}
|
|
188
|
+
//
|
|
189
|
+
// `whereItRan` leaves each file where the stack has it: for a printer that
|
|
190
|
+
// takes its working directory off a frame itself, and would not know the
|
|
191
|
+
// project's.
|
|
192
|
+
export function stackAsWritten(stack, whereItRan = false) {
|
|
193
|
+
const copy = process.env["SUPERCOV_PROJECT_ROOT"];
|
|
194
|
+
const project = whereItRan ? undefined : process.env["SUPERCOV_SOURCE_PROJECT_ROOT"];
|
|
195
|
+
const placed = (text) => copy && project && copy !== project ? text.split(copy).join(project) : text;
|
|
196
|
+
const lines = stack.split("\n");
|
|
197
|
+
const own = lines.map((line) => /^\s*at /u.test(line) && OWN_FRAME.test(line));
|
|
198
|
+
const limit = Error.stackTraceLimit;
|
|
199
|
+
let frames = 0;
|
|
200
|
+
return lines
|
|
201
|
+
.filter((line, index) => !own[index] && !(CARRIER_FRAME.test(line) && (own[index - 1] || own[index + 1])))
|
|
202
|
+
.map((line) => {
|
|
203
|
+
const frame = FRAME_PATH.exec(placed(line));
|
|
204
|
+
if (!frame || !project || !isAbsolute(frame[2]) || !frame[2].startsWith(project + sep) || !loadedAsModule(frame[2]))
|
|
205
|
+
return placed(line);
|
|
206
|
+
return `${frame[1]}${pathToFileURL(frame[2]).href}${frame[3]}`;
|
|
207
|
+
})
|
|
208
|
+
.filter((line) => !/^\s*at /u.test(line) || typeof limit !== "number" || !Number.isFinite(limit) || ++frames <= limit)
|
|
209
|
+
.join("\n");
|
|
210
|
+
}
|
|
@@ -892,31 +892,92 @@ const INSTRUMENTATION_FRAME = /[\\/]\.supercov[\\/](?:node_modules[\\/])?(?:play
|
|
|
892
892
|
// added, at the assertion, and below the runtime's frames the function the
|
|
893
893
|
// author wrote, at the wrapper. Vitest printed both, `:12:35` and `:12:1`.
|
|
894
894
|
// They are one frame again: the author's function, where the assertion is.
|
|
895
|
+
//
|
|
896
|
+
// The runtime's frames are not always one run. `node:assert` called through
|
|
897
|
+
// its module is wrapped a second time, so an assertion that fails there has
|
|
898
|
+
// frames of the runtime above the added function too, and Node's own frame
|
|
899
|
+
// for `AsyncLocalStorage.run` lies among those below it: Mocha printed the
|
|
900
|
+
// added function, that frame and the author's function, three for one.
|
|
901
|
+
const NODE_FRAME = /^\s*at .*\(?node:internal\//u;
|
|
902
|
+
const CARRIER_FRAME = /^\s*at AsyncLocalStorage\.run \(node:internal\//u;
|
|
903
|
+
// An error keeps as many frames as `Error.stackTraceLimit` says, ten unless
|
|
904
|
+
// someone changed it, counted from where it was made. The runtime's frames
|
|
905
|
+
// were counted among them, so the author's own fell off the end: a failed
|
|
906
|
+
// `node:assert` under node:test kept the function Supercov added and lost
|
|
907
|
+
// the test's, and a test's stack lost the runner's frames below it. While
|
|
908
|
+
// the runtime's frames are on the stack there is room for them, and what is
|
|
909
|
+
// left when they are taken out is cut to the length the author asked for.
|
|
910
|
+
const OWN_ROOM = 24;
|
|
911
|
+
function withStackRoom(callback) {
|
|
912
|
+
const limit = Error.stackTraceLimit;
|
|
913
|
+
if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0)
|
|
914
|
+
return callback();
|
|
915
|
+
Error.stackTraceLimit = limit + OWN_ROOM;
|
|
916
|
+
try {
|
|
917
|
+
return callback();
|
|
918
|
+
} finally {
|
|
919
|
+
// Unless the code it ran set a limit of its own.
|
|
920
|
+
if (Error.stackTraceLimit === limit + OWN_ROOM)
|
|
921
|
+
Error.stackTraceLimit = limit;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function withinStackLimit(lines) {
|
|
925
|
+
const limit = Error.stackTraceLimit;
|
|
926
|
+
if (typeof limit !== "number" || !Number.isFinite(limit) || limit < 0)
|
|
927
|
+
return lines;
|
|
928
|
+
let frames = 0;
|
|
929
|
+
return lines.filter((line) => !/^\s*at /.test(line) || ++frames <= limit);
|
|
930
|
+
}
|
|
895
931
|
function cleanInstrumentationStack(error, lexical = false) {
|
|
896
932
|
if (!error || typeof error !== "object" || typeof error.stack !== "string")
|
|
897
933
|
return error;
|
|
898
934
|
const lines = error.stack.split("\n");
|
|
899
935
|
const hidden = lines.map((line, index) => index > 0 && INSTRUMENTATION_FRAME.test(line));
|
|
900
|
-
|
|
901
|
-
if (first < 0)
|
|
936
|
+
if (!hidden.includes(true))
|
|
902
937
|
return error;
|
|
903
|
-
const last = hidden.lastIndexOf(true);
|
|
904
|
-
let visible;
|
|
905
938
|
const frame = /^(\s*at (?:async )?)(?:(.*?) \()?(.*?:\d+:\d+)\)?$/;
|
|
906
|
-
const added = lexical && first > 1 ? frame.exec(lines[first - 1]) : null;
|
|
907
|
-
const written = added && last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
|
|
908
939
|
const file = (location) => location.replace(/:\d+:\d+$/, "");
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
940
|
+
// The function Supercov added is the frame above a run of the runtime's
|
|
941
|
+
// that ends at a frame of the same file: the author's.
|
|
942
|
+
let merged;
|
|
943
|
+
let at = -1;
|
|
944
|
+
for (let start = 2; lexical && !merged && start < lines.length; start += 1) {
|
|
945
|
+
if (!hidden[start] || hidden[start - 1])
|
|
946
|
+
continue;
|
|
947
|
+
let last = start;
|
|
948
|
+
for (let index = start; index < lines.length && (hidden[index] || NODE_FRAME.test(lines[index])); index += 1) {
|
|
949
|
+
if (hidden[index])
|
|
950
|
+
last = index;
|
|
951
|
+
}
|
|
952
|
+
const added = frame.exec(lines[start - 1]);
|
|
953
|
+
const written = last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
|
|
954
|
+
// Both are frames of the author's file, or the error is another's. A name
|
|
955
|
+
// on the added one comes from a source map (Jest names a frame by what is
|
|
956
|
+
// called at its position), and it is the name the author's line would
|
|
957
|
+
// have; the function Supercov added has none of its own.
|
|
958
|
+
if (added && written && file(added[3]) === file(written[3])) {
|
|
959
|
+
const named = added[2] !== void 0 && added[2] !== "<anonymous>";
|
|
960
|
+
at = start - 1;
|
|
961
|
+
merged = {
|
|
962
|
+
last: last + 1,
|
|
963
|
+
text: named || written[2] === void 0 ? lines[at] : `${written[1]}${written[2]} (${added[3]})`
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
start = last;
|
|
967
|
+
}
|
|
968
|
+
const visible = lines.flatMap((line, index) => {
|
|
969
|
+
if (merged && index === at)
|
|
970
|
+
return [merged.text];
|
|
971
|
+
if (merged && index > at && index <= merged.last)
|
|
972
|
+
return [];
|
|
973
|
+
// Node's frame for running code under Supercov goes with the runtime's
|
|
974
|
+
// frames around it.
|
|
975
|
+
if (CARRIER_FRAME.test(line) && (hidden[index - 1] || hidden[index + 1]))
|
|
976
|
+
return [];
|
|
977
|
+
return hidden[index] ? [] : [line];
|
|
978
|
+
});
|
|
918
979
|
try {
|
|
919
|
-
error.stack = visible.join("\n");
|
|
980
|
+
error.stack = withinStackLimit(visible).join("\n");
|
|
920
981
|
} catch (e) {
|
|
921
982
|
}
|
|
922
983
|
return error;
|
|
@@ -931,7 +992,7 @@ function withNodeAssertionPhase(operation, source, callback) {
|
|
|
931
992
|
// with the frames its author would see.
|
|
932
993
|
if (!scope) {
|
|
933
994
|
try {
|
|
934
|
-
return callback
|
|
995
|
+
return withStackRoom(callback);
|
|
935
996
|
} catch (error) {
|
|
936
997
|
throw cleanInstrumentationStack(error, lexical);
|
|
937
998
|
}
|
|
@@ -968,7 +1029,7 @@ function withNodeAssertionPhase(operation, source, callback) {
|
|
|
968
1029
|
attempt.phases.push(phase);
|
|
969
1030
|
attempt.phaseIds.add(phase.id);
|
|
970
1031
|
try {
|
|
971
|
-
const result = withCoverageCarrier({ version: 1, scope, phaseId: phase.id }, callback);
|
|
1032
|
+
const result = withStackRoom(() => withCoverageCarrier({ version: 1, scope, phaseId: phase.id }, callback));
|
|
972
1033
|
if (result && typeof result.then === "function")
|
|
973
1034
|
return Promise.resolve(result).then((value) => {
|
|
974
1035
|
finishAssertionPhase(phase);
|
|
@@ -1886,6 +1947,7 @@ const directRuntimeApi = {
|
|
|
1886
1947
|
coverageContextEnvironment,
|
|
1887
1948
|
coverageContextHeaders,
|
|
1888
1949
|
cleanInstrumentationStack,
|
|
1950
|
+
withStackRoom,
|
|
1889
1951
|
coverageHit,
|
|
1890
1952
|
coverageHitV2,
|
|
1891
1953
|
coverageSnapshot,
|
|
@@ -1950,6 +2012,7 @@ export {
|
|
|
1950
2012
|
coverageContextEnvironment,
|
|
1951
2013
|
coverageContextHeaders,
|
|
1952
2014
|
cleanInstrumentationStack,
|
|
2015
|
+
withStackRoom,
|
|
1953
2016
|
coverageHit,
|
|
1954
2017
|
coverageHitV2,
|
|
1955
2018
|
coverageSnapshot,
|
|
@@ -19,9 +19,10 @@ import { runnerExecutionScope, writeRunnerEvidence } from "./runnerEvidence.mjs"
|
|
|
19
19
|
async function subtestClasses() {
|
|
20
20
|
const classes = [];
|
|
21
21
|
let fromTap;
|
|
22
|
+
let tap;
|
|
22
23
|
try {
|
|
23
24
|
const fromProject = createRequire(join(process.cwd(), "package.json"));
|
|
24
|
-
|
|
25
|
+
tap = realpathSync(dirname(fromProject.resolve("tap/package.json")));
|
|
25
26
|
fromTap = createRequire(join(tap, "package.json"));
|
|
26
27
|
}
|
|
27
28
|
catch {
|
|
@@ -54,6 +55,22 @@ async function subtestClasses() {
|
|
|
54
55
|
// libtap is CommonJS, and its exports hide lib/: reach it from its main.
|
|
55
56
|
try {
|
|
56
57
|
const lib = dirname(fromTap.resolve("libtap"));
|
|
58
|
+
// tap sets libtap up before it loads it: which frames are tap's own,
|
|
59
|
+
// and how a frame is taken through a source map. Loaded from here
|
|
60
|
+
// first, libtap kept its defaults for the whole file, and a failure
|
|
61
|
+
// listed tap's frames under the test's.
|
|
62
|
+
try {
|
|
63
|
+
fromTap(join(tap, "settings.js"));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// A tap that has no settings of its own leaves libtap's.
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
nameFromTheCopy(createRequire(join(lib, "test.js"))("stack-utils"));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Frames stay as libtap writes them.
|
|
73
|
+
}
|
|
57
74
|
classes.push(createRequire(import.meta.url)(join(lib, "test.js")));
|
|
58
75
|
}
|
|
59
76
|
catch {
|
|
@@ -62,6 +79,49 @@ async function subtestClasses() {
|
|
|
62
79
|
return classes;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
// libtap takes the directory the test runs in off a frame, which under
|
|
83
|
+
// Supercov is the copy, while a measured file's frame names the file its
|
|
84
|
+
// author wrote: the whole of `/home/me/shop/src/crypto.js` for
|
|
85
|
+
// `src/crypto.js`. A frame in the project is placed in the copy before
|
|
86
|
+
// libtap reads it, as Mocha's are.
|
|
87
|
+
function nameFromTheCopy(StackUtils) {
|
|
88
|
+
const real = (directory) => {
|
|
89
|
+
try {
|
|
90
|
+
return directory ? realpathSync(directory).replace(/\\/g, "/") : undefined;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const project = real(process.env.SUPERCOV_SOURCE_PROJECT_ROOT);
|
|
97
|
+
const copy = real(process.env.SUPERCOV_PROJECT_ROOT);
|
|
98
|
+
const { clean, at, parseLine } = StackUtils?.prototype ?? {};
|
|
99
|
+
if (!project || !copy || project === copy || typeof clean !== "function" || clean.supercovTap)
|
|
100
|
+
return;
|
|
101
|
+
const inTheProject = (text) => text.includes(`${project}/`) && !text.includes(`${copy}/`);
|
|
102
|
+
StackUtils.prototype.clean = function supercovTapClean(stack, ...rest) {
|
|
103
|
+
const lines = (Array.isArray(stack) ? stack : String(stack).split("\n")).map((line) => {
|
|
104
|
+
const text = String(line).replace(/\\/g, "/");
|
|
105
|
+
return inTheProject(text) ? text.split(`${project}/`).join(`${copy}/`) : line;
|
|
106
|
+
});
|
|
107
|
+
return Reflect.apply(clean, this, [lines, ...rest]);
|
|
108
|
+
};
|
|
109
|
+
StackUtils.prototype.clean.supercovTap = true;
|
|
110
|
+
const placed = (found) => {
|
|
111
|
+
if (typeof found?.file === "string" && found.file.startsWith(`${project}/`) && !found.file.startsWith(`${copy}/`))
|
|
112
|
+
found.file = found.file.slice(project.length + 1);
|
|
113
|
+
return found;
|
|
114
|
+
};
|
|
115
|
+
if (typeof at === "function")
|
|
116
|
+
StackUtils.prototype.at = function supercovTapAt(...given) {
|
|
117
|
+
return placed(Reflect.apply(at, this, given));
|
|
118
|
+
};
|
|
119
|
+
if (typeof parseLine === "function")
|
|
120
|
+
StackUtils.prototype.parseLine = function supercovTapParseLine(...given) {
|
|
121
|
+
return placed(Reflect.apply(parseLine, this, given));
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
65
125
|
export async function adaptTap(testFile) {
|
|
66
126
|
const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
|
|
67
127
|
if (!evidenceDirectory)
|