supercov 0.0.24 → 0.0.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "Zero-edit, runner-aware coverage completeness for JavaScript test suites",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -68,12 +68,12 @@
68
68
  "prepublishOnly": "npm run release:check"
69
69
  },
70
70
  "optionalDependencies": {
71
- "@supercov/cli-darwin-arm64": "0.0.24",
72
- "@supercov/cli-darwin-x64": "0.0.24",
73
- "@supercov/cli-linux-arm64-gnu": "0.0.24",
74
- "@supercov/cli-linux-arm64-musl": "0.0.24",
75
- "@supercov/cli-linux-x64-gnu": "0.0.24",
76
- "@supercov/cli-linux-x64-musl": "0.0.24"
71
+ "@supercov/cli-darwin-arm64": "0.0.25",
72
+ "@supercov/cli-darwin-x64": "0.0.25",
73
+ "@supercov/cli-linux-arm64-gnu": "0.0.25",
74
+ "@supercov/cli-linux-arm64-musl": "0.0.25",
75
+ "@supercov/cli-linux-x64-gnu": "0.0.25",
76
+ "@supercov/cli-linux-x64-musl": "0.0.25"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@playwright/test": ">=1.55.0",
@@ -37,6 +37,50 @@ const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?
37
37
  .replace(/[^A-Za-z0-9_-]/g, "_");
38
38
  const GENERATED_RUN_ID = "__SUPERCOV_RUN_ID__";
39
39
  const PHASE_STORAGE_KEY = "__supercov_phase";
40
+ // Wall-clock accounting for every browser round-trip this shim performs,
41
+ // enabled by SUPERCOV_PHASE_TIMING=1. One summary line per test on stderr;
42
+ // a single boolean check when disabled.
43
+ const generatedPhaseTiming = "__SUPERCOV_PHASE_TIMING__";
44
+ const PHASE_TIMING = process.env["SUPERCOV_PHASE_TIMING"] === "1" || generatedPhaseTiming === "1";
45
+ const timingBuckets = PHASE_TIMING ? new Map() : undefined;
46
+ function timingCount(bucket, milliseconds = 0) {
47
+ if (!timingBuckets)
48
+ return;
49
+ const entry = timingBuckets.get(bucket) ?? { calls: 0, ms: 0 };
50
+ entry.calls += 1;
51
+ entry.ms += milliseconds;
52
+ timingBuckets.set(bucket, entry);
53
+ }
54
+ async function timed(bucket, work) {
55
+ if (!timingBuckets)
56
+ return work();
57
+ const started = performance.now();
58
+ try {
59
+ return await work();
60
+ }
61
+ finally {
62
+ timingCount(bucket, performance.now() - started);
63
+ }
64
+ }
65
+ function timingReport(label) {
66
+ if (!timingBuckets)
67
+ return;
68
+ const summary = Object.fromEntries([...timingBuckets.entries()].map(([bucket, entry]) => [bucket, { calls: entry.calls, ms: Math.round(entry.ms) }]));
69
+ const line = JSON.stringify({ label, summary });
70
+ console.error(`[supercov-timing] ${line}`);
71
+ // Pooled runners swallow worker stderr for passing tests, so the report
72
+ // also lands as a file beside the evidence, which rides the workspace
73
+ // mount back to the host.
74
+ try {
75
+ const resolved = resolve(process.cwd(), ".supercov/phase-timing");
76
+ mkdirSync(resolved, { recursive: true });
77
+ appendJsonLineSync(resolve(resolved, `phase-timing-${process.pid}.jsonl`), `${line}\n`);
78
+ }
79
+ catch {
80
+ // Timing must never affect the run.
81
+ }
82
+ timingBuckets.clear();
83
+ }
40
84
  const ACTION_METHODS = new Set([
41
85
  "blur",
42
86
  "check",
@@ -125,6 +169,7 @@ class CoveragePhaseController {
125
169
  async collectRuntimeSnapshots() {
126
170
  if (this.runtimeSnapshots)
127
171
  return this.runtimeSnapshots;
172
+ timingCount("collectRuntimeSnapshots.cold");
128
173
  const snapshots = [];
129
174
  for (const page of this.allPages()) {
130
175
  for (const frame of page.frames()) {
@@ -160,6 +205,7 @@ class CoveragePhaseController {
160
205
  async registerPage(page) {
161
206
  if (this.pages.has(page))
162
207
  return;
208
+ timingCount("registerPage");
163
209
  this.pages.add(page);
164
210
  await this.registerContext(page.context());
165
211
  const cdp = await page.context().newCDPSession(page).catch(() => undefined);
@@ -251,19 +297,21 @@ class CoveragePhaseController {
251
297
  .catch(() => undefined);
252
298
  }
253
299
  async beginAction(operation) {
300
+ timingCount("beginAction");
254
301
  const phase = this.createPhase("action", operation);
255
302
  this.lastActionId = phase.id;
256
303
  this.activePhaseId = phase.id;
257
- await this.activateInBrowser(phase.id);
304
+ await timed("action.activateInBrowser", () => this.activateInBrowser(phase.id));
258
305
  return phase;
259
306
  }
260
307
  beginAssertion(operation, source = callerSource()) {
308
+ timingCount("beginAssertion");
261
309
  const phase = this.createPhase("assertion", operation, this.lastActionId, source);
262
310
  this.activePhaseId = phase.id;
263
311
  // Playwright queues browser protocol commands in order. Starting this
264
312
  // evaluation before an async locator assertion is sufficient to tag its
265
313
  // polling work without turning synchronous expect matchers into promises.
266
- void this.activateInBrowser(phase.id);
314
+ void timed("assertion.activateInBrowser", () => this.activateInBrowser(phase.id));
267
315
  return phase;
268
316
  }
269
317
  requestPhaseId() {
@@ -409,8 +457,8 @@ class CoveragePhaseController {
409
457
  return scoped;
410
458
  }
411
459
  async activateInBrowser(phaseId) {
412
- await Promise.all([...this.contexts].map((context) => this.updateContextHeaders(context, phaseId)));
413
- await Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
460
+ await timed("activate.contextHeaders", () => Promise.all([...this.contexts].map((context) => this.updateContextHeaders(context, phaseId))));
461
+ await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
414
462
  .evaluate(({ id, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
415
463
  globalThis.__SUPERCOV_PHASE_ID__ = id;
416
464
  const coverageGlobal = globalThis;
@@ -430,15 +478,15 @@ class CoveragePhaseController {
430
478
  scopeValue: encodeCoverageScope(this.scope),
431
479
  phaseCookie: COVERAGE_PHASE_COOKIE,
432
480
  })
433
- .catch(() => undefined)));
434
- await Promise.all([...this.workers].map((worker) => worker
481
+ .catch(() => undefined))));
482
+ await timed("activate.workerEvaluate", () => Promise.all([...this.workers].map((worker) => worker
435
483
  .evaluate((id) => {
436
484
  const coverageGlobal = globalThis;
437
485
  coverageGlobal.__SUPERCOV_PHASE_ID__ = id;
438
486
  coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ ?? "unscoped", id);
439
487
  }, phaseId)
440
- .catch(() => undefined)));
441
- await Promise.all([...this.pages].map((page) => this.activatePage(page, phaseId)));
488
+ .catch(() => undefined))));
489
+ await timed("activate.pageScript", () => Promise.all([...this.pages].map((page) => this.activatePage(page, phaseId))));
442
490
  }
443
491
  async updateContextHeaders(context, phaseId) {
444
492
  await context
@@ -877,7 +925,8 @@ const instrumentedTest = base.extend({
877
925
  }
878
926
  }
879
927
  finally {
880
- await controller.dispose();
928
+ await timed("controller.dispose", () => controller.dispose());
929
+ timingReport(testInfo.title);
881
930
  directRuntime()?.activateCoverageScope();
882
931
  if (activeController === controller)
883
932
  activeController = undefined;
@@ -157,7 +157,12 @@ function createState() {
157
157
  probeV2Clock: { epoch: Number.NaN, fast: false },
158
158
  probeV2ContextEpochs: /* @__PURE__ */ new Map(),
159
159
  probeV2NextEpoch: 1,
160
- probeV2HookInstalled: false
160
+ probeV2HookInstalled: false,
161
+ pendingServerAppends: /* @__PURE__ */ new Map(),
162
+ createdEvidenceDirectories: /* @__PURE__ */ new Set(),
163
+ serverFlushScheduled: false,
164
+ serverExitHookInstalled: false,
165
+ serverTransportFailure: void 0
161
166
  };
162
167
  if (!isBrowser)
163
168
  return state2;
@@ -313,7 +318,9 @@ function resetCoverage(testId2) {
313
318
  runtimeGlobal.__SUPERCOV_MCDC_SNAPSHOT__ = decisionSnapshot;
314
319
  runtimeGlobal.__SUPERCOV_COVERAGE_SNAPSHOT__ = coverageSnapshot;
315
320
  runtimeGlobal.__SUPERCOV_RESET__ = resetCoverage;
316
- function persistBrowser() {
321
+ var persistBrowserScheduled = false;
322
+ var persistBrowserListenersInstalled = false;
323
+ function persistBrowserNow() {
317
324
  if (!isBrowser)
318
325
  return;
319
326
  try {
@@ -321,6 +328,32 @@ function persistBrowser() {
321
328
  } catch (e) {
322
329
  }
323
330
  }
331
+ function persistBrowser() {
332
+ if (!isBrowser)
333
+ return;
334
+ // Persistence exists so evidence survives navigation, not to mirror every
335
+ // event: serializing the whole snapshot per event is quadratic in a
336
+ // render burst. Coalesce to one write per macrotask and flush when the
337
+ // page is actually leaving.
338
+ if (!persistBrowserListenersInstalled) {
339
+ persistBrowserListenersInstalled = true;
340
+ try {
341
+ addEventListener("pagehide", persistBrowserNow);
342
+ addEventListener("visibilitychange", () => {
343
+ if (typeof document !== "undefined" && document.visibilityState === "hidden")
344
+ persistBrowserNow();
345
+ });
346
+ } catch (e) {
347
+ }
348
+ }
349
+ if (persistBrowserScheduled)
350
+ return;
351
+ persistBrowserScheduled = true;
352
+ setTimeout(() => {
353
+ persistBrowserScheduled = false;
354
+ persistBrowserNow();
355
+ }, 0);
356
+ }
324
357
  function attemptKey(scope) {
325
358
  return `${scope.runId}\0${scope.workerId}\0${scope.attemptId}`;
326
359
  }
@@ -432,6 +465,66 @@ if (!isBrowser) {
432
465
  runtimeGlobal.__SUPERCOV_BUFFER_EXIT_INSTALLED__ = true;
433
466
  }
434
467
  }
468
+ function serverTransportError(runId, cause) {
469
+ const detail = cause instanceof Error ? cause.message : String(cause);
470
+ const failure = new Error(`Supercov could not persist coverage evidence for run ${runId}: ${detail}`);
471
+ failure.code = "SUPERCOV_EVIDENCE_TRANSPORT_FAILED";
472
+ failure.cause = cause;
473
+ return failure;
474
+ }
475
+ function flushServerAppends() {
476
+ const pending = state.pendingServerAppends;
477
+ if (pending.size === 0)
478
+ return;
479
+ const fs = getFs();
480
+ for (const [path, entry] of pending) {
481
+ pending.delete(path);
482
+ try {
483
+ if (!fs)
484
+ throw new Error("node:fs is unavailable");
485
+ if (!state.createdEvidenceDirectories.has(entry.directory)) {
486
+ fs.mkdirSync(entry.directory, { recursive: true });
487
+ state.createdEvidenceDirectories.add(entry.directory);
488
+ }
489
+ fs.appendFileSync(path, entry.lines.join(""));
490
+ } catch (cause) {
491
+ const failure = serverTransportError(entry.runId, cause);
492
+ state.serverTransportFailure = failure;
493
+ throw failure;
494
+ }
495
+ }
496
+ }
497
+ function enqueueServerAppend(directory, path, line, runId) {
498
+ // Fail closed with the original context: after one transport failure the
499
+ // very next probe re-raises it synchronously.
500
+ if (state.serverTransportFailure)
501
+ throw state.serverTransportFailure;
502
+ const entry = state.pendingServerAppends.get(path);
503
+ if (entry) {
504
+ entry.lines.push(line);
505
+ if (entry.lines.length >= 2048)
506
+ flushServerAppends();
507
+ } else {
508
+ state.pendingServerAppends.set(path, { directory, runId, lines: [line] });
509
+ }
510
+ if (!state.serverExitHookInstalled && typeof process !== "undefined") {
511
+ state.serverExitHookInstalled = true;
512
+ try {
513
+ process.on("exit", flushServerAppends);
514
+ } catch (e) {
515
+ }
516
+ }
517
+ // One synchronous write per event-loop turn instead of one per probe: a
518
+ // request handler's burst of first-touch records becomes a single append
519
+ // that is still on disk before the process yields past this turn.
520
+ if (!state.serverFlushScheduled) {
521
+ state.serverFlushScheduled = true;
522
+ queueMicrotask(() => {
523
+ state.serverFlushScheduled = false;
524
+ flushServerAppends();
525
+ });
526
+ }
527
+ }
435
528
  function appendServer(record) {
436
529
  var _a8, _b, _c, _d;
437
530
  if (state.runtimeSnapshots) {
@@ -487,16 +580,13 @@ function appendServer(record) {
487
580
  appendDurableBackgroundRecord(fs, runId, serialized);
488
581
  return;
489
582
  }
490
- fs.mkdirSync(directory, { recursive: true });
491
- fs.appendFileSync(path, JSON.stringify(serialized) + "\n");
583
+ enqueueServerAppend(directory, path, JSON.stringify(serialized) + "\n", runId);
492
584
  if (deduplicationKey)
493
585
  state.persistedServerRecords.add(deduplicationKey);
494
586
  } catch (cause) {
495
- const detail = cause instanceof Error ? cause.message : String(cause);
496
- const failure = new Error(`Supercov could not persist coverage evidence for run ${runId}: ${detail}`);
497
- failure.code = "SUPERCOV_EVIDENCE_TRANSPORT_FAILED";
498
- failure.cause = cause;
499
- throw failure;
587
+ if (cause instanceof Error && cause.code === "SUPERCOV_EVIDENCE_TRANSPORT_FAILED")
588
+ throw cause;
589
+ throw serverTransportError(runId, cause);
500
590
  }
501
591
  }
502
592
  function environmentRequestContext() {