oxmutate 1.2.0 → 1.2.2
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/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/bin.js +0 -1
- package/dist/checker.js +1 -32
- package/dist/child.js +4 -46
- package/dist/cli.js +0 -10
- package/dist/command-runner.js +0 -27
- package/dist/config.js +0 -29
- package/dist/coordinator.js +5 -129
- package/dist/directives.js +0 -17
- package/dist/files.js +0 -10
- package/dist/frozen.js +0 -15
- package/dist/heartbeat.js +1 -50
- package/dist/index.js +0 -4
- package/dist/lanes.js +1 -30
- package/dist/native.js +0 -13
- package/dist/platform.js +0 -14
- package/dist/progress.js +0 -9
- package/dist/reports.js +0 -28
- package/dist/resolve.js +0 -10
- package/dist/runner.js +6 -115
- package/dist/runtime.js +0 -16
- package/dist/scheduler.js +0 -20
- package/dist/spec-oracle.js +0 -23
- package/dist/spec.js +0 -28
- package/dist/vite-plugin.js +0 -5
- package/dist/vite.js +0 -2
- package/package.json +2 -2
package/dist/lanes.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The CI lanes' selection rules: which mutants a run owns.
|
|
3
|
-
*
|
|
4
|
-
* - `since <ref>` (the PR lane): mutants on lines the change added or modified, from
|
|
5
|
-
* `git diff -U0 <ref>`, working tree included, so a local run and CI select the same set.
|
|
6
|
-
* - `shard i/n` (a CI matrix): a mutant belongs to shard `fnv(id) mod n`, so a shard's share
|
|
7
|
-
* does not move when unrelated code changes, and the shards partition the run exactly.
|
|
8
|
-
*/
|
|
9
1
|
import { ambientRuntime, run } from 'bellpull';
|
|
10
2
|
const NEW_FILE = '+++ ';
|
|
11
|
-
/** FNV-1a 32-bit parameters. */
|
|
12
3
|
const FNV_OFFSET = 0x811c9dc5;
|
|
13
4
|
const FNV_PRIME = 0x01000193;
|
|
14
|
-
/** Added or changed line ranges per repository-relative path, new side. */
|
|
15
5
|
export async function changedLines(root, base) {
|
|
16
6
|
const diff = await run('git', ['diff', '-U0', '--no-color', '--no-ext-diff', base, '--'], { cwd: root, runtime: ambientRuntime() });
|
|
17
7
|
if (!diff.ok)
|
|
@@ -33,23 +23,17 @@ export function parseDiff(diff) {
|
|
|
33
23
|
const start = Number(hunk[1]);
|
|
34
24
|
const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
|
|
35
25
|
if (count === 0)
|
|
36
|
-
continue;
|
|
37
|
-
// Stryker disable next-line OptionalChaining: the get after a set always finds the list; `?.` is only for the type
|
|
26
|
+
continue;
|
|
38
27
|
(out.get(file) ?? out.set(file, []).get(file))?.push([start, start + count - 1]);
|
|
39
28
|
}
|
|
40
29
|
return out;
|
|
41
30
|
}
|
|
42
31
|
export function touches(ranges, file, startLine, endLine) {
|
|
43
|
-
// Stryker disable next-line ArrayDeclaration: a string element destructures to letters, and a number compared with a letter is false, as with no ranges
|
|
44
32
|
return (ranges.get(file) ?? []).some(([a, b]) => startLine <= b && endLine >= a);
|
|
45
33
|
}
|
|
46
|
-
/** FNV-1a 32 over the stable id: the shard a mutant belongs to. */
|
|
47
34
|
export function shardOf(id, count) {
|
|
48
35
|
let h = FNV_OFFSET;
|
|
49
|
-
// Ids are hex (`native/src/stable_id.rs`), so a code point is a code unit and the shards are
|
|
50
|
-
// the ones `charCodeAt` gave.
|
|
51
36
|
for (let i = 0; i < id.length; i++) {
|
|
52
|
-
// Stryker disable next-line NumericLiteral: `i < id.length`, so codePointAt never returns undefined here
|
|
53
37
|
h ^= id.codePointAt(i) ?? 0;
|
|
54
38
|
h = Math.imul(h, FNV_PRIME) >>> 0;
|
|
55
39
|
}
|
|
@@ -59,30 +43,18 @@ export function parseShard(value) {
|
|
|
59
43
|
const m = /^(\d+)\/(\d+)$/.exec(value);
|
|
60
44
|
const index = Number(m?.[1]);
|
|
61
45
|
const count = Number(m?.[2]);
|
|
62
|
-
// `count < 1` needs no check of its own: with 1 <= index <= count it cannot hold.
|
|
63
46
|
if (!m || index < 1 || index > count)
|
|
64
47
|
throw new Error(`--shard must be i/n with 1 <= i <= n, got "${value}"`);
|
|
65
48
|
return { index: index - 1, count };
|
|
66
49
|
}
|
|
67
|
-
/**
|
|
68
|
-
* The range end of Stryker's pattern (`mutate-pattern-constants.js`): `<glob>:<line>[:<col>]-<line>[:<col>]`.
|
|
69
|
-
* Only the suffix is matched; the glob is what comes before it. Stryker's lazy `^(.*?):` glob group
|
|
70
|
-
* backtracks polynomially (CodeQL js/polynomial-redos), and the leftmost match is the same split.
|
|
71
|
-
*/
|
|
72
50
|
const RANGE = /:(\d+)(?::(\d+))?-(\d+)(?::(\d+))?$/;
|
|
73
|
-
/** A `mutate` entry split into its glob and, when it names one, its range. */
|
|
74
51
|
export function parseMutateSpec(spec) {
|
|
75
52
|
const m = RANGE.exec(spec);
|
|
76
53
|
if (!m)
|
|
77
54
|
return { glob: spec };
|
|
78
|
-
// Stryker disable next-line StringLiteral: Number('') is 0 as Number('0') is
|
|
79
55
|
const [, startLine, startColumn = '0', endLine, endColumn = String(Number.MAX_SAFE_INTEGER)] = m;
|
|
80
56
|
return { glob: spec.slice(0, m.index), range: { start: { line: Number(startLine), column: Number(startColumn) }, end: { line: Number(endLine), column: Number(endColumn) } } };
|
|
81
57
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Is the mutant wholly inside the range, as Stryker decides it (`locationIncluded`)? The mutant's
|
|
84
|
-
* columns are the report's, 1-based with the end exclusive; the range's are Stryker's, 0-based.
|
|
85
|
-
*/
|
|
86
58
|
export function withinRange(range, start, end) {
|
|
87
59
|
const from = start.column - 1;
|
|
88
60
|
const to = end.column - 1;
|
|
@@ -90,4 +62,3 @@ export function withinRange(range, start, end) {
|
|
|
90
62
|
const endIn = range.end.line > end.line || (range.end.line === end.line && range.end.column >= to);
|
|
91
63
|
return startIn && endIn;
|
|
92
64
|
}
|
|
93
|
-
//# sourceMappingURL=lanes.js.map
|
package/dist/native.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The native core (`native/`, Rust on oxc), loaded for this platform. It ships as one prebuilt
|
|
3
|
-
* `.node` per platform (PRINCIPLES rule 9, `native-engine`): 0.1 bundles all six in the tarball as
|
|
4
|
-
* `binding/oxmutate.<tag>.node` (`platform.ts` names the tag); development builds
|
|
5
|
-
* `binding/oxmutate.<platform>-<arch>.node`; the `oxmutate-<tag>` platform package comes last.
|
|
6
|
-
*/
|
|
7
1
|
import { existsSync } from 'node:fs';
|
|
8
2
|
import path from 'node:path';
|
|
9
3
|
import { fileURLToPath } from 'node:url';
|
|
10
4
|
import { bindingNames, platformPackage } from './platform.js';
|
|
11
5
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
6
|
const target = `${process.platform}-${process.arch}`;
|
|
13
|
-
/** This machine, as `platform.ts` reads it: glibc's runtime version from `process.report` (absent on musl). */
|
|
14
7
|
function machine() {
|
|
15
8
|
const glibc = process.platform === 'linux' ? process.report?.getReport()?.header?.glibcVersionRuntime : undefined;
|
|
16
9
|
return { platform: process.platform, arch: process.arch, glibc };
|
|
@@ -20,13 +13,8 @@ export function native() {
|
|
|
20
13
|
if (binding)
|
|
21
14
|
return binding;
|
|
22
15
|
const self = machine();
|
|
23
|
-
// `binding/` beside `dist/` (published) or `src/` (development), then one level up.
|
|
24
16
|
const candidates = [path.join(here, '..', 'binding'), path.join(here, '..', '..', 'binding')].flatMap((dir) => bindingNames(self).map((name) => path.join(dir, name)));
|
|
25
17
|
try {
|
|
26
|
-
// `process.dlopen` loads a native addon straight from its path, the way `require` does it
|
|
27
|
-
// underneath. Since 0.1 the six release cores ship inside the tarball as
|
|
28
|
-
// `binding/oxmutate.<tag>.node`; the platform package (its `main`, `oxmutate.node`) is last,
|
|
29
|
-
// for when those publish.
|
|
30
18
|
const file = candidates.find((c) => existsSync(c)) ?? fileURLToPath(import.meta.resolve(platformPackage(self)));
|
|
31
19
|
const addon = { exports: {} };
|
|
32
20
|
process.dlopen(addon, file);
|
|
@@ -37,4 +25,3 @@ export function native() {
|
|
|
37
25
|
}
|
|
38
26
|
return binding;
|
|
39
27
|
}
|
|
40
|
-
//# sourceMappingURL=native.js.map
|
package/dist/platform.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which native core a machine needs, as napi-rs names it: `<platform>-<arch>[-gnu|-musl|-msvc]`.
|
|
3
|
-
* The tag names the bundled core (`binding/oxmutate.<tag>.node`), the platform package
|
|
4
|
-
* (`oxmutate-<tag>`, `npm/<tag>`) and the release matrix leg, so all three agree by construction.
|
|
5
|
-
* Pure: the machine is passed in, so every branch is testable on any one machine.
|
|
6
|
-
*/
|
|
7
|
-
/** `darwin-arm64`, `linux-x64-gnu`, `linux-x64-musl`, `win32-x64-msvc`, … */
|
|
8
1
|
export function platformTag({ platform, arch, glibc }) {
|
|
9
2
|
if (platform === 'win32')
|
|
10
3
|
return `win32-${arch}-msvc`;
|
|
@@ -12,16 +5,9 @@ export function platformTag({ platform, arch, glibc }) {
|
|
|
12
5
|
return `linux-${arch}-${glibc ? 'gnu' : 'musl'}`;
|
|
13
6
|
return `${platform}-${arch}`;
|
|
14
7
|
}
|
|
15
|
-
/** The npm package that carries the tag's core once the platform packages publish. */
|
|
16
8
|
export function platformPackage(machine) {
|
|
17
9
|
return `oxmutate-${platformTag(machine)}`;
|
|
18
10
|
}
|
|
19
|
-
/**
|
|
20
|
-
* The files to try, in order, under each `binding/` directory: the bundled release core for this
|
|
21
|
-
* tag, then the development build `scripts/copy-binding.mjs` writes (`<platform>-<arch>`, which is
|
|
22
|
-
* the same name on macOS).
|
|
23
|
-
*/
|
|
24
11
|
export function bindingNames(machine) {
|
|
25
12
|
return [...new Set([`oxmutate.${platformTag(machine)}.node`, `oxmutate.${machine.platform}-${machine.arch}.node`])];
|
|
26
13
|
}
|
|
27
|
-
//# sourceMappingURL=platform.js.map
|
package/dist/progress.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stryker's `progress` reporters, one implementation: the phase, its own count and the time so
|
|
3
|
-
* far, on stderr. On a terminal the line rewrites itself every second (`progress`); anywhere else,
|
|
4
|
-
* or with `progress-append-only` (Stryker's `ci` alias), a new line every few seconds, so a CI log
|
|
5
|
-
* shows the run moving without a carriage-return mess. Off unless a progress reporter is named.
|
|
6
|
-
*/
|
|
7
1
|
const REPORTERS = new Set(['progress', 'progress-append-only', 'ci', 'dots']);
|
|
8
|
-
// Stryker disable next-line NumericLiteral: a repaint interval; no test can see 1,000 ms against 1,001
|
|
9
2
|
const TTY_EVERY_MS = 1000;
|
|
10
3
|
const APPEND_EVERY_MS = 10_000;
|
|
11
4
|
const MS_PER_S = 1000;
|
|
@@ -13,7 +6,6 @@ export function startProgress(reporters, out, now = () => performance.now()) {
|
|
|
13
6
|
const enabled = reporters.some((r) => REPORTERS.has(r));
|
|
14
7
|
const rewrite = Boolean(out.isTTY) && reporters.includes('progress');
|
|
15
8
|
const start = now();
|
|
16
|
-
// Stryker disable next-line StringLiteral: line() runs only after phase() has set the name, so the initial value is never printed
|
|
17
9
|
let name = '';
|
|
18
10
|
let detail;
|
|
19
11
|
let timer;
|
|
@@ -38,4 +30,3 @@ export function startProgress(reporters, out, now = () => performance.now()) {
|
|
|
38
30
|
},
|
|
39
31
|
};
|
|
40
32
|
}
|
|
41
|
-
//# sourceMappingURL=progress.js.map
|
package/dist/reports.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The three readers of a run: Stryker's JSON report (dashboards, the HTML elements, burgee's
|
|
3
|
-
* ratchet), SARIF 2.1.0 (code scanning annotates the pull request), and a terminal summary that
|
|
4
|
-
* ends with what to do next.
|
|
5
|
-
*/
|
|
6
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
2
|
import { createRequire } from 'node:module';
|
|
8
3
|
import path from 'node:path';
|
|
9
|
-
/** The package's own version, read from its manifest so `--version` and the reports never drift from it. */
|
|
10
|
-
// Stryker disable next-line StringLiteral: with no encoding readFileSync returns a Buffer, which JSON.parse reads as the same UTF-8 text
|
|
11
4
|
export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
12
|
-
/** A replacement longer than this is cut, in the SARIF message and in the terminal list. */
|
|
13
5
|
const REPLACEMENT_WIDTH = 60;
|
|
14
6
|
const ELLIPSIS = '...';
|
|
15
7
|
const MS_PER_S = 1000;
|
|
16
|
-
/** Survivors the terminal summary lists before it says how many it left out. */
|
|
17
8
|
const DEFAULT_SURVIVORS_SHOWN = 10;
|
|
18
|
-
/** `mutation-testing-report-schema` 2, the format Stryker writes (`jsonReporter`). */
|
|
19
9
|
export function strykerReport(r) {
|
|
20
10
|
const files = {};
|
|
21
11
|
for (const [file, source] of Object.entries(r.sources)) {
|
|
@@ -41,9 +31,7 @@ export function strykerReport(r) {
|
|
|
41
31
|
}
|
|
42
32
|
const testFiles = new Map();
|
|
43
33
|
for (const t of r.tests) {
|
|
44
|
-
// Forward slashes on every OS, as Stryker's report keys its files.
|
|
45
34
|
const file = path.relative(r.root, t.file).split(path.sep).join('/');
|
|
46
|
-
// Stryker disable next-line OptionalChaining: the get after a set always finds the entry; `?.` is only for the type
|
|
47
35
|
(testFiles.get(file) ?? testFiles.set(file, { tests: [] }).get(file))?.tests.push({ id: t.id, name: t.name });
|
|
48
36
|
}
|
|
49
37
|
return {
|
|
@@ -56,7 +44,6 @@ export function strykerReport(r) {
|
|
|
56
44
|
'x-oxmutate': { stats: r.stats, score: r.score, unusedConfig: r.options.unused, frozenOracle: r.frozen, specOracles: r.spec },
|
|
57
45
|
};
|
|
58
46
|
}
|
|
59
|
-
/** SARIF 2.1.0: one result per surviving or uncovered mutant, fingerprinted by stable id. */
|
|
60
47
|
export function sarifReport(r) {
|
|
61
48
|
const reported = r.mutants.filter((m) => m.status === 'Survived' || m.status === 'NoCoverage');
|
|
62
49
|
const rules = [...new Set(reported.map((m) => m.mutatorName))].sort();
|
|
@@ -105,7 +92,6 @@ export function describe(m) {
|
|
|
105
92
|
return `No test reaches this code: changing it to \`${change}\` (${m.mutatorName}) would go unnoticed. fix: add a test that executes ${m.file}:${String(m.start.line)}.`;
|
|
106
93
|
return `${String(m.coveredBy.length)} test(s) run this code, and all still pass when it becomes \`${change}\` (${m.mutatorName}). fix: assert the behaviour this line decides.`;
|
|
107
94
|
}
|
|
108
|
-
/** What the run had to correct, one phrase per kind of correction that happened. */
|
|
109
95
|
function corrections(s) {
|
|
110
96
|
return [
|
|
111
97
|
s.watchdogKills && `${String(s.watchdogKills)} runaway(s) stopped`,
|
|
@@ -118,11 +104,9 @@ function corrections(s) {
|
|
|
118
104
|
s.crashes && `${String(s.crashes)} worker crash(es)`,
|
|
119
105
|
].filter((c) => Boolean(c));
|
|
120
106
|
}
|
|
121
|
-
/** One line per spec oracle: its F metrics (metrics.md §6). */
|
|
122
107
|
function specLines(r) {
|
|
123
108
|
return (r.spec ?? []).map((o) => ` spec ${o.name} (${o.backend}, ${String(o.traces)} traces): kills ${o.specKillShare.toFixed(1)}% of ${String(o.modelled)} modelled mutants, reaches ${o.mappingCoverage.toFixed(1)}%; ${String(o.testSpecGap.length)} test-spec gap(s), ${String(o.specEscapes.length)} escape(s)`);
|
|
124
109
|
}
|
|
125
|
-
/** The frozen oracle's line, when the change edited tests. */
|
|
126
110
|
function frozenLine(r) {
|
|
127
111
|
const f = r.frozen;
|
|
128
112
|
if (!f?.changedTests.length)
|
|
@@ -130,7 +114,6 @@ function frozenLine(r) {
|
|
|
130
114
|
const sign = f.delta >= 0 ? '+' : '';
|
|
131
115
|
return [` frozen oracle vs ${f.base}: ${f.headScore.toFixed(2)}% with the change's tests, ${f.baseScore.toFixed(2)}% with the base's (${sign}${f.delta.toFixed(2)}), ${String(f.weakened.length)} mutant(s) the change stopped detecting`];
|
|
132
116
|
}
|
|
133
|
-
/** The terminal summary: the score, what to look at first, what the run corrected. */
|
|
134
117
|
export function textSummary(r, opts = {}) {
|
|
135
118
|
const count = (s) => r.mutants.filter((m) => m.status === s).length;
|
|
136
119
|
const s = r.stats;
|
|
@@ -161,29 +144,19 @@ export function textSummary(r, opts = {}) {
|
|
|
161
144
|
lines.push('', `Config keys not used: ${r.options.unused.map((u) => u.key).join(', ')} (see the JSON report for why)`);
|
|
162
145
|
return `${lines.join('\n')}\n`;
|
|
163
146
|
}
|
|
164
|
-
/** The web component Stryker's HTML report renders with, at the version Stryker 10.0.0 ships. */
|
|
165
147
|
const ELEMENTS = 'mutation-testing-elements/dist/mutation-test-elements.js';
|
|
166
148
|
const ELEMENTS_CDN = 'https://cdn.jsdelivr.net/npm/mutation-testing-elements@3.8.4/dist/mutation-test-elements.js';
|
|
167
|
-
/** sha384 of that file, the same bytes as the npm tarball's (both measured 2026-10-27). */
|
|
168
149
|
const ELEMENTS_SRI = 'sha384-xebNisAYQHHbrYIm991U9KOZ96oO+vRACR8kd+zHzWkU3pcRuigrsI7XS2tj3rqq';
|
|
169
|
-
/**
|
|
170
|
-
* Stryker's HTML report: the JSON report rendered by `mutation-testing-elements`. The component is
|
|
171
|
-
* inlined from the project's own install when it has one (Stryker brings it), so the page works
|
|
172
|
-
* offline as Stryker's does; otherwise the page loads that exact version from a CDN. oxmutate does
|
|
173
|
-
* not depend on it.
|
|
174
|
-
*/
|
|
175
150
|
export function htmlReport(r) {
|
|
176
151
|
let script;
|
|
177
152
|
try {
|
|
178
153
|
const elements = createRequire(path.join(r.root, 'package.json')).resolve(ELEMENTS);
|
|
179
|
-
// Stryker disable next-line StringLiteral: without an encoding readFileSync returns a Buffer, which the template turns into the same UTF-8 text
|
|
180
154
|
const component = readFileSync(elements, 'utf8');
|
|
181
155
|
script = `<script>${component}</script>`;
|
|
182
156
|
}
|
|
183
157
|
catch {
|
|
184
158
|
script = `<script src="${ELEMENTS_CDN}" integrity="${ELEMENTS_SRI}" crossorigin="anonymous"></script>`;
|
|
185
159
|
}
|
|
186
|
-
// `<` as \u003c: still the same JSON, and no `</script>` inside a source file can end the tag.
|
|
187
160
|
const report = JSON.stringify(strykerReport(r)).replaceAll('<', '\\u003c');
|
|
188
161
|
return `<!DOCTYPE html>
|
|
189
162
|
<html>
|
|
@@ -222,4 +195,3 @@ export function writeReports(r) {
|
|
|
222
195
|
write(r.options.htmlReport, htmlReport(r));
|
|
223
196
|
return written;
|
|
224
197
|
}
|
|
225
|
-
//# sourceMappingURL=reports.js.map
|
package/dist/resolve.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve a bare specifier the way `import` would, from the project under test (not from this
|
|
3
|
-
* package). `createRequire().resolve` follows the `require` condition and lands on Vitest's
|
|
4
|
-
* CommonJS entry, which Vitest refuses to load; this follows `import`, then `default`.
|
|
5
|
-
*/
|
|
6
1
|
import { readFileSync } from 'node:fs';
|
|
7
2
|
import { createRequire } from 'node:module';
|
|
8
3
|
import path from 'node:path';
|
|
@@ -10,7 +5,6 @@ import { pathToFileURL } from 'node:url';
|
|
|
10
5
|
function pick(entry) {
|
|
11
6
|
if (typeof entry === 'string')
|
|
12
7
|
return entry;
|
|
13
|
-
// Stryker disable next-line ConditionalExpression: a number or boolean entry has no condition properties, so the loop below returns undefined too
|
|
14
8
|
if (!entry || typeof entry !== 'object')
|
|
15
9
|
return undefined;
|
|
16
10
|
for (const condition of ['import', 'node', 'default']) {
|
|
@@ -26,9 +20,6 @@ export function resolveEsm(specifier, fromDir) {
|
|
|
26
20
|
const subpath = `.${specifier.slice(name.length)}`;
|
|
27
21
|
const req = createRequire(path.join(fromDir, 'package.json'));
|
|
28
22
|
const pkgJson = req.resolve(`${name}/package.json`);
|
|
29
|
-
// A string `exports` exports no `./package.json`, so the resolve above has already thrown: here
|
|
30
|
-
// `exports` is a map or absent.
|
|
31
|
-
// Stryker disable next-line StringLiteral: without an encoding readFileSync returns a Buffer, which JSON.parse reads as UTF-8 text all the same
|
|
32
23
|
const pkg = JSON.parse(readFileSync(pkgJson, 'utf8'));
|
|
33
24
|
const exportsMap = pkg.exports;
|
|
34
25
|
let target;
|
|
@@ -40,4 +31,3 @@ export function resolveEsm(specifier, fromDir) {
|
|
|
40
31
|
throw new Error(`oxmutate: cannot resolve ${specifier} from ${fromDir}`);
|
|
41
32
|
return pathToFileURL(path.join(path.dirname(pkgJson), target)).href;
|
|
42
33
|
}
|
|
43
|
-
//# sourceMappingURL=resolve.js.map
|
package/dist/runner.js
CHANGED
|
@@ -1,39 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oxmutate's Vitest runner: loaded by Vitest (`test.runner`) inside its own worker, so hooks,
|
|
3
|
-
* `expect`, `vi`, `vi.mock` and the environment stay Vitest's.
|
|
4
|
-
*
|
|
5
|
-
* Three modes, set by the coordinator through the environment (`OXMUTATE_MODE`):
|
|
6
|
-
*
|
|
7
|
-
* - `dry`: run the suite unmutated once, recording which tests reach which mutant (the
|
|
8
|
-
* instrumented code counts hits into the namespace while `currentTestId` is set).
|
|
9
|
-
* - `mutant`: for each test, re-invoke the test's own function once per covering mutant with
|
|
10
|
-
* the switch flipped, each invocation bracketed by the suite's `beforeEach`/`afterEach` and the
|
|
11
|
-
* runner's own per-try reset (the mechanism Vitest uses for retries, so snapshot counters
|
|
12
|
-
* start over). Then run the test once more, unmutated: that run is what Vitest reports, and a
|
|
13
|
-
* failure there means an earlier mutant polluted shared state (the canary).
|
|
14
|
-
* - `isolated`: one mutant was armed before import (static mutants and canary re-runs). Run
|
|
15
|
-
* normally; any failed test kills it.
|
|
16
|
-
* - `static`: a batch of static mutants decided in this live worker (staticwarm.qnt). Before the file
|
|
17
|
-
* runs, each mutant is armed, the module registry is reset and the file is collected again, so
|
|
18
|
-
* its tests close over modules evaluated under the mutant; they run with their hooks, then an
|
|
19
|
-
* unarmed re-collection must pass. A file whose own collection touches globals, or a mutant that
|
|
20
|
-
* leaves a trace, sends the rest of the batch back to the fresh-worker path (`static-fallback`).
|
|
21
|
-
*
|
|
22
|
-
* Before each mutant the runner writes a progress line synchronously, so the coordinator can
|
|
23
|
-
* attribute and kill a worker stuck in a synchronous loop that the hit counter cannot break.
|
|
24
|
-
*/
|
|
25
1
|
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
2
|
import { createRequire } from 'node:module';
|
|
27
3
|
import path from 'node:path';
|
|
28
|
-
import { BEAT_ENV, markTest, startHeartbeat } from './heartbeat.js';
|
|
4
|
+
import { BEAT_ENV, markTest, realNow, startHeartbeat } from './heartbeat.js';
|
|
29
5
|
import { resolveEsm } from './resolve.js';
|
|
30
6
|
import { ACTIVE_MUTANT_ENV, FILE_KEY, NAMESPACE, cwd, env } from './runtime.js';
|
|
31
|
-
/**
|
|
32
|
-
* The project's own Vitest, never the one beside this file: two Vitest instances in one worker
|
|
33
|
-
* disagree about the current test, the snapshot state and `expect`.
|
|
34
|
-
*/
|
|
35
7
|
const fromProject = async (specifier, dir = cwd()) => import(resolveEsm(specifier, dir));
|
|
36
|
-
/** The family's refusal shape for a missing optional peer (as `controlroom/ink`'s): a code and a fix. */
|
|
37
8
|
export class PeerMissingError extends Error {
|
|
38
9
|
code = 'E_PEER_MISSING';
|
|
39
10
|
fix = "npm install vitest — oxmutate runs the project's own Vitest, so the project brings it";
|
|
@@ -46,7 +17,6 @@ const missing = (cause) => {
|
|
|
46
17
|
const code = cause?.code;
|
|
47
18
|
return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
48
19
|
};
|
|
49
|
-
/** Vitest 5 exports `TestRunner` from `vitest`; Vitest 4 from `vitest/runners` + `@vitest/runner`. */
|
|
50
20
|
async function loadBase() {
|
|
51
21
|
const vitest = (await fromProject('vitest').catch((cause) => {
|
|
52
22
|
if (missing(cause))
|
|
@@ -66,39 +36,27 @@ const { Base, statics } = await loadBase();
|
|
|
66
36
|
const mode = env['OXMUTATE_MODE'] ?? 'dry';
|
|
67
37
|
const resultsPath = env['OXMUTATE_RESULTS'];
|
|
68
38
|
const progressPath = env['OXMUTATE_PROGRESS'];
|
|
69
|
-
startHeartbeat(env[BEAT_ENV]);
|
|
39
|
+
startHeartbeat(env[BEAT_ENV]);
|
|
70
40
|
const ns = (globalThis[NAMESPACE] ??= {});
|
|
71
41
|
ns.mutantCoverage ??= { static: {}, perTest: {} };
|
|
72
42
|
const plan = mode === 'mutant' && env['OXMUTATE_PLAN'] ? JSON.parse(readFileSync(env['OXMUTATE_PLAN'], 'utf8')) : { tests: {}, hitLimits: {}, skip: [] };
|
|
73
43
|
const decided = new Set(plan.skip);
|
|
74
|
-
/** The replay tests' own bail: a mutant the tests killed is still replayed, and the reverse. */
|
|
75
44
|
const specTests = new Set(plan.specTests ?? []);
|
|
76
45
|
const decidedBySpec = new Set();
|
|
77
|
-
/** `isolated`: when set, only these tests run (the mutant's covering tests); the rest pass untouched. */
|
|
78
46
|
const only = mode === 'isolated' && env['OXMUTATE_ONLY_TESTS'] ? new Set(JSON.parse(env['OXMUTATE_ONLY_TESTS'])) : undefined;
|
|
79
|
-
/** `static`: the static mutants this file run decides in the live worker. */
|
|
80
47
|
const staticIds = mode === 'static' && env['OXMUTATE_STATIC'] ? JSON.parse(env['OXMUTATE_STATIC']) : [];
|
|
81
|
-
/** `static`: each mutant's hit-count limit. */
|
|
82
48
|
const staticLimits = mode === 'static' && env['OXMUTATE_HIT_LIMITS'] ? JSON.parse(env['OXMUTATE_HIT_LIMITS']) : {};
|
|
83
|
-
// `isolated`: the mutant is armed from process start, so its hit counter is too. A loop it causes
|
|
84
|
-
// stops at the limit, as in warm runs, instead of holding the worker until the watchdog's budget:
|
|
85
|
-
// on oxmutate's own suite 17 runaways left the isolated phase idle 140 of 186 s (2026-10-30).
|
|
86
49
|
if (mode === 'isolated' && env['OXMUTATE_HIT_LIMIT']) {
|
|
87
50
|
ns.hitCount = 0;
|
|
88
51
|
ns.hitLimit = Number(env['OXMUTATE_HIT_LIMIT']);
|
|
89
52
|
}
|
|
90
|
-
/** Did the armed mutant's switch pass its limit, i.e. was this run stopped as a runaway? */
|
|
91
53
|
const ranAway = () => ns.hitCount !== undefined && ns.hitLimit !== undefined && ns.hitCount > ns.hitLimit;
|
|
92
|
-
// ── `static` mode: what a registry reset does not reset ─────────────────────────────────────────
|
|
93
|
-
/** Built-ins a module may patch in place; their own methods are compared by identity. */
|
|
94
54
|
const PROTOTYPES = {
|
|
95
55
|
Object: Object.prototype, Array: Array.prototype, String: String.prototype, Number: Number.prototype,
|
|
96
56
|
Boolean: Boolean.prototype, Function: Function.prototype, Promise: Promise.prototype, Map: Map.prototype,
|
|
97
57
|
Set: Set.prototype, RegExp: RegExp.prototype, Date: Date.prototype, Error: Error.prototype,
|
|
98
58
|
};
|
|
99
|
-
/** Keys Vitest and oxmutate own: they change by design while a file runs. */
|
|
100
59
|
const OWN_GLOBAL = /^(__vitest|__oxmutate__$|__VITEST|__vite)/;
|
|
101
|
-
/** globalThis, process.env and the built-in prototypes, as a map a later look can be compared with. */
|
|
102
60
|
function snapshot() {
|
|
103
61
|
const out = new Map();
|
|
104
62
|
const g = globalThis;
|
|
@@ -116,30 +74,20 @@ function snapshot() {
|
|
|
116
74
|
}
|
|
117
75
|
return out;
|
|
118
76
|
}
|
|
119
|
-
/** How many differing keys a `static-fallback` record names. */
|
|
120
77
|
const DIFF_KEYS = 8;
|
|
121
|
-
/** The keys whose presence or value differs between two snapshots (the first few, for a report). */
|
|
122
78
|
const snapshotDiff = (a, b) => [...new Set([...a.keys(), ...b.keys()])].filter((k) => !(a.has(k) && b.has(k) && Object.is(a.get(k), b.get(k)))).slice(0, DIFF_KEYS);
|
|
123
79
|
const sameSnapshot = (a, b) => snapshotDiff(a, b).length === 0;
|
|
124
|
-
/** The state before this worker evaluated the test file: the baseline every later look compares with. */
|
|
125
80
|
let pristine;
|
|
126
|
-
/** Set when a mutant left state behind; the rest of this context runs no mutants. */
|
|
127
81
|
let polluted = false;
|
|
128
|
-
/** `static` mode: the most mutants decided between two unarmed re-collections (staticwarm.qnt `grouped`). */
|
|
129
82
|
const STATIC_CANARY_MAX = 4;
|
|
130
|
-
/** Mutants between canaries; doubles while the canary stays clean, back to 1 when it fails. */
|
|
131
83
|
let canaryEvery = 1;
|
|
132
84
|
const MAX_CANARY_EVERY = 8;
|
|
133
|
-
/** How much of a test's error a `polluted` record keeps, and of a file's collection error. */
|
|
134
85
|
const POLLUTION_ERROR_CHARS = 300;
|
|
135
86
|
const FILE_ERROR_CHARS = 500;
|
|
136
87
|
function emit(record) {
|
|
137
88
|
if (resultsPath)
|
|
138
89
|
appendFileSync(resultsPath, `${JSON.stringify(record)}\n`);
|
|
139
90
|
}
|
|
140
|
-
// The suites whose hooks wrap `test`, outermost first. Vitest 5 leaves a top-level `describe`'s
|
|
141
|
-
// `suite` unset and keeps file-level hooks on `test.file`, so the walk adds the file itself: without
|
|
142
|
-
// it a file-level `beforeEach` reset never re-ran between mutants and every canary read as pollution.
|
|
143
91
|
function chain(test) {
|
|
144
92
|
const suites = [];
|
|
145
93
|
for (let s = test.suite; s; s = s.suite)
|
|
@@ -169,11 +117,6 @@ async function afterEach(test, context, cleanups) {
|
|
|
169
117
|
await cleanup();
|
|
170
118
|
}
|
|
171
119
|
const isHitLimit = (e) => e instanceof Error && /Hit count limit/.test(e.message);
|
|
172
|
-
/**
|
|
173
|
-
* Runs a file collected outside Vitest's own run (`static` mode): each suite's beforeAll/afterAll,
|
|
174
|
-
* each test with its beforeEach/afterEach chain, stopping at the first failure, which is all a
|
|
175
|
-
* mutant's verdict needs.
|
|
176
|
-
*/
|
|
177
120
|
async function runCollected(file, runnable) {
|
|
178
121
|
const failed = [];
|
|
179
122
|
let error;
|
|
@@ -219,7 +162,7 @@ async function runCollected(file, runnable) {
|
|
|
219
162
|
await runSuite(file);
|
|
220
163
|
}
|
|
221
164
|
catch (e) {
|
|
222
|
-
failed.push(file.id);
|
|
165
|
+
failed.push(file.id);
|
|
223
166
|
note(file, e);
|
|
224
167
|
}
|
|
225
168
|
return { failed, ran, ...(error !== undefined && { error }) };
|
|
@@ -231,11 +174,6 @@ function fullName(t) {
|
|
|
231
174
|
return parts.join(' > ');
|
|
232
175
|
}
|
|
233
176
|
export default class OxmutateRunner extends Base {
|
|
234
|
-
/**
|
|
235
|
-
* Import-time hits (static mutants) are attributed to the test file being collected, so a static
|
|
236
|
-
* mutant is later re-run against the files that load its module, not the whole suite (Stryker
|
|
237
|
-
* re-runs everything for each one).
|
|
238
|
-
*/
|
|
239
177
|
onCollectStart(file) {
|
|
240
178
|
if (mode === 'dry')
|
|
241
179
|
ns.currentTestId = `${FILE_KEY}${file.filepath ?? file.file?.filepath ?? ''}`;
|
|
@@ -253,7 +191,6 @@ export default class OxmutateRunner extends Base {
|
|
|
253
191
|
if (mode !== 'mutant') {
|
|
254
192
|
if (only && !only.has(test.id))
|
|
255
193
|
return;
|
|
256
|
-
// Only a wait inside a test body can be an idle stall; collection at low CPU is a slow machine.
|
|
257
194
|
markTest(true);
|
|
258
195
|
try {
|
|
259
196
|
await fn();
|
|
@@ -264,8 +201,6 @@ export default class OxmutateRunner extends Base {
|
|
|
264
201
|
return;
|
|
265
202
|
}
|
|
266
203
|
const context = test.context;
|
|
267
|
-
// Once a mutant has polluted this context, no further verdict from it can be trusted: the
|
|
268
|
-
// rest of the unit is left unrun, and the coordinator re-queues it into a fresh context.
|
|
269
204
|
const spec = specTests.has(test.id);
|
|
270
205
|
const done = spec ? decidedBySpec : decided;
|
|
271
206
|
const ids = polluted ? [] : (plan.tests[test.id] ?? []).filter((id) => !done.has(id));
|
|
@@ -283,8 +218,6 @@ export default class OxmutateRunner extends Base {
|
|
|
283
218
|
await teardown();
|
|
284
219
|
await setup();
|
|
285
220
|
};
|
|
286
|
-
// Mutants run since the last clean canary. The first of them started from a clean state, so its
|
|
287
|
-
// verdict stands whatever the next canary says; the rest are suspects if it fails.
|
|
288
221
|
let unchecked = [];
|
|
289
222
|
const canary = async () => {
|
|
290
223
|
if (!unchecked.length)
|
|
@@ -303,11 +236,6 @@ export default class OxmutateRunner extends Base {
|
|
|
303
236
|
return false;
|
|
304
237
|
}
|
|
305
238
|
};
|
|
306
|
-
// The mutant is armed through the test's own `beforeEach`, as Stryker arms it: the dry run credits
|
|
307
|
-
// code a hook runs to the test, so a mutant in a fixture builder or a seed is planned here, and with
|
|
308
|
-
// the hooks run unarmed it always survived, to be killed later by an isolated run that costs a
|
|
309
|
-
// fresh process (58 of 400 on a Mongo-backed suite, 2026-10-29). The previous run's `afterEach`
|
|
310
|
-
// runs first, unarmed: its cleanup belongs to that run.
|
|
311
239
|
const runArmed = async (id) => {
|
|
312
240
|
await teardown();
|
|
313
241
|
const t0 = performance.now();
|
|
@@ -333,13 +261,8 @@ export default class OxmutateRunner extends Base {
|
|
|
333
261
|
if (done.has(id))
|
|
334
262
|
continue;
|
|
335
263
|
if (progressPath)
|
|
336
|
-
writeFileSync(progressPath, `${id} ${String(
|
|
264
|
+
writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
|
|
337
265
|
let { status, ms } = await runArmed(id);
|
|
338
|
-
// A kill that follows unchecked runs may be one of their leftovers, which this run's own cleanup
|
|
339
|
-
// (a `finally`) then wipes, so the batch canary would pass and vouch for a wrong kill. Survivors
|
|
340
|
-
// are confirmed in isolation later; kills are checked here: a failing canary is pollution as
|
|
341
|
-
// usual (this verdict goes back with the unit), a passing one leaves the state clean to decide
|
|
342
|
-
// the mutant again from (.sdlc/intents/oxmutate/canary.qnt).
|
|
343
266
|
if (status !== 'Survived' && unchecked.length > 0) {
|
|
344
267
|
if (!(await canary()))
|
|
345
268
|
break;
|
|
@@ -349,10 +272,6 @@ export default class OxmutateRunner extends Base {
|
|
|
349
272
|
done.add(id);
|
|
350
273
|
emit({ type: 'mutant', id, test: test.id, status, ms, ...(spec ? { spec: true } : {}) });
|
|
351
274
|
unchecked.push(id);
|
|
352
|
-
// The canary: the same test, unmutated, must still pass. If it fails, a mutant left state
|
|
353
|
-
// behind (a cache, a registry, a fixture) that would decide the next verdicts. Run after every
|
|
354
|
-
// mutant at first, then every 2, 4, 8 while it stays clean: pollution is rare per test, and the
|
|
355
|
-
// canary was half of all executions (burgee/paratext, 2026-10-27).
|
|
356
275
|
if (unchecked.length >= canaryEvery && !(await canary()))
|
|
357
276
|
break;
|
|
358
277
|
}
|
|
@@ -370,11 +289,6 @@ export default class OxmutateRunner extends Base {
|
|
|
370
289
|
await cleanup();
|
|
371
290
|
}
|
|
372
291
|
}
|
|
373
|
-
/**
|
|
374
|
-
* The baseline is taken as the test file itself is imported (`source: 'collect'`), after setup
|
|
375
|
-
* files: those run before every collection, re-collections included, and write the same values
|
|
376
|
-
* each time (the family's colour setup sets TERM, COLORTERM and NO_COLOR), so they are not leaks.
|
|
377
|
-
*/
|
|
378
292
|
importFile(filepath, source) {
|
|
379
293
|
if (mode === 'static' && source === 'collect' && !pristine)
|
|
380
294
|
pristine = snapshot();
|
|
@@ -385,18 +299,8 @@ export default class OxmutateRunner extends Base {
|
|
|
385
299
|
await this.decideStatic(suite);
|
|
386
300
|
return super.onBeforeRunSuite?.(suite);
|
|
387
301
|
}
|
|
388
|
-
/**
|
|
389
|
-
* `static` mode (.sdlc/intents/oxmutate/staticwarm.qnt). Re-evaluating the project's modules in
|
|
390
|
-
* this worker costs ~0.5 ms where a fresh worker per (mutant, file) cost ~80 ms (burgee/paratext,
|
|
391
|
-
* 2026-10-30). What a registry reset leaves behind is guarded three ways: the file's own unarmed
|
|
392
|
-
* collection must leave globalThis, process.env and the built-in prototypes as they were; so must
|
|
393
|
-
* each mutant; and an unarmed re-collection after each mutant must pass. Anything else hands the
|
|
394
|
-
* rest of the batch back to the fresh-worker path.
|
|
395
|
-
*/
|
|
396
302
|
async decideStatic(suite) {
|
|
397
303
|
const filepath = suite.filepath;
|
|
398
|
-
// The tests this run will execute, after Vitest's own filters (a name pattern, `.only`, `.skip`):
|
|
399
|
-
// a re-collected file has the same test ids, and runs exactly these.
|
|
400
304
|
const runnable = new Set();
|
|
401
305
|
const visit = (t) => {
|
|
402
306
|
if (t.type === 'test' && t.mode === 'run')
|
|
@@ -416,13 +320,11 @@ export default class OxmutateRunner extends Base {
|
|
|
416
320
|
if (atCollect.length)
|
|
417
321
|
return fallback('the file changed globals while it was collected', atCollect);
|
|
418
322
|
const vitest = (await fromProject('vitest'));
|
|
419
|
-
// `collectTests` is exported for Vitest's browser runner from an internal subpath: if a Vitest
|
|
420
|
-
// moves or drops it, the import fails and the fresh-worker path decides, as on Vitest 4.
|
|
421
323
|
const browser = (await fromProject('vitest/internal/browser').catch(() => ({})));
|
|
422
324
|
const { collectTests } = browser;
|
|
423
325
|
const { vi } = vitest;
|
|
424
326
|
if (!collectTests || !vi)
|
|
425
|
-
return fallback('this Vitest has no public collect');
|
|
327
|
+
return fallback('this Vitest has no public collect');
|
|
426
328
|
const evaluate = async (armed) => {
|
|
427
329
|
ns.activeMutant = armed;
|
|
428
330
|
ns.hitCount = armed ? 0 : undefined;
|
|
@@ -439,12 +341,6 @@ export default class OxmutateRunner extends Base {
|
|
|
439
341
|
ns.hitLimit = undefined;
|
|
440
342
|
}
|
|
441
343
|
};
|
|
442
|
-
// Verdicts since the last clean unarmed re-collection are held, not emitted: the re-collection runs
|
|
443
|
-
// after the first mutant, then after every 2, then every STATIC_CANARY_MAX while it stays clean, and
|
|
444
|
-
// one that fails sends every held mutant back with the rest. Each mutant's own snapshot diff still
|
|
445
|
-
// runs at once. The re-collection runs the file's tests unarmed, and after every mutant it was half
|
|
446
|
-
// of the live static phase, 6,713 s of compat-oracle's 11,193 s weekly run (sc8-profile.md,
|
|
447
|
-
// staticwarm.qnt `grouped`).
|
|
448
344
|
let held = [];
|
|
449
345
|
let every = 1;
|
|
450
346
|
const back = (reason, keys = []) => {
|
|
@@ -455,7 +351,7 @@ export default class OxmutateRunner extends Base {
|
|
|
455
351
|
while (rest.length) {
|
|
456
352
|
const id = rest.shift();
|
|
457
353
|
if (progressPath)
|
|
458
|
-
writeFileSync(progressPath, `${id} ${String(
|
|
354
|
+
writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
|
|
459
355
|
held.push({ id, verdict: await evaluate(id) });
|
|
460
356
|
const left = snapshotDiff(before, snapshot());
|
|
461
357
|
if (left.length)
|
|
@@ -478,7 +374,6 @@ export default class OxmutateRunner extends Base {
|
|
|
478
374
|
writeFileSync(progressPath, '');
|
|
479
375
|
vi.resetModules();
|
|
480
376
|
}
|
|
481
|
-
/** Reset per-try state (snapshot counters, assertion counts) the way Vitest does for a retry. */
|
|
482
377
|
async retryReset(test, attempt) {
|
|
483
378
|
await super.onBeforeTryTask?.(test, { retry: attempt, repeats: 0 });
|
|
484
379
|
}
|
|
@@ -495,7 +390,6 @@ export default class OxmutateRunner extends Base {
|
|
|
495
390
|
for (const f of files) {
|
|
496
391
|
const filepath = (f.file?.filepath ?? f.filepath);
|
|
497
392
|
visit(f, filepath);
|
|
498
|
-
// A file that fails to collect has no failing test to show for it: report the file.
|
|
499
393
|
const result = f.result;
|
|
500
394
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
501
395
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
@@ -515,8 +409,6 @@ export default class OxmutateRunner extends Base {
|
|
|
515
409
|
};
|
|
516
410
|
for (const f of files) {
|
|
517
411
|
visit(f);
|
|
518
|
-
// A file that failed with no failing test (it threw at import, or in a hook) failed under
|
|
519
|
-
// the mutant: a kill, as Stryker scores it. Its id stands in for the test that failed.
|
|
520
412
|
if (f.result?.state === 'fail' && !failed.length)
|
|
521
413
|
failed.push(f.id);
|
|
522
414
|
}
|
|
@@ -526,4 +418,3 @@ export default class OxmutateRunner extends Base {
|
|
|
526
418
|
return super.onAfterRunFiles?.(files);
|
|
527
419
|
}
|
|
528
420
|
}
|
|
529
|
-
//# sourceMappingURL=runner.js.map
|
package/dist/runtime.js
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The contract between instrumented code and the runner. The native core emits calls to
|
|
3
|
-
* `oxmAct(id)` and `oxmCov(...ids)` that read and write this namespace on `globalThis`
|
|
4
|
-
* (`native/src/instrument.rs`, `NAMESPACE`). Changing a field here without changing the emitter
|
|
5
|
-
* breaks every instrumented file.
|
|
6
|
-
*/
|
|
7
1
|
export const NAMESPACE = '__oxmutate__';
|
|
8
|
-
/**
|
|
9
|
-
* Coverage key prefix for hits that happen while a test file is being collected (imports): the
|
|
10
|
-
* runner writes `file:<path>` keys, and the coordinator reads them back as static mutants' loaders.
|
|
11
|
-
*/
|
|
12
2
|
export const FILE_KEY = 'file:';
|
|
13
|
-
/** Arms one mutant from process start, for static mutants and isolated re-runs. */
|
|
14
3
|
export const ACTIVE_MUTANT_ENV = '__OXMUTATE_ACTIVE_MUTANT__';
|
|
15
|
-
// ── The process seam ────────────────────────────────────────────────────────────────────────────
|
|
16
|
-
// The one file in the package that names the process (burgee's `process-reference-lock.test.ts`):
|
|
17
|
-
// the coordinator, the servers, the runner and the CLI read it here, so a test can see every
|
|
18
|
-
// place the package touches it. `env` is the live object: a write here is a write to the process.
|
|
19
4
|
export const env = process.env;
|
|
20
5
|
export const argv = process.argv;
|
|
21
6
|
export const pid = process.pid;
|
|
22
7
|
export const cwd = () => process.cwd();
|
|
23
8
|
export const stderr = process.stderr;
|
|
24
9
|
export const exit = (code) => process.exit(code);
|
|
25
|
-
//# sourceMappingURL=runtime.js.map
|