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
|
Binary file
|
package/dist/bin.js
CHANGED
package/dist/checker.js
CHANGED
|
@@ -1,46 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The type checker (Stryker's `checkers: ['typescript']`): a surviving mutant that does not
|
|
3
|
-
* type-check is a CompileError, not a survivor. A survivor is what someone writes a test for, and
|
|
4
|
-
* a test cannot kill a program the compiler refuses; Stryker's TypeScript checker exists for the
|
|
5
|
-
* same reason.
|
|
6
|
-
*
|
|
7
|
-
* It checks survivors only, where Stryker checks every mutant before running it. A killed mutant
|
|
8
|
-
* that would not have compiled changes nothing anyone acts on, and a full project type check per
|
|
9
|
-
* mutant is the costliest step there is. The difference: such a mutant is Killed here where Stryker
|
|
10
|
-
* would score it CompileError, which can move a score that enables the checker by its share.
|
|
11
|
-
*
|
|
12
|
-
* The project's own compiler, `tsgo` when installed and `tsc` otherwise, over its own tsconfig.
|
|
13
|
-
* Survivors are checked in batches with at most one mutant per file, and each error is laid at the
|
|
14
|
-
* mutant in the file it names. An error in a file no mutant of the batch touched (a type that
|
|
15
|
-
* flowed across files) sends that batch back one mutant at a time, so no verdict is a guess.
|
|
16
|
-
*/
|
|
17
1
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
2
|
import path from 'node:path';
|
|
19
3
|
import { ambientRuntime, isWindows, run as spawn } from 'bellpull';
|
|
20
4
|
import { makeSandbox } from './command-runner.js';
|
|
21
5
|
const OUTPUT_TAIL = 1500;
|
|
22
|
-
/**
|
|
23
|
-
* `src/a.ts(3,7): error TS2322: …` — tsc's and tsgo's `--pretty false` shape. No anchors: the lazy
|
|
24
|
-
* first group already makes the leftmost match start the line, and the greedy last one ends it.
|
|
25
|
-
*/
|
|
26
6
|
const DIAGNOSTIC = /(.+?)\((\d+),(\d+)\): error (TS\d+: .*)/;
|
|
27
|
-
/** The source with one mutant applied. Columns are UTF-16 and 1-based, as JavaScript counts. */
|
|
28
7
|
export function applyMutant(source, m) {
|
|
29
8
|
const starts = [0];
|
|
30
9
|
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1))
|
|
31
10
|
starts.push(i + 1);
|
|
32
|
-
// Stryker disable next-line NumericLiteral: `starts` holds every line the checker reports, so the fallback never runs
|
|
33
11
|
const at = (p) => (starts[p.line - 1] ?? 0) + p.column - 1;
|
|
34
12
|
return source.slice(0, at(m.start)) + m.replacement + source.slice(at(m.end));
|
|
35
13
|
}
|
|
36
|
-
/** Errors per file (repository-relative), from the compiler's output. */
|
|
37
14
|
export function parseDiagnostics(output, cwd) {
|
|
38
15
|
const errors = new Map();
|
|
39
16
|
for (const line of output.split('\n')) {
|
|
40
17
|
const hit = DIAGNOSTIC.exec(line.trim());
|
|
41
18
|
if (!hit)
|
|
42
19
|
continue;
|
|
43
|
-
// Stryker disable next-line StringLiteral: all four groups take part in every match, so the defaults are never used; they are there for the type
|
|
44
20
|
const [, name = '', lineNo = '', , message = ''] = hit;
|
|
45
21
|
const file = path.relative(cwd, path.resolve(cwd, name)).split(path.sep).join('/');
|
|
46
22
|
if (!errors.has(file))
|
|
@@ -48,7 +24,6 @@ export function parseDiagnostics(output, cwd) {
|
|
|
48
24
|
}
|
|
49
25
|
return errors;
|
|
50
26
|
}
|
|
51
|
-
/** Batches of survivors with at most one per file, so each error names its mutant. */
|
|
52
27
|
export function batches(items) {
|
|
53
28
|
const out = [];
|
|
54
29
|
for (const m of items) {
|
|
@@ -60,7 +35,6 @@ export function batches(items) {
|
|
|
60
35
|
}
|
|
61
36
|
return out;
|
|
62
37
|
}
|
|
63
|
-
/** npm's .bin entry for tsgo: `tsgo.cmd` on Windows, where the extensionless file beside it is a shell script. */
|
|
64
38
|
export const tsgoBin = (windows) => (windows ? 'tsgo.cmd' : 'tsgo');
|
|
65
39
|
function compiler(o) {
|
|
66
40
|
if (o.command)
|
|
@@ -68,12 +42,10 @@ function compiler(o) {
|
|
|
68
42
|
const tsgo = path.join(o.root, 'node_modules', '.bin', tsgoBin(isWindows(ambientRuntime())));
|
|
69
43
|
return existsSync(tsgo) ? [tsgo] : ['npx', 'tsc'];
|
|
70
44
|
}
|
|
71
|
-
/** The undetected mutants that do not type-check, each with the compiler's first error in its file. */
|
|
72
45
|
export async function checkSurvivors(o, survivors) {
|
|
73
46
|
const failing = new Map();
|
|
74
47
|
if (survivors.length === 0)
|
|
75
48
|
return failing;
|
|
76
|
-
// Stryker disable next-line StringLiteral: compiler() never returns an empty list, so the default is never taken; it is there for the type
|
|
77
49
|
const [command = '', ...prefix] = compiler(o);
|
|
78
50
|
const args = [...prefix, '--noEmit', '--pretty', 'false', '-p', o.tsconfig];
|
|
79
51
|
const sandboxes = Array.from({ length: Math.max(1, Math.min(o.concurrency, survivors.length)) }, () => makeSandbox(o.root));
|
|
@@ -82,7 +54,6 @@ export async function checkSurvivors(o, survivors) {
|
|
|
82
54
|
const r = await spawn(command, args, { cwd: sandbox, runtime: ambientRuntime() });
|
|
83
55
|
return { ok: r.ok, errors: parseDiagnostics(`${r.stdout}\n${r.stderr}`, sandbox), output: `${r.stdout}${r.stderr}` };
|
|
84
56
|
};
|
|
85
|
-
// Stryker disable next-line StringLiteral: there is always at least one sandbox, so the fallback is never used; it is there for the type
|
|
86
57
|
const base = await check(sandboxes[0] ?? '');
|
|
87
58
|
if (!base.ok)
|
|
88
59
|
throw new Error(`the project does not type-check with no mutant applied (\`${[command, ...args].join(' ')}\`): ${base.output.trim().slice(-OUTPUT_TAIL)}. fix: make the type check pass first, or drop checkers`);
|
|
@@ -98,7 +69,7 @@ export async function checkSurvivors(o, survivors) {
|
|
|
98
69
|
const touched = new Set(batch.map((m) => m.file));
|
|
99
70
|
if (batch.length > 1 && [...r.errors.keys()].some((f) => !touched.has(f))) {
|
|
100
71
|
for (const m of batch)
|
|
101
|
-
await run(sandbox, [m]);
|
|
72
|
+
await run(sandbox, [m]);
|
|
102
73
|
return;
|
|
103
74
|
}
|
|
104
75
|
for (const m of batch) {
|
|
@@ -115,9 +86,7 @@ export async function checkSurvivors(o, survivors) {
|
|
|
115
86
|
return failing;
|
|
116
87
|
}
|
|
117
88
|
finally {
|
|
118
|
-
// No `force`: each sandbox was made above and nothing else removes it.
|
|
119
89
|
for (const s of sandboxes)
|
|
120
90
|
rmSync(s, { recursive: true });
|
|
121
91
|
}
|
|
122
92
|
}
|
|
123
|
-
//# sourceMappingURL=checker.js.map
|
package/dist/child.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A Vitest server in its own process. It starts the project's Vitest once and then runs work
|
|
3
|
-
* units sent by the coordinator over IPC, re-running the same instance (~0.2 s a run against
|
|
4
|
-
* ~1.5 s for a fresh Vitest). The coordinator kills the process when a mutant loops
|
|
5
|
-
* synchronously, a hang that no in-worker timeout and no `cancelCurrentRun()` can interrupt
|
|
6
|
-
* (both measured, 2026-10-26), and hands the rest of the unit to another server.
|
|
7
|
-
*
|
|
8
|
-
* node child.js <server.json> IPC: ← { type: 'run', unit } → { type: 'ready' | 'done' }
|
|
9
|
-
*
|
|
10
|
-
* Vitest is resolved from the project being tested, like Stryker's vitest-runner does, so the
|
|
11
|
-
* project's own Vitest version and config run its tests.
|
|
12
|
-
*/
|
|
13
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
2
|
import path from 'node:path';
|
|
15
3
|
import { fileURLToPath } from 'node:url';
|
|
@@ -18,9 +6,11 @@ import { BEAT_ENV, beatFile, CPU_SAMPLE_MS } from './heartbeat.js';
|
|
|
18
6
|
import { resolveEsm } from './resolve.js';
|
|
19
7
|
import { ACTIVE_MUTANT_ENV, argv, env, exit, pid } from './runtime.js';
|
|
20
8
|
import { instrumentedSources } from './vite-plugin.js';
|
|
9
|
+
process.on('disconnect', () => exit(ExitCode.OK));
|
|
10
|
+
if (!process.connected)
|
|
11
|
+
exit(ExitCode.OK);
|
|
21
12
|
const config = JSON.parse(readFileSync(argv[2] ?? '', 'utf8'));
|
|
22
13
|
env['NODE_ENV'] ??= 'test';
|
|
23
|
-
// Before Vitest starts any worker, so each one inherits it (heartbeat.ts).
|
|
24
14
|
env[BEAT_ENV] = beatFile(path.dirname(config.instrumentedMap), pid);
|
|
25
15
|
const vitestNode = (await import(resolveEsm('vitest/node', config.root)));
|
|
26
16
|
const runner = fileURLToPath(new URL('./runner.js', import.meta.url));
|
|
@@ -37,25 +27,13 @@ const vitest = await vitestNode.createVitest('test', {
|
|
|
37
27
|
reporters: env['OXMUTATE_DEBUG_VITEST'] ? ['default'] : [silent],
|
|
38
28
|
coverage: { enabled: false },
|
|
39
29
|
passWithNoTests: true,
|
|
40
|
-
// Bail is ours (per mutant); Vitest must run every test it is handed.
|
|
41
30
|
bail: 0,
|
|
42
|
-
// Vitest 5's persistent module cache keys on the file on disk, not on what a plugin serves,
|
|
43
|
-
// so it served a previous run's instrumentation: every mutant whose id had changed since got
|
|
44
|
-
// no coverage (burgee/paratext, 2026-10-27: 6% agreement until this was off). Off under both
|
|
45
|
-
// spellings (top-level in Vitest 5, `experimental` in 4.x).
|
|
46
31
|
fsModuleCache: false,
|
|
47
32
|
experimental: { fsModuleCache: false },
|
|
48
33
|
...(config.inlineDeps && { server: { deps: { inline: true } } }),
|
|
49
|
-
},
|
|
50
|
-
// A private Vite cache per run, so no cache outside this run can answer for a mutated file.
|
|
51
|
-
{ plugins: [instrumentedSources(config.instrumentedMap)], cacheDir: path.join(path.dirname(config.instrumentedMap), 'vite-cache') });
|
|
52
|
-
// A config with `projects` resolves each project's `runner` and `isolate` from its own inline
|
|
53
|
-
// config, never from these options, so its tests ran without oxmutate's runner and the dry run
|
|
54
|
-
// recorded none (burgee, weekly run 40783405734, 2026-10-28). Each project keeps its own pool:
|
|
55
|
-
// burgee's run in forks, and its process.env lock fails under threads.
|
|
34
|
+
}, { plugins: [instrumentedSources(config.instrumentedMap)], cacheDir: path.join(path.dirname(config.instrumentedMap), 'vite-cache') });
|
|
56
35
|
for (const project of vitest.projects)
|
|
57
36
|
Object.assign(project.config, { runner, isolate: true });
|
|
58
|
-
/** Has the runner recorded a kill for this mutant in this unit's results yet? */
|
|
59
37
|
function killed(results, id) {
|
|
60
38
|
let text = '';
|
|
61
39
|
try {
|
|
@@ -66,17 +44,11 @@ function killed(results, id) {
|
|
|
66
44
|
}
|
|
67
45
|
return text.split('\n').some((line) => line.includes(`"id":"${id}"`) && line.includes('"killed":true'));
|
|
68
46
|
}
|
|
69
|
-
/** An `isolated` unit: its mutants armed one after another, each across its files. */
|
|
70
47
|
async function runIsolated(unit) {
|
|
71
|
-
// Stop a file at its first failing test: one failure kills the mutant, and the rest of the
|
|
72
|
-
// file is wasted time (burgee/paratext hyperlinks.test.ts: 2.3 s a run, the first failure
|
|
73
|
-
// typically in its first tests). Read per start, so dry and warm runs still run everything
|
|
74
|
-
// (measured on Vitest 4.1, 2026-10-27).
|
|
75
48
|
vitest.config.bail = 1;
|
|
76
49
|
try {
|
|
77
50
|
for (const item of unit.batch ?? []) {
|
|
78
51
|
env[ACTIVE_MUTANT_ENV] = item.id;
|
|
79
|
-
// The runaway guard warm runs have: a loop the mutant causes stops at the limit, not the watchdog.
|
|
80
52
|
if (item.hitLimit)
|
|
81
53
|
env['OXMUTATE_HIT_LIMIT'] = String(item.hitLimit);
|
|
82
54
|
else
|
|
@@ -86,8 +58,6 @@ async function runIsolated(unit) {
|
|
|
86
58
|
else
|
|
87
59
|
delete env['OXMUTATE_ONLY_TESTS'];
|
|
88
60
|
writeFileSync(unit.progress, `${item.id} ${String(Date.now())}\n`);
|
|
89
|
-
// One file at a time, cheapest first (the coordinator orders them), stopping at the first
|
|
90
|
-
// file that kills the mutant: Stryker runs every file for every isolated mutant.
|
|
91
61
|
for (const file of item.files) {
|
|
92
62
|
await vitest.start([file]);
|
|
93
63
|
if (killed(unit.results, item.id))
|
|
@@ -104,8 +74,6 @@ async function runIsolated(unit) {
|
|
|
104
74
|
writeFileSync(unit.progress, '');
|
|
105
75
|
}
|
|
106
76
|
async function runUnit(unit) {
|
|
107
|
-
// Workers copy the environment when they start, so each run sees the unit's settings
|
|
108
|
-
// (verified across re-runs of one instance, 2026-10-26).
|
|
109
77
|
env['OXMUTATE_MODE'] = unit.mode;
|
|
110
78
|
env['OXMUTATE_RESULTS'] = unit.results;
|
|
111
79
|
env['OXMUTATE_PROGRESS'] = unit.progress;
|
|
@@ -123,30 +91,21 @@ async function runUnit(unit) {
|
|
|
123
91
|
if (unit.mode === 'isolated')
|
|
124
92
|
await runIsolated(unit);
|
|
125
93
|
else {
|
|
126
|
-
// A dry run names itself in the progress file, so the watchdog holds it to its budget.
|
|
127
94
|
if (unit.mode === 'dry')
|
|
128
95
|
writeFileSync(unit.progress, `dry ${String(Date.now())}\n`);
|
|
129
96
|
await vitest.start(unit.files.length ? unit.files : undefined);
|
|
130
97
|
}
|
|
131
98
|
}
|
|
132
|
-
/** How long `close` waits for `vitest.close()` before the server exits regardless. */
|
|
133
99
|
const CLOSE_GRACE_MS = 2000;
|
|
134
100
|
process.on('message', (msg) => {
|
|
135
101
|
if (msg.type === 'run' && msg.unit) {
|
|
136
102
|
runUnit(msg.unit).then(() => process.send?.({ type: 'done' }), (error) => process.send?.({ type: 'done', error: error instanceof Error ? error.message : String(error) }));
|
|
137
103
|
}
|
|
138
104
|
else if (msg.type === 'close') {
|
|
139
|
-
// A worker thread stuck in a runaway mutant never lets `vitest.close()` settle: exit anyway.
|
|
140
105
|
setTimeout(() => exit(ExitCode.OK), CLOSE_GRACE_MS).unref();
|
|
141
106
|
void vitest.close().finally(() => exit(ExitCode.OK));
|
|
142
107
|
}
|
|
143
108
|
});
|
|
144
|
-
// The coordinator is gone (finished, crashed or killed): nothing will read this server's results,
|
|
145
|
-
// and a worker thread may still be looping in a mutant. Two such servers were found spinning at
|
|
146
|
-
// 100% CPU 11 hours after their run ended, and slowed the next run 50x (2026-10-27).
|
|
147
|
-
process.on('disconnect', () => exit(ExitCode.OK));
|
|
148
|
-
// The server's CPU time, every thread (the test worker included), for the watchdog to tell an idle
|
|
149
|
-
// stall from a loop (heartbeat.ts). This thread stays free while a worker loops, so the samples come.
|
|
150
109
|
const sendCpu = () => {
|
|
151
110
|
const { user, system } = process.cpuUsage();
|
|
152
111
|
process.send?.({ type: 'cpu', at: Date.now(), us: user + system });
|
|
@@ -154,4 +113,3 @@ const sendCpu = () => {
|
|
|
154
113
|
sendCpu();
|
|
155
114
|
setInterval(sendCpu, CPU_SAMPLE_MS).unref();
|
|
156
115
|
process.send?.({ type: 'ready' });
|
|
157
|
-
//# sourceMappingURL=child.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oxmutate's command line, declared once on burgee: help, `--json`, `--schema`, `--format=agent`,
|
|
3
|
-
* completions and the MCP server are projected from this declaration (spec R17). Handlers return
|
|
4
|
-
* data; the text a person reads rides along under burgee's text symbol, so the agent surfaces
|
|
5
|
-
* carry exactly the data.
|
|
6
|
-
*/
|
|
7
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
2
|
import path from 'node:path';
|
|
9
3
|
import { defineCommand, defineProgram, ExitCode } from 'burgee';
|
|
@@ -31,7 +25,6 @@ function survivorRecord(m) {
|
|
|
31
25
|
function summaryData(r, written) {
|
|
32
26
|
const count = (s) => r.mutants.filter((m) => m.status === s).length;
|
|
33
27
|
const data = {
|
|
34
|
-
// burgee N7: an idempotent command says whether it changed anything (reports were written).
|
|
35
28
|
changed: written.length > 0,
|
|
36
29
|
version: VERSION,
|
|
37
30
|
score: Number(r.score.mutationScore.toFixed(2)),
|
|
@@ -50,8 +43,6 @@ function summaryData(r, written) {
|
|
|
50
43
|
data[TEXT] = () => textSummary(r);
|
|
51
44
|
return data;
|
|
52
45
|
}
|
|
53
|
-
/** The `run` flags that override a config key, as the `Partial<Options>` loadOptions applies last. */
|
|
54
|
-
/** How many weakened mutants the error line names; the JSON report has them all. */
|
|
55
46
|
const WEAKENED_SHOWN = 3;
|
|
56
47
|
function overridesFrom(o, root) {
|
|
57
48
|
const overrides = {};
|
|
@@ -162,4 +153,3 @@ export const program = defineProgram({
|
|
|
162
153
|
description: 'Mutation testing on the oxc engine: Stryker-compatible, built for CI and coding agents',
|
|
163
154
|
commands: [runCommand, survivorsCommand],
|
|
164
155
|
});
|
|
165
|
-
//# sourceMappingURL=cli.js.map
|
package/dist/command-runner.js
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The command runner (Stryker's `testRunner: 'command'`): any test command, one process per
|
|
3
|
-
* mutant, the escape hatch for a suite that is not Vitest. A non-zero exit kills the mutant, a run
|
|
4
|
-
* past its budget is a timeout, and a zero exit is a survivor. There is no coverage, so every
|
|
5
|
-
* mutant runs the whole command, as with Stryker's.
|
|
6
|
-
*
|
|
7
|
-
* An arbitrary command cannot be served sources from memory, so this is the one path with a
|
|
8
|
-
* sandbox: a copy of the project under `<root>/.oxmutate-tmp/`, with the instrumented files
|
|
9
|
-
* written into it. Inside the project, as Stryker's `.stryker-tmp` is, so module resolution walks
|
|
10
|
-
* up to the same `node_modules` (a monorepo's hoisted ones included). The armed mutant reaches the
|
|
11
|
-
* instrumented code through `__OXMUTATE_ACTIVE_MUTANT__`, which the runtime header reads.
|
|
12
|
-
*/
|
|
13
1
|
import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
2
|
import path from 'node:path';
|
|
15
3
|
import { ambientRuntime, run as spawn } from 'bellpull';
|
|
16
4
|
import { ACTIVE_MUTANT_ENV, env } from './runtime.js';
|
|
17
5
|
const SANDBOX_DIR = '.oxmutate-tmp';
|
|
18
|
-
/** Stryker's default `ignorePatterns`, plus this sandbox. */
|
|
19
6
|
const SKIP = new Set(['node_modules', '.git', 'reports', 'coverage', '.stryker-tmp', SANDBOX_DIR]);
|
|
20
|
-
/**
|
|
21
|
-
* A copy of the project under `<root>/.oxmutate-tmp/`, for the paths that need files on disk (the
|
|
22
|
-
* command runner, the type checker). Inside the project so module resolution walks up to the same
|
|
23
|
-
* `node_modules`; entry by entry, since a directory cannot be copied into itself.
|
|
24
|
-
*/
|
|
25
7
|
export function makeSandbox(root) {
|
|
26
8
|
const parent = path.join(root, SANDBOX_DIR);
|
|
27
9
|
mkdirSync(parent, { recursive: true });
|
|
@@ -32,21 +14,13 @@ export function makeSandbox(root) {
|
|
|
32
14
|
return sandbox;
|
|
33
15
|
}
|
|
34
16
|
const MS_PER_MINUTE = 60_000;
|
|
35
|
-
/** How much of a failing dry run's output the error keeps. */
|
|
36
17
|
const OUTPUT_TAIL = 1500;
|
|
37
|
-
/**
|
|
38
|
-
* The least slack a process-per-mutant run gets: Stryker's own default `timeoutMS`. oxmutate's
|
|
39
|
-
* 500 ms default is sized for a warm in-process run; a fresh process's start-up varies by seconds
|
|
40
|
-
* on a loaded machine, and at 500 ms the e2e fixture read false timeouts (2026-10-27).
|
|
41
|
-
*/
|
|
42
18
|
const PROCESS_SLACK_MS = 5000;
|
|
43
|
-
/** Runs the command over every mutant not already decided; returns the dry run's time. */
|
|
44
19
|
export async function runCommandMutants(o, mutants) {
|
|
45
20
|
const sandbox = makeSandbox(o.root);
|
|
46
21
|
try {
|
|
47
22
|
for (const [file, code] of Object.entries(o.codeMap))
|
|
48
23
|
writeFileSync(path.join(sandbox, path.relative(o.root, file)), code);
|
|
49
|
-
// Stryker splits the command on spaces and runs it without a shell; so does this.
|
|
50
24
|
const [command = '', ...args] = o.command.trim().split(/\s+/);
|
|
51
25
|
const once = (timeout, id) => spawn(command, args, { cwd: sandbox, runtime: ambientRuntime(), timeout, env: id ? { ...env, [ACTIVE_MUTANT_ENV]: id } : { ...env } });
|
|
52
26
|
const dryBudget = o.dryRunTimeoutMinutes * MS_PER_MINUTE;
|
|
@@ -80,4 +54,3 @@ export async function runCommandMutants(o, mutants) {
|
|
|
80
54
|
rmSync(sandbox, { recursive: true, force: true });
|
|
81
55
|
}
|
|
82
56
|
}
|
|
83
|
-
//# sourceMappingURL=command-runner.js.map
|
package/dist/config.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Options, read from `stryker.config.*` as it stands (migration is a no-op: the file Stryker reads
|
|
3
|
-
* is the file oxmutate reads) and from `oxmutate.config.*`, which wins where both set a key.
|
|
4
|
-
* Every key Stryker documents that oxmutate does not use is reported, never silently dropped
|
|
5
|
-
* (spec R15).
|
|
6
|
-
*/
|
|
7
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
2
|
import { availableParallelism } from 'node:os';
|
|
9
3
|
import path from 'node:path';
|
|
10
4
|
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import {} from './native.js';
|
|
12
5
|
import { resolveEsm } from './resolve.js';
|
|
13
|
-
import {} from './spec-oracle.js';
|
|
14
6
|
const NOT_NEEDED = {
|
|
15
7
|
inPlace: 'oxmutate never copies or rewrites the project; instrumented sources are served from memory',
|
|
16
8
|
tempDirName: 'no sandbox directory exists',
|
|
@@ -35,10 +27,6 @@ const NOT_NEEDED = {
|
|
|
35
27
|
force: 'use --no-incremental',
|
|
36
28
|
checkerNodeArgs: 'not implemented',
|
|
37
29
|
};
|
|
38
|
-
/**
|
|
39
|
-
* Defaults where the config sets nothing. Stryker's, except the per-mutant slack: Stryker's
|
|
40
|
-
* `timeoutMS` is 5000, and a warm run needs far less (cli.ts `--mutant-timeout`).
|
|
41
|
-
*/
|
|
42
30
|
const MAX_DEFAULT_CONCURRENCY = 8;
|
|
43
31
|
const DEFAULT_TIMEOUT_MS = 500;
|
|
44
32
|
const DEFAULT_TIMEOUT_FACTOR = 1.5;
|
|
@@ -48,7 +36,6 @@ const DEFAULT_DRY_RUN_MINUTES = 5;
|
|
|
48
36
|
const USED = new Set(['mutate', 'vitest', 'concurrency', 'timeoutMS', 'timeoutFactor', 'thresholds', 'ignoreStatic', 'reporters', 'jsonReporter', 'htmlReporter', 'incremental', 'incrementalFile', '$schema', 'mutator', 'dryRunOnly', 'dryRunTimeoutMinutes', 'buildCommand', 'testRunnerNodeArgs', 'allowEmpty', 'warnings', 'testRunner', 'commandRunner', 'checkers', 'tsconfigFile']);
|
|
49
37
|
async function loadFile(file) {
|
|
50
38
|
if (file.endsWith('.json')) {
|
|
51
|
-
// Stryker disable next-line StringLiteral: without an encoding readFileSync returns a Buffer, which JSON.parse reads as UTF-8 text all the same
|
|
52
39
|
return JSON.parse(readFileSync(file, 'utf8'));
|
|
53
40
|
}
|
|
54
41
|
const mod = (await import(pathToFileURL(file).href));
|
|
@@ -63,17 +50,9 @@ function find(root, base) {
|
|
|
63
50
|
}
|
|
64
51
|
return undefined;
|
|
65
52
|
}
|
|
66
|
-
/** The Vite (and Vite+) config files, in the order Vite and Vitest look for them. */
|
|
67
53
|
const VITE_CONFIGS = ['vite.config', 'vitest.config'].flatMap((base) => ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'].map((ext) => `${base}${ext}`));
|
|
68
|
-
/**
|
|
69
|
-
* The `mutate` block of the project's Vite config — the Vite+ shape, one file for test, lint, fmt
|
|
70
|
-
* and mutate. Loaded with the project's own Vite (`loadConfigFromFile`), so a TypeScript config and
|
|
71
|
-
* its imports work as Vite sees them. Inside the block the file list is `include`, as Vitest names
|
|
72
|
-
* it; Stryker's `mutate` is accepted too. No Vite installed, or no block: nothing.
|
|
73
|
-
*/
|
|
74
54
|
async function fromViteConfig(root) {
|
|
75
55
|
const file = VITE_CONFIGS.map((f) => path.join(root, f)).find((f) => existsSync(f));
|
|
76
|
-
// Stryker disable next-line ConditionalExpression: without the guard Vite searches the root for the same names, finds none, and returns null, which returns undefined below
|
|
77
56
|
if (!file)
|
|
78
57
|
return undefined;
|
|
79
58
|
let vite;
|
|
@@ -83,14 +62,11 @@ async function fromViteConfig(root) {
|
|
|
83
62
|
catch {
|
|
84
63
|
return undefined;
|
|
85
64
|
}
|
|
86
|
-
// Stryker disable next-line StringLiteral: the log level only decides whether Vite prints, and a config that loads prints nothing at any level
|
|
87
65
|
const level = 'silent';
|
|
88
66
|
const loaded = await vite.loadConfigFromFile({ command: 'serve', mode: 'test' }, file, root, level);
|
|
89
|
-
// Stryker disable next-line OptionalChaining: given a file that exists, Vite returns a config or throws; null is its answer only when it found no file
|
|
90
67
|
const block = loaded?.config['mutate'];
|
|
91
68
|
if (!block || typeof block !== 'object')
|
|
92
69
|
return undefined;
|
|
93
|
-
// `include` is the Vitest spelling; Stryker's `mutate`, when given too, is spread over it.
|
|
94
70
|
const { include, ...rest } = block;
|
|
95
71
|
return { file, block: { mutate: include, ...rest } };
|
|
96
72
|
}
|
|
@@ -140,7 +116,6 @@ export async function loadOptions(root, overrides = {}, configFile) {
|
|
|
140
116
|
allowEmpty: raw['allowEmpty'] === true,
|
|
141
117
|
specOracles: raw['specOracles'] ?? [],
|
|
142
118
|
ignore: raw['ignore'] ?? [],
|
|
143
|
-
// Stryker disable next-line ArrayDeclaration: a placeholder element is not 'typescript' either, so includes() is false as for an empty list
|
|
144
119
|
typeCheck: (raw['checkers'] ?? []).includes('typescript'),
|
|
145
120
|
tsconfigFile: raw['tsconfigFile'] ?? 'tsconfig.json',
|
|
146
121
|
typeChecker: typeof raw['typeChecker'] === 'string' ? raw['typeChecker'] : undefined,
|
|
@@ -149,17 +124,14 @@ export async function loadOptions(root, overrides = {}, configFile) {
|
|
|
149
124
|
...overrides,
|
|
150
125
|
};
|
|
151
126
|
}
|
|
152
|
-
// One `\s` after next-line is enough: the list's class takes any further whitespace, and is trimmed.
|
|
153
127
|
const DIRECTIVE = /\/\/\s*(?:Stryker|oxmutate)\s+disable\s+next-line\s([\w,\s]+?|all)\s*:\s*(.*)/i;
|
|
154
128
|
const NO_REASON = /\/\/\s*(?:Stryker|oxmutate)\s+disable\s+next-line\s([\w,\s]+?|all)\s*$/i;
|
|
155
|
-
/** Disable directives in a file. A directive without a reason is an error (burgee's own rule). */
|
|
156
129
|
export function disables(source, file) {
|
|
157
130
|
const out = [];
|
|
158
131
|
const errors = [];
|
|
159
132
|
source.split('\n').forEach((line, i) => {
|
|
160
133
|
const m = DIRECTIVE.exec(line);
|
|
161
134
|
if (m) {
|
|
162
|
-
// Stryker disable next-line StringLiteral: both groups take part in every match, so the fallback is never used; it is there for the type
|
|
163
135
|
const [list, reason] = [(m[1] ?? '').trim(), (m[2] ?? '').trim()];
|
|
164
136
|
out.push({ line: i + 2, mutators: list.toLowerCase() === 'all' ? 'all' : list.split(',').map((s) => s.trim()).filter(Boolean), reason });
|
|
165
137
|
}
|
|
@@ -169,4 +141,3 @@ export function disables(source, file) {
|
|
|
169
141
|
});
|
|
170
142
|
return { disables: out, errors };
|
|
171
143
|
}
|
|
172
|
-
//# sourceMappingURL=config.js.map
|