oxmutate 1.1.0 → 1.2.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 +3 -2
- 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 +10 -43
- package/dist/cli.js +0 -10
- package/dist/command-runner.js +0 -27
- package/dist/config.js +0 -29
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.js +25 -135
- package/dist/directives.js +38 -0
- package/dist/files.js +0 -10
- package/dist/frozen.js +0 -15
- package/dist/heartbeat.js +62 -0
- 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 +1 -29
- package/dist/resolve.js +0 -10
- package/dist/runner.js +33 -113
- package/dist/runtime.js +1 -16
- package/dist/scheduler.js +38 -26
- 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/index.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oxmutate's programmatic API. The CLI (`oxmutate run`) is this, declared on burgee.
|
|
3
|
-
*/
|
|
4
1
|
export { loadOptions } from './config.js';
|
|
5
2
|
export { InstrumentFault, run } from './coordinator.js';
|
|
6
3
|
export { changedLines, parseShard, shardOf } from './lanes.js';
|
|
7
4
|
export { native } from './native.js';
|
|
8
5
|
export { sarifReport, strykerReport, textSummary, VERSION } from './reports.js';
|
|
9
|
-
//# sourceMappingURL=index.js.map
|
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;
|
|
@@ -142,7 +125,7 @@ export function textSummary(r, opts = {}) {
|
|
|
142
125
|
`oxmutate ${VERSION}: ${String(r.mutants.length)} mutants in ${String(s.files)} files, ${(s.wallMs / MS_PER_S).toFixed(1)} s`,
|
|
143
126
|
` mutation score ${r.score.mutationScore.toFixed(2)}% (covered ${r.score.covered.toFixed(2)}%)`,
|
|
144
127
|
` killed ${String(count('Killed'))} · timeout ${String(count('Timeout'))} · survived ${String(count('Survived'))} · no coverage ${String(count('NoCoverage'))} · ignored ${String(count('Ignored'))} · errors ${String(count('RuntimeError') + count('CompileError'))}`,
|
|
145
|
-
` instrument ${s.instrumentMs.toFixed(0)} ms · dry run ${(s.dryRunMs / MS_PER_S).toFixed(1)} s · mutants ${(s.mutationMs / MS_PER_S).toFixed(1)} s · isolated ${(s.isolatedMs / MS_PER_S).toFixed(1)} s`,
|
|
128
|
+
` instrument ${s.instrumentMs.toFixed(0)} ms · dry run ${(s.dryRunMs / MS_PER_S).toFixed(1)} s · mutants ${(s.mutationMs / MS_PER_S).toFixed(1)} s · static ${(s.staticMs / MS_PER_S).toFixed(1)} s · isolated ${(s.isolatedMs / MS_PER_S).toFixed(1)} s`,
|
|
146
129
|
];
|
|
147
130
|
const corrected = corrections(s);
|
|
148
131
|
if (corrected.length)
|
|
@@ -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,38 +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';
|
|
4
|
+
import { BEAT_ENV, markTest, realNow, startHeartbeat } from './heartbeat.js';
|
|
28
5
|
import { resolveEsm } from './resolve.js';
|
|
29
6
|
import { ACTIVE_MUTANT_ENV, FILE_KEY, NAMESPACE, cwd, env } from './runtime.js';
|
|
30
|
-
/**
|
|
31
|
-
* The project's own Vitest, never the one beside this file: two Vitest instances in one worker
|
|
32
|
-
* disagree about the current test, the snapshot state and `expect`.
|
|
33
|
-
*/
|
|
34
7
|
const fromProject = async (specifier, dir = cwd()) => import(resolveEsm(specifier, dir));
|
|
35
|
-
/** The family's refusal shape for a missing optional peer (as `controlroom/ink`'s): a code and a fix. */
|
|
36
8
|
export class PeerMissingError extends Error {
|
|
37
9
|
code = 'E_PEER_MISSING';
|
|
38
10
|
fix = "npm install vitest — oxmutate runs the project's own Vitest, so the project brings it";
|
|
@@ -45,7 +17,6 @@ const missing = (cause) => {
|
|
|
45
17
|
const code = cause?.code;
|
|
46
18
|
return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
47
19
|
};
|
|
48
|
-
/** Vitest 5 exports `TestRunner` from `vitest`; Vitest 4 from `vitest/runners` + `@vitest/runner`. */
|
|
49
20
|
async function loadBase() {
|
|
50
21
|
const vitest = (await fromProject('vitest').catch((cause) => {
|
|
51
22
|
if (missing(cause))
|
|
@@ -65,38 +36,27 @@ const { Base, statics } = await loadBase();
|
|
|
65
36
|
const mode = env['OXMUTATE_MODE'] ?? 'dry';
|
|
66
37
|
const resultsPath = env['OXMUTATE_RESULTS'];
|
|
67
38
|
const progressPath = env['OXMUTATE_PROGRESS'];
|
|
39
|
+
startHeartbeat(env[BEAT_ENV]);
|
|
68
40
|
const ns = (globalThis[NAMESPACE] ??= {});
|
|
69
41
|
ns.mutantCoverage ??= { static: {}, perTest: {} };
|
|
70
42
|
const plan = mode === 'mutant' && env['OXMUTATE_PLAN'] ? JSON.parse(readFileSync(env['OXMUTATE_PLAN'], 'utf8')) : { tests: {}, hitLimits: {}, skip: [] };
|
|
71
43
|
const decided = new Set(plan.skip);
|
|
72
|
-
/** The replay tests' own bail: a mutant the tests killed is still replayed, and the reverse. */
|
|
73
44
|
const specTests = new Set(plan.specTests ?? []);
|
|
74
45
|
const decidedBySpec = new Set();
|
|
75
|
-
/** `isolated`: when set, only these tests run (the mutant's covering tests); the rest pass untouched. */
|
|
76
46
|
const only = mode === 'isolated' && env['OXMUTATE_ONLY_TESTS'] ? new Set(JSON.parse(env['OXMUTATE_ONLY_TESTS'])) : undefined;
|
|
77
|
-
/** `static`: the static mutants this file run decides in the live worker. */
|
|
78
47
|
const staticIds = mode === 'static' && env['OXMUTATE_STATIC'] ? JSON.parse(env['OXMUTATE_STATIC']) : [];
|
|
79
|
-
/** `static`: each mutant's hit-count limit. */
|
|
80
48
|
const staticLimits = mode === 'static' && env['OXMUTATE_HIT_LIMITS'] ? JSON.parse(env['OXMUTATE_HIT_LIMITS']) : {};
|
|
81
|
-
// `isolated`: the mutant is armed from process start, so its hit counter is too. A loop it causes
|
|
82
|
-
// stops at the limit, as in warm runs, instead of holding the worker until the watchdog's budget:
|
|
83
|
-
// on oxmutate's own suite 17 runaways left the isolated phase idle 140 of 186 s (2026-10-30).
|
|
84
49
|
if (mode === 'isolated' && env['OXMUTATE_HIT_LIMIT']) {
|
|
85
50
|
ns.hitCount = 0;
|
|
86
51
|
ns.hitLimit = Number(env['OXMUTATE_HIT_LIMIT']);
|
|
87
52
|
}
|
|
88
|
-
/** Did the armed mutant's switch pass its limit, i.e. was this run stopped as a runaway? */
|
|
89
53
|
const ranAway = () => ns.hitCount !== undefined && ns.hitLimit !== undefined && ns.hitCount > ns.hitLimit;
|
|
90
|
-
// ── `static` mode: what a registry reset does not reset ─────────────────────────────────────────
|
|
91
|
-
/** Built-ins a module may patch in place; their own methods are compared by identity. */
|
|
92
54
|
const PROTOTYPES = {
|
|
93
55
|
Object: Object.prototype, Array: Array.prototype, String: String.prototype, Number: Number.prototype,
|
|
94
56
|
Boolean: Boolean.prototype, Function: Function.prototype, Promise: Promise.prototype, Map: Map.prototype,
|
|
95
57
|
Set: Set.prototype, RegExp: RegExp.prototype, Date: Date.prototype, Error: Error.prototype,
|
|
96
58
|
};
|
|
97
|
-
/** Keys Vitest and oxmutate own: they change by design while a file runs. */
|
|
98
59
|
const OWN_GLOBAL = /^(__vitest|__oxmutate__$|__VITEST|__vite)/;
|
|
99
|
-
/** globalThis, process.env and the built-in prototypes, as a map a later look can be compared with. */
|
|
100
60
|
function snapshot() {
|
|
101
61
|
const out = new Map();
|
|
102
62
|
const g = globalThis;
|
|
@@ -114,28 +74,20 @@ function snapshot() {
|
|
|
114
74
|
}
|
|
115
75
|
return out;
|
|
116
76
|
}
|
|
117
|
-
/** How many differing keys a `static-fallback` record names. */
|
|
118
77
|
const DIFF_KEYS = 8;
|
|
119
|
-
/** The keys whose presence or value differs between two snapshots (the first few, for a report). */
|
|
120
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);
|
|
121
79
|
const sameSnapshot = (a, b) => snapshotDiff(a, b).length === 0;
|
|
122
|
-
/** The state before this worker evaluated the test file: the baseline every later look compares with. */
|
|
123
80
|
let pristine;
|
|
124
|
-
/** Set when a mutant left state behind; the rest of this context runs no mutants. */
|
|
125
81
|
let polluted = false;
|
|
126
|
-
|
|
82
|
+
const STATIC_CANARY_MAX = 4;
|
|
127
83
|
let canaryEvery = 1;
|
|
128
84
|
const MAX_CANARY_EVERY = 8;
|
|
129
|
-
/** How much of a test's error a `polluted` record keeps, and of a file's collection error. */
|
|
130
85
|
const POLLUTION_ERROR_CHARS = 300;
|
|
131
86
|
const FILE_ERROR_CHARS = 500;
|
|
132
87
|
function emit(record) {
|
|
133
88
|
if (resultsPath)
|
|
134
89
|
appendFileSync(resultsPath, `${JSON.stringify(record)}\n`);
|
|
135
90
|
}
|
|
136
|
-
// The suites whose hooks wrap `test`, outermost first. Vitest 5 leaves a top-level `describe`'s
|
|
137
|
-
// `suite` unset and keeps file-level hooks on `test.file`, so the walk adds the file itself: without
|
|
138
|
-
// it a file-level `beforeEach` reset never re-ran between mutants and every canary read as pollution.
|
|
139
91
|
function chain(test) {
|
|
140
92
|
const suites = [];
|
|
141
93
|
for (let s = test.suite; s; s = s.suite)
|
|
@@ -165,11 +117,6 @@ async function afterEach(test, context, cleanups) {
|
|
|
165
117
|
await cleanup();
|
|
166
118
|
}
|
|
167
119
|
const isHitLimit = (e) => e instanceof Error && /Hit count limit/.test(e.message);
|
|
168
|
-
/**
|
|
169
|
-
* Runs a file collected outside Vitest's own run (`static` mode): each suite's beforeAll/afterAll,
|
|
170
|
-
* each test with its beforeEach/afterEach chain, stopping at the first failure, which is all a
|
|
171
|
-
* mutant's verdict needs.
|
|
172
|
-
*/
|
|
173
120
|
async function runCollected(file, runnable) {
|
|
174
121
|
const failed = [];
|
|
175
122
|
let error;
|
|
@@ -215,7 +162,7 @@ async function runCollected(file, runnable) {
|
|
|
215
162
|
await runSuite(file);
|
|
216
163
|
}
|
|
217
164
|
catch (e) {
|
|
218
|
-
failed.push(file.id);
|
|
165
|
+
failed.push(file.id);
|
|
219
166
|
note(file, e);
|
|
220
167
|
}
|
|
221
168
|
return { failed, ran, ...(error !== undefined && { error }) };
|
|
@@ -227,11 +174,6 @@ function fullName(t) {
|
|
|
227
174
|
return parts.join(' > ');
|
|
228
175
|
}
|
|
229
176
|
export default class OxmutateRunner extends Base {
|
|
230
|
-
/**
|
|
231
|
-
* Import-time hits (static mutants) are attributed to the test file being collected, so a static
|
|
232
|
-
* mutant is later re-run against the files that load its module, not the whole suite (Stryker
|
|
233
|
-
* re-runs everything for each one).
|
|
234
|
-
*/
|
|
235
177
|
onCollectStart(file) {
|
|
236
178
|
if (mode === 'dry')
|
|
237
179
|
ns.currentTestId = `${FILE_KEY}${file.filepath ?? file.file?.filepath ?? ''}`;
|
|
@@ -249,12 +191,16 @@ export default class OxmutateRunner extends Base {
|
|
|
249
191
|
if (mode !== 'mutant') {
|
|
250
192
|
if (only && !only.has(test.id))
|
|
251
193
|
return;
|
|
252
|
-
|
|
194
|
+
markTest(true);
|
|
195
|
+
try {
|
|
196
|
+
await fn();
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
markTest(false);
|
|
200
|
+
}
|
|
253
201
|
return;
|
|
254
202
|
}
|
|
255
203
|
const context = test.context;
|
|
256
|
-
// Once a mutant has polluted this context, no further verdict from it can be trusted: the
|
|
257
|
-
// rest of the unit is left unrun, and the coordinator re-queues it into a fresh context.
|
|
258
204
|
const spec = specTests.has(test.id);
|
|
259
205
|
const done = spec ? decidedBySpec : decided;
|
|
260
206
|
const ids = polluted ? [] : (plan.tests[test.id] ?? []).filter((id) => !done.has(id));
|
|
@@ -272,8 +218,6 @@ export default class OxmutateRunner extends Base {
|
|
|
272
218
|
await teardown();
|
|
273
219
|
await setup();
|
|
274
220
|
};
|
|
275
|
-
// Mutants run since the last clean canary. The first of them started from a clean state, so its
|
|
276
|
-
// verdict stands whatever the next canary says; the rest are suspects if it fails.
|
|
277
221
|
let unchecked = [];
|
|
278
222
|
const canary = async () => {
|
|
279
223
|
if (!unchecked.length)
|
|
@@ -292,11 +236,6 @@ export default class OxmutateRunner extends Base {
|
|
|
292
236
|
return false;
|
|
293
237
|
}
|
|
294
238
|
};
|
|
295
|
-
// The mutant is armed through the test's own `beforeEach`, as Stryker arms it: the dry run credits
|
|
296
|
-
// code a hook runs to the test, so a mutant in a fixture builder or a seed is planned here, and with
|
|
297
|
-
// the hooks run unarmed it always survived, to be killed later by an isolated run that costs a
|
|
298
|
-
// fresh process (58 of 400 on a Mongo-backed suite, 2026-10-29). The previous run's `afterEach`
|
|
299
|
-
// runs first, unarmed: its cleanup belongs to that run.
|
|
300
239
|
const runArmed = async (id) => {
|
|
301
240
|
await teardown();
|
|
302
241
|
const t0 = performance.now();
|
|
@@ -322,13 +261,8 @@ export default class OxmutateRunner extends Base {
|
|
|
322
261
|
if (done.has(id))
|
|
323
262
|
continue;
|
|
324
263
|
if (progressPath)
|
|
325
|
-
writeFileSync(progressPath, `${id} ${String(
|
|
264
|
+
writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
|
|
326
265
|
let { status, ms } = await runArmed(id);
|
|
327
|
-
// A kill that follows unchecked runs may be one of their leftovers, which this run's own cleanup
|
|
328
|
-
// (a `finally`) then wipes, so the batch canary would pass and vouch for a wrong kill. Survivors
|
|
329
|
-
// are confirmed in isolation later; kills are checked here: a failing canary is pollution as
|
|
330
|
-
// usual (this verdict goes back with the unit), a passing one leaves the state clean to decide
|
|
331
|
-
// the mutant again from (.sdlc/intents/oxmutate/canary.qnt).
|
|
332
266
|
if (status !== 'Survived' && unchecked.length > 0) {
|
|
333
267
|
if (!(await canary()))
|
|
334
268
|
break;
|
|
@@ -338,10 +272,6 @@ export default class OxmutateRunner extends Base {
|
|
|
338
272
|
done.add(id);
|
|
339
273
|
emit({ type: 'mutant', id, test: test.id, status, ms, ...(spec ? { spec: true } : {}) });
|
|
340
274
|
unchecked.push(id);
|
|
341
|
-
// The canary: the same test, unmutated, must still pass. If it fails, a mutant left state
|
|
342
|
-
// behind (a cache, a registry, a fixture) that would decide the next verdicts. Run after every
|
|
343
|
-
// mutant at first, then every 2, 4, 8 while it stays clean: pollution is rare per test, and the
|
|
344
|
-
// canary was half of all executions (burgee/paratext, 2026-10-27).
|
|
345
275
|
if (unchecked.length >= canaryEvery && !(await canary()))
|
|
346
276
|
break;
|
|
347
277
|
}
|
|
@@ -359,11 +289,6 @@ export default class OxmutateRunner extends Base {
|
|
|
359
289
|
await cleanup();
|
|
360
290
|
}
|
|
361
291
|
}
|
|
362
|
-
/**
|
|
363
|
-
* The baseline is taken as the test file itself is imported (`source: 'collect'`), after setup
|
|
364
|
-
* files: those run before every collection, re-collections included, and write the same values
|
|
365
|
-
* each time (the family's colour setup sets TERM, COLORTERM and NO_COLOR), so they are not leaks.
|
|
366
|
-
*/
|
|
367
292
|
importFile(filepath, source) {
|
|
368
293
|
if (mode === 'static' && source === 'collect' && !pristine)
|
|
369
294
|
pristine = snapshot();
|
|
@@ -374,18 +299,8 @@ export default class OxmutateRunner extends Base {
|
|
|
374
299
|
await this.decideStatic(suite);
|
|
375
300
|
return super.onBeforeRunSuite?.(suite);
|
|
376
301
|
}
|
|
377
|
-
/**
|
|
378
|
-
* `static` mode (.sdlc/intents/oxmutate/staticwarm.qnt). Re-evaluating the project's modules in
|
|
379
|
-
* this worker costs ~0.5 ms where a fresh worker per (mutant, file) cost ~80 ms (burgee/paratext,
|
|
380
|
-
* 2026-10-30). What a registry reset leaves behind is guarded three ways: the file's own unarmed
|
|
381
|
-
* collection must leave globalThis, process.env and the built-in prototypes as they were; so must
|
|
382
|
-
* each mutant; and an unarmed re-collection after each mutant must pass. Anything else hands the
|
|
383
|
-
* rest of the batch back to the fresh-worker path.
|
|
384
|
-
*/
|
|
385
302
|
async decideStatic(suite) {
|
|
386
303
|
const filepath = suite.filepath;
|
|
387
|
-
// The tests this run will execute, after Vitest's own filters (a name pattern, `.only`, `.skip`):
|
|
388
|
-
// a re-collected file has the same test ids, and runs exactly these.
|
|
389
304
|
const runnable = new Set();
|
|
390
305
|
const visit = (t) => {
|
|
391
306
|
if (t.type === 'test' && t.mode === 'run')
|
|
@@ -405,13 +320,11 @@ export default class OxmutateRunner extends Base {
|
|
|
405
320
|
if (atCollect.length)
|
|
406
321
|
return fallback('the file changed globals while it was collected', atCollect);
|
|
407
322
|
const vitest = (await fromProject('vitest'));
|
|
408
|
-
// `collectTests` is exported for Vitest's browser runner from an internal subpath: if a Vitest
|
|
409
|
-
// moves or drops it, the import fails and the fresh-worker path decides, as on Vitest 4.
|
|
410
323
|
const browser = (await fromProject('vitest/internal/browser').catch(() => ({})));
|
|
411
324
|
const { collectTests } = browser;
|
|
412
325
|
const { vi } = vitest;
|
|
413
326
|
if (!collectTests || !vi)
|
|
414
|
-
return fallback('this Vitest has no public collect');
|
|
327
|
+
return fallback('this Vitest has no public collect');
|
|
415
328
|
const evaluate = async (armed) => {
|
|
416
329
|
ns.activeMutant = armed;
|
|
417
330
|
ns.hitCount = armed ? 0 : undefined;
|
|
@@ -428,28 +341,39 @@ export default class OxmutateRunner extends Base {
|
|
|
428
341
|
ns.hitLimit = undefined;
|
|
429
342
|
}
|
|
430
343
|
};
|
|
344
|
+
let held = [];
|
|
345
|
+
let every = 1;
|
|
346
|
+
const back = (reason, keys = []) => {
|
|
347
|
+
rest.unshift(...held.map((h) => h.id));
|
|
348
|
+
held = [];
|
|
349
|
+
fallback(reason, keys);
|
|
350
|
+
};
|
|
431
351
|
while (rest.length) {
|
|
432
|
-
const id = rest
|
|
352
|
+
const id = rest.shift();
|
|
433
353
|
if (progressPath)
|
|
434
|
-
writeFileSync(progressPath, `${id} ${String(
|
|
435
|
-
|
|
354
|
+
writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
|
|
355
|
+
held.push({ id, verdict: await evaluate(id) });
|
|
436
356
|
const left = snapshotDiff(before, snapshot());
|
|
437
357
|
if (left.length)
|
|
438
|
-
return
|
|
358
|
+
return back('the mutant left a trace', left);
|
|
359
|
+
if (held.length < every && rest.length)
|
|
360
|
+
continue;
|
|
439
361
|
const unarmed = await evaluate(undefined);
|
|
362
|
+
emit({ type: 'recollection' });
|
|
440
363
|
if (unarmed.failed.length > 0)
|
|
441
|
-
return
|
|
364
|
+
return back('the unarmed re-collection failed', unarmed.error ? [unarmed.error] : []);
|
|
442
365
|
const after = snapshotDiff(before, snapshot());
|
|
443
366
|
if (after.length)
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
|
|
367
|
+
return back('the unarmed re-collection left a trace', after);
|
|
368
|
+
for (const { id: done, verdict } of held)
|
|
369
|
+
emit({ type: 'isolated', id: done, killed: verdict.failed.length > 0 && !verdict.timeout, failed: verdict.failed, ran: verdict.ran, ...(verdict.timeout && { timeout: true }) });
|
|
370
|
+
held = [];
|
|
371
|
+
every = Math.min(STATIC_CANARY_MAX, every * 2);
|
|
447
372
|
}
|
|
448
373
|
if (progressPath)
|
|
449
374
|
writeFileSync(progressPath, '');
|
|
450
375
|
vi.resetModules();
|
|
451
376
|
}
|
|
452
|
-
/** Reset per-try state (snapshot counters, assertion counts) the way Vitest does for a retry. */
|
|
453
377
|
async retryReset(test, attempt) {
|
|
454
378
|
await super.onBeforeTryTask?.(test, { retry: attempt, repeats: 0 });
|
|
455
379
|
}
|
|
@@ -466,7 +390,6 @@ export default class OxmutateRunner extends Base {
|
|
|
466
390
|
for (const f of files) {
|
|
467
391
|
const filepath = (f.file?.filepath ?? f.filepath);
|
|
468
392
|
visit(f, filepath);
|
|
469
|
-
// A file that fails to collect has no failing test to show for it: report the file.
|
|
470
393
|
const result = f.result;
|
|
471
394
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
472
395
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
@@ -486,8 +409,6 @@ export default class OxmutateRunner extends Base {
|
|
|
486
409
|
};
|
|
487
410
|
for (const f of files) {
|
|
488
411
|
visit(f);
|
|
489
|
-
// A file that failed with no failing test (it threw at import, or in a hook) failed under
|
|
490
|
-
// the mutant: a kill, as Stryker scores it. Its id stands in for the test that failed.
|
|
491
412
|
if (f.result?.state === 'fail' && !failed.length)
|
|
492
413
|
failed.push(f.id);
|
|
493
414
|
}
|
|
@@ -497,4 +418,3 @@ export default class OxmutateRunner extends Base {
|
|
|
497
418
|
return super.onAfterRunFiles?.(files);
|
|
498
419
|
}
|
|
499
420
|
}
|
|
500
|
-
//# sourceMappingURL=runner.js.map
|