supercov 4.24.7 → 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/atomic.mjs +42 -2
- 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 +87 -31
- package/runtime/javascript/resolve-loader.mjs +49 -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
|
@@ -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];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two events JSON.stringify would write alike: the same keys in the same
|
|
3
|
+
* order, with the same values. Comparing is several times cheaper than
|
|
4
|
+
* writing both, and a test's pages hand over thousands of events each time
|
|
5
|
+
* one of them leaves.
|
|
6
|
+
*/
|
|
7
|
+
function sameEvent(left, right) {
|
|
8
|
+
if (left === right)
|
|
9
|
+
return true;
|
|
10
|
+
if (left === null || right === null || typeof left !== "object" || typeof right !== "object")
|
|
11
|
+
return false;
|
|
12
|
+
const leftKeys = Object.keys(left);
|
|
13
|
+
const rightKeys = Object.keys(right);
|
|
14
|
+
if (leftKeys.length !== rightKeys.length)
|
|
15
|
+
return false;
|
|
16
|
+
for (let index = 0; index < leftKeys.length; index += 1) {
|
|
17
|
+
const key = leftKeys[index];
|
|
18
|
+
if (key !== rightKeys[index])
|
|
19
|
+
return false;
|
|
20
|
+
const leftValue = left[key];
|
|
21
|
+
const rightValue = right[key];
|
|
22
|
+
if (leftValue === rightValue)
|
|
23
|
+
continue;
|
|
24
|
+
if (leftValue === null || rightValue === null || typeof leftValue !== "object" || typeof rightValue !== "object" ||
|
|
25
|
+
JSON.stringify(leftValue) !== JSON.stringify(rightValue))
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One snapshot holding everything in `snapshots`, each observation once: a
|
|
32
|
+
* document that left gave its evidence, and the next one on its origin
|
|
33
|
+
* restored the same evidence and added to it.
|
|
34
|
+
*/
|
|
35
|
+
export function mergeSnapshots(snapshots) {
|
|
36
|
+
const hits = new Set();
|
|
37
|
+
const events = new Map();
|
|
38
|
+
const decisions = new Map();
|
|
39
|
+
const logicals = new Map();
|
|
40
|
+
const vectorsOf = (map, key, initial) => {
|
|
41
|
+
let entry = map.get(key);
|
|
42
|
+
if (!entry) {
|
|
43
|
+
entry = { ...initial, seen: new Map() };
|
|
44
|
+
map.set(key, entry);
|
|
45
|
+
}
|
|
46
|
+
return entry;
|
|
47
|
+
};
|
|
48
|
+
let previousEvents;
|
|
49
|
+
for (const snapshot of snapshots) {
|
|
50
|
+
for (const id of snapshot?.hits ?? [])
|
|
51
|
+
hits.add(id);
|
|
52
|
+
// A document restores the evidence the one before it on its origin
|
|
53
|
+
// left, in order, before adding its own: the events it repeats from
|
|
54
|
+
// the snapshot before are in `events` already, and are compared
|
|
55
|
+
// instead of serialized.
|
|
56
|
+
const list = snapshot?.events ?? [];
|
|
57
|
+
let start = 0;
|
|
58
|
+
if (previousEvents !== undefined) {
|
|
59
|
+
const shared = Math.min(previousEvents.length, list.length);
|
|
60
|
+
while (start < shared && sameEvent(previousEvents[start], list[start]))
|
|
61
|
+
start += 1;
|
|
62
|
+
}
|
|
63
|
+
for (let index = start; index < list.length; index += 1)
|
|
64
|
+
events.set(JSON.stringify(list[index]), list[index]);
|
|
65
|
+
previousEvents = list;
|
|
66
|
+
for (const decision of snapshot?.decisions ?? []) {
|
|
67
|
+
const entry = vectorsOf(decisions, decision.id, { id: decision.id });
|
|
68
|
+
for (const vector of decision.vectors ?? [])
|
|
69
|
+
entry.seen.set(JSON.stringify(vector), vector);
|
|
70
|
+
}
|
|
71
|
+
for (const logical of snapshot?.logicals ?? []) {
|
|
72
|
+
const entry = vectorsOf(logicals, logical.id, { id: logical.id });
|
|
73
|
+
for (const vector of logical.vectors ?? [])
|
|
74
|
+
entry.seen.set(JSON.stringify(vector), vector);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
decisions: [...decisions.values()].map(({ seen, id }) => ({ id, vectors: [...seen.values()] })),
|
|
79
|
+
hits: [...hits],
|
|
80
|
+
events: [...events.values()],
|
|
81
|
+
logicals: [...logicals.values()].map(({ seen, id }) => ({ id, vectors: [...seen.values()] })),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A snapshot as JSON.parse would give it from the text a page's runtime
|
|
86
|
+
* wrote compactly (runtime.mjs, compactSnapshotText): each event in its own
|
|
87
|
+
* key order, its strings from the shared table. Anything else is returned
|
|
88
|
+
* as it is.
|
|
89
|
+
*/
|
|
90
|
+
export function expandSnapshot(value) {
|
|
91
|
+
if (!value || typeof value !== "object" || value.supercovCompactSnapshot !== 1)
|
|
92
|
+
return value;
|
|
93
|
+
const { order, rest, strings, shapes, rows, hits } = value;
|
|
94
|
+
const events = [];
|
|
95
|
+
for (let position = 0; position < rows.length;) {
|
|
96
|
+
const fields = shapes[rows[position++]];
|
|
97
|
+
const event = {};
|
|
98
|
+
for (const field of fields) {
|
|
99
|
+
const raw = rows[position++];
|
|
100
|
+
event[field.slice(1)] = field[0] === "s" ? strings[raw] : raw;
|
|
101
|
+
}
|
|
102
|
+
events.push(event);
|
|
103
|
+
}
|
|
104
|
+
const snapshot = {};
|
|
105
|
+
for (const key of order)
|
|
106
|
+
snapshot[key] = key === "events" ? events : key === "hits" ? hits.map((index) => strings[index]) : rest[key];
|
|
107
|
+
return snapshot;
|
|
108
|
+
}
|
|
@@ -7,6 +7,42 @@ export const COVERAGE_PHASE_COOKIE = "__supercov_phase";
|
|
|
7
7
|
// so the two cannot be one.
|
|
8
8
|
export const COVERAGE_SCOPE_SITE_COOKIE = "__supercov_scope_site";
|
|
9
9
|
export const COVERAGE_PHASE_SITE_COOKIE = "__supercov_phase_site";
|
|
10
|
+
// The phase a new document starts in: the last action's, since a document
|
|
11
|
+
// loads because of one (a navigation, a click that navigates). An assertion
|
|
12
|
+
// that starts while it loads does not change it.
|
|
13
|
+
export const COVERAGE_DOCUMENT_PHASE_COOKIE = "__supercov_document_phase";
|
|
14
|
+
export const COVERAGE_DOCUMENT_PHASE_SITE_COOKIE = "__supercov_document_phase_site";
|
|
15
|
+
/**
|
|
16
|
+
* Runs at the start of every document of a context, as an init script, so it
|
|
17
|
+
* reads nothing from this module. A document starts in the phase its cookie
|
|
18
|
+
* names. Only this attempt's phase is taken: a copy of the script that a
|
|
19
|
+
* context kept from an earlier test leaves a later test's phase alone.
|
|
20
|
+
*/
|
|
21
|
+
export function startDocumentInPhase({ attemptId, phaseCookie, phaseSiteCookie }) {
|
|
22
|
+
try {
|
|
23
|
+
let phase;
|
|
24
|
+
for (const part of document.cookie.split(";")) {
|
|
25
|
+
const at = part.indexOf("=");
|
|
26
|
+
const name = part.slice(0, at).trim();
|
|
27
|
+
if (name !== phaseCookie && name !== phaseSiteCookie)
|
|
28
|
+
continue;
|
|
29
|
+
const value = decodeURIComponent(part.slice(at + 1));
|
|
30
|
+
if (!value.startsWith(`${attemptId}:`))
|
|
31
|
+
continue;
|
|
32
|
+
phase = value;
|
|
33
|
+
if (name === phaseCookie)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (!phase)
|
|
37
|
+
return;
|
|
38
|
+
const coverageGlobal = globalThis;
|
|
39
|
+
coverageGlobal.__SUPERCOV_PHASE_ID__ = phase;
|
|
40
|
+
coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ ?? "unscoped", phase);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A document that cannot read cookies keeps its stored phase.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
10
46
|
export const COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
|
|
11
47
|
export const DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
|
|
12
48
|
// Buffer as it was when Supercov loaded. Tests replace globals -- axios's set
|