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.
|
|
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.
|
|
72
|
-
"@supercov/cli-darwin-x64": "0.0.
|
|
73
|
-
"@supercov/cli-linux-arm64-gnu": "0.0.
|
|
74
|
-
"@supercov/cli-linux-arm64-musl": "0.0.
|
|
75
|
-
"@supercov/cli-linux-x64-gnu": "0.0.
|
|
76
|
-
"@supercov/cli-linux-x64-musl": "0.0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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() {
|