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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Supercov AVA adapter. The preload imports it in each AVA worker -- a
|
|
2
|
+
// worker thread, or a child process under --no-worker-threads -- and it
|
|
3
|
+
// patches AVA's own Runner, so a suite runs exactly as written. AVA runs a
|
|
4
|
+
// file's tests concurrently, so a test's coverage cannot be told apart by
|
|
5
|
+
// time: each test, with its beforeEach and afterEach hooks, runs inside a
|
|
6
|
+
// coverage carrier of its own, as node:test subtests do, and what ran under
|
|
7
|
+
// it is written with the test's outcome once it settles. Loading the test
|
|
8
|
+
// file and before/after hooks run outside any test and count for the run.
|
|
9
|
+
// It computes no coverage.
|
|
10
|
+
import { realpathSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { beginBufferedServerEvidence, flushBufferedServerEvidence, takeNodeAssertionPhases, withCoverageCarrier, } from "./runtime.mjs";
|
|
14
|
+
import { runnerExecutionScope, writeRunnerEvidence } from "./runnerEvidence.mjs";
|
|
15
|
+
|
|
16
|
+
// The installed ava package the entrypoint belongs to.
|
|
17
|
+
function avaRoot(entrypoint) {
|
|
18
|
+
let real;
|
|
19
|
+
try {
|
|
20
|
+
real = realpathSync(entrypoint).replaceAll("\\", "/");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const at = real.lastIndexOf("/node_modules/ava/");
|
|
26
|
+
return at < 0 ? undefined : real.slice(0, at + "/node_modules/ava".length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function adaptAva(entrypoint) {
|
|
30
|
+
const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
|
|
31
|
+
const root = avaRoot(entrypoint);
|
|
32
|
+
if (!evidenceDirectory || !root)
|
|
33
|
+
return;
|
|
34
|
+
let loaded;
|
|
35
|
+
try {
|
|
36
|
+
loaded = await import(pathToFileURL(join(root, "lib", "runner.js")).href);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const Runner = loaded.default ?? loaded.Runner;
|
|
42
|
+
const runTest = Runner?.prototype?.runTest;
|
|
43
|
+
if (typeof runTest !== "function" || runTest.supercovAva)
|
|
44
|
+
return;
|
|
45
|
+
Runner.prototype.runTest = function supercovAvaRunTest(task, contextRef) {
|
|
46
|
+
// AVA refuses two tests with one title in a file, so the title names
|
|
47
|
+
// the test.
|
|
48
|
+
const identity = { runner: "ava", file: this.file, name: task.title };
|
|
49
|
+
const scope = runnerExecutionScope(identity);
|
|
50
|
+
beginBufferedServerEvidence(scope);
|
|
51
|
+
const finish = (status) => writeRunnerEvidence(identity, status, scope, evidenceDirectory, takeNodeAssertionPhases(scope), flushBufferedServerEvidence(scope));
|
|
52
|
+
let settled;
|
|
53
|
+
try {
|
|
54
|
+
settled = Promise.resolve(withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(runTest, this, [task, contextRef])));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
finish("failed");
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return settled.then((passed) => {
|
|
61
|
+
finish(passed ? "passed" : "failed");
|
|
62
|
+
return passed;
|
|
63
|
+
}, (error) => {
|
|
64
|
+
finish("failed");
|
|
65
|
+
throw error;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
Runner.prototype.runTest.supercovAva = true;
|
|
69
|
+
// A skipped or todo test never reaches runTest; AVA announces it.
|
|
70
|
+
const start = Runner.prototype.start;
|
|
71
|
+
if (typeof start === "function")
|
|
72
|
+
Runner.prototype.start = function supercovAvaStart(...args) {
|
|
73
|
+
// AVA's emitter hands a listener { data: state }.
|
|
74
|
+
this.on("stateChange", ({ data: state } = {}) => {
|
|
75
|
+
if (state?.type !== "selected-test" || !(state.skip || state.todo))
|
|
76
|
+
return;
|
|
77
|
+
const identity = { runner: "ava", file: this.file, name: state.title };
|
|
78
|
+
writeRunnerEvidence(identity, "skipped", runnerExecutionScope(identity), evidenceDirectory);
|
|
79
|
+
});
|
|
80
|
+
return Reflect.apply(start, this, args);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Instrumented sources load this before anything else. Under the preload it
|
|
3
|
+
// finds the runtime already installed and does nothing. A process that runs
|
|
4
|
+
// the workspace from somewhere else -- a container or VM the workspace is
|
|
5
|
+
// mounted into, started by an SDK that passed on none of Supercov's settings,
|
|
6
|
+
// or a worker the preload never reached -- has no runtime, and without one the
|
|
7
|
+
// first probe would throw. Such a process installs the runtime itself, with
|
|
8
|
+
// the run's settings read from beside this file and their paths moved to
|
|
9
|
+
// wherever the workspace is seen from here, so its coverage lands in the
|
|
10
|
+
// run's own evidence directory.
|
|
11
|
+
const installed = globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIRECT_RUNTIME__;
|
|
12
|
+
if (installed) {
|
|
13
|
+
globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= installed;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
try {
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
// <workspace>/.supercov/node_modules/bootstrap.cjs
|
|
20
|
+
const root = path.resolve(__dirname, "..", "..");
|
|
21
|
+
const settings = JSON.parse(fs.readFileSync(path.join(__dirname, "run.json"), "utf8"));
|
|
22
|
+
const moved = (value) => typeof value === "string" && settings.root !== root
|
|
23
|
+
? value.split(settings.root).join(root)
|
|
24
|
+
: value;
|
|
25
|
+
// Settings carried over from the host name the host's paths, which do
|
|
26
|
+
// not exist here; replace them all rather than keep a mixture.
|
|
27
|
+
const foreign = process.env.SUPERCOV_PROJECT_ROOT !== root;
|
|
28
|
+
for (const [name, value] of Object.entries(settings.environment)) {
|
|
29
|
+
if (foreign || process.env[name] === undefined)
|
|
30
|
+
process.env[name] = moved(value);
|
|
31
|
+
}
|
|
32
|
+
process.env.SUPERCOV_PROJECT_ROOT = root;
|
|
33
|
+
process.env.SUPERCOV_EXECUTION_LOG_SHARD ??= `bootstrap-${process.pid}-${Date.now()}`;
|
|
34
|
+
require("./runtime.mjs");
|
|
35
|
+
// Counted in the run's summary: coverage from here belongs to no test.
|
|
36
|
+
require("./launchSupervisor.mjs").recordExecution({ event: "guest-process", guestRoot: root });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Without its settings or its runtime nothing can be recorded here,
|
|
40
|
+
// and the first probe fails as it did before this file existed.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
module.exports = globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
//
|
|
10
10
|
// CommonJS on purpose: Jest's module system loads setup files without the
|
|
11
11
|
// ESM flag, and the runtime it needs is already on the sandbox global.
|
|
12
|
-
const { createHash } = require("node:crypto");
|
|
13
|
-
const { mkdirSync,
|
|
12
|
+
const { createHash, randomUUID } = require("node:crypto");
|
|
13
|
+
const { appendFileSync, closeSync, fsyncSync, mkdirSync, openSync, statSync, writeFileSync } = require("node:fs");
|
|
14
14
|
const { relative, resolve, sep } = require("node:path");
|
|
15
|
+
const { Buffer } = require("node:buffer");
|
|
15
16
|
|
|
16
17
|
// jest-environment-node exposes the outer process's own globals to the sandbox
|
|
17
18
|
// through getters, so this is the same runtime instance the instrumented
|
|
@@ -48,7 +49,11 @@ function currentDeclaration() {
|
|
|
48
49
|
const occurrence = names.get(name) ?? 0;
|
|
49
50
|
names.set(name, occurrence + 1);
|
|
50
51
|
if (child === state.currentlyRunningTest)
|
|
51
|
-
found = {
|
|
52
|
+
found = {
|
|
53
|
+
occurrence,
|
|
54
|
+
retry: Math.max((child.invocations ?? 1) - 1, 0),
|
|
55
|
+
path: [...parents, child.name],
|
|
56
|
+
};
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
|
@@ -71,13 +76,43 @@ function provenance(testFile) {
|
|
|
71
76
|
? { runner: "jest", kind, source: "path" }
|
|
72
77
|
: { runner: "jest", kind: "unit", source: "runner-default" };
|
|
73
78
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// One journal per writer, as atomic.mjs's appendEvidenceRecord keeps it: a
|
|
80
|
+
// file per test cost a directory, a file and a rename each. Jest gives every
|
|
81
|
+
// test file a fresh module registry, so a worker starts one journal per file.
|
|
82
|
+
let journal;
|
|
83
|
+
function writeEvidence(payload) {
|
|
84
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
85
|
+
if (journal) {
|
|
86
|
+
let size = -1;
|
|
87
|
+
try {
|
|
88
|
+
size = statSync(journal.path).size;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// A missing journal is started again below.
|
|
92
|
+
}
|
|
93
|
+
if (size !== journal.size)
|
|
94
|
+
journal = undefined;
|
|
95
|
+
}
|
|
96
|
+
if (!journal) {
|
|
97
|
+
const directory = resolve(process.cwd(), evidenceDirectory);
|
|
98
|
+
mkdirSync(directory, { recursive: true });
|
|
99
|
+
const writer = (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}`).replace(/[^A-Za-z0-9_-]/g, "_");
|
|
100
|
+
journal = { path: resolve(directory, `jest-worker-${writer}-${randomUUID()}.mcdc.jsonl`), size: 0 };
|
|
101
|
+
}
|
|
102
|
+
if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1") {
|
|
103
|
+
const descriptor = openSync(journal.path, "a", 0o600);
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(descriptor, line);
|
|
106
|
+
fsyncSync(descriptor);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
closeSync(descriptor);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
appendFileSync(journal.path, line, { mode: 0o600 });
|
|
114
|
+
}
|
|
115
|
+
journal.size += Buffer.byteLength(line);
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof afterAll === "function") {
|
|
@@ -96,7 +131,7 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
|
|
|
96
131
|
emittedSetupFiles.add(testFile);
|
|
97
132
|
const setupSnapshot = runtime.coverageSnapshot();
|
|
98
133
|
if (setupSnapshot.hits.length || setupSnapshot.decisions.length) {
|
|
99
|
-
writeEvidence(
|
|
134
|
+
writeEvidence({
|
|
100
135
|
testId: `jest:${digest(testFile)}:setup`,
|
|
101
136
|
test: `${testFile} > module setup`,
|
|
102
137
|
testFile,
|
|
@@ -126,7 +161,9 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
|
|
|
126
161
|
retry,
|
|
127
162
|
attemptId: `${testKey}-${retry}`,
|
|
128
163
|
};
|
|
129
|
-
|
|
164
|
+
// Named as the reporter names it; the id keeps Jest's full name.
|
|
165
|
+
const name = declaration?.path?.join(" > ") ?? fullName;
|
|
166
|
+
active = { scope, testFile, fullName, name, retry };
|
|
130
167
|
runtime.activateCoverageScope(scope);
|
|
131
168
|
runtime.resetCoverage(testId);
|
|
132
169
|
});
|
|
@@ -139,12 +176,12 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
|
|
|
139
176
|
if (!current)
|
|
140
177
|
return;
|
|
141
178
|
const { scope } = current;
|
|
142
|
-
writeEvidence(
|
|
179
|
+
writeEvidence({
|
|
143
180
|
testId: scope.testId,
|
|
144
181
|
scope,
|
|
145
|
-
test: current.
|
|
182
|
+
test: current.name,
|
|
146
183
|
testFile: current.testFile,
|
|
147
|
-
title: current.
|
|
184
|
+
title: current.name,
|
|
148
185
|
retry: current.retry,
|
|
149
186
|
// The reporter's record carries the outcome.
|
|
150
187
|
status: "unknown",
|
|
@@ -26,8 +26,16 @@ export default async function supercovJestConfig() {
|
|
|
26
26
|
// configuration is not read and Jest's defaults apply.
|
|
27
27
|
process.emitWarning(`[supercov] the project's Jest configuration was not read (${error?.message ?? error}); Jest's defaults apply`);
|
|
28
28
|
}
|
|
29
|
+
// Jest's coverage measures the instrumented copy, probes included, and a
|
|
30
|
+
// suite at 100% read 83.33% of branches: its coverageThreshold failed
|
|
31
|
+
// passing runs. Jest still collects and reports coverage.
|
|
32
|
+
const { coverageThreshold, ...own } = config;
|
|
33
|
+
const collecting = own.collectCoverage === true ||
|
|
34
|
+
process.argv.some((argument) => /^--(?:coverage|collectCoverage|collect-coverage)(?:=true)?$/.test(argument));
|
|
35
|
+
if (coverageThreshold && collecting)
|
|
36
|
+
process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__?.("Jest");
|
|
29
37
|
return {
|
|
30
|
-
...
|
|
38
|
+
...own,
|
|
31
39
|
// Concurrent tests in one file would share the worker's scope.
|
|
32
40
|
maxConcurrency: 1,
|
|
33
41
|
setupFiles: [here("jestRuntime.cjs"), ...asList(config.setupFiles)],
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
2
|
import { relative, resolve, sep } from "node:path";
|
|
4
3
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
5
|
-
import {
|
|
4
|
+
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
6
5
|
function digest(value) {
|
|
7
6
|
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
8
7
|
}
|
|
@@ -57,7 +56,9 @@ export default class SupercovJestReporter {
|
|
|
57
56
|
const record = (attempt, attemptStatus, flaky) => {
|
|
58
57
|
const payload = {
|
|
59
58
|
testId,
|
|
60
|
-
|
|
59
|
+
// Named by its describe blocks, `small > charges`, as every
|
|
60
|
+
// JavaScript runner is; the id keeps Jest's own full name.
|
|
61
|
+
test: [...(result.ancestorTitles ?? []), result.title].join(" > "),
|
|
61
62
|
testFile,
|
|
62
63
|
title: result.title,
|
|
63
64
|
retry: attempt,
|
|
@@ -69,9 +70,7 @@ export default class SupercovJestReporter {
|
|
|
69
70
|
browser: [],
|
|
70
71
|
server: [],
|
|
71
72
|
};
|
|
72
|
-
|
|
73
|
-
mkdirSync(directory, { recursive: true });
|
|
74
|
-
atomicWriteFileSync(resolve(directory, "mcdc.json"), `${JSON.stringify(payload)}\n`);
|
|
73
|
+
appendEvidenceRecord(resolve(process.cwd(), evidenceDirectory), "jest-status", payload);
|
|
75
74
|
};
|
|
76
75
|
// `jest.retryTimes` reports one result after the last attempt; every
|
|
77
76
|
// attempt before it failed, or there would have been no retry. The
|
|
@@ -54,6 +54,10 @@ function executionLogPath(path, token = executionLog.token) {
|
|
|
54
54
|
? `${path.slice(0, -".jsonl".length)}${suffix}`
|
|
55
55
|
: `${path}${suffix}`;
|
|
56
56
|
}
|
|
57
|
+
/** Append one event to the run's execution log, as the supervisor does. */
|
|
58
|
+
export function recordExecution(value) {
|
|
59
|
+
record(value);
|
|
60
|
+
}
|
|
57
61
|
function record(value) {
|
|
58
62
|
const configuredPath = process.env["SUPERCOV_EXECUTION_LOG"];
|
|
59
63
|
if (!configuredPath)
|
|
@@ -254,15 +258,32 @@ function launchArgv(value) {
|
|
|
254
258
|
return value.cmd;
|
|
255
259
|
return [String(value.command)];
|
|
256
260
|
}
|
|
261
|
+
// A plain map of environment variables, as `run(argv, { ...process.env })`
|
|
262
|
+
// passes one: string values under variable names, at least one of them in
|
|
263
|
+
// capitals, and nothing an options object would carry. A name is anything
|
|
264
|
+
// without `=`: Windows' own environment has `ProgramFiles(x86)`, and a map
|
|
265
|
+
// copied from it was taken for options and its guest left unconfigured.
|
|
266
|
+
function environmentMap(value) {
|
|
267
|
+
const entries = Object.entries(value);
|
|
268
|
+
return (entries.length > 0 &&
|
|
269
|
+
entries.every(([name, item]) => typeof item === "string" && /^[^=\0]+$/.test(name)) &&
|
|
270
|
+
entries.some(([name]) => /^[A-Z_][A-Z0-9_]*$/.test(name)) &&
|
|
271
|
+
!launchOptions(value));
|
|
272
|
+
}
|
|
257
273
|
function injectRemoteLaunch(options, mapping) {
|
|
258
|
-
const
|
|
274
|
+
const coverage = {
|
|
275
|
+
...process.env,
|
|
276
|
+
SUPERCOV_EXECUTION_LOG_SHARD: `${process.pid}-${++remoteLaunchSequence}`,
|
|
277
|
+
};
|
|
278
|
+
if (environmentMap(options))
|
|
279
|
+
return guestCoverageEnvironment(mapping, coverage, options);
|
|
280
|
+
// The SDK's own name for a command's environment: `env`, `environment`
|
|
281
|
+
// or, as E2B spells it, `envs`. A new one is called `env`.
|
|
282
|
+
const environmentKey = ["env", "environment", "envs"].find((key) => key in options) ?? "env";
|
|
259
283
|
const existing = options[environmentKey];
|
|
260
284
|
return {
|
|
261
285
|
...options,
|
|
262
|
-
[environmentKey]: guestCoverageEnvironment(mapping,
|
|
263
|
-
...process.env,
|
|
264
|
-
SUPERCOV_EXECUTION_LOG_SHARD: `${process.pid}-${++remoteLaunchSequence}`,
|
|
265
|
-
}, existing && typeof existing === "object"
|
|
286
|
+
[environmentKey]: guestCoverageEnvironment(mapping, coverage, existing && typeof existing === "object"
|
|
266
287
|
? existing
|
|
267
288
|
: {}),
|
|
268
289
|
};
|
|
@@ -418,6 +439,21 @@ export function wrapCapabilityObject(value, mapping) {
|
|
|
418
439
|
guestRoot: mapping.guestRoot,
|
|
419
440
|
});
|
|
420
441
|
}
|
|
442
|
+
else if (typeof property === "string" &&
|
|
443
|
+
/env/i.test(property) &&
|
|
444
|
+
args.length === 1 &&
|
|
445
|
+
args[0] &&
|
|
446
|
+
typeof args[0] === "object" &&
|
|
447
|
+
environmentMap(args[0])) {
|
|
448
|
+
// A builder that takes the command's environment by itself
|
|
449
|
+
// (`withEnvironment(env)`) before it starts the command.
|
|
450
|
+
callArguments = [injectRemoteLaunch(args[0], mapping)];
|
|
451
|
+
record({
|
|
452
|
+
event: "remote-launch",
|
|
453
|
+
command: commandSummary([property]),
|
|
454
|
+
guestRoot: mapping.guestRoot,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
421
457
|
return wrapResult(Reflect.apply(member, target, callArguments), mapping);
|
|
422
458
|
};
|
|
423
459
|
},
|
|
@@ -435,6 +471,7 @@ export function wrapImportedCapability(value) {
|
|
|
435
471
|
return value;
|
|
436
472
|
if (isClassConstructor(value)) {
|
|
437
473
|
patchBuilder(value);
|
|
474
|
+
patchMountMethods(value);
|
|
438
475
|
return value;
|
|
439
476
|
}
|
|
440
477
|
const object = value;
|
|
@@ -489,6 +526,49 @@ export function wrapImportedCapability(value) {
|
|
|
489
526
|
importedCapabilityProxies.set(object, proxy);
|
|
490
527
|
return proxy;
|
|
491
528
|
}
|
|
529
|
+
const patchedMountMethods = new WeakSet();
|
|
530
|
+
/**
|
|
531
|
+
* A builder made with `new` (`new GenericContainer(image)`) takes its mounts
|
|
532
|
+
* on an instance method, `withBindMounts([{ source, target }])`. The class
|
|
533
|
+
* itself must stay the exact value it is -- registries compare constructors
|
|
534
|
+
* -- so its mount methods are patched in place instead: called with a mount
|
|
535
|
+
* of the workspace, the builder they return is followed, and its later calls
|
|
536
|
+
* (`withEnvironment`, `start`) see the mapping. Called without one, they do
|
|
537
|
+
* what they always did.
|
|
538
|
+
*/
|
|
539
|
+
function patchMountMethods(builder) {
|
|
540
|
+
for (let prototype = builder.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)) {
|
|
541
|
+
for (const name of Object.getOwnPropertyNames(prototype)) {
|
|
542
|
+
if (!/mount/i.test(name))
|
|
543
|
+
continue;
|
|
544
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
545
|
+
if (!descriptor ||
|
|
546
|
+
typeof descriptor.value !== "function" ||
|
|
547
|
+
!descriptor.writable ||
|
|
548
|
+
patchedMountMethods.has(descriptor.value))
|
|
549
|
+
continue;
|
|
550
|
+
const original = descriptor.value;
|
|
551
|
+
const patched = function supercovMountMethod(...args) {
|
|
552
|
+
const hostRoot = process.env["SUPERCOV_PROJECT_ROOT"];
|
|
553
|
+
const mapping = hostRoot
|
|
554
|
+
? args.map((argument) => discoverWorkspaceMapping(argument, hostRoot)).find(Boolean)
|
|
555
|
+
: undefined;
|
|
556
|
+
const result = Reflect.apply(original, this, args);
|
|
557
|
+
if (!mapping)
|
|
558
|
+
return result;
|
|
559
|
+
record({
|
|
560
|
+
event: "workspace-capability",
|
|
561
|
+
hostRoot: mapping.hostRoot,
|
|
562
|
+
guestRoot: mapping.guestRoot,
|
|
563
|
+
cacheIdentities: [],
|
|
564
|
+
});
|
|
565
|
+
return wrapResult(result, mapping);
|
|
566
|
+
};
|
|
567
|
+
patchedMountMethods.add(patched);
|
|
568
|
+
Object.defineProperty(prototype, name, { ...descriptor, value: patched });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
492
572
|
function patchBuilder(builder) {
|
|
493
573
|
if (patchedBuilders.has(builder))
|
|
494
574
|
return;
|
|
@@ -545,20 +625,52 @@ function inspectExports(value, depth = 0) {
|
|
|
545
625
|
}
|
|
546
626
|
}
|
|
547
627
|
}
|
|
628
|
+
// spawn, fork and execFile take (file, args?, options?, callback?): options
|
|
629
|
+
// follow an argument list, or an explicit null or undefined in its place. exec
|
|
630
|
+
// takes (command, options?, callback?), and calls execFile as (file, options,
|
|
631
|
+
// callback) itself -- that object is the options, not an argument list.
|
|
548
632
|
function childOptionsIndex(method, args) {
|
|
549
|
-
if (method === "spawn" ||
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
: 1;
|
|
553
|
-
if (method === "exec" ||
|
|
554
|
-
method === "execSync" ||
|
|
633
|
+
if (method === "spawn" ||
|
|
634
|
+
method === "spawnSync" ||
|
|
635
|
+
method === "fork" ||
|
|
555
636
|
method === "execFile" ||
|
|
556
637
|
method === "execFileSync")
|
|
557
|
-
return Array.isArray(args[1]) || (args.length > 2
|
|
558
|
-
|
|
559
|
-
|
|
638
|
+
return Array.isArray(args[1]) || (args[1] == null && args.length > 2) ? 2 : 1;
|
|
639
|
+
if (method === "exec" || method === "execSync")
|
|
640
|
+
return 1;
|
|
560
641
|
return undefined;
|
|
561
642
|
}
|
|
643
|
+
// util.promisify(exec) and util.promisify(execFile) resolve { stdout, stderr }
|
|
644
|
+
// through a function Node keeps on the original, and that function calls the
|
|
645
|
+
// original. A wrapper carries its own, which calls the wrapper, or a promisified
|
|
646
|
+
// call resolves the bare stdout string and skips the wrapper.
|
|
647
|
+
const promisifyCustom = Symbol.for("nodejs.util.promisify.custom");
|
|
648
|
+
function keepPromisifiedShape(original, wrapper) {
|
|
649
|
+
if (typeof original[promisifyCustom] !== "function")
|
|
650
|
+
return wrapper;
|
|
651
|
+
Object.defineProperty(wrapper, promisifyCustom, {
|
|
652
|
+
configurable: true,
|
|
653
|
+
value: (...args) => {
|
|
654
|
+
let settle;
|
|
655
|
+
let reject;
|
|
656
|
+
const promise = new Promise((ok, fail) => {
|
|
657
|
+
settle = ok;
|
|
658
|
+
reject = fail;
|
|
659
|
+
});
|
|
660
|
+
promise.child = wrapper(...args, (error, stdout, stderr) => {
|
|
661
|
+
if (error !== null) {
|
|
662
|
+
error.stdout = stdout;
|
|
663
|
+
error.stderr = stderr;
|
|
664
|
+
reject(error);
|
|
665
|
+
}
|
|
666
|
+
else
|
|
667
|
+
settle({ stdout, stderr });
|
|
668
|
+
});
|
|
669
|
+
return promise;
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
return wrapper;
|
|
673
|
+
}
|
|
562
674
|
function injectChildEnvironment(method, args) {
|
|
563
675
|
const index = childOptionsIndex(method, args);
|
|
564
676
|
if (index === undefined)
|
|
@@ -617,7 +729,7 @@ function patchChildProcesses() {
|
|
|
617
729
|
configurable: true,
|
|
618
730
|
enumerable: true,
|
|
619
731
|
writable: true,
|
|
620
|
-
value: (...args) => Reflect.apply(original, childProcess, injectChildEnvironment(method, args)),
|
|
732
|
+
value: keepPromisifiedShape(original, (...args) => Reflect.apply(original, childProcess, injectChildEnvironment(method, args))),
|
|
621
733
|
});
|
|
622
734
|
}
|
|
623
735
|
syncBuiltinESMExports();
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Supercov Mocha adapter. The preload imports it in every Mocha process --
|
|
2
|
+
// the CLI, the child the CLI forks for Node options, and each --parallel
|
|
3
|
+
// worker -- and it patches Mocha's own Runner, so a suite runs exactly as
|
|
4
|
+
// written: no reporter, --require or configuration is added. Each test
|
|
5
|
+
// attempt gets its exact scope when Mocha announces it, before its
|
|
6
|
+
// beforeEach hooks, and its coverage, assertion phases and outcome are
|
|
7
|
+
// written after its afterEach hooks. Code that runs outside any test --
|
|
8
|
+
// loading the test files, before/after all hooks -- is written as setup. It
|
|
9
|
+
// computes no coverage.
|
|
10
|
+
import { realpathSync } from "node:fs";
|
|
11
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
14
|
+
import { inferTestProvenance } from "./provenance.mjs";
|
|
15
|
+
import { runnerExecutionScope } from "./runnerEvidence.mjs";
|
|
16
|
+
|
|
17
|
+
const SETUP_HOOK = /^"(?:before|after) all" hook/;
|
|
18
|
+
|
|
19
|
+
function localFile(path) {
|
|
20
|
+
return path ? relative(process.cwd(), path).split(sep).join("/") : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The installed mocha package the entrypoint belongs to.
|
|
24
|
+
function mochaRoot(entrypoint) {
|
|
25
|
+
let real;
|
|
26
|
+
try {
|
|
27
|
+
real = realpathSync(entrypoint).replaceAll("\\", "/");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const at = real.lastIndexOf("/node_modules/mocha/");
|
|
33
|
+
return at < 0 ? undefined : real.slice(0, at + "/node_modules/mocha".length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function adaptMocha(entrypoint) {
|
|
37
|
+
const runtime = globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIRECT_RUNTIME__;
|
|
38
|
+
const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
|
|
39
|
+
const root = mochaRoot(entrypoint);
|
|
40
|
+
if (!runtime || !evidenceDirectory || !root)
|
|
41
|
+
return;
|
|
42
|
+
// Mocha 12 is ESM and exports { Runner }; 10 and 11 are CommonJS and
|
|
43
|
+
// export the class itself. Either way this is the instance Mocha uses.
|
|
44
|
+
let loaded;
|
|
45
|
+
try {
|
|
46
|
+
loaded = await import(pathToFileURL(join(root, "lib", "runner.js")).href);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const Runner = loaded.Runner ?? loaded.default?.Runner ?? loaded.default;
|
|
52
|
+
const run = Runner?.prototype?.run;
|
|
53
|
+
if (typeof run !== "function" || run.supercovMocha)
|
|
54
|
+
return;
|
|
55
|
+
// One snapshot per test, as under Jest; the server transport would only
|
|
56
|
+
// repeat it, unattributed.
|
|
57
|
+
runtime.enableRuntimeSnapshotEvidence();
|
|
58
|
+
const directory = resolve(process.cwd(), evidenceDirectory);
|
|
59
|
+
const write = (payload) => appendEvidenceRecord(directory, "mocha", payload);
|
|
60
|
+
// A --parallel parent overrides run() and never reaches this: its workers
|
|
61
|
+
// run the tests, each with a Runner of its own.
|
|
62
|
+
Runner.prototype.run = function supercovMochaRun(...args) {
|
|
63
|
+
observe(this, runtime, write);
|
|
64
|
+
return Reflect.apply(run, this, args);
|
|
65
|
+
};
|
|
66
|
+
Runner.prototype.run.supercovMocha = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function observe(runner, runtime, write) {
|
|
70
|
+
// Tests that share a file and a full title are told apart by the order in
|
|
71
|
+
// which their first attempts ran; a retry keeps its test's identity.
|
|
72
|
+
const firstAttempts = new Map();
|
|
73
|
+
const identities = new WeakMap();
|
|
74
|
+
let active;
|
|
75
|
+
let setups = 0;
|
|
76
|
+
function identity(test) {
|
|
77
|
+
const known = identities.get(test);
|
|
78
|
+
if (known)
|
|
79
|
+
return known;
|
|
80
|
+
const file = test.file;
|
|
81
|
+
const name = test.titlePath().join(" > ");
|
|
82
|
+
const key = `${file ?? ""}\0${name}`;
|
|
83
|
+
let occurrence = firstAttempts.get(key) ?? 0;
|
|
84
|
+
if (test.currentRetry() > 0)
|
|
85
|
+
occurrence = Math.max(occurrence - 1, 0);
|
|
86
|
+
else
|
|
87
|
+
firstAttempts.set(key, occurrence + 1);
|
|
88
|
+
const value = {
|
|
89
|
+
runner: "mocha",
|
|
90
|
+
file,
|
|
91
|
+
name,
|
|
92
|
+
...(occurrence ? { registrationOrdinal: occurrence } : {}),
|
|
93
|
+
retry: test.currentRetry(),
|
|
94
|
+
};
|
|
95
|
+
identities.set(test, value);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function record(id, status, scope, snapshot) {
|
|
99
|
+
const testFile = localFile(id.file);
|
|
100
|
+
write({
|
|
101
|
+
testId: scope ? scope.testId : runnerExecutionScope(id).testId,
|
|
102
|
+
...(scope ? { scope } : {}),
|
|
103
|
+
test: id.name,
|
|
104
|
+
...(testFile ? { testFile } : {}),
|
|
105
|
+
title: id.name.split(" > ").at(-1) ?? id.name,
|
|
106
|
+
retry: id.retry ?? 0,
|
|
107
|
+
status,
|
|
108
|
+
...(id.role ? { role: id.role } : {}),
|
|
109
|
+
provenance: inferTestProvenance({
|
|
110
|
+
runner: "mocha",
|
|
111
|
+
file: testFile,
|
|
112
|
+
explicitKind: process.env.SUPERCOV_TEST_KIND,
|
|
113
|
+
}),
|
|
114
|
+
...(scope ? { phases: runtime.takeNodeAssertionPhases(scope) } : {}),
|
|
115
|
+
runtime: snapshot ? [snapshot] : [],
|
|
116
|
+
browser: [],
|
|
117
|
+
server: [],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// What ran since the last test or setup ended, outside any test.
|
|
121
|
+
function flushSetup(name) {
|
|
122
|
+
const snapshot = runtime.coverageSnapshot();
|
|
123
|
+
runtime.resetCoverage();
|
|
124
|
+
if (!snapshot.hits.length && !snapshot.decisions.length)
|
|
125
|
+
return;
|
|
126
|
+
setups += 1;
|
|
127
|
+
const id = { runner: "mocha", name, role: "setup", registrationOrdinal: setups };
|
|
128
|
+
record(id, "passed", runnerExecutionScope(id), snapshot);
|
|
129
|
+
}
|
|
130
|
+
function flushActive() {
|
|
131
|
+
const current = active;
|
|
132
|
+
active = undefined;
|
|
133
|
+
if (!current)
|
|
134
|
+
return;
|
|
135
|
+
record(current.id, current.status, current.scope, runtime.coverageSnapshot());
|
|
136
|
+
runtime.resetCoverage();
|
|
137
|
+
runtime.activateCoverageScope();
|
|
138
|
+
}
|
|
139
|
+
runner.on("start", () => flushSetup("module setup"));
|
|
140
|
+
runner.on("test", (test) => {
|
|
141
|
+
flushActive();
|
|
142
|
+
flushSetup(`${test.parent?.fullTitle() || "root suite"} > hooks`);
|
|
143
|
+
const id = identity(test);
|
|
144
|
+
const scope = runnerExecutionScope(id);
|
|
145
|
+
active = { test, id, scope, status: "unknown" };
|
|
146
|
+
runtime.activateCoverageScope(scope);
|
|
147
|
+
runtime.resetCoverage(scope.testId);
|
|
148
|
+
});
|
|
149
|
+
runner.on("pass", (test) => {
|
|
150
|
+
if (active?.test === test)
|
|
151
|
+
active.status = "passed";
|
|
152
|
+
});
|
|
153
|
+
// A failed attempt Mocha will run again, and a failing beforeEach or
|
|
154
|
+
// afterEach hook, fail the attempt that is running.
|
|
155
|
+
runner.on("retry", (test) => {
|
|
156
|
+
if (active?.test === test)
|
|
157
|
+
active.status = "failed";
|
|
158
|
+
});
|
|
159
|
+
runner.on("fail", (runnable) => {
|
|
160
|
+
if (active && (active.test === runnable || runnable.type === "hook"))
|
|
161
|
+
active.status = "failed";
|
|
162
|
+
else if (runnable.type === "test")
|
|
163
|
+
record(identity(runnable), "failed");
|
|
164
|
+
});
|
|
165
|
+
runner.on("pending", (test) => {
|
|
166
|
+
if (active?.test === test)
|
|
167
|
+
active.status = "skipped";
|
|
168
|
+
else
|
|
169
|
+
record(identity(test), "skipped");
|
|
170
|
+
});
|
|
171
|
+
runner.on("hook", (hook) => {
|
|
172
|
+
if (SETUP_HOOK.test(hook.title ?? ""))
|
|
173
|
+
flushActive();
|
|
174
|
+
});
|
|
175
|
+
runner.on("suite end", flushActive);
|
|
176
|
+
runner.on("end", () => {
|
|
177
|
+
flushActive();
|
|
178
|
+
flushSetup("hooks");
|
|
179
|
+
});
|
|
180
|
+
}
|