supercov 4.24.8 → 4.25.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/package.json +9 -9
- package/runtime/javascript/launchSupervisor.mjs +10 -0
- package/runtime/javascript/playwright.mjs +426 -179
- package/runtime/javascript/playwrightCacheThread.mjs +6 -0
- package/runtime/javascript/register.mjs +84 -10
- package/runtime/javascript/resolve-loader.mjs +45 -3
- package/runtime/javascript/runnerEvidence.mjs +16 -1
- package/runtime/javascript/runtime.mjs +223 -22
- package/runtime/javascript/snapshots.mjs +108 -0
- package/runtime/javascript/transport.mjs +36 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The thread Node runs module hooks in, where Playwright compiles ES modules
|
|
2
|
+
// and writes them into its cache: written whole there too (atomic.mjs says
|
|
3
|
+
// why). Supercov's own hooks run in the importing thread where Node can, so
|
|
4
|
+
// this goes to that thread on its own.
|
|
5
|
+
import { writePlaywrightCacheWhole } from "./atomic.mjs";
|
|
6
|
+
writePlaywrightCacheWhole();
|
|
@@ -14,6 +14,16 @@ import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervi
|
|
|
14
14
|
import { __supercovBindCapabilityWrapper } from "./capability.mjs";
|
|
15
15
|
import { adaptRunnerEntry, noteSkippedCoverageThresholds } from "./runnerEntry.mjs";
|
|
16
16
|
import { writePlaywrightCacheWhole } from "./atomic.mjs";
|
|
17
|
+
// Supercov runs Node with --enable-source-maps so a stack through an
|
|
18
|
+
// instrumented copy names the project's file. The flag also reads the maps
|
|
19
|
+
// every package ships, which a plain run never does: a third of the CPU a
|
|
20
|
+
// measured Vitest suite added. Where Node can, Supercov's flag maps the
|
|
21
|
+
// project's files alone, unless the command asked for maps itself: a second
|
|
22
|
+
// flag in NODE_OPTIONS, or one on its own command line.
|
|
23
|
+
if ((process.env.NODE_OPTIONS ?? "").split(/\s+/).filter((option) => option === "--enable-source-maps").length === 1 &&
|
|
24
|
+
!process.execArgv.includes("--enable-source-maps") &&
|
|
25
|
+
typeof Module.setSourceMapsSupport === "function")
|
|
26
|
+
Module.setSourceMapsSupport(true, { nodeModules: false, generatedCode: true });
|
|
17
27
|
installLaunchSupervisor();
|
|
18
28
|
// Playwright's compile cache, written whole (atomic.mjs says why).
|
|
19
29
|
writePlaywrightCacheWhole();
|
|
@@ -195,8 +205,16 @@ const nodeAssertWrapper = new URL("./nodeAssert.mjs", import.meta.url).href;
|
|
|
195
205
|
const nodeAssertStrictWrapper = new URL("./nodeAssertStrict.mjs", import.meta.url).href;
|
|
196
206
|
const isPlaywrightEntrypoint = /\/(?:node_modules\/\.bin\/playwright|node_modules\/(?:@playwright\/test|playwright)\/(?:cli\.js|.*\/program\.js))$/.test(entrypoint);
|
|
197
207
|
const isJestEntrypoint = /\/node_modules\/(?:\.bin\/jest|(?:jest|jest-cli)\/bin\/jest\.js)$/.test(entrypoint);
|
|
198
|
-
if (generatedPlaywrightConfig && isPlaywrightEntrypoint)
|
|
208
|
+
if (generatedPlaywrightConfig && isPlaywrightEntrypoint) {
|
|
199
209
|
process.env.SUPERCOV_INSIDE_PLAYWRIGHT = "1";
|
|
210
|
+
// Every process the runner starts inherits SUPERCOV_INSIDE_PLAYWRIGHT, the
|
|
211
|
+
// app server and each mock server too, and loading the wrapper cost each of
|
|
212
|
+
// them 190 ms before its first line: a server Playwright waits on missed its
|
|
213
|
+
// first look, about 4 s a run. The runner's children are marked as not test
|
|
214
|
+
// workers, and the launch supervisor marks the ones it forks from its worker
|
|
215
|
+
// entry as workers, so only those load the wrapper.
|
|
216
|
+
process.env.SUPERCOV_PLAYWRIGHT_WORKER = "0";
|
|
217
|
+
}
|
|
200
218
|
// Linters, formatters and type checks read the project to judge it, not to
|
|
201
219
|
// run it. The workspace holds rewritten copies, and ESLint failed semver,
|
|
202
220
|
// js-yaml and picomatch on code nobody wrote ("Strings must use singlequote",
|
|
@@ -966,13 +984,41 @@ if (missingFile) {
|
|
|
966
984
|
}
|
|
967
985
|
};
|
|
968
986
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
987
|
+
const loaderData = {
|
|
988
|
+
typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS",
|
|
989
|
+
supportFile,
|
|
990
|
+
missingFile,
|
|
991
|
+
};
|
|
992
|
+
// Where Node can (22.15, 23.5), the hooks run in this thread. Off it, every
|
|
993
|
+
// Node process started a thread of its own and compiled a module loader
|
|
994
|
+
// there, and each import and require waited on a message to it: on a Next.js
|
|
995
|
+
// app's Vitest suite on one worker, a third of the time a measured run added.
|
|
996
|
+
if (typeof Module.registerHooks === "function") {
|
|
997
|
+
const hooks = await import("./resolve-loader.mjs");
|
|
998
|
+
hooks.initialize(loaderData);
|
|
999
|
+
Module.registerHooks({ resolve: hooks.resolveSync, load: hooks.loadSync });
|
|
1000
|
+
// Playwright compiles ES modules in the hooks thread its own loader
|
|
1001
|
+
// starts, which these hooks no longer reach: the cache patch goes to that
|
|
1002
|
+
// thread by itself, ahead of the first module anyone registers off this
|
|
1003
|
+
// one, which is what starts it. Supercov names the cache for every run,
|
|
1004
|
+
// and registering it unasked started the thread in every Node process --
|
|
1005
|
+
// a Vitest worker -- and sent each of its imports through it.
|
|
1006
|
+
if (process.env.PWTEST_CACHE_DIR) {
|
|
1007
|
+
const registerOffThread = Module.register;
|
|
1008
|
+
let cacheThreadRegistered = false;
|
|
1009
|
+
Module.register = function (...args) {
|
|
1010
|
+
if (!cacheThreadRegistered) {
|
|
1011
|
+
cacheThreadRegistered = true;
|
|
1012
|
+
registerOffThread.call(this, new URL("./playwrightCacheThread.mjs", import.meta.url));
|
|
1013
|
+
}
|
|
1014
|
+
return registerOffThread.apply(this, args);
|
|
1015
|
+
};
|
|
1016
|
+
syncBuiltinESMExports();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
register(new URL("./resolve-loader.mjs", import.meta.url), { data: loaderData });
|
|
1021
|
+
}
|
|
976
1022
|
if (process.env.SUPERCOV_DEBUG === "1") {
|
|
977
1023
|
console.error("[supercov] preload", { entrypoint });
|
|
978
1024
|
}
|
|
@@ -1038,9 +1084,21 @@ if (generatedPlaywrightConfig &&
|
|
|
1038
1084
|
}
|
|
1039
1085
|
process.argv.push("--config", generatedPlaywrightConfig);
|
|
1040
1086
|
}
|
|
1087
|
+
// A process the runner marked as not a test worker skips the wrapper, unless
|
|
1088
|
+
// it was started from Playwright's own files: a worker Playwright starts some
|
|
1089
|
+
// other way than its worker entry still has the wrapper, only later. The
|
|
1090
|
+
// runner itself has it too: it lists each test with the fixtures its spec's
|
|
1091
|
+
// `test` carries, and puts tests with the same worker fixtures in one worker.
|
|
1092
|
+
// A CommonJS spec it listed with Playwright's own `test` ran in the worker
|
|
1093
|
+
// with the wrapper's, beside a spec through a facade that has Playwright's
|
|
1094
|
+
// own in both, and the worker refused the pair: "Playwright detected
|
|
1095
|
+
// inconsistent test.use() options". An ES module spec already had the wrapper
|
|
1096
|
+
// in the runner, through the module hooks.
|
|
1097
|
+
const isPlaywrightProcess = /\/node_modules\/(?:@playwright\/test|playwright)\//.test(entrypoint);
|
|
1098
|
+
const mayBePlaywrightWorker = process.env.SUPERCOV_PLAYWRIGHT_WORKER !== "0" || isPlaywrightProcess;
|
|
1041
1099
|
if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
|
|
1042
1100
|
process.env.SUPERCOV_INSIDE_PLAYWRIGHT === "1" &&
|
|
1043
|
-
|
|
1101
|
+
(isPlaywrightEntrypoint || mayBePlaywrightWorker) &&
|
|
1044
1102
|
process.env.SUPERCOV_INSIDE_VITEST !== "1" &&
|
|
1045
1103
|
process.env.VITEST !== "true") {
|
|
1046
1104
|
const target = playwrightTarget ?? "@playwright/test";
|
|
@@ -1048,6 +1106,16 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
|
|
|
1048
1106
|
const originalPlaywrightConfig = process.env.SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG
|
|
1049
1107
|
?.replaceAll("\\", "/");
|
|
1050
1108
|
const wrapper = await import(__rewriteRelativeImportExtension(new URL("./playwright.mjs", import.meta.url)));
|
|
1109
|
+
// A spec transpiled from `import test from "@playwright/test"` reads the
|
|
1110
|
+
// module's `default` when it says it is an ES module, and the module
|
|
1111
|
+
// itself otherwise: the wrapper's namespace says nothing and is no
|
|
1112
|
+
// function, and a Next.js app's spec failed with `(0 , _test.default) is not
|
|
1113
|
+
// a function`. Its exports, marked as an ES module's, with `test` itself
|
|
1114
|
+
// as the default. Not a function that calls `test` (Playwright names a
|
|
1115
|
+
// test by where it was declared, read from the stack, and an extra frame
|
|
1116
|
+
// moved it), nor the exports assigned onto the wrapper's `test` (its
|
|
1117
|
+
// fixtures then differed between the runner and a worker).
|
|
1118
|
+
const commonjsWrapper = { ...wrapper, __esModule: true, default: wrapper.test };
|
|
1051
1119
|
const originalLoad = Module._load;
|
|
1052
1120
|
Module._load = function supercovLoad(request, parent, isMain) {
|
|
1053
1121
|
const parentFile = parent?.filename?.replaceAll("\\", "/");
|
|
@@ -1065,7 +1133,7 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
|
|
|
1065
1133
|
? parentFile.startsWith(`${normalizedRoot}/`)
|
|
1066
1134
|
: parentFile.includes("/tests/"));
|
|
1067
1135
|
if (request === target && belongsToProject)
|
|
1068
|
-
return
|
|
1136
|
+
return commonjsWrapper;
|
|
1069
1137
|
return originalLoad.call(this, request, parent, isMain);
|
|
1070
1138
|
};
|
|
1071
1139
|
}
|
|
@@ -1118,7 +1186,13 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1") {
|
|
|
1118
1186
|
}
|
|
1119
1187
|
}
|
|
1120
1188
|
const originalLoad = Module._load;
|
|
1189
|
+
const redirected = new Set(["test", "node:test", "assert", "node:assert", "assert/strict", "node:assert/strict"]);
|
|
1121
1190
|
Module._load = function supercovNodeTestLoad(request, parent, isMain) {
|
|
1191
|
+
// Every require comes here, cached or not, thousands in a test
|
|
1192
|
+
// worker: only these six names are redirected, and nothing else is
|
|
1193
|
+
// looked at for any other.
|
|
1194
|
+
if (!redirected.has(request))
|
|
1195
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
1122
1196
|
const parentFile = parent?.filename?.replaceAll("\\", "/");
|
|
1123
1197
|
const belongsToProject = Boolean(projectRoot &&
|
|
1124
1198
|
parentFile?.startsWith(`${projectRoot}/`) &&
|
|
@@ -115,7 +115,9 @@ function throughRequireHook(resolved) {
|
|
|
115
115
|
return resolved;
|
|
116
116
|
return { ...resolved, format: "commonjs" };
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
// A specifier Supercov answers itself (`url`), or sends on as another
|
|
119
|
+
// (`next`, answered as it is); undefined for every other.
|
|
120
|
+
function redirected(specifier, context) {
|
|
119
121
|
// Some transpilers preserve the source-relative runtime import while moving
|
|
120
122
|
// only the transformed application file to an output directory. The
|
|
121
123
|
// source-local copy keeps strict rootDir compilers happy; this fallback
|
|
@@ -161,8 +163,16 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
161
163
|
shortCircuit: true,
|
|
162
164
|
};
|
|
163
165
|
}
|
|
164
|
-
return
|
|
166
|
+
return { next: REPLACEMENT };
|
|
165
167
|
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
171
|
+
const redirect = redirected(specifier, context);
|
|
172
|
+
if (redirect?.url)
|
|
173
|
+
return redirect;
|
|
174
|
+
if (redirect?.next)
|
|
175
|
+
return nextResolve(redirect.next, context);
|
|
166
176
|
try {
|
|
167
177
|
return throughRequireHook(await nextResolve(specifier, context));
|
|
168
178
|
}
|
|
@@ -172,6 +182,28 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
172
182
|
throw error;
|
|
173
183
|
}
|
|
174
184
|
}
|
|
185
|
+
// The same hooks run in the loading thread (`module.registerHooks`), where
|
|
186
|
+
// the next hook answers at once. There they see `require` calls too, which
|
|
187
|
+
// the hooks off the thread never did, and the CommonJS loader routes itself:
|
|
188
|
+
// a TypeScript file it was given back as CommonJS was compiled as JavaScript,
|
|
189
|
+
// past the handler ts-node/register installed for it.
|
|
190
|
+
export function resolveSync(specifier, context, nextResolve) {
|
|
191
|
+
if (context.conditions?.includes("require"))
|
|
192
|
+
return nextResolve(specifier, context);
|
|
193
|
+
const redirect = redirected(specifier, context);
|
|
194
|
+
if (redirect?.url)
|
|
195
|
+
return redirect;
|
|
196
|
+
if (redirect?.next)
|
|
197
|
+
return nextResolve(redirect.next, context);
|
|
198
|
+
try {
|
|
199
|
+
return throughRequireHook(nextResolve(specifier, context));
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error?.code === "ERR_MODULE_NOT_FOUND")
|
|
203
|
+
noteMissing(specifier, context.parentURL);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
175
207
|
|
|
176
208
|
// Node's own test coverage filters files by `--test-coverage-include` and
|
|
177
209
|
// `--test-coverage-exclude` globs relative to the working directory, and the
|
|
@@ -204,7 +236,17 @@ function mapToCopy(source, filename) {
|
|
|
204
236
|
}
|
|
205
237
|
}
|
|
206
238
|
export async function load(url, context, nextLoad) {
|
|
207
|
-
|
|
239
|
+
return loaded(url, await nextLoad(url, context));
|
|
240
|
+
}
|
|
241
|
+
export function loadSync(url, context, nextLoad) {
|
|
242
|
+
// Every module this process loads, required or imported: a server's
|
|
243
|
+
// runtime rescans the module cache for new ones only when this moved.
|
|
244
|
+
globalThis.__SUPERCOV_MODULE_LOADS__ = (globalThis.__SUPERCOV_MODULE_LOADS__ ?? 0) + 1;
|
|
245
|
+
if (context.conditions?.includes("require"))
|
|
246
|
+
return nextLoad(url, context);
|
|
247
|
+
return loaded(url, nextLoad(url, context));
|
|
248
|
+
}
|
|
249
|
+
function loaded(url, result) {
|
|
208
250
|
noteLoaded(url);
|
|
209
251
|
if (!NODE_COVERAGE_FILTERS || result.source == null || !url.startsWith("file:"))
|
|
210
252
|
return result;
|
|
@@ -98,12 +98,27 @@ export function callerLocation(boundary = callerLocation) {
|
|
|
98
98
|
return {};
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A compact attempt file names its scope on a writer's first line only; each
|
|
103
|
+
* line without one is the attempt of the line before it.
|
|
104
|
+
*/
|
|
105
|
+
function inheritingScope() {
|
|
106
|
+
let current;
|
|
107
|
+
return (line) => {
|
|
108
|
+
const record = JSON.parse(line);
|
|
109
|
+
if (record.scope)
|
|
110
|
+
current = record.scope;
|
|
111
|
+
else if (current)
|
|
112
|
+
record.scope = current;
|
|
113
|
+
return record;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
101
116
|
export function readScopedServerEvidence(scope, evidencePath = serverEvidencePath(scope)) {
|
|
102
117
|
try {
|
|
103
118
|
return readFileSync(evidencePath, "utf8")
|
|
104
119
|
.split("\n")
|
|
105
120
|
.filter(Boolean)
|
|
106
|
-
.map((
|
|
121
|
+
.map(inheritingScope())
|
|
107
122
|
.filter((record) => record.scope?.attemptId === scope.attemptId);
|
|
108
123
|
}
|
|
109
124
|
catch {
|
|
@@ -37,7 +37,8 @@ var COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
|
|
|
37
37
|
var DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
|
|
38
38
|
function configuredServerEvidenceRoot() {
|
|
39
39
|
var _a8;
|
|
40
|
-
|
|
40
|
+
const configured = typeof process !== "undefined" ? (_a8 = process.env) == null ? void 0 : _a8["SUPERCOV_SERVER_EVIDENCE_ROOT"] : void 0;
|
|
41
|
+
return configured ? configured : DEFAULT_SERVER_EVIDENCE_ROOT;
|
|
41
42
|
}
|
|
42
43
|
function nonEmpty(value) {
|
|
43
44
|
return typeof value === "string" && value.length > 0;
|
|
@@ -117,6 +118,18 @@ function serverEvidenceDirectory(scope, root = configuredServerEvidenceRoot()) {
|
|
|
117
118
|
function serverEvidencePath(scope, root = configuredServerEvidenceRoot()) {
|
|
118
119
|
return `${serverEvidenceDirectory(scope, root)}/${pathComponent(scope.attemptId)}.jsonl`;
|
|
119
120
|
}
|
|
121
|
+
// A scope's evidence directory and file, made once per scope and root: a
|
|
122
|
+
// server works them out for every record it writes, thousands per test.
|
|
123
|
+
var scopedEvidenceLocations = /* @__PURE__ */ new WeakMap();
|
|
124
|
+
function scopedEvidenceLocation(scope) {
|
|
125
|
+
const root = configuredServerEvidenceRoot();
|
|
126
|
+
const known = scopedEvidenceLocations.get(scope);
|
|
127
|
+
if (known !== void 0 && known.root === root)
|
|
128
|
+
return known;
|
|
129
|
+
const location = { root, directory: serverEvidenceDirectory(scope, root), path: serverEvidencePath(scope, root) };
|
|
130
|
+
scopedEvidenceLocations.set(scope, location);
|
|
131
|
+
return location;
|
|
132
|
+
}
|
|
120
133
|
function backgroundEvidenceDirectory(runId, root = configuredServerEvidenceRoot()) {
|
|
121
134
|
return `${serverRunEvidenceDirectory(runId, root)}/background`;
|
|
122
135
|
}
|
|
@@ -241,6 +254,7 @@ function activateProbeV2Key(key, force = false) {
|
|
|
241
254
|
if (state.probeV2NextEpoch >= 4294967295) {
|
|
242
255
|
state.probeV2NextEpoch = 1;
|
|
243
256
|
state.probeV2ContextEpochs.clear();
|
|
257
|
+
state.probeV2EpochGeneration = (state.probeV2EpochGeneration || 0) + 1;
|
|
244
258
|
for (const file of state.probeV2Files) {
|
|
245
259
|
file.hitEpochs.fill(0);
|
|
246
260
|
file.decisionObservationEpochs.fill(0);
|
|
@@ -260,8 +274,38 @@ function activateProbeV2Key(key, force = false) {
|
|
|
260
274
|
state.probeV2Clock.epoch = epoch;
|
|
261
275
|
return epoch;
|
|
262
276
|
}
|
|
277
|
+
// The async hook activates the current request's context before every
|
|
278
|
+
// callback the server runs, and building its key and looking it up was most of
|
|
279
|
+
// a Next server's time in the hook. A request's context object is never
|
|
280
|
+
// changed once made, and its key is made of it alone, so each keeps the epoch
|
|
281
|
+
// it was given until the epochs are cleared.
|
|
282
|
+
// Every copy of the runtime in a process shares one state, and the hook is the
|
|
283
|
+
// first copy's, so the cache and its generation live on that state too: a
|
|
284
|
+
// reset from another copy has to reach them.
|
|
285
|
+
var emptyProbeV2Context = Object.freeze({});
|
|
263
286
|
function activateProbeV2Context(context) {
|
|
264
|
-
|
|
287
|
+
// Work no request owns is keyed by the test running when it runs, which
|
|
288
|
+
// changes under the same context: only a request's context keeps its epoch.
|
|
289
|
+
if (!context.scope)
|
|
290
|
+
return activateProbeV2Key(probeV2ContextKey(context));
|
|
291
|
+
if (state.probeV2ContextEpochCache === void 0) {
|
|
292
|
+
state.probeV2ContextEpochCache = /* @__PURE__ */ new WeakMap();
|
|
293
|
+
state.probeV2EpochGeneration = 0;
|
|
294
|
+
}
|
|
295
|
+
const cached = state.probeV2ContextEpochCache.get(context);
|
|
296
|
+
if (cached !== void 0 && cached.generation === state.probeV2EpochGeneration) {
|
|
297
|
+
state.probeV2Clock.epoch = cached.epoch;
|
|
298
|
+
return cached.epoch;
|
|
299
|
+
}
|
|
300
|
+
const epoch = activateProbeV2Key(probeV2ContextKey(context));
|
|
301
|
+
state.probeV2ContextEpochCache.set(context, { generation: state.probeV2EpochGeneration, epoch });
|
|
302
|
+
return epoch;
|
|
303
|
+
}
|
|
304
|
+
// The context the async hook leaves to a callback's first probe: the
|
|
305
|
+
// request's store, or else the one the environment carries.
|
|
306
|
+
function resolveProbeV2Epoch() {
|
|
307
|
+
var _a8, _b;
|
|
308
|
+
return activateProbeV2Context((_b = (_a8 = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore()) != null ? _a8 : environmentRequestContext()) != null ? _b : emptyProbeV2Context);
|
|
265
309
|
}
|
|
266
310
|
function withProbeV2Context(context, callback) {
|
|
267
311
|
const previous = state.probeV2Clock.epoch;
|
|
@@ -283,10 +327,14 @@ function installProbeV2AsyncHook() {
|
|
|
283
327
|
return;
|
|
284
328
|
const epochs = [];
|
|
285
329
|
createHook({
|
|
330
|
+
// NaN, which no epoch equals: the callback's first probe works out
|
|
331
|
+
// its context. Most callbacks a server or a test runner runs reach no
|
|
332
|
+
// instrumented code, and working it out here, for every one, read the
|
|
333
|
+
// environment twice and built a key each time: on a Next app's suite,
|
|
334
|
+
// more of the server's time than any of Supercov's other work.
|
|
286
335
|
before() {
|
|
287
|
-
var _a9, _b;
|
|
288
336
|
epochs.push(state.probeV2Clock.epoch);
|
|
289
|
-
|
|
337
|
+
state.probeV2Clock.epoch = NaN;
|
|
290
338
|
},
|
|
291
339
|
after() {
|
|
292
340
|
var _a9;
|
|
@@ -329,6 +377,7 @@ function resetCoverage(testId2) {
|
|
|
329
377
|
state.logicals.clear();
|
|
330
378
|
state.eventKeys.clear();
|
|
331
379
|
state.probeV2ContextEpochs.clear();
|
|
380
|
+
state.probeV2EpochGeneration = (state.probeV2EpochGeneration || 0) + 1;
|
|
332
381
|
activateProbeV2Key(`reset\0${state.probeV2NextEpoch}`, true);
|
|
333
382
|
if (testId2)
|
|
334
383
|
runtimeGlobal.__SUPERCOV_MCDC_TEST_ID__ = testId2;
|
|
@@ -341,6 +390,78 @@ function resetCoverage(testId2) {
|
|
|
341
390
|
}
|
|
342
391
|
runtimeGlobal.__SUPERCOV_MCDC_SNAPSHOT__ = decisionSnapshot;
|
|
343
392
|
runtimeGlobal.__SUPERCOV_COVERAGE_SNAPSHOT__ = coverageSnapshot;
|
|
393
|
+
// A snapshot as the text the Playwright collector reads back. Its events
|
|
394
|
+
// become rows over one table of the strings they repeat -- point ids, phase
|
|
395
|
+
// ids, kinds -- each row in the key order of its event (`shapes`), and its
|
|
396
|
+
// hits indexes into the same table; snapshots.mjs's expandSnapshot gives
|
|
397
|
+
// back what JSON.parse of the whole would have. A large frontend's page
|
|
398
|
+
// holds tens of thousands of events, each naming its point, phase and
|
|
399
|
+
// environment in full, read back at the end of every test and from every
|
|
400
|
+
// page that leaves: relaying them cost the browser's own process more than
|
|
401
|
+
// its renderers spent running the app.
|
|
402
|
+
function compactSnapshotText(snapshot) {
|
|
403
|
+
if (!snapshot || typeof snapshot !== "object" || !Array.isArray(snapshot.events) || !Array.isArray(snapshot.hits))
|
|
404
|
+
return JSON.stringify(snapshot);
|
|
405
|
+
const strings = [];
|
|
406
|
+
const stringIndexes = /* @__PURE__ */ new Map();
|
|
407
|
+
const intern = (value) => {
|
|
408
|
+
let index = stringIndexes.get(value);
|
|
409
|
+
if (index === void 0) {
|
|
410
|
+
index = strings.length;
|
|
411
|
+
strings.push(value);
|
|
412
|
+
stringIndexes.set(value, index);
|
|
413
|
+
}
|
|
414
|
+
return index;
|
|
415
|
+
};
|
|
416
|
+
const shapes = [];
|
|
417
|
+
const shapeIndexes = /* @__PURE__ */ new Map();
|
|
418
|
+
const rows = [];
|
|
419
|
+
for (let position = 0; position < snapshot.events.length; position += 1) {
|
|
420
|
+
const event = snapshot.events[position];
|
|
421
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
422
|
+
return JSON.stringify(snapshot);
|
|
423
|
+
const fields = [];
|
|
424
|
+
const values = [];
|
|
425
|
+
const keys = Object.keys(event);
|
|
426
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
427
|
+
const value = event[keys[index]];
|
|
428
|
+
// What JSON.stringify leaves out of an object.
|
|
429
|
+
if (value === void 0 || typeof value === "function" || typeof value === "symbol")
|
|
430
|
+
continue;
|
|
431
|
+
if (typeof value === "string") {
|
|
432
|
+
fields.push(`s${keys[index]}`);
|
|
433
|
+
values.push(intern(value));
|
|
434
|
+
} else {
|
|
435
|
+
fields.push(`v${keys[index]}`);
|
|
436
|
+
values.push(value);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const name = fields.join("\0");
|
|
440
|
+
let shape = shapeIndexes.get(name);
|
|
441
|
+
if (shape === void 0) {
|
|
442
|
+
shape = shapes.length;
|
|
443
|
+
shapes.push(fields);
|
|
444
|
+
shapeIndexes.set(name, shape);
|
|
445
|
+
}
|
|
446
|
+
rows.push(shape);
|
|
447
|
+
for (let index = 0; index < values.length; index += 1)
|
|
448
|
+
rows.push(values[index]);
|
|
449
|
+
}
|
|
450
|
+
const hits = [];
|
|
451
|
+
for (let index = 0; index < snapshot.hits.length; index += 1) {
|
|
452
|
+
if (typeof snapshot.hits[index] !== "string")
|
|
453
|
+
return JSON.stringify(snapshot);
|
|
454
|
+
hits.push(intern(snapshot.hits[index]));
|
|
455
|
+
}
|
|
456
|
+
const rest = {};
|
|
457
|
+
const keys = Object.keys(snapshot);
|
|
458
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
459
|
+
if (keys[index] !== "events" && keys[index] !== "hits")
|
|
460
|
+
rest[keys[index]] = snapshot[keys[index]];
|
|
461
|
+
}
|
|
462
|
+
return JSON.stringify({ supercovCompactSnapshot: 1, order: keys, rest, strings, shapes, rows, hits });
|
|
463
|
+
}
|
|
464
|
+
runtimeGlobal.__SUPERCOV_COMPACT_SNAPSHOT__ = compactSnapshotText;
|
|
344
465
|
runtimeGlobal.__SUPERCOV_RESET__ = resetCoverage;
|
|
345
466
|
// A dedicated worker blocked in `Atomics.wait` runs no evaluation, so the
|
|
346
467
|
// test runner could not read its evidence and reported the test as a lower
|
|
@@ -417,11 +538,32 @@ function pushWorkerEvidence() {
|
|
|
417
538
|
runtimeGlobal.__SUPERCOV_WORKER_EVIDENCE__ = () => Array.from(received, ([token, pushed]) => ({ token, url: pushed.url, current: pushed.current, snapshot: pushed.snapshot }));
|
|
418
539
|
})();
|
|
419
540
|
var persistBrowserListenersInstalled = false;
|
|
541
|
+
// How much evidence the document holds. Every part of it only grows until a
|
|
542
|
+
// reset, so the same size twice is the same evidence.
|
|
543
|
+
function evidenceSize() {
|
|
544
|
+
let size = state.hits.size + state.events.length;
|
|
545
|
+
for (const decision of state.decisions.values())
|
|
546
|
+
size += decision.vectors.size;
|
|
547
|
+
for (const vectors of state.logicals.values())
|
|
548
|
+
size += vectors.size;
|
|
549
|
+
return size;
|
|
550
|
+
}
|
|
551
|
+
// The evidence's size when this document gave it to the Playwright collector
|
|
552
|
+
// on the way out. Its next document need not inherit it then: carried through
|
|
553
|
+
// localStorage as well, every page of a test parsed all the evidence of the
|
|
554
|
+
// pages before it on load and wrote it all again on leaving, a test of thirty
|
|
555
|
+
// pages paying for the first one thirty times, and the collector merged the
|
|
556
|
+
// same evidence once per page. What runs after the hand-over -- the rest of a
|
|
557
|
+
// handler that set `location.href`, a render it started -- is still carried.
|
|
558
|
+
var handedOverSize = -1;
|
|
420
559
|
function persistBrowserNow() {
|
|
421
560
|
if (!isBrowser)
|
|
422
561
|
return;
|
|
423
562
|
try {
|
|
424
|
-
|
|
563
|
+
if (handedOverSize === evidenceSize())
|
|
564
|
+
localStorage.removeItem(storageKey);
|
|
565
|
+
else
|
|
566
|
+
localStorage.setItem(storageKey, JSON.stringify(coverageSnapshot()));
|
|
425
567
|
} catch (e) {
|
|
426
568
|
}
|
|
427
569
|
}
|
|
@@ -433,8 +575,10 @@ function persistBrowserNow() {
|
|
|
433
575
|
// it does.
|
|
434
576
|
function handOverBrowser() {
|
|
435
577
|
try {
|
|
436
|
-
if (typeof runtimeGlobal.__supercovLeaving === "function")
|
|
437
|
-
runtimeGlobal.__supercovLeaving(
|
|
578
|
+
if (typeof runtimeGlobal.__supercovLeaving === "function") {
|
|
579
|
+
runtimeGlobal.__supercovLeaving(compactSnapshotText(coverageSnapshot()));
|
|
580
|
+
handedOverSize = evidenceSize();
|
|
581
|
+
}
|
|
438
582
|
} catch (e) {
|
|
439
583
|
}
|
|
440
584
|
}
|
|
@@ -468,6 +612,19 @@ function persistBrowser() {
|
|
|
468
612
|
function attemptKey(scope) {
|
|
469
613
|
return `${scope.runId}\0${scope.workerId}\0${scope.attemptId}`;
|
|
470
614
|
}
|
|
615
|
+
// An attempt's file says its scope once per writing process: each line
|
|
616
|
+
// without one is of the scope of the line before it in the same file.
|
|
617
|
+
var scopedPathsWritten = /* @__PURE__ */ new Set();
|
|
618
|
+
function serverLine(record, path) {
|
|
619
|
+
if (!path)
|
|
620
|
+
return JSON.stringify(record);
|
|
621
|
+
const { scope, ...line } = record;
|
|
622
|
+
if (scope && !scopedPathsWritten.has(path)) {
|
|
623
|
+
scopedPathsWritten.add(path);
|
|
624
|
+
line.scope = scope;
|
|
625
|
+
}
|
|
626
|
+
return JSON.stringify(line);
|
|
627
|
+
}
|
|
471
628
|
function serverRecordKey(record) {
|
|
472
629
|
var _a8;
|
|
473
630
|
const suffix = record.type === "decision" ? `${record.id}:${vectorKey(record.vector)}` : record.id;
|
|
@@ -530,7 +687,7 @@ function flushBufferedServerEvidence(scope) {
|
|
|
530
687
|
return void 0;
|
|
531
688
|
try {
|
|
532
689
|
makeEvidenceDirectory(fs, buffered.directory);
|
|
533
|
-
fs.appendFileSync(buffered.path, [...buffered.records.values()].map((record) =>
|
|
690
|
+
fs.appendFileSync(buffered.path, [...buffered.records.values()].map((record) => serverLine(record, buffered.path)).join("\n") + "\n");
|
|
534
691
|
return buffered.path;
|
|
535
692
|
} catch (e) {
|
|
536
693
|
return void 0;
|
|
@@ -758,7 +915,8 @@ function flushOnTermination(flush) {
|
|
|
758
915
|
}
|
|
759
916
|
} catch (e) {
|
|
760
917
|
}
|
|
761
|
-
|
|
918
|
+
const signals = ["SIGTERM", "SIGINT", "SIGHUP", "SIGBREAK"];
|
|
919
|
+
for (const signal of signals) {
|
|
762
920
|
try {
|
|
763
921
|
process.on(signal, () => {
|
|
764
922
|
drain();
|
|
@@ -770,6 +928,24 @@ function flushOnTermination(flush) {
|
|
|
770
928
|
} catch (e) {
|
|
771
929
|
}
|
|
772
930
|
}
|
|
931
|
+
// A listener keeps nothing alive, and a signal a process sends itself is
|
|
932
|
+
// handled on a later turn of its event loop: `process.kill(process.pid,
|
|
933
|
+
// "SIGTERM")` as a script's last line exited 0, once nothing else held the
|
|
934
|
+
// loop open, where alone it ended by the signal. A signal to itself that
|
|
935
|
+
// only this listener would take is drained and left to act at once.
|
|
936
|
+
try {
|
|
937
|
+
const kill = process.kill;
|
|
938
|
+
const numbered = { 1: "SIGHUP", 2: "SIGINT", 15: "SIGTERM", 21: "SIGBREAK" };
|
|
939
|
+
process.kill = function supercovKill(pid, signal) {
|
|
940
|
+
const name = signal === void 0 ? "SIGTERM" : typeof signal === "number" ? numbered[signal] : signal;
|
|
941
|
+
if (Number(pid) === process.pid && signals.indexOf(name) >= 0 && process.listenerCount(name) <= 1) {
|
|
942
|
+
drain();
|
|
943
|
+
process.removeAllListeners(name);
|
|
944
|
+
}
|
|
945
|
+
return kill.apply(this, arguments);
|
|
946
|
+
};
|
|
947
|
+
} catch (e) {
|
|
948
|
+
}
|
|
773
949
|
}
|
|
774
950
|
// Rust compiled to wasm and measured by the Rust frontend keeps what it ran
|
|
775
951
|
// in the module's memory, behind `__supercov_region_<crate>`. When the run
|
|
@@ -1024,8 +1200,9 @@ function appendServer(record) {
|
|
|
1024
1200
|
if (!runId)
|
|
1025
1201
|
return;
|
|
1026
1202
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1203
|
+
const location = scope ? scopedEvidenceLocation(scope) : void 0;
|
|
1204
|
+
const directory = location == null ? void 0 : location.directory;
|
|
1205
|
+
const path = location == null ? void 0 : location.path;
|
|
1029
1206
|
// Work done while handling a request that named no test -- a webhook a
|
|
1030
1207
|
// mock server sends -- says so, with the request's number.
|
|
1031
1208
|
const stored = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore();
|
|
@@ -1050,7 +1227,7 @@ function appendServer(record) {
|
|
|
1050
1227
|
appendDurableBackgroundRecord(fs, runId, serialized);
|
|
1051
1228
|
return;
|
|
1052
1229
|
}
|
|
1053
|
-
enqueueServerAppend(directory, path,
|
|
1230
|
+
enqueueServerAppend(directory, path, serverLine(serialized, path) + "\n", runId);
|
|
1054
1231
|
if (deduplicationKey)
|
|
1055
1232
|
state.persistedServerRecords.add(deduplicationKey);
|
|
1056
1233
|
} catch (cause) {
|
|
@@ -1639,13 +1816,24 @@ function noteSupport(scope) {
|
|
|
1639
1816
|
const moduleBuiltin = typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("node:module") : void 0;
|
|
1640
1817
|
const cache = moduleBuiltin && moduleBuiltin._cache;
|
|
1641
1818
|
const root = process.env.SUPERCOV_PROJECT_ROOT;
|
|
1642
|
-
|
|
1819
|
+
// The in-thread module hooks count every load (resolve-loader.mjs): a
|
|
1820
|
+
// cache nothing was loaded into since the last request holds nothing
|
|
1821
|
+
// new. Listing a Next server's thousands of keys on every request was
|
|
1822
|
+
// the most of its time any of Supercov's code took. Without the count
|
|
1823
|
+
// (off-thread hooks), every request looks.
|
|
1824
|
+
const loads = runtimeGlobal.__SUPERCOV_MODULE_LOADS__;
|
|
1825
|
+
const unchanged = typeof loads === "number" && loads === supportNoted.loads;
|
|
1826
|
+
supportNoted.loads = loads;
|
|
1827
|
+
if (cache && root && !unchanged) {
|
|
1643
1828
|
for (const name of Object.keys(cache)) {
|
|
1829
|
+
// Each is looked at once, whether it is noted or not.
|
|
1830
|
+
if (supportNoted.modules.has(name))
|
|
1831
|
+
continue;
|
|
1832
|
+
supportNoted.modules.add(name);
|
|
1644
1833
|
// The copy lives under `.supercov/`: the project root decides.
|
|
1645
1834
|
const inside = name.indexOf(root) === 0 ? name.slice(root.length).replace(/^[\\/]/, "") : "";
|
|
1646
|
-
if (
|
|
1835
|
+
if (inside === "" || /^\.supercov[\\/]/.test(inside) || /(?:^|[\\/])node_modules[\\/]/.test(inside))
|
|
1647
1836
|
continue;
|
|
1648
|
-
supportNoted.modules.add(name);
|
|
1649
1837
|
lines.push({ loaded: name });
|
|
1650
1838
|
}
|
|
1651
1839
|
}
|
|
@@ -1789,6 +1977,8 @@ function recordCoverageHit(id) {
|
|
|
1789
1977
|
activateProbeV2Context(currentRequestContext());
|
|
1790
1978
|
epoch = clock.epoch;
|
|
1791
1979
|
clock.epoch = previousEpoch;
|
|
1980
|
+
} else if (epoch !== epoch) {
|
|
1981
|
+
epoch = resolveProbeV2Epoch();
|
|
1792
1982
|
}
|
|
1793
1983
|
if (!state.hitEpochs)
|
|
1794
1984
|
state.hitEpochs = /* @__PURE__ */ new Map();
|
|
@@ -1814,24 +2004,31 @@ function recordCoverageHit(id) {
|
|
|
1814
2004
|
}, phaseId ? { phaseId } : {}));
|
|
1815
2005
|
}
|
|
1816
2006
|
}
|
|
2007
|
+
// A file's ids, as the instrumenter writes them: one string of ids separated
|
|
2008
|
+
// by spaces, or a list where an id holds one.
|
|
2009
|
+
function listedIds(ids) {
|
|
2010
|
+
return typeof ids === "string" ? ids === "" ? [] : ids.split(" ") : ids;
|
|
2011
|
+
}
|
|
1817
2012
|
function registerProbeV2(definition) {
|
|
1818
2013
|
var _a8, _b;
|
|
2014
|
+
const decisions = listedIds(definition.decisions);
|
|
2015
|
+
const pointIds = listedIds(definition.pointIds);
|
|
1819
2016
|
const file = {
|
|
1820
2017
|
// Each decision's id and its number of conditions: the module carries
|
|
1821
2018
|
// no source text, which the manifest holds.
|
|
1822
|
-
decisions
|
|
2019
|
+
decisions,
|
|
1823
2020
|
decisionWidths: definition.decisionWidths,
|
|
1824
|
-
pointIds
|
|
2021
|
+
pointIds,
|
|
1825
2022
|
clock: state.probeV2Clock,
|
|
1826
|
-
hitEpochs: new Uint32Array(
|
|
2023
|
+
hitEpochs: new Uint32Array(pointIds.length),
|
|
1827
2024
|
decisionEpochs: definition.decisionWidths.map((width) => width <= 6 ? new Uint32Array(2 * 3 ** width) : /* @__PURE__ */ new Map()),
|
|
1828
|
-
decisionVectorCounts:
|
|
2025
|
+
decisionVectorCounts: decisions.map((_, index) => {
|
|
1829
2026
|
var _a9, _b2;
|
|
1830
2027
|
return (_b2 = (_a9 = definition.decisionVectorCounts) == null ? void 0 : _a9[index]) != null ? _b2 : 0;
|
|
1831
2028
|
}),
|
|
1832
|
-
decisionObservationEpochs: new Uint32Array(
|
|
1833
|
-
decisionObservationCounts: new Uint16Array(
|
|
1834
|
-
decisionCompleteEpochs: new Uint32Array(
|
|
2029
|
+
decisionObservationEpochs: new Uint32Array(decisions.length),
|
|
2030
|
+
decisionObservationCounts: new Uint16Array(decisions.length),
|
|
2031
|
+
decisionCompleteEpochs: new Uint32Array(decisions.length),
|
|
1835
2032
|
pendingDefaults: new Uint32Array(definition.defaultCount || 0),
|
|
1836
2033
|
// A selection's outcomes -- short falsy, short truthy, right falsy, right
|
|
1837
2034
|
// truthy -- are four consecutive points per site from the first index.
|
|
@@ -1876,6 +2073,8 @@ function recordCoverageHitV2(file, index) {
|
|
|
1876
2073
|
const previousEpoch = file.clock.epoch;
|
|
1877
2074
|
if (fallbackEpoch)
|
|
1878
2075
|
activateProbeV2Context(currentRequestContext());
|
|
2076
|
+
else if (previousEpoch !== previousEpoch)
|
|
2077
|
+
resolveProbeV2Epoch();
|
|
1879
2078
|
try {
|
|
1880
2079
|
const epoch = file.clock.epoch;
|
|
1881
2080
|
if (file.hitEpochs[index] === epoch)
|
|
@@ -1928,6 +2127,8 @@ function recordMcdcEndV2(file, decisionIndex, encoded, value) {
|
|
|
1928
2127
|
const previousEpoch = file.clock.epoch;
|
|
1929
2128
|
if (fallbackEpoch)
|
|
1930
2129
|
activateProbeV2Context(currentRequestContext());
|
|
2130
|
+
else if (previousEpoch !== previousEpoch)
|
|
2131
|
+
resolveProbeV2Epoch();
|
|
1931
2132
|
try {
|
|
1932
2133
|
const epoch = file.clock.epoch;
|
|
1933
2134
|
const seen = file.decisionEpochs[decisionIndex];
|