supercov 0.0.45 → 0.0.47
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 +1 -1
- package/docs/agent-loop.md +17 -13
- package/docs/assertion-agent.md +134 -144
- package/docs/assertion-evidence.md +103 -9
- package/docs/assertion-maps.md +209 -204
- package/docs/assertions.md +92 -63
- package/docs/cli.md +31 -14
- package/docs/coverage-model.md +6 -0
- package/docs/evidence.md +28 -0
- package/docs/performance.md +16 -0
- package/docs/supported-suites.md +64 -147
- package/package.json +14 -12
- package/runtime/javascript/launchSupervisor.mjs +5 -0
- package/runtime/javascript/nodeTest.mjs +57 -41
- package/runtime/javascript/provenance.mjs +4 -1
- package/runtime/javascript/register.mjs +9 -5
- package/runtime/javascript/runnerEvidence.mjs +4 -1
- package/runtime/javascript/runtime.mjs +44 -3
|
@@ -87,7 +87,7 @@ function restoreUserError(error, depth = 0) {
|
|
|
87
87
|
return error;
|
|
88
88
|
}
|
|
89
89
|
const registrationCounts = new Map();
|
|
90
|
-
function wrappedRegistration(original, parentTestId) {
|
|
90
|
+
function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
91
91
|
const wrapped = function supercovNodeTest(...args) {
|
|
92
92
|
const index = callbackIndex(args);
|
|
93
93
|
if (index < 0)
|
|
@@ -115,13 +115,15 @@ function wrappedRegistration(original, parentTestId) {
|
|
|
115
115
|
const scope = runnerExecutionScope(identity);
|
|
116
116
|
const options = testOptions(args, index);
|
|
117
117
|
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
|
|
118
|
+
if (options?.skip || options?.todo || forcedStatus)
|
|
119
|
+
writeRunnerEvidence(identity, "skipped", scope, evidenceDirectory);
|
|
118
120
|
const next = [...args];
|
|
119
121
|
const execute = (callbackThis, context, done) => {
|
|
120
122
|
// A test or its hooks may intentionally modify Supercov's public
|
|
121
123
|
// environment while testing integrations. Keep this attempt's transport
|
|
122
124
|
// destination fixed to the value present when the test was registered.
|
|
123
125
|
beginBufferedServerEvidence(scope);
|
|
124
|
-
let status = options?.skip || options?.todo
|
|
126
|
+
let status = options?.skip || options?.todo || forcedStatus
|
|
125
127
|
? "skipped"
|
|
126
128
|
: "passed";
|
|
127
129
|
const contextProxy = new Proxy(context, {
|
|
@@ -138,6 +140,8 @@ function wrappedRegistration(original, parentTestId) {
|
|
|
138
140
|
}
|
|
139
141
|
if (property === "test" && typeof value === "function")
|
|
140
142
|
return wrappedRegistration(value.bind(target), scope.testId);
|
|
143
|
+
if (["after", "before", "afterEach", "beforeEach"].includes(property) && typeof value === "function")
|
|
144
|
+
return restoringHook(value.bind(target), scope);
|
|
141
145
|
return typeof value === "function" ? value.bind(target) : value;
|
|
142
146
|
},
|
|
143
147
|
});
|
|
@@ -149,6 +153,11 @@ function wrappedRegistration(original, parentTestId) {
|
|
|
149
153
|
const flushedServerEvidence = flushBufferedServerEvidence(scope);
|
|
150
154
|
writeRunnerEvidence(identity, nextStatus, scope, evidenceDirectory, takeNodeAssertionPhases(scope), flushedServerEvidence);
|
|
151
155
|
};
|
|
156
|
+
const finishBody = () => {
|
|
157
|
+
// User t.after callbacks run after the body. Register last so
|
|
158
|
+
// their assertions and cleanup probes are part of this attempt.
|
|
159
|
+
context.after(() => emit());
|
|
160
|
+
};
|
|
152
161
|
try {
|
|
153
162
|
if (callback.length >= 2) {
|
|
154
163
|
const callbackDone = (error) => {
|
|
@@ -156,7 +165,7 @@ function wrappedRegistration(original, parentTestId) {
|
|
|
156
165
|
status = "failed";
|
|
157
166
|
restoreUserError(error);
|
|
158
167
|
}
|
|
159
|
-
|
|
168
|
+
finishBody();
|
|
160
169
|
done?.(error);
|
|
161
170
|
};
|
|
162
171
|
return withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy, callbackDone]));
|
|
@@ -164,19 +173,19 @@ function wrappedRegistration(original, parentTestId) {
|
|
|
164
173
|
const result = withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy]));
|
|
165
174
|
if (result && typeof result.then === "function")
|
|
166
175
|
return Promise.resolve(result).then((value) => {
|
|
167
|
-
|
|
176
|
+
finishBody();
|
|
168
177
|
return value;
|
|
169
178
|
}, (error) => {
|
|
170
179
|
status = "failed";
|
|
171
|
-
|
|
180
|
+
finishBody();
|
|
172
181
|
throw restoreUserError(error);
|
|
173
182
|
});
|
|
174
|
-
|
|
183
|
+
finishBody();
|
|
175
184
|
return result;
|
|
176
185
|
}
|
|
177
186
|
catch (error) {
|
|
178
187
|
status = "failed";
|
|
179
|
-
|
|
188
|
+
finishBody();
|
|
180
189
|
throw restoreUserError(error);
|
|
181
190
|
}
|
|
182
191
|
};
|
|
@@ -200,45 +209,52 @@ function wrappedRegistration(original, parentTestId) {
|
|
|
200
209
|
Object.defineProperty(wrapped, property, {
|
|
201
210
|
configurable: true,
|
|
202
211
|
enumerable: true,
|
|
203
|
-
value: wrappedRegistration(member, parentTestId),
|
|
212
|
+
value: wrappedRegistration(member, parentTestId, property === "only" ? undefined : "skipped"),
|
|
204
213
|
});
|
|
205
214
|
}
|
|
206
215
|
return wrapped;
|
|
207
216
|
}
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
function restoringHook(original) {
|
|
217
|
+
// Shared hooks get their own setup scope. Their execution is visible but is
|
|
218
|
+
// never silently copied into every test. t.after and other per-test hooks keep
|
|
219
|
+
// the exact owning attempt supplied by the TestContext proxy.
|
|
220
|
+
function restoringHook(original, scope, hookName = "hook") {
|
|
212
221
|
return function supercovNodeTestHook(...args) {
|
|
213
222
|
const index = callbackIndex(args);
|
|
214
|
-
if (index < 0)
|
|
215
|
-
return Reflect.apply(original, this, args);
|
|
223
|
+
if (index < 0) return Reflect.apply(original, this, args);
|
|
216
224
|
const callback = args[index];
|
|
225
|
+
const location = callerLocation(supercovNodeTestHook);
|
|
226
|
+
let invocation = 0;
|
|
227
|
+
const execute = (receiver, context, done) => {
|
|
228
|
+
const identity = { runner: "node:test", role: "setup", name: `[${hookName}] ${callback.name || "anonymous"}`,
|
|
229
|
+
...location, registrationOrdinal: invocation++ };
|
|
230
|
+
const owner = scope ?? runnerExecutionScope(identity);
|
|
231
|
+
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
|
|
232
|
+
if (!scope) beginBufferedServerEvidence(owner);
|
|
233
|
+
let emitted = false;
|
|
234
|
+
const finish = error => {
|
|
235
|
+
if (scope || emitted) return;
|
|
236
|
+
emitted = true;
|
|
237
|
+
writeRunnerEvidence(identity, error ? "failed" : "passed", owner, evidenceDirectory,
|
|
238
|
+
takeNodeAssertionPhases(owner), flushBufferedServerEvidence(owner));
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
const result = withCoverageCarrier({ version: 1, scope: owner }, () =>
|
|
242
|
+
callback.length >= 2 ? callback.call(receiver, context, error => {
|
|
243
|
+
finish(error);
|
|
244
|
+
done?.(error ? restoreUserError(error) : undefined);
|
|
245
|
+
}) : callback.call(receiver, context));
|
|
246
|
+
if (callback.length >= 2) return result;
|
|
247
|
+
if (result && typeof result.then === "function")
|
|
248
|
+
return Promise.resolve(result).then(value => { finish(); return value; },
|
|
249
|
+
error => { finish(error); throw restoreUserError(error); });
|
|
250
|
+
finish();
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) { finish(error); throw restoreUserError(error); }
|
|
253
|
+
};
|
|
217
254
|
const next = [...args];
|
|
218
|
-
// node:test uses callback arity to distinguish promise/synchronous
|
|
219
|
-
// hooks from the legacy done-callback form. Preserve it exactly.
|
|
220
255
|
next[index] = callback.length >= 2
|
|
221
|
-
? function supercovNodeTestHookDoneCallback(context, done) {
|
|
222
|
-
|
|
223
|
-
if (error)
|
|
224
|
-
restoreUserError(error);
|
|
225
|
-
done?.(error);
|
|
226
|
-
};
|
|
227
|
-
return callback.call(this, context, restoringDone);
|
|
228
|
-
}
|
|
229
|
-
: function supercovNodeTestHookCallback(context) {
|
|
230
|
-
try {
|
|
231
|
-
const result = callback.call(this, context);
|
|
232
|
-
if (result && typeof result.then === "function")
|
|
233
|
-
return Promise.resolve(result).then(undefined, (error) => {
|
|
234
|
-
throw restoreUserError(error);
|
|
235
|
-
});
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
throw restoreUserError(error);
|
|
240
|
-
}
|
|
241
|
-
};
|
|
256
|
+
? function supercovNodeTestHookDoneCallback(context, done) { return execute(this, context, done); }
|
|
257
|
+
: function supercovNodeTestHookCallback(context) { return execute(this, context); };
|
|
242
258
|
return Reflect.apply(original, this, next);
|
|
243
259
|
};
|
|
244
260
|
}
|
|
@@ -246,10 +262,10 @@ export const test = wrappedRegistration(native.test);
|
|
|
246
262
|
export const it = wrappedRegistration(native.it);
|
|
247
263
|
export const suite = native.suite;
|
|
248
264
|
export const describe = native.describe;
|
|
249
|
-
export const before = restoringHook(native.before);
|
|
250
|
-
export const after = restoringHook(native.after);
|
|
251
|
-
export const beforeEach = restoringHook(native.beforeEach);
|
|
252
|
-
export const afterEach = restoringHook(native.afterEach);
|
|
265
|
+
export const before = restoringHook(native.before, undefined, "before");
|
|
266
|
+
export const after = restoringHook(native.after, undefined, "after");
|
|
267
|
+
export const beforeEach = restoringHook(native.beforeEach, undefined, "beforeEach");
|
|
268
|
+
export const afterEach = restoringHook(native.afterEach, undefined, "afterEach");
|
|
253
269
|
export const mock = native.mock;
|
|
254
270
|
export const snapshot = native.snapshot;
|
|
255
271
|
export const run = native.run;
|
|
@@ -7,7 +7,10 @@ const NORMALIZED_KINDS = [
|
|
|
7
7
|
function classifiedKind(value) {
|
|
8
8
|
if (!value)
|
|
9
9
|
return undefined;
|
|
10
|
-
|
|
10
|
+
// gatewayE2e.test.ts and responseIntegration.test.ts are conventional
|
|
11
|
+
// camel-case paths too. Do not infer kinds from test titles or API usage.
|
|
12
|
+
const words = value.replace(/([a-z0-9])([A-Z])/g, "$1-$2");
|
|
13
|
+
return NORMALIZED_KINDS.find(([, pattern]) => pattern.test(words))?.[0];
|
|
11
14
|
}
|
|
12
15
|
export function inferTestProvenance({ runner, file, project, explicitKind, }) {
|
|
13
16
|
if (explicitKind?.trim()) {
|
|
@@ -69,17 +69,21 @@ if (verboseDiagnostics && !process.__SUPERCOV_DIAGNOSTIC_REPORTER__) {
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// Direct instrumentation must initialize the collector before script files
|
|
73
|
+
// with no import boundary evaluate. Other modes let the runner adapter or an
|
|
74
|
+
// instrumented module load it, except when durable remote evidence requires it.
|
|
75
|
+
if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1" ||
|
|
76
|
+
process.env.SUPERCOV_DIRECT_INSTRUMENTATION === "1") {
|
|
77
77
|
// A translated remote/VM command can execute an ahead-of-run transformed
|
|
78
78
|
// test through an opaque runner that bypasses the ordinary Playwright or
|
|
79
79
|
// node:test import boundary. The remote-launch adapter marks that process;
|
|
80
80
|
// initialize the runtime before its transformed module can evaluate.
|
|
81
81
|
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
|
|
82
82
|
process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
83
|
+
// Generic builds can contain script files with no import/export syntax.
|
|
84
|
+
// They cannot import the collector without changing their module semantics,
|
|
85
|
+
// and may evaluate before any ESM instrumented file imports it.
|
|
86
|
+
globalThis.__supercovRuntime ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
83
87
|
}
|
|
84
88
|
// Workers are independent Node processes and an explicit `execArgv: []`
|
|
85
89
|
// otherwise strips the preload that supplies the isolated runtime. Preserve
|
|
@@ -36,13 +36,15 @@ export function runnerTestId(identity) {
|
|
|
36
36
|
];
|
|
37
37
|
// Preserve existing top-level, uniquely registered test IDs. Nested tests
|
|
38
38
|
// and repeated registrations need more than a shared source/name identity.
|
|
39
|
+
if (identity.role === "setup")
|
|
40
|
+
parts.push("role", "setup");
|
|
39
41
|
if (identity.parentTestId)
|
|
40
42
|
parts.push("parent", identity.parentTestId);
|
|
41
43
|
if (identity.registrationOrdinal)
|
|
42
44
|
parts.push("registration", identity.registrationOrdinal);
|
|
43
45
|
// Titles can themselves contain separator text. Domain-separate and encode
|
|
44
46
|
// the extended identity structurally so it cannot alias a literal title.
|
|
45
|
-
const key = identity.parentTestId || identity.registrationOrdinal
|
|
47
|
+
const key = identity.role === "setup" || identity.parentTestId || identity.registrationOrdinal
|
|
46
48
|
? JSON.stringify(["registration-v2", ...parts])
|
|
47
49
|
: parts.join("\0");
|
|
48
50
|
return `${identity.runner}:${createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
|
|
@@ -116,6 +118,7 @@ export function writeRunnerEvidence(identity, status, scope, evidenceDirectoryOv
|
|
|
116
118
|
title: identity.name.split(" > ").at(-1) ?? identity.name,
|
|
117
119
|
retry: identity.retry ?? 0,
|
|
118
120
|
status,
|
|
121
|
+
...(identity.role ? { role: identity.role } : {}),
|
|
119
122
|
provenance: inferTestProvenance({
|
|
120
123
|
runner: identity.runner,
|
|
121
124
|
file: testFile,
|
|
@@ -963,15 +963,56 @@ function requestCoverageContext(value) {
|
|
|
963
963
|
const phaseId = typeof rawPhaseId === "string" && rawPhaseId.length > 0 && (!scope || phaseBelongsToAttempt(rawPhaseId, scope.attemptId)) ? rawPhaseId : void 0;
|
|
964
964
|
return __spreadValues(__spreadValues({}, scope ? { scope } : {}), phaseId ? { phaseId } : {});
|
|
965
965
|
}
|
|
966
|
-
|
|
966
|
+
// Keep connection ownership on the emitter, without changing listener
|
|
967
|
+
// identity (removeListener/off and once still see the original callbacks).
|
|
968
|
+
const emitterContextMaps = runtimeGlobal.__SUPERCOV_EMITTER_CONTEXT_MAPS__ ??= new Map();
|
|
969
|
+
const emitterCoverageContexts = emitterContextMaps.get(runtimeInstance) ?? new WeakMap();
|
|
970
|
+
emitterContextMaps.set(runtimeInstance, emitterCoverageContexts);
|
|
971
|
+
const patchedEmitterInstances = runtimeGlobal.__SUPERCOV_EMITTER_PATCHED_INSTANCES__ ??= new Set();
|
|
972
|
+
function installNodeRequestPropagation() {
|
|
973
|
+
if (isBrowser || patchedEmitterInstances.has(runtimeInstance) || typeof process === "undefined") return;
|
|
974
|
+
const EventEmitter = process.getBuiltinModule?.("node:events")?.EventEmitter;
|
|
975
|
+
const Server = process.getBuiltinModule?.("node:http")?.Server;
|
|
976
|
+
const SecureServer = process.getBuiltinModule?.("node:https")?.Server;
|
|
977
|
+
if (!EventEmitter) return;
|
|
978
|
+
const original = EventEmitter.prototype.emit;
|
|
979
|
+
EventEmitter.prototype.emit = function(event, ...args) {
|
|
980
|
+
let context = emitterCoverageContexts.get(this);
|
|
981
|
+
if ((event === "request" || event === "upgrade") &&
|
|
982
|
+
((Server && this instanceof Server) || (SecureServer && this instanceof SecureServer))) {
|
|
983
|
+
// HTTP headers establish ownership even when Express/SDK listeners and
|
|
984
|
+
// the server process were created by a shared before hook.
|
|
985
|
+
const incoming = requestCoverageContext(args[0]);
|
|
986
|
+
if (incoming) context = incoming.scope ? incoming : context ?? currentRequestContext();
|
|
987
|
+
}
|
|
988
|
+
const invoke = () => Reflect.apply(original, this, [event, ...args]);
|
|
989
|
+
try {
|
|
990
|
+
return context ? withCoverageCarrier({ version: 1, ...context }, invoke) : invoke();
|
|
991
|
+
} finally {
|
|
992
|
+
if (event === "close") emitterCoverageContexts.delete(this);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
patchedEmitterInstances.add(runtimeInstance);
|
|
996
|
+
}
|
|
997
|
+
installNodeRequestPropagation();
|
|
998
|
+
|
|
999
|
+
function withRequestPhase(handler, event) {
|
|
967
1000
|
if (!serverPhaseStorage)
|
|
968
1001
|
return handler;
|
|
1002
|
+
const registeredContext = currentRequestContext();
|
|
969
1003
|
return function coverageRequestPhase(...args) {
|
|
970
1004
|
var _a8, _b, _c, _d;
|
|
971
1005
|
const requestContext = args.map((argument) => requestCoverageContext(argument)).find((context2) => context2 !== void 0);
|
|
972
|
-
|
|
1006
|
+
// An untagged request to a test-owned server must not erase that server's
|
|
1007
|
+
// owner. Shared servers have no captured test scope and stay unattributed.
|
|
1008
|
+
const inheritedContext = requestContext?.scope ? {} : registeredContext.scope ? registeredContext : requestContext === void 0 ? currentRequestContext() : {};
|
|
973
1009
|
const context = __spreadValues(__spreadValues({}, ((_a8 = requestContext == null ? void 0 : requestContext.scope) != null ? _a8 : inheritedContext.scope) ? { scope: (_b = requestContext == null ? void 0 : requestContext.scope) != null ? _b : inheritedContext.scope } : {}), ((_c = requestContext == null ? void 0 : requestContext.phaseId) != null ? _c : inheritedContext.phaseId) ? { phaseId: (_d = requestContext == null ? void 0 : requestContext.phaseId) != null ? _d : inheritedContext.phaseId } : {});
|
|
974
|
-
const invoke = () =>
|
|
1010
|
+
const invoke = () => {
|
|
1011
|
+
if (event === "connection" && context.scope && args[0] &&
|
|
1012
|
+
typeof args[0] === "object" && typeof args[0].emit === "function")
|
|
1013
|
+
emitterCoverageContexts.set(args[0], context);
|
|
1014
|
+
return Reflect.apply(handler, this, args);
|
|
1015
|
+
};
|
|
975
1016
|
return requestContext !== void 0 || context.scope || context.phaseId ? serverPhaseStorage.run(context, () => withProbeV2Context(context, invoke)) : invoke();
|
|
976
1017
|
};
|
|
977
1018
|
}
|