supercov 2.0.1 → 3.0.1
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
|
@@ -7,8 +7,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import Module, { register, syncBuiltinESMExports } from "node:module";
|
|
10
|
-
import { closeSync, openSync, unlinkSync } from "node:fs";
|
|
11
|
-
import { resolve } from "node:path";
|
|
10
|
+
import fs, { closeSync, openSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
11
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervisor.mjs";
|
|
14
14
|
import { __supercovBindCapabilityWrapper } from "./capability.mjs";
|
|
@@ -121,6 +121,42 @@ syncBuiltinESMExports();
|
|
|
121
121
|
const generatedVitestConfig = process.env.SUPERCOV_GENERATED_VITEST_CONFIG;
|
|
122
122
|
const generatedPlaywrightConfig = process.env.SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG;
|
|
123
123
|
const entrypoint = process.argv[1]?.replaceAll("\\", "/") ?? "";
|
|
124
|
+
// Node's own test coverage filters files by `--test-coverage-include` and
|
|
125
|
+
// `--test-coverage-exclude` globs relative to the working directory, and the
|
|
126
|
+
// instrumented copy's source map names the project's file, outside the
|
|
127
|
+
// isolated workspace the tests run in: `src/**` matched nothing, the report
|
|
128
|
+
// was empty and its thresholds passed with nothing checked. When those globs
|
|
129
|
+
// are given, the map this process caches names the copy's own path instead;
|
|
130
|
+
// its embedded original text is what Node reports. Nothing is written: other
|
|
131
|
+
// coverage tools read the file as it is on disk.
|
|
132
|
+
const NODE_COVERAGE_FILTERS = process.execArgv.some((argument) => /^--test-coverage-(?:include|exclude)(?:=|$)/.test(argument)) &&
|
|
133
|
+
(Boolean(process.env.NODE_V8_COVERAGE) || process.execArgv.includes("--experimental-test-coverage"));
|
|
134
|
+
const INLINE_MAP = "//# sourceMappingURL=data:application/json;base64,";
|
|
135
|
+
function mapToCopy(source, filename) {
|
|
136
|
+
if (typeof source !== "string" || !source.includes("__supercov"))
|
|
137
|
+
return source;
|
|
138
|
+
const at = source.lastIndexOf(INLINE_MAP);
|
|
139
|
+
if (at < 0)
|
|
140
|
+
return source;
|
|
141
|
+
const end = source.indexOf("\n", at);
|
|
142
|
+
const encoded = source.slice(at + INLINE_MAP.length, end < 0 ? undefined : end).trim();
|
|
143
|
+
try {
|
|
144
|
+
const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
145
|
+
if (!Array.isArray(map.sources) || map.sources.length !== 1)
|
|
146
|
+
return source;
|
|
147
|
+
map.sources = [filename.replace(/^.*[\\/]/, "")];
|
|
148
|
+
return `${source.slice(0, at)}${INLINE_MAP}${Buffer.from(JSON.stringify(map)).toString("base64")}${end < 0 ? "" : source.slice(end)}`;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return source;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (NODE_COVERAGE_FILTERS) {
|
|
155
|
+
const compile = Module.prototype._compile;
|
|
156
|
+
Module.prototype._compile = function _compile(content, filename, ...rest) {
|
|
157
|
+
return Reflect.apply(compile, this, [mapToCopy(content, filename), filename, ...rest]);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
124
160
|
const playwrightTarget = process.env.SUPERCOV_PLAYWRIGHT_MODULE;
|
|
125
161
|
const projectRoot = process.env.SUPERCOV_PROJECT_ROOT?.replaceAll("\\", "/").replace(/\/$/, "");
|
|
126
162
|
const nodeTestWrapper = new URL("./nodeTest.mjs", import.meta.url).href;
|
|
@@ -130,6 +166,212 @@ const isPlaywrightEntrypoint = /\/(?:node_modules\/\.bin\/playwright|node_module
|
|
|
130
166
|
const isJestEntrypoint = /\/node_modules\/(?:\.bin\/jest|(?:jest|jest-cli)\/bin\/jest\.js)$/.test(entrypoint);
|
|
131
167
|
if (generatedPlaywrightConfig && isPlaywrightEntrypoint)
|
|
132
168
|
process.env.SUPERCOV_INSIDE_PLAYWRIGHT = "1";
|
|
169
|
+
// Linters, formatters and type checks read the project to judge it, not to
|
|
170
|
+
// run it. The workspace holds rewritten copies, and ESLint failed semver,
|
|
171
|
+
// js-yaml and picomatch on code nobody wrote ("Strings must use singlequote",
|
|
172
|
+
// "'globalThis' is not defined"), Supercov's own .supercov/*.mjs included.
|
|
173
|
+
// Such a process reads every rewritten file as its author wrote it, and no
|
|
174
|
+
// directory listing in the workspace shows .supercov.
|
|
175
|
+
const analysisTools = "eslint|eslint_d|prettier|standard|semistandard|ts-standard|xo|jshint|tslint";
|
|
176
|
+
const isAnalysisEntrypoint = new RegExp(`/node_modules/(?:\\.bin/(?:${analysisTools})$|(?:${analysisTools})/)`).test(entrypoint) ||
|
|
177
|
+
(/\/node_modules\/(?:\.bin\/(?:tsc|vue-tsc)$|(?:typescript|vue-tsc)\/bin\/)/.test(entrypoint) &&
|
|
178
|
+
process.argv.includes("--noEmit"));
|
|
179
|
+
if (isAnalysisEntrypoint)
|
|
180
|
+
installAuthoredSourceView();
|
|
181
|
+
function installAuthoredSourceView() {
|
|
182
|
+
let authored;
|
|
183
|
+
try {
|
|
184
|
+
authored = new Set(JSON.parse(readFileSync(new URL("./authored-sources.json", import.meta.url), "utf8")));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
authored = new Set();
|
|
188
|
+
}
|
|
189
|
+
const authoredRoot = fileURLToPath(new URL("./.authored/", import.meta.url));
|
|
190
|
+
const workspace = fileURLToPath(new URL("../../", import.meta.url));
|
|
191
|
+
const roots = [...new Set([workspace, (() => {
|
|
192
|
+
try {
|
|
193
|
+
return realpathSync(workspace);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return workspace;
|
|
197
|
+
}
|
|
198
|
+
})()])];
|
|
199
|
+
const inside = (path) => {
|
|
200
|
+
if (typeof path !== "string" && !(path instanceof URL))
|
|
201
|
+
return undefined;
|
|
202
|
+
let absolute;
|
|
203
|
+
try {
|
|
204
|
+
absolute = resolve(path instanceof URL ? fileURLToPath(path) : path);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
for (const root of roots) {
|
|
210
|
+
const local = relative(root, absolute);
|
|
211
|
+
if (local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local))
|
|
212
|
+
return local.split(sep).join("/");
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
};
|
|
216
|
+
const authoredPath = (path) => {
|
|
217
|
+
const local = inside(path);
|
|
218
|
+
return local !== undefined && authored.has(local) ? resolve(authoredRoot, local) : path;
|
|
219
|
+
};
|
|
220
|
+
// A recursive listing names nested entries by relative path, or as
|
|
221
|
+
// entries whose parent is inside .supercov.
|
|
222
|
+
const supercovPath = (path) => typeof path === "string" && path.split(/[\\/]/).includes(".supercov");
|
|
223
|
+
const hidden = (entry) => {
|
|
224
|
+
if (typeof entry === "string")
|
|
225
|
+
return supercovPath(entry);
|
|
226
|
+
if (entry instanceof Uint8Array)
|
|
227
|
+
return supercovPath(new TextDecoder().decode(entry));
|
|
228
|
+
const parent = inside(entry?.parentPath ?? entry?.path ?? "");
|
|
229
|
+
return entry?.name === ".supercov" || supercovPath(parent);
|
|
230
|
+
};
|
|
231
|
+
const listed = (path, entries) => inside(path) === undefined || !Array.isArray(entries)
|
|
232
|
+
? entries
|
|
233
|
+
: entries.filter((entry) => !hidden(entry));
|
|
234
|
+
const { readFileSync: readSync, readFile: readCallback, readdirSync: listSync, readdir: listCallback } = fs;
|
|
235
|
+
const { readFile: readPromise, readdir: listPromise } = fs.promises;
|
|
236
|
+
fs.readFileSync = function readFileSync(path, ...rest) {
|
|
237
|
+
return Reflect.apply(readSync, this, [authoredPath(path), ...rest]);
|
|
238
|
+
};
|
|
239
|
+
fs.readFile = function readFile(path, ...rest) {
|
|
240
|
+
return Reflect.apply(readCallback, this, [authoredPath(path), ...rest]);
|
|
241
|
+
};
|
|
242
|
+
fs.promises.readFile = function readFile(path, ...rest) {
|
|
243
|
+
return Reflect.apply(readPromise, this, [authoredPath(path), ...rest]);
|
|
244
|
+
};
|
|
245
|
+
fs.readdirSync = function readdirSync(path, ...rest) {
|
|
246
|
+
return listed(path, Reflect.apply(listSync, this, [path, ...rest]));
|
|
247
|
+
};
|
|
248
|
+
fs.readdir = function readdir(path, ...rest) {
|
|
249
|
+
const callback = rest.at(-1);
|
|
250
|
+
if (typeof callback !== "function")
|
|
251
|
+
return Reflect.apply(listCallback, this, [path, ...rest]);
|
|
252
|
+
return Reflect.apply(listCallback, this, [path, ...rest.slice(0, -1), (error, entries) => callback(error, error ? entries : listed(path, entries))]);
|
|
253
|
+
};
|
|
254
|
+
fs.promises.readdir = async function readdir(path, ...rest) {
|
|
255
|
+
return listed(path, await Reflect.apply(listPromise, this, [path, ...rest]));
|
|
256
|
+
};
|
|
257
|
+
syncBuiltinESMExports();
|
|
258
|
+
}
|
|
259
|
+
// A project's own coverage tool measures what runs, and under Supercov that is
|
|
260
|
+
// the instrumented copy, whose probes are branches no test was written to
|
|
261
|
+
// cover: c8, Jest and Vitest each failed a 100% gate on a suite that covers
|
|
262
|
+
// every branch (80%, 83.33%, 87.5%). The tool still runs and reports; its
|
|
263
|
+
// thresholds are not checked, and the run says so once. Jest's and Vitest's
|
|
264
|
+
// thresholds are dropped from the configurations Supercov hands them, which
|
|
265
|
+
// call this too.
|
|
266
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__ ??= (tool) => {
|
|
267
|
+
if (process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS_NOTED__)
|
|
268
|
+
return;
|
|
269
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS_NOTED__ = true;
|
|
270
|
+
console.error(`[supercov] ${tool}'s coverage thresholds were not checked: under Supercov, ${tool} measures the instrumented copy, probes included. Supercov's own report has this run's coverage.`);
|
|
271
|
+
};
|
|
272
|
+
const coverageTool = /\/node_modules\/(?:\.bin\/(c8|nyc)|(c8|nyc)\/bin\/(?:c8|nyc)\.js)$/.exec(entrypoint);
|
|
273
|
+
if (coverageTool)
|
|
274
|
+
skipCoverageThresholds(coverageTool[1] ?? coverageTool[2]);
|
|
275
|
+
if (/\/node_modules\/(?:\.bin\/karma|karma\/bin\/karma)$/.test(entrypoint))
|
|
276
|
+
skipKarmaCoverageCheck();
|
|
277
|
+
// karma-coverage checks `coverageReporter.check` in the reporter karma builds
|
|
278
|
+
// from its plugin, which the karma process requires by name from the project.
|
|
279
|
+
// Loaded first, the plugin's constructor is replaced by one that leaves the
|
|
280
|
+
// check out of the configuration it is given.
|
|
281
|
+
function skipKarmaCoverageCheck() {
|
|
282
|
+
try {
|
|
283
|
+
const plugin = Module.createRequire(resolve(process.cwd(), "package.json"))("karma-coverage");
|
|
284
|
+
const entry = plugin?.["reporter:coverage"];
|
|
285
|
+
if (!Array.isArray(entry) || typeof entry[1] !== "function")
|
|
286
|
+
return;
|
|
287
|
+
const Reporter = entry[1];
|
|
288
|
+
function CoverageReporter(rootConfig, ...rest) {
|
|
289
|
+
const options = rootConfig?.coverageReporter;
|
|
290
|
+
if (options && Object.prototype.hasOwnProperty.call(options, "check")) {
|
|
291
|
+
delete options.check;
|
|
292
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__("karma-coverage");
|
|
293
|
+
}
|
|
294
|
+
return Reflect.construct(Reporter, [rootConfig, ...rest], new.target);
|
|
295
|
+
}
|
|
296
|
+
CoverageReporter.$inject = Reporter.$inject;
|
|
297
|
+
CoverageReporter.prototype = Reporter.prototype;
|
|
298
|
+
plugin["reporter:coverage"] = [entry[0], CoverageReporter];
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (process.env.SUPERCOV_DEBUG === "1")
|
|
302
|
+
console.error("[supercov] karma-coverage's check stays as configured", error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// nyc (through 18) remaps with istanbul-lib-source-maps 4, which drops an
|
|
306
|
+
// `if`'s implicit else from any file with a source map: it has no location to
|
|
307
|
+
// map. The instrumented copy has one, so nyc read one branch fewer for every
|
|
308
|
+
// `if` without an `else` than it reads without Supercov. Version 5 keeps the
|
|
309
|
+
// location in the file of the branch's other one; so does this, loaded before
|
|
310
|
+
// nyc's remapping captures the function.
|
|
311
|
+
function keepImplicitElse(toolRequire) {
|
|
312
|
+
try {
|
|
313
|
+
const path = toolRequire.resolve("istanbul-lib-source-maps/lib/get-mapping.js");
|
|
314
|
+
const getMapping = toolRequire(path);
|
|
315
|
+
const cached = Module._cache[path];
|
|
316
|
+
if (typeof getMapping !== "function" || !cached)
|
|
317
|
+
return;
|
|
318
|
+
let lastSource;
|
|
319
|
+
cached.exports = function getMappingKeepingImplicitElse(sourceMap, location, originalFile) {
|
|
320
|
+
const mapping = getMapping(sourceMap, location, originalFile);
|
|
321
|
+
if (mapping) {
|
|
322
|
+
lastSource = mapping.source;
|
|
323
|
+
return mapping;
|
|
324
|
+
}
|
|
325
|
+
const implicit = location?.start?.line === undefined && location?.end?.line === undefined;
|
|
326
|
+
const source = lastSource;
|
|
327
|
+
lastSource = undefined;
|
|
328
|
+
return implicit && source !== undefined ? { source, loc: location } : mapping;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (process.env.SUPERCOV_DEBUG === "1")
|
|
333
|
+
console.error("[supercov] nyc keeps its own remapping", error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function skipCoverageThresholds(tool) {
|
|
337
|
+
const skipped = async function skippedCoverageThresholds() {
|
|
338
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__(tool);
|
|
339
|
+
};
|
|
340
|
+
try {
|
|
341
|
+
// Resolved from the tool's own bin, so the module patched is the one
|
|
342
|
+
// its CLI loads next (npm links .bin/c8 to c8/bin/c8.js).
|
|
343
|
+
const toolRequire = Module.createRequire(realpathSync(process.argv[1]));
|
|
344
|
+
if (tool === "c8") {
|
|
345
|
+
// Every c8 threshold -- --check-coverage, --100, .c8rc, .nycrc,
|
|
346
|
+
// package.json, `c8 check-coverage` -- is checked here, and
|
|
347
|
+
// report.js takes this export when it loads.
|
|
348
|
+
toolRequire("../lib/commands/check-coverage.js").checkCoverages = skipped;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
keepImplicitElse(toolRequire);
|
|
352
|
+
// A gated nyc run, `nyc report --check-coverage` and
|
|
353
|
+
// `nyc check-coverage` all check through this method.
|
|
354
|
+
const NYC = toolRequire("../index.js");
|
|
355
|
+
NYC.prototype.checkCoverage = skipped;
|
|
356
|
+
// The instrumented copy's source map leads to the project's own
|
|
357
|
+
// file, outside the isolated workspace nyc runs in, and
|
|
358
|
+
// excluding after remapping (nyc's default) dropped it: nyc
|
|
359
|
+
// reported "All files 0". The project's excludes still apply,
|
|
360
|
+
// to the copies, before remapping.
|
|
361
|
+
const collect = NYC.prototype.getCoverageMapFromAllCoverageFiles;
|
|
362
|
+
if (typeof collect === "function") {
|
|
363
|
+
NYC.prototype.getCoverageMapFromAllCoverageFiles = function getCoverageMapFromAllCoverageFiles(...args) {
|
|
364
|
+
this.config.excludeAfterRemap = false;
|
|
365
|
+
return Reflect.apply(collect, this, args);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
if (process.env.SUPERCOV_DEBUG === "1")
|
|
372
|
+
console.error(`[supercov] ${tool}'s coverage thresholds stay as configured`, error);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
133
375
|
register(new URL("./resolve-loader.mjs", import.meta.url));
|
|
134
376
|
if (process.env.SUPERCOV_DEBUG === "1") {
|
|
135
377
|
console.error("[supercov] preload", { entrypoint });
|
|
@@ -162,6 +404,18 @@ if (generatedVitestConfig && /\/vitest(?:\.m?js)?$/.test(entrypoint)) {
|
|
|
162
404
|
index -= 1;
|
|
163
405
|
}
|
|
164
406
|
}
|
|
407
|
+
// Thresholds on the command line override the configuration's, which the
|
|
408
|
+
// generated config leaves out (see the coverage tools above). Vitest 0.x
|
|
409
|
+
// took them directly under --coverage.
|
|
410
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
411
|
+
const argument = process.argv[index];
|
|
412
|
+
if (!/^--coverage\.(?:thresholds(?:\.[^=]+)?|lines|functions|branches|statements|perFile|100|thresholdAutoUpdate)(?:=|$)/.test(argument))
|
|
413
|
+
continue;
|
|
414
|
+
const valued = !argument.includes("=") && /^(?:\d+(?:\.\d+)?|true|false)$/.test(process.argv[index + 1] ?? "");
|
|
415
|
+
process.argv.splice(index, valued ? 2 : 1);
|
|
416
|
+
index -= 1;
|
|
417
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__("Vitest");
|
|
418
|
+
}
|
|
165
419
|
if (originalConfig) {
|
|
166
420
|
process.env.SUPERCOV_ORIGINAL_VITEST_CONFIG = originalConfig;
|
|
167
421
|
}
|
|
@@ -186,6 +440,32 @@ if ((isJestEntrypoint || process.env.JEST_WORKER_ID) && process.env.SUPERCOV_EVI
|
|
|
186
440
|
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
|
|
187
441
|
process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
188
442
|
}
|
|
443
|
+
// The Mocha CLI, the child it forks for Node options, and each --parallel
|
|
444
|
+
// worker run tests through Mocha's Runner, which the adapter patches before
|
|
445
|
+
// Mocha loads it.
|
|
446
|
+
const isMochaEntrypoint = /\/node_modules\/(?:\.bin\/_?mocha|mocha\/(?:bin\/_?mocha(?:\.js)?|lib\/cli\/cli\.js|lib\/nodejs\/worker\.c?js))$/.test(entrypoint);
|
|
447
|
+
if (isMochaEntrypoint && process.env.SUPERCOV_EVIDENCE_DIR) {
|
|
448
|
+
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
|
|
449
|
+
process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
450
|
+
const { adaptMocha } = await import(new URL("./mocha.mjs", import.meta.url).href);
|
|
451
|
+
await adaptMocha(entrypoint);
|
|
452
|
+
}
|
|
453
|
+
// An AVA worker, a thread or a child process, runs one test file through
|
|
454
|
+
// AVA's Runner, which the adapter patches before the worker loads it.
|
|
455
|
+
if (/\/node_modules\/ava\/lib\/worker\/base\.js$/.test(entrypoint) && process.env.SUPERCOV_EVIDENCE_DIR) {
|
|
456
|
+
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
|
|
457
|
+
process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
458
|
+
const { adaptAva } = await import(new URL("./ava.mjs", import.meta.url).href);
|
|
459
|
+
await adaptAva(entrypoint);
|
|
460
|
+
}
|
|
461
|
+
// The tap CLI runs each test file as a process of its own, marked with
|
|
462
|
+
// TAP_CHILD_ID; the adapter patches tap's TestBase before the file loads it.
|
|
463
|
+
if (process.env.TAP_CHILD_ID && process.env.SUPERCOV_EVIDENCE_DIR && entrypoint) {
|
|
464
|
+
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
|
|
465
|
+
process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
466
|
+
const { adaptTap } = await import(new URL("./tap.mjs", import.meta.url).href);
|
|
467
|
+
await adaptTap(entrypoint);
|
|
468
|
+
}
|
|
189
469
|
if (isJestEntrypoint && process.env.SUPERCOV_EVIDENCE_DIR) {
|
|
190
470
|
// Jest reads one configuration. Ours (jest.config.mjs) reads the user's
|
|
191
471
|
// the way Jest would and adds the adapter and reporter; an explicit
|
|
@@ -210,6 +490,14 @@ if (isJestEntrypoint && process.env.SUPERCOV_EVIDENCE_DIR) {
|
|
|
210
490
|
process.argv.splice(index, 1);
|
|
211
491
|
index -= 1;
|
|
212
492
|
}
|
|
493
|
+
else if (/^--coverage-?[Tt]hreshold(?:=|$)/.test(argument ?? "")) {
|
|
494
|
+
// Overrides the configuration's coverageThreshold, which
|
|
495
|
+
// jest.config.mjs leaves out (see the coverage tools above).
|
|
496
|
+
const valued = !argument.includes("=") && process.argv[index + 1]?.startsWith("{");
|
|
497
|
+
process.argv.splice(index, valued ? 2 : 1);
|
|
498
|
+
index -= 1;
|
|
499
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__("Jest");
|
|
500
|
+
}
|
|
213
501
|
}
|
|
214
502
|
process.argv.push("--config", generatedJestConfig);
|
|
215
503
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve as resolvePath } from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
const GENERATED_TARGET = "__SUPERCOV_PLAYWRIGHT_MODULE__";
|
|
4
4
|
const TARGET = process.env.SUPERCOV_PLAYWRIGHT_MODULE ??
|
|
5
5
|
(GENERATED_TARGET.startsWith("__") ? "@playwright/test" : GENERATED_TARGET);
|
|
@@ -75,3 +75,42 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
75
75
|
}
|
|
76
76
|
return nextResolve(specifier, context);
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
// Node's own test coverage filters files by `--test-coverage-include` and
|
|
80
|
+
// `--test-coverage-exclude` globs relative to the working directory, and the
|
|
81
|
+
// instrumented copy's source map names the project's file, outside the
|
|
82
|
+
// isolated workspace the tests run in: `src/**` matched nothing, the report
|
|
83
|
+
// was empty and its thresholds passed with nothing checked. When those globs
|
|
84
|
+
// are given, the map this process caches names the copy's own path instead;
|
|
85
|
+
// its embedded original text is what Node reports. Nothing is written: other
|
|
86
|
+
// coverage tools read the file as it is on disk.
|
|
87
|
+
const NODE_COVERAGE_FILTERS = process.execArgv.some((argument) => /^--test-coverage-(?:include|exclude)(?:=|$)/.test(argument)) &&
|
|
88
|
+
(Boolean(process.env.NODE_V8_COVERAGE) || process.execArgv.includes("--experimental-test-coverage"));
|
|
89
|
+
const INLINE_MAP = "//# sourceMappingURL=data:application/json;base64,";
|
|
90
|
+
function mapToCopy(source, filename) {
|
|
91
|
+
if (typeof source !== "string" || !source.includes("__supercov"))
|
|
92
|
+
return source;
|
|
93
|
+
const at = source.lastIndexOf(INLINE_MAP);
|
|
94
|
+
if (at < 0)
|
|
95
|
+
return source;
|
|
96
|
+
const end = source.indexOf("\n", at);
|
|
97
|
+
const encoded = source.slice(at + INLINE_MAP.length, end < 0 ? undefined : end).trim();
|
|
98
|
+
try {
|
|
99
|
+
const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
100
|
+
if (!Array.isArray(map.sources) || map.sources.length !== 1)
|
|
101
|
+
return source;
|
|
102
|
+
map.sources = [filename.replace(/^.*[\\/]/, "")];
|
|
103
|
+
return `${source.slice(0, at)}${INLINE_MAP}${Buffer.from(JSON.stringify(map)).toString("base64")}${end < 0 ? "" : source.slice(end)}`;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return source;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function load(url, context, nextLoad) {
|
|
110
|
+
const result = await nextLoad(url, context);
|
|
111
|
+
if (!NODE_COVERAGE_FILTERS || result.source == null || !url.startsWith("file:"))
|
|
112
|
+
return result;
|
|
113
|
+
const text = typeof result.source === "string" ? result.source : Buffer.from(result.source).toString("utf8");
|
|
114
|
+
const mapped = mapToCopy(text, fileURLToPath(url));
|
|
115
|
+
return mapped === text ? result : { ...result, source: mapped };
|
|
116
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
6
6
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
7
7
|
import { serverEvidencePath } from "./transport.mjs";
|
|
8
8
|
|
|
@@ -131,7 +131,5 @@ export function writeRunnerEvidence(identity, status, scope, evidenceDirectoryOv
|
|
|
131
131
|
? readScopedServerEvidence(scope, serverEvidenceSource)
|
|
132
132
|
: readScopedServerEvidence(scope),
|
|
133
133
|
};
|
|
134
|
-
|
|
135
|
-
mkdirSync(directory, { recursive: true });
|
|
136
|
-
atomicWriteFileSync(resolve(directory, "mcdc.json"), `${JSON.stringify(payload)}\n`);
|
|
134
|
+
appendEvidenceRecord(resolve(process.cwd(), evidenceDirectory), identity.runner.replace(/[^A-Za-z0-9_-]/g, "_"), payload);
|
|
137
135
|
}
|