supercov 4.13.0 → 4.15.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 +1 -1
- package/docs/cli.md +76 -5
- package/docs/evidence.md +8 -1
- package/package.json +10 -11
- package/runtime/javascript/jest.cjs +18 -5
- package/runtime/javascript/jestReporter.mjs +2 -1
- package/runtime/javascript/mocha.mjs +2 -1
- package/runtime/javascript/playwright.mjs +36 -23
- package/runtime/javascript/playwrightReporter.mjs +2 -1
- package/runtime/javascript/provenance.mjs +40 -1
- package/runtime/javascript/runnerEvidence.mjs +2 -1
- package/runtime/javascript/runtime.mjs +45 -20
- package/runtime/javascript/vitest.mjs +2 -1
- package/runtime/javascript/vitestReporter.mjs +3 -1
package/docs/agent-loop.md
CHANGED
|
@@ -122,7 +122,7 @@ The file query explained the gap:
|
|
|
122
122
|
```text
|
|
123
123
|
LINE STATUS SOURCE
|
|
124
124
|
2 PARTIAL signedIn && !expired
|
|
125
|
-
Unobserved:
|
|
125
|
+
Unobserved: `!expired` was never false; needs `signedIn` true, `!expired` false, decision false
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
Both return paths had run, but neither test checked an expired session. MC/DC
|
package/docs/cli.md
CHANGED
|
@@ -159,7 +159,7 @@ npx supercov runs <run-id> [query] [options]
|
|
|
159
159
|
| `files` | See every included file, including fully covered files; `--group dir` shows coverage by directory, split by test kind |
|
|
160
160
|
| `file <path>` | Inspect the open obligations in one file |
|
|
161
161
|
| `decision <id \| path:line>` | Understand missing boolean outcomes and MC/DC witnesses |
|
|
162
|
-
| `line <path:line
|
|
162
|
+
| `line <path:line>...` | See a line's state, obligations, and covering tests; several lines, or `--from-file` with one per line, in one call |
|
|
163
163
|
| `test <id \| name>` | See the coverage attributed to one test |
|
|
164
164
|
| `tests without-evidence` | List the tests that made assertions and recorded no coverage, which the summary counts in a warning |
|
|
165
165
|
| `kinds` | Group coverage by test level, such as unit or E2E |
|
|
@@ -204,6 +204,28 @@ In `--json`, every file of `files` and `gaps` carries `totals` and `covered`
|
|
|
204
204
|
for each metric beside what is missing, so a percentage can be worked out for
|
|
205
205
|
any file or set of files.
|
|
206
206
|
|
|
207
|
+
`line` reads the run once for every location it is given, so asking for
|
|
208
|
+
twenty lines in one call costs about what one does. `--from-file` takes the
|
|
209
|
+
locations from a file, one per line, skipping blank lines and `#` comments.
|
|
210
|
+
Each line of several lists its state, its gaps and its first three covering
|
|
211
|
+
tests, with `totalTests` and `totalPhases` for the rest; asked alone, a line
|
|
212
|
+
lists every test and phase. In `--json` the reply is `coverage.lines`, one
|
|
213
|
+
entry per location in order, each location once. Locations that do not fit one
|
|
214
|
+
reply are listed in `remaining`, and `pagination.nextOffset` says where the next
|
|
215
|
+
reply starts: ask again with `--offset`, as for any page.
|
|
216
|
+
|
|
217
|
+
```sh supercov
|
|
218
|
+
npx supercov runs latest line app/routes/checkout.ts:42 app/routes/checkout.ts:57
|
|
219
|
+
npx supercov runs latest line --from-file lines.txt --json
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
An MC/DC gap names the value each missing condition never took, in `file`,
|
|
223
|
+
`line` and `decision` (`` `sum < 50` was never true; needs `express` false,
|
|
224
|
+
`sum < 50` true, decision true ``). In `--json` it is `missingOutcomes`:
|
|
225
|
+
`["true"]` or `["false"]`, both when no test evaluated the condition, and
|
|
226
|
+
none when it took both values but never decided the result alone.
|
|
227
|
+
`file --group decision` lists the missing conditions under each decision.
|
|
228
|
+
|
|
207
229
|
Run any query with `--help` to see only the options valid for that query:
|
|
208
230
|
|
|
209
231
|
```sh supercov
|
|
@@ -249,6 +271,10 @@ with `--min-lines`, `--min-statements`, `--min-functions`, `--min-branches` or
|
|
|
249
271
|
eligible obligations, in addition to the whole run. Both report the counts
|
|
250
272
|
behind the percentage and, for lines, where the gaps are.
|
|
251
273
|
|
|
274
|
+
`--kind`, `--runner`, `--tag`, `--title` and `--test-file` hold some of the
|
|
275
|
+
run's tests to a floor of their own, `--kind e2e --min-lines 60` say, from the
|
|
276
|
+
same run.
|
|
277
|
+
|
|
252
278
|
Floors are compared against the counts, never a rounded percentage: 9,999
|
|
253
279
|
covered lines out of 10,000 displays as 99.99% and fails a 100% floor, and a
|
|
254
280
|
run that displayed `100.00%` could never pass one while something is uncovered.
|
|
@@ -287,6 +313,9 @@ changed file that looks like product source but is absent from the run is named
|
|
|
287
313
|
separately, because treating it as zero uncovered lines would report success for
|
|
288
314
|
code nothing ran.
|
|
289
315
|
|
|
316
|
+
`--kind`, `--runner`, `--tag`, `--title` and `--test-file` ask whether some of
|
|
317
|
+
the tests reach the change: the E2E tests, or the tests of one file.
|
|
318
|
+
|
|
290
319
|
`--annotate github` prints workflow-command annotations on stdout, combining
|
|
291
320
|
adjacent misses into one range and capping the total (`--max-annotations`). It
|
|
292
321
|
needs no token and posts no comment.
|
|
@@ -358,9 +387,10 @@ npx supercov runs latest gaps --format jsonl > gaps.jsonl
|
|
|
358
387
|
|
|
359
388
|
Each line holds the file, the line, the source of that line and what `runs
|
|
360
389
|
<id> file` says of the obligation: for an MC/DC condition its decision, the
|
|
361
|
-
condition and the vectors observed. `--metric`, `--filter`, `--kind
|
|
362
|
-
`--runner` narrow it as they narrow
|
|
363
|
-
an agent reads a run's every gap in
|
|
390
|
+
condition and the vectors observed. `--metric`, `--filter`, `--kind`,
|
|
391
|
+
`--runner`, `--tag`, `--title` and `--test-file` narrow it as they narrow
|
|
392
|
+
`gaps`, and nothing is paged, so a tool or an agent reads a run's every gap in
|
|
393
|
+
one pass instead of a page at a time.
|
|
364
394
|
|
|
365
395
|
```sh supercov-example
|
|
366
396
|
npx supercov runs report --format lcov --output coverage/lcov.info
|
|
@@ -369,7 +399,14 @@ npx supercov runs report --format html --output coverage/report
|
|
|
369
399
|
```
|
|
370
400
|
|
|
371
401
|
Both are written from the same view `check` and `patch` read, so a viewer,
|
|
372
|
-
hosted service or CI integration sees the totals Supercov enforced.
|
|
402
|
+
hosted service or CI integration sees the totals Supercov enforced. `--kind`,
|
|
403
|
+
`--runner`, `--tag`, `--title` and `--test-file` write the coverage of only
|
|
404
|
+
those tests, from the same run:
|
|
405
|
+
|
|
406
|
+
```sh supercov-example
|
|
407
|
+
npx supercov runs report --format lcov --kind e2e --output coverage/e2e.info
|
|
408
|
+
```
|
|
409
|
+
Paths are
|
|
373
410
|
repository-relative with forward slashes, ordering is stable, and the file is
|
|
374
411
|
written atomically; an existing file is kept unless you pass `--force`. Without
|
|
375
412
|
`--output` the report goes to stdout and diagnostics to stderr, so a redirect
|
|
@@ -422,12 +459,42 @@ one as a public artifact.
|
|
|
422
459
|
| `--filter all \| passed \| failed` | Recalculate the view from all, successful, or failed attempts |
|
|
423
460
|
| `--kind <kind>` | Restrict to a test level such as `unit`, `integration`, or `e2e` |
|
|
424
461
|
| `--runner <runner>` | Restrict to one runner |
|
|
462
|
+
| `--tag <tag>` | Restrict to tests with this tag; repeat it for tests with any of several, and start a value with `!` to leave those tests out |
|
|
463
|
+
| `--title <text>` | Restrict to tests whose name contains this text |
|
|
464
|
+
| `--test-file <path>` | Restrict to tests in this file, or in this directory |
|
|
425
465
|
| `--metric all \| lines \| statements \| functions \| branches \| mcdc` | Choose a metric for `files`, `gaps`, `diff`, or `minimize` |
|
|
426
466
|
| `--limit N`, `--offset N` | Page through a collection |
|
|
427
467
|
| `--json` | Return the machine-readable form |
|
|
428
468
|
|
|
429
469
|
Collection output includes a copyable command for the next page.
|
|
430
470
|
|
|
471
|
+
### Read some of a run's tests
|
|
472
|
+
|
|
473
|
+
`--tag`, `--title` and `--test-file` read the coverage of only the tests they
|
|
474
|
+
pick, from a run already measured: the API tests of a Playwright suite, the
|
|
475
|
+
tests of one spec file, everything named `checkout`. Each takes several values
|
|
476
|
+
for tests matching any of them, and when several are given a test must match
|
|
477
|
+
each. A value starting with `!` leaves those tests out, so the browser tests of
|
|
478
|
+
a suite whose other tests are tagged read as the tests with none of the tags.
|
|
479
|
+
|
|
480
|
+
```sh supercov
|
|
481
|
+
npx supercov runs latest --tag @api
|
|
482
|
+
npx supercov runs latest gaps --test-file tests/e2e/checkout.spec.ts
|
|
483
|
+
npx supercov runs latest files --title checkout --tag smoke
|
|
484
|
+
npx supercov runs latest --tag '!api' --tag '!cron' --tag '!webhook'
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
A tag is what the runner gives a test: a Playwright tag (`{ tag: '@api' }`, or
|
|
488
|
+
an `@word` in the title), an `@word` in a Jest, Vitest, Mocha, `node:test`,
|
|
489
|
+
AVA or tap title, a pytest marker, an RSpec tag or `type`, a Cucumber tag, a
|
|
490
|
+
JUnit `@Tag` or a TestNG group. Tags are written with or without the `@` and in
|
|
491
|
+
any case. `runs <id> test <name>` lists a test's tags.
|
|
492
|
+
|
|
493
|
+
The first query with a filter works out its coverage from the run's evidence
|
|
494
|
+
and keeps it beside the run (12 s for a run of about 3,000 tests); asking again
|
|
495
|
+
reads it back in a fraction of a second. Nothing is added to the test run
|
|
496
|
+
itself.
|
|
497
|
+
|
|
431
498
|
For a large file, group and rank its decisions:
|
|
432
499
|
|
|
433
500
|
```sh supercov
|
|
@@ -514,6 +581,10 @@ npx supercov report --runs 10
|
|
|
514
581
|
npx supercov report --output artifacts/supercov-report.html --no-open
|
|
515
582
|
```
|
|
516
583
|
|
|
584
|
+
`--kind`, `--runner`, `--tag`, `--title` and `--test-file` make a report of
|
|
585
|
+
only those tests' coverage, in every run it holds; a run with none of them is
|
|
586
|
+
left out, and the report names the tests it shows.
|
|
587
|
+
|
|
517
588
|
A report includes the five newest runs; `--runs N` includes up to 20. A large
|
|
518
589
|
project's report can pass GitHub's 25 MB attachment limit; `--runs 1` makes
|
|
519
590
|
the smallest. See [Portable HTML reports](reports.md) for
|
package/docs/evidence.md
CHANGED
|
@@ -37,7 +37,9 @@ later queries faster; it can rebuild that index from the run.
|
|
|
37
37
|
|
|
38
38
|
Supercov compares a stored run with the current workspace. If relevant source,
|
|
39
39
|
tests, dependencies, configuration, or toolchain inputs changed, the run is
|
|
40
|
-
marked stale.
|
|
40
|
+
marked stale. A query of a stale run names the files that changed since it,
|
|
41
|
+
tests and source, those added since its commit included, so you can tell which
|
|
42
|
+
tests are newer than the run.
|
|
41
43
|
|
|
42
44
|
A stale run is still valid history. It simply should not be presented as the
|
|
43
45
|
current state of the repository. Run the complete suite again before choosing
|
|
@@ -126,6 +128,11 @@ proof of what the test checks. A name such as `gatewayE2e.test.ts` identifies an
|
|
|
126
128
|
E2E test. Ambiguous names keep the runner's default, and the report tells you
|
|
127
129
|
how many tests use that default.
|
|
128
130
|
|
|
131
|
+
A tag naming a level — `@unit`, `@component`, `@integration` or `@e2e` — sets
|
|
132
|
+
a test's kind, ahead of its path and runner: a Playwright tag or an `@word` in
|
|
133
|
+
a JavaScript test's title, a pytest marker, an RSpec tag, a Cucumber tag, a
|
|
134
|
+
JUnit `@Tag` or a TestNG group. Other tags stay tags, which `--tag` selects by.
|
|
135
|
+
|
|
129
136
|
If your suite has a known kind, set it when running the command:
|
|
130
137
|
|
|
131
138
|
```sh supercov-example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.0",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
"test:fixture": "cargo build -p supercov && node scripts/rust-fixture-matrix.mjs",
|
|
75
75
|
"test:packed-npx": "cargo build --release -p supercov && node scripts/packed-npx-integration.mjs",
|
|
76
76
|
"test:isolation": "cargo build -p supercov && node scripts/isolation-integration.mjs",
|
|
77
|
-
"test:filesystem": "cargo test --workspace && cargo build -p supercov && node scripts/workspace-crash-integration.mjs",
|
|
78
77
|
"test:watchdog": "cargo build -p supercov && node scripts/watchdog-integration.mjs",
|
|
79
78
|
"test:html-report": "cargo build -p supercov && node scripts/html-report-integration.mjs",
|
|
80
79
|
"test:engine-contract": "cargo build -p supercov && node scripts/engine-contract.mjs",
|
|
@@ -83,7 +82,7 @@
|
|
|
83
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",
|
|
84
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",
|
|
85
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",
|
|
86
|
-
"test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-jest-integration.mjs && node scripts/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/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/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/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",
|
|
87
86
|
"test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
|
|
88
87
|
"test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
89
88
|
"test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
|
|
@@ -120,14 +119,14 @@
|
|
|
120
119
|
"test:windows-tls": "node scripts/windows-tls-test.mjs"
|
|
121
120
|
},
|
|
122
121
|
"optionalDependencies": {
|
|
123
|
-
"@supercov/cli-darwin-arm64": "4.
|
|
124
|
-
"@supercov/cli-darwin-x64": "4.
|
|
125
|
-
"@supercov/cli-linux-arm64-gnu": "4.
|
|
126
|
-
"@supercov/cli-linux-arm64-musl": "4.
|
|
127
|
-
"@supercov/cli-linux-x64-gnu": "4.
|
|
128
|
-
"@supercov/cli-linux-x64-musl": "4.
|
|
129
|
-
"@supercov/cli-win32-arm64": "4.
|
|
130
|
-
"@supercov/cli-win32-x64": "4.
|
|
122
|
+
"@supercov/cli-darwin-arm64": "4.15.0",
|
|
123
|
+
"@supercov/cli-darwin-x64": "4.15.0",
|
|
124
|
+
"@supercov/cli-linux-arm64-gnu": "4.15.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-musl": "4.15.0",
|
|
126
|
+
"@supercov/cli-linux-x64-gnu": "4.15.0",
|
|
127
|
+
"@supercov/cli-linux-x64-musl": "4.15.0",
|
|
128
|
+
"@supercov/cli-win32-arm64": "4.15.0",
|
|
129
|
+
"@supercov/cli-win32-x64": "4.15.0"
|
|
131
130
|
},
|
|
132
131
|
"peerDependencies": {
|
|
133
132
|
"@playwright/test": ">=1.55.0",
|
|
@@ -67,14 +67,27 @@ const KINDS = [
|
|
|
67
67
|
["integration", /(^|[/_.-])(integration|int)([/_.-]|$)/i],
|
|
68
68
|
["e2e", /(^|[/_.-])(e2e|end-to-end|offline|online)([/_.-]|$)/i],
|
|
69
69
|
];
|
|
70
|
-
|
|
70
|
+
// As provenance.mjs reads them: `@word` in a test's name, without the `@`.
|
|
71
|
+
const TITLE_TAG = /(?:^|\s)@([\p{L}\p{N}_][\p{L}\p{N}_:./-]*)/gu;
|
|
72
|
+
const KIND_TAGS = new Set(["unit", "component", "integration", "e2e"]);
|
|
73
|
+
function provenance(testFile, name) {
|
|
74
|
+
const tags = [];
|
|
75
|
+
if (typeof name === "string" && name.includes("@")) {
|
|
76
|
+
for (const match of name.matchAll(TITLE_TAG))
|
|
77
|
+
if (!tags.includes(match[1]))
|
|
78
|
+
tags.push(match[1]);
|
|
79
|
+
}
|
|
80
|
+
const carried = tags.length > 0 ? { tags } : {};
|
|
71
81
|
const explicit = process.env.SUPERCOV_TEST_KIND?.trim();
|
|
72
82
|
if (explicit)
|
|
73
|
-
return { runner: "jest", kind: explicit.toLowerCase(), source: "explicit" };
|
|
83
|
+
return { runner: "jest", kind: explicit.toLowerCase(), source: "explicit", ...carried };
|
|
84
|
+
const tagged = tags.find((tag) => KIND_TAGS.has(tag.toLowerCase()));
|
|
85
|
+
if (tagged)
|
|
86
|
+
return { runner: "jest", kind: tagged.toLowerCase(), source: "tag", ...carried };
|
|
74
87
|
const kind = KINDS.find(([, pattern]) => pattern.test(testFile))?.[0];
|
|
75
88
|
return kind
|
|
76
|
-
? { runner: "jest", kind, source: "path" }
|
|
77
|
-
: { runner: "jest", kind: "unit", source: "runner-default" };
|
|
89
|
+
? { runner: "jest", kind, source: "path", ...carried }
|
|
90
|
+
: { runner: "jest", kind: "unit", source: "runner-default", ...carried };
|
|
78
91
|
}
|
|
79
92
|
// One journal per writer, as atomic.mjs's appendEvidenceRecord keeps it: a
|
|
80
93
|
// file per test cost a directory, a file and a rename each. Jest gives every
|
|
@@ -185,7 +198,7 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
|
|
|
185
198
|
retry: current.retry,
|
|
186
199
|
// The reporter's record carries the outcome.
|
|
187
200
|
status: "unknown",
|
|
188
|
-
provenance: provenance(current.testFile),
|
|
201
|
+
provenance: provenance(current.testFile, current.fullName),
|
|
189
202
|
phases: runtime.takeNodeAssertionPhases(scope),
|
|
190
203
|
runtime: [runtime.coverageSnapshot()],
|
|
191
204
|
browser: [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { relative, resolve, sep } from "node:path";
|
|
3
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
3
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
4
4
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
5
5
|
function digest(value) {
|
|
6
6
|
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
@@ -52,6 +52,7 @@ export default class SupercovJestReporter {
|
|
|
52
52
|
runner: "jest",
|
|
53
53
|
file: testFile,
|
|
54
54
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
55
|
+
tags: testTags(undefined, [result.fullName]),
|
|
55
56
|
});
|
|
56
57
|
const record = (attempt, attemptStatus, flaky) => {
|
|
57
58
|
const payload = {
|
|
@@ -11,7 +11,7 @@ import { realpathSync } from "node:fs";
|
|
|
11
11
|
import { join, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
13
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
14
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
14
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
15
15
|
import { runnerExecutionScope } from "./runnerEvidence.mjs";
|
|
16
16
|
|
|
17
17
|
const SETUP_HOOK = /^"(?:before|after) all" hook/;
|
|
@@ -156,6 +156,7 @@ function observe(runner, runtime, write) {
|
|
|
156
156
|
runner: "mocha",
|
|
157
157
|
file: testFile,
|
|
158
158
|
explicitKind: process.env.SUPERCOV_TEST_KIND,
|
|
159
|
+
tags: id.role ? [] : testTags(undefined, [id.name]),
|
|
159
160
|
}),
|
|
160
161
|
...(scope ? { phases: runtime.takeNodeAssertionPhases(scope) } : {}),
|
|
161
162
|
runtime: snapshot ? [snapshot] : [],
|
|
@@ -8,15 +8,15 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
10
|
import childProcess from "node:child_process";
|
|
11
|
-
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import http from "node:http";
|
|
13
13
|
import https from "node:https";
|
|
14
14
|
import { syncBuiltinESMExports } from "node:module";
|
|
15
|
-
import { relative, resolve, sep } from "node:path";
|
|
15
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import * as standardPlaywright from "@playwright/test";
|
|
18
18
|
import * as coverageRuntime from "./runtime.mjs";
|
|
19
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
19
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
20
20
|
import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
|
|
21
21
|
import { COVERAGE_PHASE_HEADER, COVERAGE_PHASE_COOKIE, COVERAGE_PHASE_SITE_COOKIE, COVERAGE_SCOPE_COOKIE, COVERAGE_SCOPE_HEADER, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_CARRIER_ENV, encodeCoverageCarrier, encodeCoverageScope, serverEvidenceDirectory, serverEvidencePath, } from "./transport.mjs";
|
|
22
22
|
export * from "@playwright/test";
|
|
@@ -194,7 +194,6 @@ class CoveragePhaseController {
|
|
|
194
194
|
pages = new Set();
|
|
195
195
|
workers = new Set();
|
|
196
196
|
contexts = new Set();
|
|
197
|
-
contextConfiguredHeaders = new Map();
|
|
198
197
|
cdpSessions = new Map();
|
|
199
198
|
newDocumentScriptIds = new Map();
|
|
200
199
|
pendingRegistrations = new Set();
|
|
@@ -387,7 +386,7 @@ class CoveragePhaseController {
|
|
|
387
386
|
for (const context of liveTrackedContexts()) {
|
|
388
387
|
if (!this.contexts.has(context)) {
|
|
389
388
|
const headersKnown = trackedContextConfiguredHeaders.has(context);
|
|
390
|
-
await this.registerContext(context,
|
|
389
|
+
await this.registerContext(context, { adopted: !headersKnown }).catch(() => undefined);
|
|
391
390
|
}
|
|
392
391
|
}
|
|
393
392
|
}
|
|
@@ -454,7 +453,7 @@ class CoveragePhaseController {
|
|
|
454
453
|
const context = page.context();
|
|
455
454
|
const tracked = liveTrackedContexts().has(context);
|
|
456
455
|
const headersKnown = trackedContextConfiguredHeaders.has(context);
|
|
457
|
-
await this.registerContext(context,
|
|
456
|
+
await this.registerContext(context, {
|
|
458
457
|
adopted: !this.contexts.has(context) && tracked && !headersKnown,
|
|
459
458
|
});
|
|
460
459
|
const cdp = await page.context().newCDPSession(page).catch(() => undefined);
|
|
@@ -490,11 +489,11 @@ class CoveragePhaseController {
|
|
|
490
489
|
for (const worker of page.workers())
|
|
491
490
|
void this.registerWorker(worker);
|
|
492
491
|
}
|
|
493
|
-
async registerContext(context,
|
|
492
|
+
async registerContext(context, { adopted = false } = {}) {
|
|
494
493
|
if (this.contexts.has(context) || this.disposed)
|
|
495
494
|
return;
|
|
496
495
|
this.contexts.add(context);
|
|
497
|
-
|
|
496
|
+
patchContextPrototype(context);
|
|
498
497
|
if (adopted)
|
|
499
498
|
this.adoptedContexts.add(context);
|
|
500
499
|
await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
|
|
@@ -646,14 +645,6 @@ class CoveragePhaseController {
|
|
|
646
645
|
this.disposed = true;
|
|
647
646
|
await Promise.all([...this.pendingRegistrations]);
|
|
648
647
|
await this.scriptUpdate;
|
|
649
|
-
// A worker-scoped context survives this test. Remove its test scope
|
|
650
|
-
// while preserving the exact headers supplied by the suite so work
|
|
651
|
-
// between tests cannot be charged to the attempt that just ended.
|
|
652
|
-
await Promise.all([...this.contexts]
|
|
653
|
-
.filter((context) => !this.adoptedContexts.has(context))
|
|
654
|
-
.map((context) => context
|
|
655
|
-
.setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? {})
|
|
656
|
-
.catch(() => undefined)));
|
|
657
648
|
for (const [context, listeners] of this.contextListeners) {
|
|
658
649
|
for (const [event, listener] of listeners)
|
|
659
650
|
context.off?.(event, listener);
|
|
@@ -773,7 +764,7 @@ class CoveragePhaseController {
|
|
|
773
764
|
const configuredHeaders = sourceArgs?.[0]
|
|
774
765
|
?.extraHTTPHeaders ?? this.configuredHeaders;
|
|
775
766
|
trackContext(result, configuredHeaders);
|
|
776
|
-
await this.registerContext(result
|
|
767
|
+
await this.registerContext(result);
|
|
777
768
|
}
|
|
778
769
|
if (typeof candidate["frames"] === "function" &&
|
|
779
770
|
typeof candidate["context"] === "function")
|
|
@@ -896,11 +887,10 @@ class CoveragePhaseController {
|
|
|
896
887
|
[scopeName, scope],
|
|
897
888
|
...(phase ? [[phaseName, phase]] : []),
|
|
898
889
|
];
|
|
890
|
+
// The context's own headers are left alone: they are the suite's, and
|
|
891
|
+
// a test that sets more with `setExtraHTTPHeaders` keeps them. Setting
|
|
892
|
+
// them back at every phase dropped a test's `X-Forwarded-For`.
|
|
899
893
|
await Promise.all([
|
|
900
|
-
// The context's own headers are the owner's, unchanged.
|
|
901
|
-
context
|
|
902
|
-
.setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? this.configuredHeaders)
|
|
903
|
-
.catch(() => undefined),
|
|
904
894
|
...this.appHosts.map((url) => {
|
|
905
895
|
const host = new URL(url);
|
|
906
896
|
// For the app's own site: requests, navigations and sockets.
|
|
@@ -1078,7 +1068,7 @@ function patchBrowserPrototype(browser) {
|
|
|
1078
1068
|
trackContext(context, configuredHeaders);
|
|
1079
1069
|
const controller = activeController;
|
|
1080
1070
|
if (controller && !controller.contexts.has(context))
|
|
1081
|
-
await controller.registerContext(context
|
|
1071
|
+
await controller.registerContext(context).catch(() => undefined);
|
|
1082
1072
|
return context;
|
|
1083
1073
|
});
|
|
1084
1074
|
}
|
|
@@ -1088,7 +1078,29 @@ function patchBrowserPrototype(browser) {
|
|
|
1088
1078
|
* a customer context opened mid-test is usually closed in a `finally`; either
|
|
1089
1079
|
* way the page is gone by the time the test's snapshots are collected.
|
|
1090
1080
|
*/
|
|
1081
|
+
/** The cookies Supercov gives a context to carry the test's scope. */
|
|
1082
|
+
const OWN_COOKIES = new Set([COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE]);
|
|
1083
|
+
const suiteCookies = (cookies) => (Array.isArray(cookies) ? cookies.filter((cookie) => !OWN_COOKIES.has(cookie?.name)) : cookies);
|
|
1091
1084
|
function patchContextPrototype(context) {
|
|
1085
|
+
// A test reads and saves the cookies it and its app set, never
|
|
1086
|
+
// Supercov's: an assertion on `cookies()` failed only under measurement,
|
|
1087
|
+
// and `storageState({ path })` wrote the scope into the suite's saved
|
|
1088
|
+
// login. A `clearCookies()` takes the scope too, and the test's next
|
|
1089
|
+
// action puts it back, as every action does.
|
|
1090
|
+
patchOnce(context, "cookies", (original) => async function (...args) {
|
|
1091
|
+
return suiteCookies(await original.apply(this, args));
|
|
1092
|
+
});
|
|
1093
|
+
patchOnce(context, "storageState", (original) => async function (options, ...rest) {
|
|
1094
|
+
const { path, ...others } = options && typeof options === "object" ? options : {};
|
|
1095
|
+
const state = await original.call(this, others, ...rest);
|
|
1096
|
+
const own = state && typeof state === "object" ? { ...state, cookies: suiteCookies(state.cookies) } : state;
|
|
1097
|
+
if (path) {
|
|
1098
|
+
// As Playwright writes it.
|
|
1099
|
+
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
1100
|
+
writeFileSync(path, JSON.stringify(own, undefined, 2));
|
|
1101
|
+
}
|
|
1102
|
+
return own;
|
|
1103
|
+
});
|
|
1092
1104
|
patchOnce(context, "close", (original) => async function (...args) {
|
|
1093
1105
|
for (const page of this.pages()) {
|
|
1094
1106
|
await controllerOwning(page)?.snapshotBeforeClose(page).catch(() => undefined);
|
|
@@ -1128,7 +1140,7 @@ function installBrowserLaunchTracking() {
|
|
|
1128
1140
|
trackContext(context, configuredHeaders);
|
|
1129
1141
|
const controller = activeController;
|
|
1130
1142
|
if (controller)
|
|
1131
|
-
await controller.registerContext(context
|
|
1143
|
+
await controller.registerContext(context).catch(() => undefined);
|
|
1132
1144
|
return context;
|
|
1133
1145
|
});
|
|
1134
1146
|
}
|
|
@@ -1603,6 +1615,7 @@ const instrumentedFixtures = {
|
|
|
1603
1615
|
file: testFile,
|
|
1604
1616
|
project: testInfo.project.name,
|
|
1605
1617
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
1618
|
+
tags: testTags(testInfo.tags, testInfo.titlePath),
|
|
1606
1619
|
}),
|
|
1607
1620
|
phases: mergeCollectedPhases(controller.phases, directRuntime()?.takeNodeAssertionPhases(scope) ?? []),
|
|
1608
1621
|
...(controller.workerEvidenceMissing ? { attribution: "partial" } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
4
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
5
5
|
import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
|
|
6
6
|
const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
|
|
7
7
|
const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}`)
|
|
@@ -94,6 +94,7 @@ export default class SupercovPlaywrightReporter {
|
|
|
94
94
|
file: testFile,
|
|
95
95
|
project: test.parent.project()?.name,
|
|
96
96
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
97
|
+
tags: testTags(test.tags, test.titlePath()),
|
|
97
98
|
}),
|
|
98
99
|
browser: [],
|
|
99
100
|
server: [],
|
|
@@ -49,7 +49,46 @@ function basenameKind(file) {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
return segmentKind(file.split(/[/\\]/).pop());
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// A tag naming a test level says what the test is, as an explicit kind does.
|
|
53
|
+
const KIND_TAGS = new Set(["unit", "component", "integration", "e2e"]);
|
|
54
|
+
// `@word` in a title is a tag, as Playwright reads one: `checkout @e2e`.
|
|
55
|
+
const TITLE_TAG = /(?:^|\s)@([\p{L}\p{N}_][\p{L}\p{N}_:./-]*)/gu;
|
|
56
|
+
// The tags a test carries: those its runner names, then any `@word` in its
|
|
57
|
+
// titles, without the `@`, each once. Only text the runner already handed
|
|
58
|
+
// over is read, so a run costs what it did.
|
|
59
|
+
export function testTags(named, titles) {
|
|
60
|
+
const tags = [];
|
|
61
|
+
const add = (value) => {
|
|
62
|
+
const tag = String(value).trim().replace(/^@/, "");
|
|
63
|
+
if (tag && !tags.includes(tag))
|
|
64
|
+
tags.push(tag);
|
|
65
|
+
};
|
|
66
|
+
for (const tag of Array.isArray(named) ? named : [])
|
|
67
|
+
add(tag);
|
|
68
|
+
for (const title of Array.isArray(titles) ? titles : []) {
|
|
69
|
+
if (typeof title !== "string" || !title.includes("@"))
|
|
70
|
+
continue;
|
|
71
|
+
for (const match of title.matchAll(TITLE_TAG))
|
|
72
|
+
add(match[1]);
|
|
73
|
+
}
|
|
74
|
+
return tags;
|
|
75
|
+
}
|
|
76
|
+
export function inferTestProvenance({ runner, file, project, explicitKind, tags, }) {
|
|
77
|
+
const carried = tags ?? [];
|
|
78
|
+
const kindTag = explicitKind?.trim()
|
|
79
|
+
? undefined
|
|
80
|
+
: carried.find((tag) => KIND_TAGS.has(tag.toLowerCase()));
|
|
81
|
+
const provenance = kindTag
|
|
82
|
+
? {
|
|
83
|
+
runner,
|
|
84
|
+
kind: kindTag.toLowerCase(),
|
|
85
|
+
...(project ? { project } : {}),
|
|
86
|
+
source: "tag",
|
|
87
|
+
}
|
|
88
|
+
: inferredProvenance({ runner, file, project, explicitKind });
|
|
89
|
+
return carried.length > 0 ? { ...provenance, tags: carried } : provenance;
|
|
90
|
+
}
|
|
91
|
+
function inferredProvenance({ runner, file, project, explicitKind, }) {
|
|
53
92
|
if (explicitKind?.trim()) {
|
|
54
93
|
return {
|
|
55
94
|
runner,
|
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
6
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
6
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
7
7
|
import { serverEvidencePath } from "./transport.mjs";
|
|
8
8
|
|
|
9
9
|
let cachedProcessInstanceToken;
|
|
@@ -123,6 +123,7 @@ export function writeRunnerEvidence(identity, status, scope, evidenceDirectoryOv
|
|
|
123
123
|
runner: identity.runner,
|
|
124
124
|
file: testFile,
|
|
125
125
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
126
|
+
tags: identity.role ? [] : testTags(identity.tags, [identity.name]),
|
|
126
127
|
}),
|
|
127
128
|
...(phases.length > 0 ? { phases } : {}),
|
|
128
129
|
runtime: [],
|
|
@@ -1491,30 +1491,55 @@ function installNodeRequestPropagation() {
|
|
|
1491
1491
|
const SecureServer = https && https.Server;
|
|
1492
1492
|
if (!EventEmitter) return;
|
|
1493
1493
|
const original = EventEmitter.prototype.emit;
|
|
1494
|
+
// The test a request is for, worked out once, from whichever sees it first.
|
|
1495
|
+
const requestContexts = new WeakMap();
|
|
1496
|
+
const owningRequest = (server, request, response) => {
|
|
1497
|
+
if (requestContexts.has(request))
|
|
1498
|
+
return requestContexts.get(request);
|
|
1499
|
+
let context = emitterCoverageContexts.get(server);
|
|
1500
|
+
// HTTP headers establish ownership even when Express/SDK listeners and
|
|
1501
|
+
// the server process were created by a shared before hook.
|
|
1502
|
+
const incoming = requestCoverageContext(request);
|
|
1503
|
+
noteSupport(incoming && incoming.scope);
|
|
1504
|
+
if (incoming) context = incoming.scope ? incoming : context != null ? context : currentRequestContext();
|
|
1505
|
+
if (incoming && incoming.scope)
|
|
1506
|
+
noteServedAttempt(incoming.scope);
|
|
1507
|
+
// A request that names no test, to a server no test owns, is numbered:
|
|
1508
|
+
// the report credits its work to the test running when it came.
|
|
1509
|
+
if (incoming && serverPhaseStorage && !(context && (context.scope || context.phaseId)))
|
|
1510
|
+
context = { request: nextUnscopedRequest() };
|
|
1511
|
+
// Node emits a request's body and its response's events outside the
|
|
1512
|
+
// handler's context, so a handler that reads the body -- `req.on('end')`
|
|
1513
|
+
// -- ran as no test's. The request and response carry it with them.
|
|
1514
|
+
if (incoming && context) {
|
|
1515
|
+
for (const emitter of [request, response]) {
|
|
1516
|
+
if (emitter && typeof emitter === "object" && typeof emitter.emit === "function")
|
|
1517
|
+
emitterCoverageContexts.set(emitter, context);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (request && typeof request === "object")
|
|
1521
|
+
requestContexts.set(request, incoming ? context : void 0);
|
|
1522
|
+
return incoming ? context : void 0;
|
|
1523
|
+
};
|
|
1524
|
+
// Node publishes every request here before it emits it. An `emit` of the
|
|
1525
|
+
// server's own, defined before this patch by a preload that called the
|
|
1526
|
+
// emit it found, hid every request from the patch below, and what ran on
|
|
1527
|
+
// the request's events -- a page rendered as the response went out --
|
|
1528
|
+
// was credited to no test. Nothing hides this channel.
|
|
1529
|
+
try {
|
|
1530
|
+
const channels = builtin("node:diagnostics_channel");
|
|
1531
|
+
if (channels && typeof channels.subscribe === "function")
|
|
1532
|
+
channels.subscribe("http.server.request.start", (message) => {
|
|
1533
|
+
if (message && message.request)
|
|
1534
|
+
owningRequest(message.server, message.request, message.response);
|
|
1535
|
+
});
|
|
1536
|
+
} catch {}
|
|
1494
1537
|
EventEmitter.prototype.emit = function(event, ...args) {
|
|
1495
1538
|
let context = emitterCoverageContexts.get(this);
|
|
1496
1539
|
if ((event === "request" || event === "upgrade") &&
|
|
1497
1540
|
((Server && this instanceof Server) || (SecureServer && this instanceof SecureServer))) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
const incoming = requestCoverageContext(args[0]);
|
|
1501
|
-
noteSupport(incoming && incoming.scope);
|
|
1502
|
-
if (incoming) context = incoming.scope ? incoming : context != null ? context : currentRequestContext();
|
|
1503
|
-
if (incoming && incoming.scope)
|
|
1504
|
-
noteServedAttempt(incoming.scope);
|
|
1505
|
-
// A request that names no test, to a server no test owns, is numbered:
|
|
1506
|
-
// the report credits its work to the test running when it came.
|
|
1507
|
-
if (incoming && serverPhaseStorage && !(context && (context.scope || context.phaseId)))
|
|
1508
|
-
context = { request: nextUnscopedRequest() };
|
|
1509
|
-
// Node emits a request's body and its response's events outside the
|
|
1510
|
-
// handler's context, so a handler that reads the body -- `req.on('end')`
|
|
1511
|
-
// -- ran as no test's. The request and response carry it with them.
|
|
1512
|
-
if (incoming && context) {
|
|
1513
|
-
for (const emitter of args.slice(0, 2)) {
|
|
1514
|
-
if (emitter && typeof emitter === "object" && typeof emitter.emit === "function")
|
|
1515
|
-
emitterCoverageContexts.set(emitter, context);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1541
|
+
const owner = owningRequest(this, args[0], args[1]);
|
|
1542
|
+
if (owner) context = owner;
|
|
1518
1543
|
}
|
|
1519
1544
|
const invoke = () => Reflect.apply(original, this, [event, ...args]);
|
|
1520
1545
|
try {
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { relative, resolve, sep } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach } from "vitest";
|
|
4
4
|
import { coverageSnapshot as localCoverageSnapshot, activateCoverageScope as localActivateCoverageScope, enableRuntimeSnapshotEvidence as localEnableRuntimeSnapshotEvidence, resetCoverage as localResetCoverage, takeNodeAssertionPhases as localTakeNodeAssertionPhases, } from "./runtime.mjs";
|
|
5
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
5
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
6
6
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
7
7
|
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
|
|
8
8
|
const emittedSetupFiles = new Set();
|
|
@@ -109,6 +109,7 @@ afterEach((context) => {
|
|
|
109
109
|
file: testFile,
|
|
110
110
|
project: task.file.projectName,
|
|
111
111
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
112
|
+
tags: testTags(task.tags, [...titlePath(task)]),
|
|
112
113
|
}),
|
|
113
114
|
...(scope ? { phases: runtime.takeNodeAssertionPhases(scope) } : {}),
|
|
114
115
|
runtime: [runtime.coverageSnapshot()],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { relative, resolve, sep } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { inferTestProvenance } from "./provenance.mjs";
|
|
3
|
+
import { inferTestProvenance, testTags } from "./provenance.mjs";
|
|
4
4
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
5
5
|
function sourcePath(moduleId) {
|
|
6
6
|
const absolute = moduleId.startsWith("file:")
|
|
@@ -59,6 +59,7 @@ export default class SupercovVitestReporter {
|
|
|
59
59
|
file: testFile,
|
|
60
60
|
project: testCase.project.name,
|
|
61
61
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
62
|
+
tags: testTags(testCase.tags ?? testCase.options?.tags, [testCase.fullName]),
|
|
62
63
|
}),
|
|
63
64
|
runtime: [],
|
|
64
65
|
browser: [],
|
|
@@ -104,6 +105,7 @@ export default class SupercovVitestReporter {
|
|
|
104
105
|
file: testFile,
|
|
105
106
|
project: file.projectName,
|
|
106
107
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
108
|
+
tags: testTags(task.tags, names),
|
|
107
109
|
}),
|
|
108
110
|
runtime: [],
|
|
109
111
|
browser: [],
|