supercov 2.0.1 → 3.0.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/README.md +19 -22
- package/docs/agent-loop.md +8 -13
- package/docs/assertions.md +161 -75
- package/docs/cli.md +111 -59
- package/docs/coverage-model.md +5 -6
- package/docs/evidence.md +0 -11
- package/docs/getting-started.md +1 -1
- package/docs/performance.md +6 -14
- package/docs/quality.md +1 -1
- package/docs/security.md +43 -12
- package/docs/supported-suites.md +44 -48
- package/docs/troubleshooting.md +12 -5
- package/docs/workspace-isolation.md +23 -2
- package/package.json +33 -21
- package/runtime/javascript/atomic.mjs +46 -2
- package/runtime/javascript/ava.mjs +82 -0
- package/runtime/javascript/bootstrap.cjs +43 -0
- package/runtime/javascript/jest.cjs +52 -15
- package/runtime/javascript/jest.config.mjs +9 -1
- package/runtime/javascript/jestReporter.mjs +5 -6
- package/runtime/javascript/launchSupervisor.mjs +127 -15
- package/runtime/javascript/mocha.mjs +180 -0
- package/runtime/javascript/nodeTest.mjs +57 -3
- package/runtime/javascript/playwright.mjs +241 -32
- package/runtime/javascript/provenance.mjs +1 -1
- package/runtime/javascript/register.mjs +290 -2
- package/runtime/javascript/resolve-loader.mjs +40 -1
- package/runtime/javascript/runnerEvidence.mjs +3 -5
- package/runtime/javascript/runtime.mjs +551 -61
- package/runtime/javascript/tap.mjs +130 -0
- package/runtime/javascript/transport.mjs +7 -2
- package/runtime/javascript/vitest.mjs +5 -8
- package/runtime/javascript/vitestReporter.mjs +3 -10
- package/docs/assertion-agent.md +0 -150
- package/docs/assertion-evidence.md +0 -103
- package/docs/assertion-maps.md +0 -316
- package/schemas/assertions.schema.json +0 -276
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import * as native from "node:test";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import vm from "node:vm";
|
|
@@ -86,6 +87,43 @@ function restoreUserError(error, depth = 0) {
|
|
|
86
87
|
}
|
|
87
88
|
return error;
|
|
88
89
|
}
|
|
90
|
+
// The describe/suite blocks a test is registered in, outermost first. A test
|
|
91
|
+
// is named by them as Vitest, Jest and Playwright name theirs, so two tests
|
|
92
|
+
// with one name in different suites stay apart, and `tests affected --names`
|
|
93
|
+
// gives what --test-name-pattern matches. A store rather than a stack: an
|
|
94
|
+
// async suite registers tests after it awaits.
|
|
95
|
+
const suitePath = new AsyncLocalStorage();
|
|
96
|
+
function suiteName(args, callback) {
|
|
97
|
+
return typeof args[0] === "string" ? args[0] : callback.name || "<anonymous>";
|
|
98
|
+
}
|
|
99
|
+
function wrappedSuite(original) {
|
|
100
|
+
const wrapped = function supercovNodeSuite(...args) {
|
|
101
|
+
const index = callbackIndex(args);
|
|
102
|
+
if (index < 0)
|
|
103
|
+
return Reflect.apply(original, this, args);
|
|
104
|
+
const callback = args[index];
|
|
105
|
+
const path = [...(suitePath.getStore() ?? []), suiteName(args, callback)];
|
|
106
|
+
const next = [...args];
|
|
107
|
+
next[index] = function supercovNodeSuiteCallback(...callbackArgs) {
|
|
108
|
+
return suitePath.run(path, () => Reflect.apply(callback, this, callbackArgs));
|
|
109
|
+
};
|
|
110
|
+
// node:test reports a suite at its direct caller, as it does a test.
|
|
111
|
+
const registration = registrationAt(callerLocation(supercovNodeSuite));
|
|
112
|
+
if (registration)
|
|
113
|
+
return registration(this === undefined ? original : original.bind(this), next);
|
|
114
|
+
return Reflect.apply(original, this, next);
|
|
115
|
+
};
|
|
116
|
+
for (const property of ["skip", "todo", "only"]) {
|
|
117
|
+
const member = original[property];
|
|
118
|
+
if (typeof member === "function")
|
|
119
|
+
Object.defineProperty(wrapped, property, {
|
|
120
|
+
configurable: true,
|
|
121
|
+
enumerable: true,
|
|
122
|
+
value: wrappedSuite(member),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return wrapped;
|
|
126
|
+
}
|
|
89
127
|
const registrationCounts = new Map();
|
|
90
128
|
function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
91
129
|
const wrapped = function supercovNodeTest(...args) {
|
|
@@ -96,7 +134,9 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
96
134
|
const location = callerLocation(supercovNodeTest);
|
|
97
135
|
const identity = {
|
|
98
136
|
runner: "node:test",
|
|
99
|
-
name:
|
|
137
|
+
name: parentTestId
|
|
138
|
+
? testName(args, callback)
|
|
139
|
+
: [...(suitePath.getStore() ?? []), testName(args, callback)].join(" > "),
|
|
100
140
|
...(parentTestId ? { parentTestId } : {}),
|
|
101
141
|
...location,
|
|
102
142
|
// Source-map producers disagree about whether a call expression maps to
|
|
@@ -260,8 +300,8 @@ function restoringHook(original, scope, hookName = "hook") {
|
|
|
260
300
|
}
|
|
261
301
|
export const test = wrappedRegistration(native.test);
|
|
262
302
|
export const it = wrappedRegistration(native.it);
|
|
263
|
-
export const suite = native.suite;
|
|
264
|
-
export const describe = native.describe;
|
|
303
|
+
export const suite = wrappedSuite(native.suite);
|
|
304
|
+
export const describe = wrappedSuite(native.describe);
|
|
265
305
|
export const before = restoringHook(native.before, undefined, "before");
|
|
266
306
|
export const after = restoringHook(native.after, undefined, "after");
|
|
267
307
|
export const beforeEach = restoringHook(native.beforeEach, undefined, "beforeEach");
|
|
@@ -270,4 +310,18 @@ export const mock = native.mock;
|
|
|
270
310
|
export const snapshot = native.snapshot;
|
|
271
311
|
export const run = native.run;
|
|
272
312
|
export const assert = native.assert;
|
|
313
|
+
// node:test hangs its whole API off `test` (test.mock, test.describe,
|
|
314
|
+
// test.expectFailure, test.getTestContext...). What the adapter does not wrap
|
|
315
|
+
// stays the runtime's own.
|
|
316
|
+
const adapted = { it, describe, suite, before, after, beforeEach, afterEach, test };
|
|
317
|
+
for (const property of Object.keys(native.test)) {
|
|
318
|
+
if (property in test && !(property in adapted))
|
|
319
|
+
continue;
|
|
320
|
+
Object.defineProperty(test, property, {
|
|
321
|
+
configurable: true,
|
|
322
|
+
enumerable: true,
|
|
323
|
+
writable: true,
|
|
324
|
+
value: adapted[property] ?? native.test[property],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
273
327
|
export default test;
|
|
@@ -31,6 +31,35 @@ const targetTestExport = process.env["SUPERCOV_PLAYWRIGHT_TEST_EXPORT"] ??
|
|
|
31
31
|
const adapter = (targetModule === "@playwright/test"
|
|
32
32
|
? standardPlaywright
|
|
33
33
|
: await import(__rewriteRelativeImportExtension(targetModule)));
|
|
34
|
+
// A page that loads the application as plain `<script>` files, from a static
|
|
35
|
+
// server, has no bundler to import the runtime the way a module graph would,
|
|
36
|
+
// and instrumented classic scripts read it from a global. Install the same
|
|
37
|
+
// self-contained runtime in every document before any of the page's scripts
|
|
38
|
+
// run. Every copy keys its state on the page's global, so this copy and one a
|
|
39
|
+
// bundler imports record into the same place. Its locals stay inside a
|
|
40
|
+
// function, out of the page's global scope.
|
|
41
|
+
const browserRuntimeSource = readFileSync(new URL("./runtime.mjs", import.meta.url), "utf8");
|
|
42
|
+
const browserRuntimeExport = browserRuntimeSource.lastIndexOf("\nexport {");
|
|
43
|
+
if (/^import\s/m.test(browserRuntimeSource) || browserRuntimeExport < 0)
|
|
44
|
+
throw new Error("Supercov's browser runtime must stay self-contained");
|
|
45
|
+
const browserRuntimePrelude = `(() => { if (globalThis.__SUPERCOV_DIRECT_RUNTIME__) return;\n${browserRuntimeSource.slice(0, browserRuntimeExport)}\n})();`;
|
|
46
|
+
// A worker blocked in `Atomics.wait` runs no evaluation until the page wakes
|
|
47
|
+
// it, and the page action that wakes it comes after Supercov's own call: an
|
|
48
|
+
// awaited evaluation deadlocked Actual's suite. Every worker evaluation is
|
|
49
|
+
// bounded; one that does not answer in time is left queued, and a test whose
|
|
50
|
+
// worker evidence could not be read is recorded as partial, never as complete.
|
|
51
|
+
const WORKER_ACTIVATION_WAIT_MS = 250;
|
|
52
|
+
const WORKER_SNAPSHOT_WAIT_MS = 5000;
|
|
53
|
+
const WORKER_TIMED_OUT = Symbol("worker did not answer");
|
|
54
|
+
function bounded(promise, milliseconds) {
|
|
55
|
+
let timer;
|
|
56
|
+
return Promise.race([
|
|
57
|
+
promise,
|
|
58
|
+
new Promise((resolve) => {
|
|
59
|
+
timer = setTimeout(() => resolve(WORKER_TIMED_OUT), milliseconds);
|
|
60
|
+
}),
|
|
61
|
+
]).finally(() => clearTimeout(timer));
|
|
62
|
+
}
|
|
34
63
|
const base = (adapter[targetTestExport] ?? adapter.test);
|
|
35
64
|
const baseExpect = adapter.expect;
|
|
36
65
|
const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
|
|
@@ -187,6 +216,21 @@ class CoveragePhaseController {
|
|
|
187
216
|
// closed page cannot be evaluated, so the evidence is taken on the way out.
|
|
188
217
|
earlySnapshots = [];
|
|
189
218
|
snapshottedPages = new WeakSet();
|
|
219
|
+
snapshottedWorkers = new WeakSet();
|
|
220
|
+
// Workers that did not answer an evaluation in time. They are not waited
|
|
221
|
+
// for again until one answers.
|
|
222
|
+
unresponsiveWorkers = new WeakSet();
|
|
223
|
+
// Each worker's token, under which it pushes its evidence to its pages
|
|
224
|
+
// before it blocks in Atomics.wait (see runtime.mjs).
|
|
225
|
+
workerTokens = new WeakMap();
|
|
226
|
+
// Workers that answered their registration without Supercov's runtime: a
|
|
227
|
+
// worker built from a string (a `blob:` URL), or from a script nobody
|
|
228
|
+
// instrumented. There is no phase to set in them and no evidence to read,
|
|
229
|
+
// and Actual's blocked ones were most of what each step waited for.
|
|
230
|
+
workersWithoutRuntime = new WeakSet();
|
|
231
|
+
// Set when a worker's evidence could not be read: the test's coverage is
|
|
232
|
+
// then a lower bound.
|
|
233
|
+
workerEvidenceMissing = false;
|
|
190
234
|
disposed = false;
|
|
191
235
|
// Parameter properties are stateful despite the base ESLint rule treating
|
|
192
236
|
// this as an empty constructor.
|
|
@@ -216,21 +260,79 @@ class CoveragePhaseController {
|
|
|
216
260
|
snapshots.push(...(await this.snapshotPage(page)));
|
|
217
261
|
}
|
|
218
262
|
for (const worker of this.allWorkers()) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.catch(() => ({
|
|
225
|
-
decisions: [],
|
|
226
|
-
hits: [],
|
|
227
|
-
events: [],
|
|
228
|
-
}));
|
|
229
|
-
snapshots.push(snapshot);
|
|
263
|
+
if (this.snapshottedWorkers.has(worker))
|
|
264
|
+
continue;
|
|
265
|
+
const snapshot = await this.snapshotWorker(worker, this.allPages());
|
|
266
|
+
if (snapshot)
|
|
267
|
+
snapshots.push(snapshot);
|
|
230
268
|
}
|
|
231
269
|
this.runtimeSnapshots = snapshots;
|
|
232
270
|
return snapshots;
|
|
233
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* A worker's evidence, or nothing when it cannot be read. A worker that
|
|
274
|
+
* closed with its page has nothing left to read; one that did not answer
|
|
275
|
+
* in time is still running and its evidence is missing from this test.
|
|
276
|
+
*/
|
|
277
|
+
async snapshotWorker(worker, pages) {
|
|
278
|
+
if (this.workersWithoutRuntime.has(worker)) {
|
|
279
|
+
// Asked once more, without waiting: it may have loaded an
|
|
280
|
+
// instrumented script since it was registered.
|
|
281
|
+
const answer = await bounded(worker
|
|
282
|
+
.evaluate(() => globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__?.())
|
|
283
|
+
.catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
|
|
284
|
+
if (answer && answer !== WORKER_TIMED_OUT) {
|
|
285
|
+
this.snapshottedWorkers.add(worker);
|
|
286
|
+
return answer;
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const answer = await bounded(worker
|
|
291
|
+
.evaluate(() => {
|
|
292
|
+
const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
293
|
+
return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
|
|
294
|
+
})
|
|
295
|
+
.catch(() => undefined), WORKER_SNAPSHOT_WAIT_MS);
|
|
296
|
+
if (answer === WORKER_TIMED_OUT) {
|
|
297
|
+
this.unresponsiveWorkers.add(worker);
|
|
298
|
+
// Blocked in Atomics.wait, it pushed what it recorded before it
|
|
299
|
+
// blocked; that is all of it while the push is current.
|
|
300
|
+
const pushed = await this.pushedWorkerEvidence(worker, pages);
|
|
301
|
+
if (pushed?.current) {
|
|
302
|
+
this.snapshottedWorkers.add(worker);
|
|
303
|
+
return pushed.snapshot;
|
|
304
|
+
}
|
|
305
|
+
this.workerEvidenceMissing = true;
|
|
306
|
+
return pushed?.snapshot;
|
|
307
|
+
}
|
|
308
|
+
this.snapshottedWorkers.add(worker);
|
|
309
|
+
return answer;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* A worker's latest push, found by the token it answered with when it was
|
|
313
|
+
* registered, or else by its URL when no other worker of the page pushed
|
|
314
|
+
* from the same one: a worker that blocks on its first message may never
|
|
315
|
+
* have answered at all.
|
|
316
|
+
*/
|
|
317
|
+
async pushedWorkerEvidence(worker, pages) {
|
|
318
|
+
const token = this.workerTokens.get(worker);
|
|
319
|
+
const url = worker.url();
|
|
320
|
+
for (const page of pages) {
|
|
321
|
+
const pushed = await bounded(page
|
|
322
|
+
.evaluate(({ token, url }) => {
|
|
323
|
+
const entries = globalThis.__SUPERCOV_WORKER_EVIDENCE__?.() ?? [];
|
|
324
|
+
const byToken = token ? entries.find((entry) => entry.token === token) : undefined;
|
|
325
|
+
if (byToken)
|
|
326
|
+
return byToken;
|
|
327
|
+
const byUrl = entries.filter((entry) => entry.url === url);
|
|
328
|
+
return byUrl.length === 1 ? byUrl[0] : undefined;
|
|
329
|
+
}, { token, url })
|
|
330
|
+
.catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
|
|
331
|
+
if (pushed && pushed !== WORKER_TIMED_OUT)
|
|
332
|
+
return pushed;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
234
336
|
async snapshotPage(page) {
|
|
235
337
|
const snapshots = [];
|
|
236
338
|
for (const frame of page.frames()) {
|
|
@@ -253,6 +355,16 @@ class CoveragePhaseController {
|
|
|
253
355
|
return;
|
|
254
356
|
this.snapshottedPages.add(page);
|
|
255
357
|
this.earlySnapshots.push(...(await this.snapshotPage(page)));
|
|
358
|
+
// A dedicated worker ends with the page that owns it, so it is read
|
|
359
|
+
// now; read after the page closed, a worker that ran the test's code
|
|
360
|
+
// reported nothing.
|
|
361
|
+
for (const worker of page.workers()) {
|
|
362
|
+
if (this.snapshottedWorkers.has(worker))
|
|
363
|
+
continue;
|
|
364
|
+
const snapshot = await this.snapshotWorker(worker, [page]);
|
|
365
|
+
if (snapshot)
|
|
366
|
+
this.earlySnapshots.push(snapshot);
|
|
367
|
+
}
|
|
256
368
|
}
|
|
257
369
|
/**
|
|
258
370
|
* Register every live context the process created outside the wrapped
|
|
@@ -299,6 +411,7 @@ class CoveragePhaseController {
|
|
|
299
411
|
this.contextConfiguredHeaders.set(context, configuredHeaders);
|
|
300
412
|
if (adopted)
|
|
301
413
|
this.adoptedContexts.add(context);
|
|
414
|
+
await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
|
|
302
415
|
// A context that outlives its test (a worker-scoped browser fixture)
|
|
303
416
|
// is registered again by every later test, so this script accumulates
|
|
304
417
|
// on it and each new document runs every copy in registration order.
|
|
@@ -359,15 +472,19 @@ class CoveragePhaseController {
|
|
|
359
472
|
return;
|
|
360
473
|
this.workers.add(worker);
|
|
361
474
|
const phaseId = this.requestPhaseId();
|
|
362
|
-
await worker
|
|
475
|
+
const registration = await worker
|
|
363
476
|
.evaluate(({ attemptId, scopeHeader, scopeValue, phaseHeader, phase }) => {
|
|
364
477
|
globalThis.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
|
|
365
478
|
if (phase)
|
|
366
479
|
globalThis.__SUPERCOV_PHASE_ID__ = phase;
|
|
367
480
|
globalThis.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(attemptId, phase);
|
|
481
|
+
const answer = {
|
|
482
|
+
token: globalThis.__SUPERCOV_WORKER_TOKEN__,
|
|
483
|
+
runtime: typeof globalThis.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__ === "function",
|
|
484
|
+
};
|
|
368
485
|
const originalFetch = globalThis.fetch?.bind(globalThis);
|
|
369
486
|
if (!originalFetch)
|
|
370
|
-
return;
|
|
487
|
+
return answer;
|
|
371
488
|
globalThis.fetch = ((input, init) => {
|
|
372
489
|
const headers = new Headers(init?.headers ??
|
|
373
490
|
(input instanceof Request ? input.headers : undefined));
|
|
@@ -376,6 +493,7 @@ class CoveragePhaseController {
|
|
|
376
493
|
headers.set(phaseHeader, phase);
|
|
377
494
|
return originalFetch(input, { ...init, headers });
|
|
378
495
|
});
|
|
496
|
+
return answer;
|
|
379
497
|
}, {
|
|
380
498
|
attemptId: this.scope.attemptId,
|
|
381
499
|
scopeHeader: COVERAGE_SCOPE_HEADER,
|
|
@@ -384,6 +502,10 @@ class CoveragePhaseController {
|
|
|
384
502
|
phase: phaseId,
|
|
385
503
|
})
|
|
386
504
|
.catch(() => undefined);
|
|
505
|
+
if (typeof registration?.token === "string")
|
|
506
|
+
this.workerTokens.set(worker, registration.token);
|
|
507
|
+
if (registration?.runtime === false)
|
|
508
|
+
this.workersWithoutRuntime.add(worker);
|
|
387
509
|
}
|
|
388
510
|
async beginAction(operation) {
|
|
389
511
|
timingCount("beginAction");
|
|
@@ -565,10 +687,25 @@ class CoveragePhaseController {
|
|
|
565
687
|
};
|
|
566
688
|
return scoped;
|
|
567
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Tell every context, frame, worker and future document of the test about
|
|
692
|
+
* a new phase. The four go to different targets and none waits on another,
|
|
693
|
+
* so they run together: an action waits for the slowest instead of their
|
|
694
|
+
* sum, which was most of what Actual's suite spent under Supercov (47 of
|
|
695
|
+
* the 43 extra seconds, with the database worker's bounded wait the
|
|
696
|
+
* largest part).
|
|
697
|
+
*/
|
|
568
698
|
async activateInBrowser(phaseId) {
|
|
569
|
-
await
|
|
570
|
-
.
|
|
571
|
-
|
|
699
|
+
await Promise.all([
|
|
700
|
+
timed("activate.contextHeaders", () => Promise.all([...this.contexts]
|
|
701
|
+
.filter((context) => !this.adoptedContexts.has(context))
|
|
702
|
+
.map((context) => this.updateContextHeaders(context, phaseId)))),
|
|
703
|
+
this.activateFrames(phaseId),
|
|
704
|
+
this.activateWorkers(phaseId),
|
|
705
|
+
timed("activate.pageScript", () => Promise.all([...this.pages].map((page) => this.activatePage(page, phaseId)))),
|
|
706
|
+
]);
|
|
707
|
+
}
|
|
708
|
+
async activateFrames(phaseId) {
|
|
572
709
|
await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
|
|
573
710
|
.evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
|
|
574
711
|
globalThis.__SUPERCOV_PHASE_ID__ = id;
|
|
@@ -595,14 +732,25 @@ class CoveragePhaseController {
|
|
|
595
732
|
phaseCookie: COVERAGE_PHASE_COOKIE,
|
|
596
733
|
})
|
|
597
734
|
.catch(() => undefined))));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
735
|
+
}
|
|
736
|
+
async activateWorkers(phaseId) {
|
|
737
|
+
await timed("activate.workerEvaluate", () => Promise.all([...this.workers].map(async (worker) => {
|
|
738
|
+
const activation = worker
|
|
739
|
+
.evaluate((id) => {
|
|
740
|
+
const coverageGlobal = globalThis;
|
|
741
|
+
coverageGlobal.__SUPERCOV_PHASE_ID__ = id;
|
|
742
|
+
coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ ?? "unscoped", id);
|
|
743
|
+
}, phaseId)
|
|
744
|
+
.then(() => this.unresponsiveWorkers.delete(worker))
|
|
745
|
+
.catch(() => undefined);
|
|
746
|
+
// Evaluations run in order, so one left queued still applies this
|
|
747
|
+
// phase before any later one; it is only not waited for. One with
|
|
748
|
+
// no runtime has nothing to apply unless it loads some later.
|
|
749
|
+
if (this.unresponsiveWorkers.has(worker) || this.workersWithoutRuntime.has(worker))
|
|
750
|
+
return;
|
|
751
|
+
if ((await bounded(activation, WORKER_ACTIVATION_WAIT_MS)) === WORKER_TIMED_OUT)
|
|
752
|
+
this.unresponsiveWorkers.add(worker);
|
|
753
|
+
})));
|
|
606
754
|
}
|
|
607
755
|
async updateContextHeaders(context, phaseId) {
|
|
608
756
|
await context
|
|
@@ -804,6 +952,30 @@ globalThis.__SUPERCOV_ASSERTION_PHASE_BRIDGE__ = (operation, source, callback) =
|
|
|
804
952
|
throw error;
|
|
805
953
|
}
|
|
806
954
|
};
|
|
955
|
+
// The same bridge for an assertion that runs in place: the runtime enters the
|
|
956
|
+
// carrier and calls release when the call returns, finish when it settles.
|
|
957
|
+
globalThis.__SUPERCOV_ASSERTION_PHASE_OPENER__ = (operation, source) => {
|
|
958
|
+
const controller = activeController;
|
|
959
|
+
if (!controller || !directRuntime())
|
|
960
|
+
return undefined;
|
|
961
|
+
const phase = controller.beginAssertion(operation, source);
|
|
962
|
+
bridgedAssertionDepth += 1;
|
|
963
|
+
let held = true;
|
|
964
|
+
const release = () => {
|
|
965
|
+
if (held) {
|
|
966
|
+
held = false;
|
|
967
|
+
bridgedAssertionDepth -= 1;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
return {
|
|
971
|
+
carrier: { version: 1, scope: controller.scope, phaseId: phase.id },
|
|
972
|
+
release,
|
|
973
|
+
finish: (error) => {
|
|
974
|
+
release();
|
|
975
|
+
controller.finish(phase, error);
|
|
976
|
+
},
|
|
977
|
+
};
|
|
978
|
+
};
|
|
807
979
|
function activeCoverageHeaders() {
|
|
808
980
|
const controller = activeController;
|
|
809
981
|
if (!controller)
|
|
@@ -890,9 +1062,10 @@ function scopedChildOptions(args, optionIndex) {
|
|
|
890
1062
|
: {};
|
|
891
1063
|
const options = {
|
|
892
1064
|
...existing,
|
|
1065
|
+
// An env the caller passed is the child's whole environment; only
|
|
1066
|
+
// Supercov's own variables join it. Without one, it inherits ours.
|
|
893
1067
|
env: {
|
|
894
|
-
...process.env,
|
|
895
|
-
...(existing.env ?? {}),
|
|
1068
|
+
...(existing.env ?? process.env),
|
|
896
1069
|
SUPERCOV_RUN_ID: controller.scope.runId,
|
|
897
1070
|
[COVERAGE_CARRIER_ENV]: encodeCoverageCarrier({
|
|
898
1071
|
version: 1,
|
|
@@ -909,13 +1082,48 @@ function scopedChildOptions(args, optionIndex) {
|
|
|
909
1082
|
scoped[optionIndex] = options;
|
|
910
1083
|
return scoped;
|
|
911
1084
|
}
|
|
1085
|
+
// spawn, fork and execFile take (file, args?, options?, callback?): options
|
|
1086
|
+
// follow an argument list, or an explicit null or undefined in its place.
|
|
912
1087
|
function childOptionIndex(method, args) {
|
|
913
|
-
if (method === "spawn" ||
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1088
|
+
if (method === "spawn" ||
|
|
1089
|
+
method === "spawnSync" ||
|
|
1090
|
+
method === "fork" ||
|
|
1091
|
+
method === "execFile" ||
|
|
1092
|
+
method === "execFileSync")
|
|
1093
|
+
return Array.isArray(args[1]) || (args[1] == null && args.length > 2) ? 2 : 1;
|
|
917
1094
|
return 1;
|
|
918
1095
|
}
|
|
1096
|
+
// util.promisify(exec) and util.promisify(execFile) resolve { stdout, stderr }
|
|
1097
|
+
// through a function Node keeps on the original, and that function calls the
|
|
1098
|
+
// original. A wrapper carries its own, which calls the wrapper, or a promisified
|
|
1099
|
+
// call resolves the bare stdout string and skips the wrapper.
|
|
1100
|
+
const promisifyCustom = Symbol.for("nodejs.util.promisify.custom");
|
|
1101
|
+
function keepPromisifiedShape(original, wrapper) {
|
|
1102
|
+
if (typeof original[promisifyCustom] !== "function")
|
|
1103
|
+
return wrapper;
|
|
1104
|
+
Object.defineProperty(wrapper, promisifyCustom, {
|
|
1105
|
+
configurable: true,
|
|
1106
|
+
value: (...args) => {
|
|
1107
|
+
let settle;
|
|
1108
|
+
let reject;
|
|
1109
|
+
const promise = new Promise((ok, fail) => {
|
|
1110
|
+
settle = ok;
|
|
1111
|
+
reject = fail;
|
|
1112
|
+
});
|
|
1113
|
+
promise.child = wrapper(...args, (error, stdout, stderr) => {
|
|
1114
|
+
if (error !== null) {
|
|
1115
|
+
error.stdout = stdout;
|
|
1116
|
+
error.stderr = stderr;
|
|
1117
|
+
reject(error);
|
|
1118
|
+
}
|
|
1119
|
+
else
|
|
1120
|
+
settle({ stdout, stderr });
|
|
1121
|
+
});
|
|
1122
|
+
return promise;
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
return wrapper;
|
|
1126
|
+
}
|
|
919
1127
|
function installChildProcessScopePropagation() {
|
|
920
1128
|
for (const method of [
|
|
921
1129
|
"exec",
|
|
@@ -927,9 +1135,9 @@ function installChildProcessScopePropagation() {
|
|
|
927
1135
|
"spawnSync",
|
|
928
1136
|
]) {
|
|
929
1137
|
const original = childProcess[method];
|
|
930
|
-
childProcess[method] = function (...args) {
|
|
1138
|
+
childProcess[method] = keepPromisifiedShape(original, function (...args) {
|
|
931
1139
|
return Reflect.apply(original, childProcess, scopedChildOptions(args, childOptionIndex(method, args)));
|
|
932
|
-
};
|
|
1140
|
+
});
|
|
933
1141
|
}
|
|
934
1142
|
syncBuiltinESMExports();
|
|
935
1143
|
}
|
|
@@ -1154,6 +1362,7 @@ const instrumentedFixtures = {
|
|
|
1154
1362
|
explicitKind: process.env["SUPERCOV_TEST_KIND"],
|
|
1155
1363
|
}),
|
|
1156
1364
|
phases: mergeCollectedPhases(controller.phases, directRuntime()?.takeNodeAssertionPhases(scope) ?? []),
|
|
1365
|
+
...(controller.workerEvidenceMissing ? { attribution: "partial" } : {}),
|
|
1157
1366
|
browser,
|
|
1158
1367
|
server,
|
|
1159
1368
|
};
|
|
@@ -100,7 +100,7 @@ export function inferTestProvenance({ runner, file, project, explicitKind, }) {
|
|
|
100
100
|
source: "path",
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
const defaultKind = runner === "vitest" || runner === "jest" || runner === "node:test"
|
|
103
|
+
const defaultKind = runner === "vitest" || runner === "jest" || runner === "node:test" || runner === "mocha" || runner === "ava" || runner === "tap"
|
|
104
104
|
? "unit"
|
|
105
105
|
: "unknown";
|
|
106
106
|
return {
|