supercov 0.0.42 → 0.0.44
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 +23 -3
- package/analyzers/typescript/README.md +59 -0
- package/analyzers/typescript/bin/compiler-identity.mjs +78 -0
- package/analyzers/typescript/bin/identity.mjs +71 -0
- package/analyzers/typescript/bin/query.mjs +29 -0
- package/analyzers/typescript/dist/analyze.js +5273 -0
- package/analyzers/typescript/dist/archive.js +337 -0
- package/analyzers/typescript/dist/awaited-observations.js +376 -0
- package/analyzers/typescript/dist/build-identity.json +1 -0
- package/analyzers/typescript/dist/compiler.js +32 -0
- package/analyzers/typescript/dist/frontend.js +75 -0
- package/analyzers/typescript/dist/mock-counts.js +2517 -0
- package/analyzers/typescript/dist/native-frontend.js +271 -0
- package/analyzers/typescript/dist/pragmas.js +186 -0
- package/analyzers/typescript/dist/types.js +1 -0
- package/analyzers/typescript/package.json +27 -0
- package/analyzers/typescript/src/analyze.ts +6180 -0
- package/analyzers/typescript/src/archive.ts +471 -0
- package/analyzers/typescript/src/awaited-observations.ts +561 -0
- package/analyzers/typescript/src/compiler.ts +49 -0
- package/analyzers/typescript/src/frontend.ts +136 -0
- package/analyzers/typescript/src/mock-counts.ts +3219 -0
- package/analyzers/typescript/src/native-frontend.ts +315 -0
- package/analyzers/typescript/src/pragmas.ts +284 -0
- package/analyzers/typescript/src/types.ts +45 -0
- package/analyzers/typescript/tsconfig.json +12 -0
- package/docs/agent-loop.md +129 -31
- package/docs/assertion-evidence.md +694 -0
- package/docs/cli.md +25 -15
- package/docs/code-verification.md +4 -0
- package/docs/coverage-model.md +6 -6
- package/docs/evidence.md +6 -6
- package/docs/getting-started.md +60 -72
- package/docs/performance.md +4 -4
- package/docs/supported-suites.md +40 -9
- package/docs/troubleshooting.md +8 -8
- package/docs/verification.md +14 -2
- package/docs/workspace-isolation.md +1 -1
- package/package.json +35 -15
- package/runtime/javascript/jest.cjs +134 -0
- package/runtime/javascript/jest.config.mjs +39 -0
- package/runtime/javascript/jestReporter.mjs +77 -0
- package/runtime/javascript/nodeAssertAdapter.mjs +32 -8
- package/runtime/javascript/nodeTest.mjs +13 -5
- package/runtime/javascript/register.mjs +22 -4
- package/runtime/javascript/runnerEvidence.mjs +33 -11
- package/runtime/javascript/runtime.mjs +73 -16
package/docs/verification.md
CHANGED
|
@@ -16,7 +16,7 @@ Before merging, check three things:
|
|
|
16
16
|
3. **The test protects behavior.** It should make a meaningful assertion without
|
|
17
17
|
weakening existing assertions or changing application code for the metric.
|
|
18
18
|
|
|
19
|
-
```sh
|
|
19
|
+
```sh supercov
|
|
20
20
|
npx supercov diff <baseline-run> <new-run>
|
|
21
21
|
npx supercov runs <new-run> test "new test name"
|
|
22
22
|
```
|
|
@@ -34,7 +34,7 @@ Supercov separates four states:
|
|
|
34
34
|
- **stale** — the run is valid history but no longer describes the current
|
|
35
35
|
workspace.
|
|
36
36
|
|
|
37
|
-
```sh
|
|
37
|
+
```sh supercov
|
|
38
38
|
npx supercov runs latest
|
|
39
39
|
npx supercov runs latest scope
|
|
40
40
|
npx supercov runs latest gaps
|
|
@@ -78,6 +78,18 @@ self-consistent calculation error does not pass unnoticed. Python and Ruby
|
|
|
78
78
|
gates run real suites through their supported runners and assert the resulting
|
|
79
79
|
coverage totals, test identity, and measurement limits.
|
|
80
80
|
|
|
81
|
+
Rust numbers are also checked against an independent LLVM line-coverage
|
|
82
|
+
oracle over real crates. `npm run oracle:rust` runs the same tests plain, under
|
|
83
|
+
the oracle, and under Supercov on twenty-six crates from bytes to tokio,
|
|
84
|
+
compares line coverage file by file, and fails if Supercov ran any suite
|
|
85
|
+
differently from plain Cargo. The two tools count differently — the oracle
|
|
86
|
+
counts every executable region after monomorphisation, Supercov the lines its
|
|
87
|
+
own obligations sit on — so the per-file comparison is read by a person: a file
|
|
88
|
+
at 0% in one and 70% in the other means lost evidence or a wrong denominator,
|
|
89
|
+
and this check found five such defects before 0.0.42. It runs locally and
|
|
90
|
+
costs no hosted minutes; `scripts/rust-coverage-oracle.mjs` documents what
|
|
91
|
+
each flag means.
|
|
92
|
+
|
|
81
93
|
These checks reduce risk; they do not replace reviewing the assertions and
|
|
82
94
|
behavior protected by a new test.
|
|
83
95
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.44",
|
|
4
|
+
"description": "Coverage for coding agents and software factories 🌙",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -16,6 +16,21 @@
|
|
|
16
16
|
"bin",
|
|
17
17
|
"runtime/javascript",
|
|
18
18
|
"docs",
|
|
19
|
+
"analyzers/typescript/bin",
|
|
20
|
+
"analyzers/typescript/dist/analyze.js",
|
|
21
|
+
"analyzers/typescript/dist/mock-counts.js",
|
|
22
|
+
"analyzers/typescript/dist/archive.js",
|
|
23
|
+
"analyzers/typescript/dist/awaited-observations.js",
|
|
24
|
+
"analyzers/typescript/dist/compiler.js",
|
|
25
|
+
"analyzers/typescript/dist/frontend.js",
|
|
26
|
+
"analyzers/typescript/dist/native-frontend.js",
|
|
27
|
+
"analyzers/typescript/dist/pragmas.js",
|
|
28
|
+
"analyzers/typescript/dist/types.js",
|
|
29
|
+
"analyzers/typescript/dist/build-identity.json",
|
|
30
|
+
"analyzers/typescript/src",
|
|
31
|
+
"analyzers/typescript/package.json",
|
|
32
|
+
"analyzers/typescript/tsconfig.json",
|
|
33
|
+
"analyzers/typescript/README.md",
|
|
19
34
|
"README.md"
|
|
20
35
|
],
|
|
21
36
|
"engines": {
|
|
@@ -34,6 +49,7 @@
|
|
|
34
49
|
},
|
|
35
50
|
"scripts": {
|
|
36
51
|
"build": "cargo build -p supercov",
|
|
52
|
+
"build:asserted-typescript": "npm --prefix analyzers/typescript run build",
|
|
37
53
|
"sync:rust-assets": "node scripts/sync-rust-package-assets.mjs",
|
|
38
54
|
"test:rust-assets": "node scripts/sync-rust-package-assets.mjs --check",
|
|
39
55
|
"check:rustc-backend-spike": "(cd spikes/rustc-backend && RUSTC_BOOTSTRAP=1 RUSTUP_TOOLCHAIN=1.95.0 cargo fmt --check && RUSTC_BOOTSTRAP=1 RUSTUP_TOOLCHAIN=1.95.0 cargo clippy --all-targets -- -D warnings)",
|
|
@@ -41,16 +57,19 @@
|
|
|
41
57
|
"test:rust-compiler-spikes": "cargo build -p supercov && node scripts/rust-libtest-companion-spike.mjs && node scripts/rust-async-attribution-spike.mjs && node scripts/rust-subprocess-attribution-spike.mjs && node scripts/rust-custom-harness-spike.mjs && node scripts/rust-libtest-builder-lifecycle-spike.mjs",
|
|
42
58
|
"test": "cargo test --workspace",
|
|
43
59
|
"test:runtime": "node --test tests/runtime/*.test.mjs",
|
|
60
|
+
"test:asserted-typescript": "npm --prefix analyzers/typescript test",
|
|
61
|
+
"test:asserted-integration": "cargo build -p supercov && npm --prefix analyzers/typescript run build && SUPERCOV_ASSERTED_INTEGRATION=1 node --test analyzers/typescript/tests/archive.integration.test.mjs",
|
|
62
|
+
"test:asserted-package": "cargo build -p supercov && npm run build:asserted-typescript && node scripts/asserted-packed-integration.mjs",
|
|
44
63
|
"test:fixture": "cargo build -p supercov && node scripts/rust-fixture-matrix.mjs",
|
|
45
|
-
"test:packed-npx": "cargo build --release -p supercov && node scripts/packed-npx-integration.mjs",
|
|
64
|
+
"test:packed-npx": "cargo build --release -p supercov && npm run build:asserted-typescript && node scripts/packed-npx-integration.mjs",
|
|
46
65
|
"test:isolation": "cargo build -p supercov && node scripts/isolation-integration.mjs",
|
|
47
66
|
"test:filesystem": "cargo test --workspace && cargo build -p supercov && node scripts/workspace-crash-integration.mjs",
|
|
48
67
|
"test:watchdog": "cargo build -p supercov && node scripts/watchdog-integration.mjs",
|
|
49
68
|
"test:engine-contract": "cargo build -p supercov && node scripts/engine-contract.mjs",
|
|
50
69
|
"test:agent": "cargo build -p supercov && node scripts/agent-query-eval.mjs",
|
|
51
|
-
"test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && cargo build -p supercov && npm run test:runtime && npm run test:rust-assets && 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-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-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",
|
|
70
|
+
"test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && cargo build -p supercov && npm run test:runtime && npm run test:rust-assets && 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-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",
|
|
52
71
|
"test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
|
|
53
|
-
"test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
72
|
+
"test:native-package": "cargo build --release -p supercov && npm run build:asserted-typescript && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
54
73
|
"test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
|
|
55
74
|
"test:pypi-registry": "node scripts/pypi-registry-integration.mjs",
|
|
56
75
|
"build:rubygem": "cargo build --release -p supercov && node scripts/build-rubygem.mjs",
|
|
@@ -61,11 +80,12 @@
|
|
|
61
80
|
"test:test262": "cargo build --release -p supercov && node scripts/test262-equivalence.mjs",
|
|
62
81
|
"benchmark:check": "cargo build --release -p supercov && node scripts/rust-transform-benchmark.mjs",
|
|
63
82
|
"benchmark:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-benchmark.mjs",
|
|
64
|
-
"check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:runtime && npm run test:rust-assets && node scripts/package-preflight.mjs && node scripts/verify-binstall-metadata.mjs --offline",
|
|
83
|
+
"check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:runtime && npm run test:rust-assets && npm run test:asserted-typescript && npm run test:asserted-integration && npm run test:asserted-package && node scripts/package-preflight.mjs && node scripts/verify-binstall-metadata.mjs --offline",
|
|
65
84
|
"release:check": "cargo clean && node scripts/sweep-target.mjs && npm run check && npm run test:engine && 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-monitoring && 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",
|
|
66
85
|
"release:bump": "node scripts/bump-version.mjs",
|
|
67
86
|
"sweep": "node scripts/sweep-target.mjs",
|
|
68
|
-
"
|
|
87
|
+
"oracle:rust": "cargo build --release -p supercov && node scripts/rust-coverage-oracle.mjs",
|
|
88
|
+
"prepack": "npm run build:asserted-typescript && node scripts/package-preflight.mjs",
|
|
69
89
|
"prepublishOnly": "npm run release:check",
|
|
70
90
|
"test:child-attribution": "cargo build -p supercov && node scripts/rust-child-attribution-integration.mjs",
|
|
71
91
|
"test:host-loader": "cargo build -p supercov && node scripts/rust-host-loader-integration.mjs",
|
|
@@ -74,14 +94,14 @@
|
|
|
74
94
|
"test:rust-public-cargo": "cargo build -p supercov && node scripts/rust-public-cargo-integration.mjs"
|
|
75
95
|
},
|
|
76
96
|
"optionalDependencies": {
|
|
77
|
-
"@supercov/cli-darwin-arm64": "0.0.
|
|
78
|
-
"@supercov/cli-darwin-x64": "0.0.
|
|
79
|
-
"@supercov/cli-linux-arm64-gnu": "0.0.
|
|
80
|
-
"@supercov/cli-linux-arm64-musl": "0.0.
|
|
81
|
-
"@supercov/cli-linux-x64-gnu": "0.0.
|
|
82
|
-
"@supercov/cli-linux-x64-musl": "0.0.
|
|
83
|
-
"@supercov/cli-win32-arm64": "0.0.
|
|
84
|
-
"@supercov/cli-win32-x64": "0.0.
|
|
97
|
+
"@supercov/cli-darwin-arm64": "0.0.44",
|
|
98
|
+
"@supercov/cli-darwin-x64": "0.0.44",
|
|
99
|
+
"@supercov/cli-linux-arm64-gnu": "0.0.44",
|
|
100
|
+
"@supercov/cli-linux-arm64-musl": "0.0.44",
|
|
101
|
+
"@supercov/cli-linux-x64-gnu": "0.0.44",
|
|
102
|
+
"@supercov/cli-linux-x64-musl": "0.0.44",
|
|
103
|
+
"@supercov/cli-win32-arm64": "0.0.44",
|
|
104
|
+
"@supercov/cli-win32-x64": "0.0.44"
|
|
85
105
|
},
|
|
86
106
|
"peerDependencies": {
|
|
87
107
|
"@playwright/test": ">=1.55.0",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Supercov Jest adapter. Jest loads it through `setupFilesAfterEnv` of the
|
|
3
|
+
// configuration the preload substitutes (jest.config.mjs), so it runs inside
|
|
4
|
+
// every test file's environment with Jest's globals in scope. It assigns each
|
|
5
|
+
// test attempt its exact scope before the test runs and writes the attempt's
|
|
6
|
+
// coverage and assertion phases after it. Jest tells the environment nothing
|
|
7
|
+
// about an outcome, so the reporter (jestReporter.mjs) records that; the two
|
|
8
|
+
// records share a test id and the report joins them. It computes no coverage.
|
|
9
|
+
//
|
|
10
|
+
// CommonJS on purpose: Jest's module system loads setup files without the
|
|
11
|
+
// ESM flag, and the runtime it needs is already on the sandbox global.
|
|
12
|
+
const { createHash } = require("node:crypto");
|
|
13
|
+
const { mkdirSync, renameSync, writeFileSync } = require("node:fs");
|
|
14
|
+
const { relative, resolve, sep } = require("node:path");
|
|
15
|
+
|
|
16
|
+
// jest-environment-node exposes the outer process's own globals to the sandbox
|
|
17
|
+
// through getters, so this is the same runtime instance the instrumented
|
|
18
|
+
// sources under test call into.
|
|
19
|
+
const runtime =
|
|
20
|
+
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??
|
|
21
|
+
(typeof process !== "undefined" ? process.__SUPERCOV_DIRECT_RUNTIME__ : undefined);
|
|
22
|
+
const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
|
|
23
|
+
|
|
24
|
+
function digest(value) {
|
|
25
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
26
|
+
}
|
|
27
|
+
function localFile(path) {
|
|
28
|
+
return relative(process.cwd(), path).split(sep).join("/");
|
|
29
|
+
}
|
|
30
|
+
// Must match jestReporter.mjs: the reporter has the same file and full name.
|
|
31
|
+
function testIdentity(testFile, fullName) {
|
|
32
|
+
return `jest:${digest(`${testFile}\0${fullName}`)}`;
|
|
33
|
+
}
|
|
34
|
+
// Mirrors provenance.mjs, which is ESM and out of reach here.
|
|
35
|
+
const KINDS = [
|
|
36
|
+
["unit", /(^|[/_.-])unit([/_.-]|$)/i],
|
|
37
|
+
["component", /(^|[/_.-])(component|components|ct)([/_.-]|$)/i],
|
|
38
|
+
["integration", /(^|[/_.-])(integration|int)([/_.-]|$)/i],
|
|
39
|
+
["e2e", /(^|[/_.-])(e2e|end-to-end|offline|online)([/_.-]|$)/i],
|
|
40
|
+
];
|
|
41
|
+
function provenance(testFile) {
|
|
42
|
+
const explicit = process.env.SUPERCOV_TEST_KIND?.trim();
|
|
43
|
+
if (explicit)
|
|
44
|
+
return { runner: "jest", kind: explicit.toLowerCase(), source: "explicit" };
|
|
45
|
+
const kind = KINDS.find(([, pattern]) => pattern.test(testFile))?.[0];
|
|
46
|
+
return kind
|
|
47
|
+
? { runner: "jest", kind, source: "path" }
|
|
48
|
+
: { runner: "jest", kind: "unit", source: "runner-default" };
|
|
49
|
+
}
|
|
50
|
+
function writeEvidence(suffix, payload) {
|
|
51
|
+
const directory = resolve(process.cwd(), evidenceDirectory, suffix);
|
|
52
|
+
mkdirSync(directory, { recursive: true });
|
|
53
|
+
const target = resolve(directory, "mcdc.json");
|
|
54
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
55
|
+
writeFileSync(temporary, `${JSON.stringify(payload)}\n`);
|
|
56
|
+
renameSync(temporary, target);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof afterEach === "function") {
|
|
60
|
+
// One in-memory snapshot per test; the server JSONL transport would be
|
|
61
|
+
// redundant and unattributed, as under Vitest.
|
|
62
|
+
runtime.enableRuntimeSnapshotEvidence();
|
|
63
|
+
const attempts = new Map();
|
|
64
|
+
const emittedSetupFiles = new Set();
|
|
65
|
+
let active;
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
const state = expect.getState();
|
|
68
|
+
const testFile = localFile(state.testPath ?? "unknown");
|
|
69
|
+
if (!emittedSetupFiles.has(testFile)) {
|
|
70
|
+
// Code the test file ran while loading, before its first test.
|
|
71
|
+
emittedSetupFiles.add(testFile);
|
|
72
|
+
const setupSnapshot = runtime.coverageSnapshot();
|
|
73
|
+
if (setupSnapshot.hits.length || setupSnapshot.decisions.length) {
|
|
74
|
+
writeEvidence(`jest-${digest(testFile)}-setup`, {
|
|
75
|
+
testId: `jest:${digest(testFile)}:setup`,
|
|
76
|
+
test: `${testFile} > module setup`,
|
|
77
|
+
testFile,
|
|
78
|
+
title: "module setup",
|
|
79
|
+
retry: 0,
|
|
80
|
+
status: "passed",
|
|
81
|
+
provenance: provenance(testFile),
|
|
82
|
+
role: "setup",
|
|
83
|
+
runtime: [setupSnapshot],
|
|
84
|
+
browser: [],
|
|
85
|
+
server: [],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const fullName = state.currentTestName ?? "test";
|
|
90
|
+
const testId = testIdentity(testFile, fullName);
|
|
91
|
+
const retry = attempts.get(testId) ?? 0;
|
|
92
|
+
attempts.set(testId, retry + 1);
|
|
93
|
+
const testKey = digest(testId);
|
|
94
|
+
const scope = {
|
|
95
|
+
version: 1,
|
|
96
|
+
runId: process.env.SUPERCOV_RUN_ID ?? "unscoped",
|
|
97
|
+
workerId: `jest-${process.env.JEST_WORKER_ID ?? process.pid}`,
|
|
98
|
+
testId,
|
|
99
|
+
testKey,
|
|
100
|
+
retry,
|
|
101
|
+
attemptId: `${testKey}-${retry}`,
|
|
102
|
+
};
|
|
103
|
+
active = { scope, testFile, fullName, retry };
|
|
104
|
+
runtime.activateCoverageScope(scope);
|
|
105
|
+
runtime.resetCoverage(testId);
|
|
106
|
+
});
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
const current = active;
|
|
109
|
+
active = undefined;
|
|
110
|
+
if (!current)
|
|
111
|
+
return;
|
|
112
|
+
const { scope } = current;
|
|
113
|
+
writeEvidence(`jest-${scope.attemptId}`, {
|
|
114
|
+
testId: scope.testId,
|
|
115
|
+
scope,
|
|
116
|
+
test: current.fullName,
|
|
117
|
+
testFile: current.testFile,
|
|
118
|
+
title: current.fullName,
|
|
119
|
+
retry: current.retry,
|
|
120
|
+
// The reporter's record carries the outcome.
|
|
121
|
+
status: "unknown",
|
|
122
|
+
provenance: provenance(current.testFile),
|
|
123
|
+
phases: runtime.takeNodeAssertionPhases(scope),
|
|
124
|
+
runtime: [runtime.coverageSnapshot()],
|
|
125
|
+
browser: [],
|
|
126
|
+
server: [],
|
|
127
|
+
});
|
|
128
|
+
// One runtime state serves every test file a worker runs, so what this
|
|
129
|
+
// test recorded must not leak into the next file's module-setup
|
|
130
|
+
// snapshot (Vitest isolates files; Jest does not).
|
|
131
|
+
runtime.resetCoverage();
|
|
132
|
+
runtime.activateCoverageScope();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Supercov's Jest configuration, handed to Jest through `--config` by the
|
|
2
|
+
// preload (register.mjs) in place of the user's. It reads the user's own
|
|
3
|
+
// configuration the way Jest reads it -- a jest.config.* file in any format
|
|
4
|
+
// Jest accepts, or the "jest" key of package.json -- and adds the adapter
|
|
5
|
+
// (jest.cjs) and the reporter (jestReporter.mjs). Nothing of the user's is
|
|
6
|
+
// replaced: their setup files and reporters keep running.
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const here = (name) => fileURLToPath(new URL(`./${name}`, import.meta.url));
|
|
12
|
+
const asList = (value) => (value === undefined ? [] : Array.isArray(value) ? value : [value]);
|
|
13
|
+
|
|
14
|
+
export default async function supercovJestConfig() {
|
|
15
|
+
let config = { rootDir: process.cwd() };
|
|
16
|
+
try {
|
|
17
|
+
// The project's own jest-config, so version and resolution match Jest's.
|
|
18
|
+
const projectRequire = createRequire(resolve(process.cwd(), "package.json"));
|
|
19
|
+
const { readInitialOptions } = projectRequire("jest-config");
|
|
20
|
+
const original = process.env["SUPERCOV_ORIGINAL_JEST_CONFIG"] || undefined;
|
|
21
|
+
const read = await readInitialOptions(original, { skipMultipleConfigError: true });
|
|
22
|
+
config = read.config ?? config;
|
|
23
|
+
config.rootDir ??= read.configPath ? dirname(read.configPath) : process.cwd();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// jest-config before 29.3 has no readInitialOptions; the project's
|
|
26
|
+
// configuration is not read and Jest's defaults apply.
|
|
27
|
+
process.emitWarning(`[supercov] the project's Jest configuration was not read (${error?.message ?? error}); Jest's defaults apply`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...config,
|
|
31
|
+
// Concurrent tests in one file would share the worker's scope.
|
|
32
|
+
maxConcurrency: 1,
|
|
33
|
+
setupFilesAfterEnv: [...asList(config.setupFilesAfterEnv), here("jest.cjs")],
|
|
34
|
+
reporters: [
|
|
35
|
+
...(config.reporters === undefined ? ["default"] : asList(config.reporters)),
|
|
36
|
+
here("jestReporter.mjs"),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { relative, resolve, sep } from "node:path";
|
|
4
|
+
import { inferTestProvenance } from "./provenance.mjs";
|
|
5
|
+
import { atomicWriteFileSync } from "./atomic.mjs";
|
|
6
|
+
function digest(value) {
|
|
7
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
8
|
+
}
|
|
9
|
+
function localFile(path) {
|
|
10
|
+
return relative(process.cwd(), path).split(sep).join("/");
|
|
11
|
+
}
|
|
12
|
+
function attemptStatus(status) {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case "passed":
|
|
15
|
+
case "failed":
|
|
16
|
+
return status;
|
|
17
|
+
case "pending":
|
|
18
|
+
case "skipped":
|
|
19
|
+
case "todo":
|
|
20
|
+
case "disabled":
|
|
21
|
+
return "skipped";
|
|
22
|
+
default:
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Records each Jest attempt's outcome, which the test environment never
|
|
28
|
+
* learns: a test that times out, one skipped before its hooks, one that
|
|
29
|
+
* failed after its last hook. The test id formula must match jest.cjs, whose
|
|
30
|
+
* record carries the attempt's coverage; the report joins the two.
|
|
31
|
+
*/
|
|
32
|
+
export default class SupercovJestReporter {
|
|
33
|
+
onTestCaseResult(test, result) {
|
|
34
|
+
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
|
|
35
|
+
if (!evidenceDirectory)
|
|
36
|
+
return;
|
|
37
|
+
const testFile = localFile(test.path);
|
|
38
|
+
const testId = `jest:${digest(`${testFile}\0${result.fullName}`)}`;
|
|
39
|
+
const retry = Math.max((result.invocations ?? 1) - 1, 0);
|
|
40
|
+
const status = attemptStatus(result.status);
|
|
41
|
+
const provenance = inferTestProvenance({
|
|
42
|
+
runner: "jest",
|
|
43
|
+
file: testFile,
|
|
44
|
+
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
45
|
+
});
|
|
46
|
+
const record = (attempt, attemptStatus, flaky) => {
|
|
47
|
+
const payload = {
|
|
48
|
+
testId,
|
|
49
|
+
test: result.fullName,
|
|
50
|
+
testFile,
|
|
51
|
+
title: result.title,
|
|
52
|
+
retry: attempt,
|
|
53
|
+
status: attemptStatus,
|
|
54
|
+
expectedStatus: "passed",
|
|
55
|
+
flaky,
|
|
56
|
+
provenance,
|
|
57
|
+
runtime: [],
|
|
58
|
+
browser: [],
|
|
59
|
+
server: [],
|
|
60
|
+
};
|
|
61
|
+
const directory = resolve(process.cwd(), evidenceDirectory, `jest-${digest(testId)}-${attempt}-status`);
|
|
62
|
+
mkdirSync(directory, { recursive: true });
|
|
63
|
+
atomicWriteFileSync(resolve(directory, "mcdc.json"), `${JSON.stringify(payload)}\n`);
|
|
64
|
+
};
|
|
65
|
+
// `jest.retryTimes` reports one result after the last attempt; every
|
|
66
|
+
// attempt before it failed, or there would have been no retry. The
|
|
67
|
+
// adapter wrote each attempt's coverage without an outcome.
|
|
68
|
+
for (let attempt = 0; attempt < retry; attempt += 1)
|
|
69
|
+
record(attempt, "failed", false);
|
|
70
|
+
record(retry, status, retry > 0 && status === "passed");
|
|
71
|
+
}
|
|
72
|
+
// Jest calls these on every reporter.
|
|
73
|
+
onRunComplete() { }
|
|
74
|
+
getLastError() {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { withNodeAssertionPhase } from "./runtime.mjs";
|
|
2
|
+
import { relative, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
const ASSERTION_METHODS = new Set([
|
|
3
5
|
"deepEqual",
|
|
4
6
|
"deepStrictEqual",
|
|
@@ -19,18 +21,40 @@ const ASSERTION_METHODS = new Set([
|
|
|
19
21
|
"strictEqual",
|
|
20
22
|
"throws",
|
|
21
23
|
]);
|
|
22
|
-
function assertionSource() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
function assertionSource(boundary) {
|
|
25
|
+
// Omit our own frames by function identity, not basename: a user's helper
|
|
26
|
+
// may itself be called runtime.mjs or nodeAssertAdapter.mjs. Never search
|
|
27
|
+
// deeper for a plausible-looking caller when the immediate frame is opaque.
|
|
28
|
+
// A loader may have transformed this file without composing source maps.
|
|
29
|
+
// Qualify stack coordinates so they cannot masquerade as the original
|
|
30
|
+
// source positions emitted by lexical probes and used for oracle matching.
|
|
31
|
+
try {
|
|
32
|
+
const error = {};
|
|
33
|
+
Error.captureStackTrace(error, boundary);
|
|
34
|
+
if (typeof error.stack !== "string")
|
|
35
|
+
return undefined;
|
|
36
|
+
const entry = error.stack.split("\n")[1]?.trim();
|
|
37
|
+
const match = entry && /(?:^at (?:async )?| \()((?:file:\/\/\/|\/|[A-Za-z]:[\\/]).*):(\d+):(\d+)\)?$/.exec(entry);
|
|
38
|
+
if (!match)
|
|
39
|
+
return undefined;
|
|
40
|
+
const file = match[1].startsWith("file:") ? fileURLToPath(match[1]) : match[1];
|
|
41
|
+
const roots = [process.env.SUPERCOV_PROJECT_ROOT, process.env.SUPERCOV_SOURCE_PROJECT_ROOT, process.cwd()].filter(Boolean);
|
|
42
|
+
for (const root of roots) {
|
|
43
|
+
const path = relative(root, file);
|
|
44
|
+
if (path !== ".." && !path.startsWith(`..${sep}`) && !/^(?:[A-Za-z]:|[\\/])/.test(path))
|
|
45
|
+
return `runtime-stack:${path.split(sep).join("/")}:${match[2]}:${match[3]}`;
|
|
46
|
+
}
|
|
47
|
+
return `runtime-stack:${file.split(sep).join("/")}:${match[2]}:${match[3]}`;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Custom stack formatters must not change the user's assertion outcome.
|
|
27
51
|
return undefined;
|
|
28
|
-
|
|
52
|
+
}
|
|
29
53
|
}
|
|
30
54
|
function wrapAssertion(original, operation) {
|
|
31
55
|
return new Proxy(original, {
|
|
32
|
-
apply(target, thisArgument, argumentsList) {
|
|
33
|
-
return withNodeAssertionPhase(operation, assertionSource(), () => Reflect.apply(target, thisArgument, argumentsList));
|
|
56
|
+
apply: function assertionApply(target, thisArgument, argumentsList) {
|
|
57
|
+
return withNodeAssertionPhase(operation, () => assertionSource(assertionApply), () => Reflect.apply(target, thisArgument, argumentsList));
|
|
34
58
|
},
|
|
35
59
|
});
|
|
36
60
|
}
|
|
@@ -2,7 +2,7 @@ import * as native from "node:test";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import vm from "node:vm";
|
|
4
4
|
import { beginBufferedServerEvidence, flushBufferedServerEvidence, takeNodeAssertionPhases, withCoverageCarrier, } from "./runtime.mjs";
|
|
5
|
-
import { callerLocation, runnerExecutionScope, writeRunnerEvidence, } from "./runnerEvidence.mjs";
|
|
5
|
+
import { callerLocation, runnerExecutionScope, runnerTestId, writeRunnerEvidence, } from "./runnerEvidence.mjs";
|
|
6
6
|
function callbackIndex(args) {
|
|
7
7
|
for (let index = args.length - 1; index >= 0; index -= 1)
|
|
8
8
|
if (typeof args[index] === "function")
|
|
@@ -86,16 +86,18 @@ function restoreUserError(error, depth = 0) {
|
|
|
86
86
|
}
|
|
87
87
|
return error;
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
const registrationCounts = new Map();
|
|
90
|
+
function wrappedRegistration(original, parentTestId) {
|
|
90
91
|
const wrapped = function supercovNodeTest(...args) {
|
|
91
92
|
const index = callbackIndex(args);
|
|
92
93
|
if (index < 0)
|
|
93
94
|
return Reflect.apply(original, this, args);
|
|
94
95
|
const callback = args[index];
|
|
95
|
-
const location = callerLocation(
|
|
96
|
+
const location = callerLocation(supercovNodeTest);
|
|
96
97
|
const identity = {
|
|
97
98
|
runner: "node:test",
|
|
98
99
|
name: testName(args, callback),
|
|
100
|
+
...(parentTestId ? { parentTestId } : {}),
|
|
99
101
|
...location,
|
|
100
102
|
// Source-map producers disagree about whether a call expression maps to
|
|
101
103
|
// its first token or the first token on its source line. The line and
|
|
@@ -104,6 +106,12 @@ function wrappedRegistration(original) {
|
|
|
104
106
|
// keeps one identity when esbuild, Babel, SWC, or TypeScript rewrites it.
|
|
105
107
|
...(location.line === undefined ? {} : { column: 1 }),
|
|
106
108
|
};
|
|
109
|
+
// Allocate at registration, never callback completion: concurrent tests
|
|
110
|
+
// can finish in any order. Count each source/name within its parent.
|
|
111
|
+
const registrationKey = runnerTestId(identity);
|
|
112
|
+
const registrationOrdinal = registrationCounts.get(registrationKey) ?? 0;
|
|
113
|
+
registrationCounts.set(registrationKey, registrationOrdinal + 1);
|
|
114
|
+
identity.registrationOrdinal = registrationOrdinal;
|
|
107
115
|
const scope = runnerExecutionScope(identity);
|
|
108
116
|
const options = testOptions(args, index);
|
|
109
117
|
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
|
|
@@ -129,7 +137,7 @@ function wrappedRegistration(original) {
|
|
|
129
137
|
};
|
|
130
138
|
}
|
|
131
139
|
if (property === "test" && typeof value === "function")
|
|
132
|
-
return wrappedRegistration(value.bind(target));
|
|
140
|
+
return wrappedRegistration(value.bind(target), scope.testId);
|
|
133
141
|
return typeof value === "function" ? value.bind(target) : value;
|
|
134
142
|
},
|
|
135
143
|
});
|
|
@@ -192,7 +200,7 @@ function wrappedRegistration(original) {
|
|
|
192
200
|
Object.defineProperty(wrapped, property, {
|
|
193
201
|
configurable: true,
|
|
194
202
|
enumerable: true,
|
|
195
|
-
value: wrappedRegistration(member),
|
|
203
|
+
value: wrappedRegistration(member, parentTestId),
|
|
196
204
|
});
|
|
197
205
|
}
|
|
198
206
|
return wrapped;
|
|
@@ -9,6 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import Module, { register, syncBuiltinESMExports } from "node:module";
|
|
10
10
|
import { closeSync, openSync, unlinkSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervisor.mjs";
|
|
13
14
|
import { __supercovBindCapabilityWrapper } from "./capability.mjs";
|
|
14
15
|
installLaunchSupervisor();
|
|
@@ -115,7 +116,6 @@ syncBuiltinESMExports();
|
|
|
115
116
|
// without editing package scripts, Vitest configs, setup files, or test imports.
|
|
116
117
|
const generatedVitestConfig = process.env.SUPERCOV_GENERATED_VITEST_CONFIG;
|
|
117
118
|
const generatedPlaywrightConfig = process.env.SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG;
|
|
118
|
-
const generatedJestConfig = process.env.SUPERCOV_GENERATED_JEST_CONFIG;
|
|
119
119
|
const entrypoint = process.argv[1]?.replaceAll("\\", "/") ?? "";
|
|
120
120
|
const playwrightTarget = process.env.SUPERCOV_PLAYWRIGHT_MODULE;
|
|
121
121
|
const projectRoot = process.env.SUPERCOV_PROJECT_ROOT?.replaceAll("\\", "/").replace(/\/$/, "");
|
|
@@ -171,19 +171,37 @@ if (generatedVitestConfig && /\/vitest(?:\.m?js)?$/.test(entrypoint)) {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
if (
|
|
174
|
+
if ((isJestEntrypoint || process.env.JEST_WORKER_ID) && process.env.SUPERCOV_EVIDENCE_DIR) {
|
|
175
|
+
// Jest evaluates instrumented modules in its own module system, past the
|
|
176
|
+
// loader hook that imports the runtime on first use elsewhere, and
|
|
177
|
+
// jest-environment-node exposes to each test's sandbox only the globals
|
|
178
|
+
// the outer process held when that class loaded. The Jest process and
|
|
179
|
+
// each of its workers (JEST_WORKER_ID) therefore load the runtime first;
|
|
180
|
+
// without this, a suite with more than one test file failed on every
|
|
181
|
+
// instrumented module's first line inside the workers.
|
|
182
|
+
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
|
|
183
|
+
process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
184
|
+
}
|
|
185
|
+
if (isJestEntrypoint && process.env.SUPERCOV_EVIDENCE_DIR) {
|
|
186
|
+
// Jest reads one configuration. Ours (jest.config.mjs) reads the user's
|
|
187
|
+
// the way Jest would and adds the adapter and reporter; an explicit
|
|
188
|
+
// --config on the command line reaches it through the environment.
|
|
175
189
|
for (let index = 2; index < process.argv.length; index += 1) {
|
|
176
190
|
const argument = process.argv[index];
|
|
177
191
|
if (argument === "--config" || argument === "-c") {
|
|
178
|
-
|
|
192
|
+
const value = process.argv[index + 1];
|
|
193
|
+
if (value)
|
|
194
|
+
process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = resolve(value);
|
|
195
|
+
process.argv.splice(index, value ? 2 : 1);
|
|
179
196
|
index -= 1;
|
|
180
197
|
}
|
|
181
198
|
else if (argument?.startsWith("--config=")) {
|
|
199
|
+
process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = resolve(argument.slice("--config=".length));
|
|
182
200
|
process.argv.splice(index, 1);
|
|
183
201
|
index -= 1;
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
|
-
process.argv.push("--config",
|
|
204
|
+
process.argv.push("--config", fileURLToPath(new URL("./jest.config.mjs", import.meta.url)));
|
|
187
205
|
}
|
|
188
206
|
if (generatedPlaywrightConfig &&
|
|
189
207
|
isPlaywrightEntrypoint) {
|
|
@@ -27,13 +27,24 @@ function localFile(file) {
|
|
|
27
27
|
return relative(process.cwd(), absolute).split(sep).join("/");
|
|
28
28
|
}
|
|
29
29
|
export function runnerTestId(identity) {
|
|
30
|
-
const
|
|
30
|
+
const parts = [
|
|
31
31
|
identity.runner,
|
|
32
32
|
localFile(identity.file) ?? "unknown",
|
|
33
33
|
identity.line ?? 0,
|
|
34
34
|
identity.column ?? 0,
|
|
35
35
|
identity.name,
|
|
36
|
-
]
|
|
36
|
+
];
|
|
37
|
+
// Preserve existing top-level, uniquely registered test IDs. Nested tests
|
|
38
|
+
// and repeated registrations need more than a shared source/name identity.
|
|
39
|
+
if (identity.parentTestId)
|
|
40
|
+
parts.push("parent", identity.parentTestId);
|
|
41
|
+
if (identity.registrationOrdinal)
|
|
42
|
+
parts.push("registration", identity.registrationOrdinal);
|
|
43
|
+
// Titles can themselves contain separator text. Domain-separate and encode
|
|
44
|
+
// the extended identity structurally so it cannot alias a literal title.
|
|
45
|
+
const key = identity.parentTestId || identity.registrationOrdinal
|
|
46
|
+
? JSON.stringify(["registration-v2", ...parts])
|
|
47
|
+
: parts.join("\0");
|
|
37
48
|
return `${identity.runner}:${createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
|
|
38
49
|
}
|
|
39
50
|
export function runnerExecutionScope(identity) {
|
|
@@ -50,24 +61,35 @@ export function runnerExecutionScope(identity) {
|
|
|
50
61
|
testId,
|
|
51
62
|
testKey,
|
|
52
63
|
retry,
|
|
53
|
-
|
|
64
|
+
// Different processes/worker threads can execute the same registration.
|
|
65
|
+
// Their files must not collide even though the stable test ID is shared.
|
|
66
|
+
attemptId: `${testKey}-${retry}-${processInstanceToken()}`,
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
|
-
export function callerLocation(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
69
|
+
export function callerLocation(boundary = callerLocation) {
|
|
70
|
+
// Omit the registration wrapper by identity. A user file may have the same
|
|
71
|
+
// basename as a runtime adapter. Parse the location suffix, not individual
|
|
72
|
+
// path segments: spaces, parentheses and Unicode are valid filenames.
|
|
73
|
+
try {
|
|
74
|
+
const error = {};
|
|
75
|
+
Error.captureStackTrace(error, boundary);
|
|
76
|
+
if (typeof error.stack !== "string")
|
|
77
|
+
return {};
|
|
78
|
+
const entry = error.stack.split("\n")[1]?.trim();
|
|
79
|
+
const match = entry && /(?:^at (?:async )?| \()((?:file:\/\/|\/|[A-Za-z]:[\\/]|\\\\).*):(\d+):(\d+)\)?$/.exec(entry);
|
|
62
80
|
if (!match)
|
|
63
|
-
|
|
81
|
+
return {};
|
|
64
82
|
return {
|
|
65
83
|
file: match[1],
|
|
66
84
|
line: Number(match[2]),
|
|
67
85
|
column: Number(match[3]),
|
|
68
86
|
};
|
|
69
87
|
}
|
|
70
|
-
|
|
88
|
+
catch {
|
|
89
|
+
// Custom formatters must not prevent registration. Missing provenance
|
|
90
|
+
// is explicit, never replaced with an unrelated deeper caller.
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
71
93
|
}
|
|
72
94
|
export function readScopedServerEvidence(scope, evidencePath = serverEvidencePath(scope)) {
|
|
73
95
|
try {
|