supercov 0.0.41 → 0.0.43
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 -4
- package/analyzers/typescript/README.md +55 -0
- package/analyzers/typescript/bin/compiler-identity.mjs +78 -0
- package/analyzers/typescript/bin/identity.mjs +67 -0
- package/analyzers/typescript/bin/query.mjs +29 -0
- package/analyzers/typescript/dist/analyze.js +3972 -0
- package/analyzers/typescript/dist/archive.js +309 -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/native-frontend.js +271 -0
- package/analyzers/typescript/dist/pragmas.js +143 -0
- package/analyzers/typescript/dist/types.js +1 -0
- package/analyzers/typescript/package.json +27 -0
- package/analyzers/typescript/src/analyze.ts +4538 -0
- package/analyzers/typescript/src/archive.ts +438 -0
- package/analyzers/typescript/src/compiler.ts +49 -0
- package/analyzers/typescript/src/frontend.ts +136 -0
- package/analyzers/typescript/src/native-frontend.ts +315 -0
- package/analyzers/typescript/src/pragmas.ts +218 -0
- package/analyzers/typescript/src/types.ts +45 -0
- package/analyzers/typescript/tsconfig.json +12 -0
- package/docs/agent-loop.md +13 -0
- package/docs/assertion-evidence.md +135 -0
- package/docs/cli.md +10 -0
- package/docs/code-verification.md +182 -0
- package/docs/supported-suites.md +44 -11
- package/docs/troubleshooting.md +13 -0
- package/docs/verification.md +12 -0
- package/package.json +33 -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/register.mjs +22 -4
- package/runtime/javascript/runtime.mjs +51 -13
|
@@ -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
|
+
}
|
|
@@ -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) {
|
|
@@ -144,6 +144,12 @@ function createState() {
|
|
|
144
144
|
decisions: /* @__PURE__ */ new Map(),
|
|
145
145
|
hits: /* @__PURE__ */ new Set(),
|
|
146
146
|
events: [],
|
|
147
|
+
// Position of the test-file statement currently executing ("file:line:column"), set by the
|
|
148
|
+
// instrumented test module; production hits recorded meanwhile are attributed to it.
|
|
149
|
+
testStatement: void 0,
|
|
150
|
+
// Per value-position logical expression (`a && b`, `a || b`, `a ?? b`): the distinct
|
|
151
|
+
// (right operand evaluated?, result truthy?) pairs seen, keyed by branch id.
|
|
152
|
+
logicals: /* @__PURE__ */ new Map(),
|
|
147
153
|
eventKeys: /* @__PURE__ */ new Set(),
|
|
148
154
|
bufferedAttempts: /* @__PURE__ */ new Set(),
|
|
149
155
|
serverBuffers: /* @__PURE__ */ new Map(),
|
|
@@ -293,17 +299,26 @@ function decisionSnapshot() {
|
|
|
293
299
|
vectors: [...decision.vectors.values()]
|
|
294
300
|
}));
|
|
295
301
|
}
|
|
302
|
+
function logicalSnapshot() {
|
|
303
|
+
return [...state.logicals.entries()].map(([id, vectors]) => ({
|
|
304
|
+
id,
|
|
305
|
+
vectors: [...vectors.values()]
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
296
308
|
function coverageSnapshot() {
|
|
297
309
|
return {
|
|
298
310
|
decisions: decisionSnapshot(),
|
|
299
311
|
hits: [...state.hits],
|
|
300
|
-
events: state.events
|
|
312
|
+
events: state.events,
|
|
313
|
+
logicals: logicalSnapshot()
|
|
301
314
|
};
|
|
302
315
|
}
|
|
303
316
|
function resetCoverage(testId2) {
|
|
304
317
|
state.decisions.clear();
|
|
305
318
|
state.hits.clear();
|
|
306
319
|
state.events.length = 0;
|
|
320
|
+
state.testStatement = void 0;
|
|
321
|
+
state.logicals.clear();
|
|
307
322
|
state.eventKeys.clear();
|
|
308
323
|
state.probeV2ContextEpochs.clear();
|
|
309
324
|
activateProbeV2Key(`reset\0${state.probeV2NextEpoch}`, true);
|
|
@@ -604,21 +619,22 @@ function appendServer(record) {
|
|
|
604
619
|
var _a8, _b, _c, _d;
|
|
605
620
|
if (state.runtimeSnapshots) {
|
|
606
621
|
const timestampMs = (_a8 = record.timestampMs) != null ? _a8 : Date.now();
|
|
622
|
+
const statement = record.statementId ? { statementId: record.statementId } : {};
|
|
607
623
|
if (record.type === "decision") {
|
|
608
|
-
recordBrowserEvent(__spreadProps(__spreadValues({
|
|
624
|
+
recordBrowserEvent(__spreadProps(__spreadValues(__spreadValues({
|
|
609
625
|
type: "decision",
|
|
610
626
|
id: record.meta.id,
|
|
611
627
|
vector: record.vector,
|
|
612
628
|
timestampMs
|
|
613
|
-
}, record.phaseId ? { phaseId: record.phaseId } : {}), {
|
|
629
|
+
}, record.phaseId ? { phaseId: record.phaseId } : {}), statement), {
|
|
614
630
|
environment: "server"
|
|
615
631
|
}));
|
|
616
632
|
} else {
|
|
617
|
-
recordBrowserEvent(__spreadProps(__spreadValues({
|
|
633
|
+
recordBrowserEvent(__spreadProps(__spreadValues(__spreadValues({
|
|
618
634
|
type: "hit",
|
|
619
635
|
id: record.id,
|
|
620
636
|
timestampMs
|
|
621
|
-
}, record.phaseId ? { phaseId: record.phaseId } : {}), {
|
|
637
|
+
}, record.phaseId ? { phaseId: record.phaseId } : {}), statement), {
|
|
622
638
|
environment: "server"
|
|
623
639
|
}));
|
|
624
640
|
}
|
|
@@ -962,25 +978,31 @@ function recordBrowserEvent(event) {
|
|
|
962
978
|
state.events.push(event);
|
|
963
979
|
return true;
|
|
964
980
|
}
|
|
981
|
+
function testStatement(source) {
|
|
982
|
+
state.testStatement = typeof source === "string" && source.length > 0 ? source : void 0;
|
|
983
|
+
}
|
|
984
|
+
function statementFields() {
|
|
985
|
+
return state.testStatement ? { statementId: state.testStatement } : {};
|
|
986
|
+
}
|
|
965
987
|
function coverageHit(id) {
|
|
966
988
|
state.hits.add(id);
|
|
967
989
|
const timestampMs = Date.now();
|
|
968
990
|
const phaseId = currentPhaseId();
|
|
969
991
|
if (isBrowser) {
|
|
970
|
-
if (recordBrowserEvent(__spreadProps(__spreadValues({
|
|
992
|
+
if (recordBrowserEvent(__spreadProps(__spreadValues(__spreadValues({
|
|
971
993
|
type: "hit",
|
|
972
994
|
id,
|
|
973
995
|
timestampMs
|
|
974
|
-
}, phaseId ? { phaseId } : {}), {
|
|
996
|
+
}, phaseId ? { phaseId } : {}), statementFields()), {
|
|
975
997
|
environment: "browser"
|
|
976
998
|
})))
|
|
977
999
|
persistBrowser();
|
|
978
1000
|
} else {
|
|
979
|
-
appendServer(__spreadValues({
|
|
1001
|
+
appendServer(__spreadValues(__spreadValues({
|
|
980
1002
|
type: "hit",
|
|
981
1003
|
id,
|
|
982
1004
|
timestampMs
|
|
983
|
-
}, phaseId ? { phaseId } : {}));
|
|
1005
|
+
}, phaseId ? { phaseId } : {}), statementFields()));
|
|
984
1006
|
}
|
|
985
1007
|
}
|
|
986
1008
|
function registerProbeV2(definition) {
|
|
@@ -1113,6 +1135,20 @@ function selectionRight(frame, value, inferredName) {
|
|
|
1113
1135
|
}
|
|
1114
1136
|
function selectionEnd(frame, value) {
|
|
1115
1137
|
coverageHit(frame.rightEvaluated ? frame.rightId : frame.shortId);
|
|
1138
|
+
// Per-operand outcomes for the verifier: which side was selected and whether the result is
|
|
1139
|
+
// truthy. With the operator (known from the manifest) this gives each operand's outcome.
|
|
1140
|
+
const separator = frame.shortId.lastIndexOf(":");
|
|
1141
|
+
if (separator > 0) {
|
|
1142
|
+
const branchId = frame.shortId.slice(0, separator);
|
|
1143
|
+
const right = frame.rightEvaluated;
|
|
1144
|
+
const truthy = Boolean(value);
|
|
1145
|
+
let vectors = state.logicals.get(branchId);
|
|
1146
|
+
if (!vectors) {
|
|
1147
|
+
vectors = /* @__PURE__ */ new Map();
|
|
1148
|
+
state.logicals.set(branchId, vectors);
|
|
1149
|
+
}
|
|
1150
|
+
vectors.set(`${right ? "R" : "S"}${truthy ? "T" : "F"}`, { right, truthy });
|
|
1151
|
+
}
|
|
1116
1152
|
return value;
|
|
1117
1153
|
}
|
|
1118
1154
|
function optionalSelect(shortId, continuedId, value) {
|
|
@@ -1201,22 +1237,22 @@ function mcdcEnd(frame, value) {
|
|
|
1201
1237
|
const timestampMs = Date.now();
|
|
1202
1238
|
const phaseId = currentPhaseId();
|
|
1203
1239
|
if (isBrowser) {
|
|
1204
|
-
if (recordBrowserEvent(__spreadProps(__spreadValues({
|
|
1240
|
+
if (recordBrowserEvent(__spreadProps(__spreadValues(__spreadValues({
|
|
1205
1241
|
type: "decision",
|
|
1206
1242
|
id: decision.meta.id,
|
|
1207
1243
|
vector,
|
|
1208
1244
|
timestampMs
|
|
1209
|
-
}, phaseId ? { phaseId } : {}), {
|
|
1245
|
+
}, phaseId ? { phaseId } : {}), statementFields()), {
|
|
1210
1246
|
environment: "browser"
|
|
1211
1247
|
})))
|
|
1212
1248
|
persistBrowser();
|
|
1213
1249
|
} else {
|
|
1214
|
-
appendServer(__spreadValues({
|
|
1250
|
+
appendServer(__spreadValues(__spreadValues({
|
|
1215
1251
|
type: "decision",
|
|
1216
1252
|
meta: decision.meta,
|
|
1217
1253
|
vector,
|
|
1218
1254
|
timestampMs
|
|
1219
|
-
}, phaseId ? { phaseId } : {}));
|
|
1255
|
+
}, phaseId ? { phaseId } : {}), statementFields()));
|
|
1220
1256
|
}
|
|
1221
1257
|
return value;
|
|
1222
1258
|
}
|
|
@@ -1257,6 +1293,7 @@ const directRuntimeApi = {
|
|
|
1257
1293
|
selectionEnd,
|
|
1258
1294
|
selectionRight,
|
|
1259
1295
|
takeNodeAssertionPhases,
|
|
1296
|
+
testStatement,
|
|
1260
1297
|
tryBegin,
|
|
1261
1298
|
tryCatch,
|
|
1262
1299
|
tryEnd,
|
|
@@ -1306,6 +1343,7 @@ export {
|
|
|
1306
1343
|
selectionEnd,
|
|
1307
1344
|
selectionRight,
|
|
1308
1345
|
takeNodeAssertionPhases,
|
|
1346
|
+
testStatement,
|
|
1309
1347
|
tryBegin,
|
|
1310
1348
|
tryCatch,
|
|
1311
1349
|
tryEnd,
|