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.
@@ -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
- return typeof process !== "undefined" && ((_a8 = process.env) == null ? void 0 : _a8["SUPERCOV_SERVER_EVIDENCE_ROOT"]) ? process.env["SUPERCOV_SERVER_EVIDENCE_ROOT"] : DEFAULT_SERVER_EVIDENCE_ROOT;
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
- return activateProbeV2Key(probeV2ContextKey(context));
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
- activateProbeV2Context((_b = (_a9 = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore()) != null ? _a9 : environmentRequestContext()) != null ? _b : {});
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
- localStorage.setItem(storageKey, JSON.stringify(coverageSnapshot()));
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(JSON.stringify(coverageSnapshot()));
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) => JSON.stringify(record)).join("\n") + "\n");
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
- for (const signal of ["SIGTERM", "SIGINT", "SIGHUP", "SIGBREAK"]) {
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 directory = scope ? serverEvidenceDirectory(scope) : void 0;
1028
- const path = scope ? serverEvidencePath(scope) : void 0;
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, JSON.stringify(serialized) + "\n", runId);
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
- if (cache && root) {
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 (supportNoted.modules.has(name) || inside === "" || /^\.supercov[\\/]/.test(inside) || /(?:^|[\\/])node_modules[\\/]/.test(inside))
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: definition.decisions,
2019
+ decisions,
1823
2020
  decisionWidths: definition.decisionWidths,
1824
- pointIds: definition.pointIds,
2021
+ pointIds,
1825
2022
  clock: state.probeV2Clock,
1826
- hitEpochs: new Uint32Array(definition.pointIds.length),
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: definition.decisions.map((_, index) => {
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(definition.decisions.length),
1833
- decisionObservationCounts: new Uint16Array(definition.decisions.length),
1834
- decisionCompleteEpochs: new Uint32Array(definition.decisions.length),
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