@mutineerjs/mutineer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/admin/assets/index-B7nXq-e7.js +32 -0
- package/dist/admin/assets/index-B7nXq-e7.js.map +1 -0
- package/dist/admin/assets/index-BDQLkBUE.js +32 -0
- package/dist/admin/assets/index-BDQLkBUE.js.map +1 -0
- package/dist/admin/assets/index-DVkP-Tc7.css +1 -0
- package/dist/admin/index.html +13 -0
- package/dist/admin/server/admin.d.ts +6 -0
- package/dist/admin/server/admin.js +234 -0
- package/dist/bin/mutate-vitest.d.ts +2 -0
- package/dist/bin/mutate-vitest.js +90 -0
- package/dist/bin/mutineer.d.ts +2 -0
- package/dist/bin/mutineer.js +46 -0
- package/dist/core/__tests__/module.spec.d.ts +1 -0
- package/dist/core/__tests__/module.spec.js +6 -0
- package/dist/core/module.d.ts +11 -0
- package/dist/core/module.js +14 -0
- package/dist/core/sfc.d.ts +12 -0
- package/dist/core/sfc.js +54 -0
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.js +1 -0
- package/dist/core/variant-utils.d.ts +30 -0
- package/dist/core/variant-utils.js +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mutators/__tests__/registry.spec.d.ts +1 -0
- package/dist/mutators/__tests__/registry.spec.js +43 -0
- package/dist/mutators/__tests__/utils.spec.d.ts +1 -0
- package/dist/mutators/__tests__/utils.spec.js +15 -0
- package/dist/mutators/registry.d.ts +37 -0
- package/dist/mutators/registry.js +101 -0
- package/dist/mutators/types.d.ts +39 -0
- package/dist/mutators/types.js +7 -0
- package/dist/mutators/utils.d.ts +37 -0
- package/dist/mutators/utils.js +151 -0
- package/dist/plugin/viteMutate.d.ts +15 -0
- package/dist/plugin/viteMutate.js +52 -0
- package/dist/plugin/vitest.setup.d.ts +47 -0
- package/dist/plugin/vitest.setup.js +118 -0
- package/dist/plugin/withVitest.d.ts +13 -0
- package/dist/plugin/withVitest.js +30 -0
- package/dist/runner/__tests__/discover.spec.d.ts +1 -0
- package/dist/runner/__tests__/discover.spec.js +59 -0
- package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
- package/dist/runner/__tests__/orchestrator.spec.js +55 -0
- package/dist/runner/adapters/__tests__/jest.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/jest.spec.js +88 -0
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +59 -0
- package/dist/runner/adapters/__tests__/vitest.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/vitest.spec.js +118 -0
- package/dist/runner/adapters/index.d.ts +10 -0
- package/dist/runner/adapters/index.js +9 -0
- package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/adapters/jest/__tests__/index.spec.js +88 -0
- package/dist/runner/adapters/jest/index.d.ts +24 -0
- package/dist/runner/adapters/jest/index.js +216 -0
- package/dist/runner/adapters/jest/worker-runtime.d.ts +37 -0
- package/dist/runner/adapters/jest/worker-runtime.js +171 -0
- package/dist/runner/adapters/jest-worker-runtime.d.ts +37 -0
- package/dist/runner/adapters/jest-worker-runtime.js +171 -0
- package/dist/runner/adapters/jest.d.ts +24 -0
- package/dist/runner/adapters/jest.js +216 -0
- package/dist/runner/adapters/types.d.ts +89 -0
- package/dist/runner/adapters/types.js +8 -0
- package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/adapters/vitest/__tests__/index.spec.js +118 -0
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +59 -0
- package/dist/runner/adapters/vitest/index.d.ts +33 -0
- package/dist/runner/adapters/vitest/index.js +267 -0
- package/dist/runner/adapters/vitest/worker-runtime.d.ts +25 -0
- package/dist/runner/adapters/vitest/worker-runtime.js +118 -0
- package/dist/runner/adapters/vitest-worker-runtime.d.ts +25 -0
- package/dist/runner/adapters/vitest-worker-runtime.js +118 -0
- package/dist/runner/adapters/vitest.d.ts +33 -0
- package/dist/runner/adapters/vitest.js +267 -0
- package/dist/runner/args.d.ts +50 -0
- package/dist/runner/args.js +123 -0
- package/dist/runner/cache.d.ts +38 -0
- package/dist/runner/cache.js +118 -0
- package/dist/runner/changed.d.ts +22 -0
- package/dist/runner/changed.js +210 -0
- package/dist/runner/cleanup.d.ts +4 -0
- package/dist/runner/cleanup.js +21 -0
- package/dist/runner/config.d.ts +13 -0
- package/dist/runner/config.js +94 -0
- package/dist/runner/discover.d.ts +7 -0
- package/dist/runner/discover.js +258 -0
- package/dist/runner/jest/__tests__/adapter.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/adapter.spec.js +110 -0
- package/dist/runner/jest/adapter.d.ts +24 -0
- package/dist/runner/jest/adapter.js +191 -0
- package/dist/runner/jest/index.d.ts +8 -0
- package/dist/runner/jest/index.js +7 -0
- package/dist/runner/jest/pool.d.ts +47 -0
- package/dist/runner/jest/pool.js +307 -0
- package/dist/runner/jest/resolver.cjs +61 -0
- package/dist/runner/jest/resolver.d.cts +11 -0
- package/dist/runner/jest/worker-runtime.d.ts +30 -0
- package/dist/runner/jest/worker-runtime.js +98 -0
- package/dist/runner/jest/worker.d.mts +1 -0
- package/dist/runner/jest/worker.mjs +55 -0
- package/dist/runner/orchestrator.d.ts +13 -0
- package/dist/runner/orchestrator.js +387 -0
- package/dist/runner/pool/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/index.spec.js +83 -0
- package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/pool-plugin.spec.js +59 -0
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +78 -0
- package/dist/runner/pool/index.d.ts +8 -0
- package/dist/runner/pool/index.js +9 -0
- package/dist/runner/pool/jest/pool.d.ts +52 -0
- package/dist/runner/pool/jest/pool.js +309 -0
- package/dist/runner/pool/jest/worker.d.mts +1 -0
- package/dist/runner/pool/jest/worker.mjs +60 -0
- package/dist/runner/pool/jest-pool.d.ts +52 -0
- package/dist/runner/pool/jest-pool.js +309 -0
- package/dist/runner/pool/jest-worker.d.mts +1 -0
- package/dist/runner/pool/jest-worker.mjs +60 -0
- package/dist/runner/pool/plugin.d.ts +18 -0
- package/dist/runner/pool/plugin.js +60 -0
- package/dist/runner/pool/pool-plugin.d.ts +18 -0
- package/dist/runner/pool/pool-plugin.js +60 -0
- package/dist/runner/pool/pool-redirect-loader.d.ts +19 -0
- package/dist/runner/pool/pool-redirect-loader.js +116 -0
- package/dist/runner/pool/pool-redirect-loader.mjs +146 -0
- package/dist/runner/pool/redirect-loader.d.ts +19 -0
- package/dist/runner/pool/redirect-loader.js +116 -0
- package/dist/runner/pool/vitest/pool.d.ts +70 -0
- package/dist/runner/pool/vitest/pool.js +376 -0
- package/dist/runner/pool/vitest/worker.d.mts +15 -0
- package/dist/runner/pool/vitest/worker.mjs +96 -0
- package/dist/runner/pool/vitest-worker.d.mts +15 -0
- package/dist/runner/pool/vitest-worker.mjs +96 -0
- package/dist/runner/shared/index.d.ts +9 -0
- package/dist/runner/shared/index.js +8 -0
- package/dist/runner/shared/mutant-paths.d.ts +15 -0
- package/dist/runner/shared/mutant-paths.js +30 -0
- package/dist/runner/shared/redirect-state.d.ts +45 -0
- package/dist/runner/shared/redirect-state.js +50 -0
- package/dist/runner/shared-module-redirect.d.ts +56 -0
- package/dist/runner/shared-module-redirect.js +84 -0
- package/dist/runner/types.d.ts +88 -0
- package/dist/runner/types.js +8 -0
- package/dist/runner/variants.d.ts +21 -0
- package/dist/runner/variants.js +66 -0
- package/dist/runner/vitest/__tests__/adapter.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/adapter.spec.js +131 -0
- package/dist/runner/vitest/__tests__/plugin.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +65 -0
- package/dist/runner/vitest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/pool.spec.js +106 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -0
- package/dist/runner/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +75 -0
- package/dist/runner/vitest/adapter.d.ts +33 -0
- package/dist/runner/vitest/adapter.js +277 -0
- package/dist/runner/vitest/index.d.ts +11 -0
- package/dist/runner/vitest/index.js +10 -0
- package/dist/runner/vitest/plugin.d.ts +12 -0
- package/dist/runner/vitest/plugin.js +49 -0
- package/dist/runner/vitest/pool.d.ts +65 -0
- package/dist/runner/vitest/pool.js +376 -0
- package/dist/runner/vitest/redirect-loader.d.ts +30 -0
- package/dist/runner/vitest/redirect-loader.js +123 -0
- package/dist/runner/vitest/worker-runtime.d.ts +16 -0
- package/dist/runner/vitest/worker-runtime.js +105 -0
- package/dist/runner/vitest/worker.d.mts +15 -0
- package/dist/runner/vitest/worker.mjs +92 -0
- package/dist/types/api.d.ts +20 -0
- package/dist/types/api.js +1 -0
- package/dist/types/config.d.ts +48 -0
- package/dist/types/config.js +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.js +11 -0
- package/dist/types/mutant.d.ts +44 -0
- package/dist/types/mutant.js +7 -0
- package/dist/utils/PoolSpinner.d.ts +5 -0
- package/dist/utils/PoolSpinner.js +6 -0
- package/dist/utils/ProgressBar.d.ts +11 -0
- package/dist/utils/ProgressBar.js +9 -0
- package/dist/utils/__tests__/coverage.spec.d.ts +1 -0
- package/dist/utils/__tests__/coverage.spec.js +91 -0
- package/dist/utils/__tests__/progress.spec.d.ts +1 -0
- package/dist/utils/__tests__/progress.spec.js +50 -0
- package/dist/utils/__tests__/summary.spec.d.ts +1 -0
- package/dist/utils/__tests__/summary.spec.js +54 -0
- package/dist/utils/coverage.d.ts +57 -0
- package/dist/utils/coverage.js +204 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +90 -0
- package/dist/utils/summary.d.ts +12 -0
- package/dist/utils/summary.js +107 -0
- package/package.json +59 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Worker Pool
|
|
3
|
+
*
|
|
4
|
+
* Manages a pool of persistent Vitest worker processes that can run
|
|
5
|
+
* multiple mutations without restarting, providing significant speedup
|
|
6
|
+
* over the per-spawn approach.
|
|
7
|
+
*
|
|
8
|
+
* Each worker:
|
|
9
|
+
* - Starts Vitest in watch mode via programmatic API
|
|
10
|
+
* - Receives mutations via stdin (JSON)
|
|
11
|
+
* - Uses dynamic redirect loader to swap module at runtime
|
|
12
|
+
* - Returns results via stdout (JSON)
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import * as readline from 'node:readline';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { EventEmitter } from 'node:events';
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
class VitestWorker extends EventEmitter {
|
|
22
|
+
constructor(id, cwd, vitestConfig, debug = false) {
|
|
23
|
+
super();
|
|
24
|
+
this.cwd = cwd;
|
|
25
|
+
this.vitestConfig = vitestConfig;
|
|
26
|
+
this.debug = debug;
|
|
27
|
+
this.process = null;
|
|
28
|
+
this.rl = null;
|
|
29
|
+
this.pendingTask = null;
|
|
30
|
+
this.ready = false;
|
|
31
|
+
this.shuttingDown = false;
|
|
32
|
+
this.id = id;
|
|
33
|
+
}
|
|
34
|
+
async start() {
|
|
35
|
+
const workerJs = path.join(__dirname, 'worker.js');
|
|
36
|
+
const workerMts = path.join(__dirname, 'worker.mjs');
|
|
37
|
+
const workerTs = path.join(__dirname, 'worker.mts');
|
|
38
|
+
const workerScript = fs.existsSync(workerJs)
|
|
39
|
+
? workerJs
|
|
40
|
+
: fs.existsSync(workerMts)
|
|
41
|
+
? workerMts
|
|
42
|
+
: workerTs;
|
|
43
|
+
const loaderJs = path.join(__dirname, '..', 'redirect-loader.js');
|
|
44
|
+
const loaderMjs = path.join(__dirname, '..', 'redirect-loader.mjs');
|
|
45
|
+
const loaderTs = path.join(__dirname, '..', 'redirect-loader.ts');
|
|
46
|
+
const loaderScript = fs.existsSync(loaderJs)
|
|
47
|
+
? loaderJs
|
|
48
|
+
: fs.existsSync(loaderMjs)
|
|
49
|
+
? loaderMjs
|
|
50
|
+
: loaderTs;
|
|
51
|
+
const env = {
|
|
52
|
+
...process.env,
|
|
53
|
+
MUTINEER_WORKER_ID: this.id,
|
|
54
|
+
MUTINEER_CWD: this.cwd,
|
|
55
|
+
...(this.vitestConfig ? { MUTINEER_VITEST_CONFIG: this.vitestConfig } : {}),
|
|
56
|
+
...(this.debug ? { MUTINEER_DEBUG: '1' } : {}),
|
|
57
|
+
};
|
|
58
|
+
this.log('Starting worker process');
|
|
59
|
+
this.process = spawn(process.execPath, [
|
|
60
|
+
'--experimental-strip-types',
|
|
61
|
+
'--experimental-transform-types',
|
|
62
|
+
'--no-warnings',
|
|
63
|
+
'--import', loaderScript,
|
|
64
|
+
workerScript,
|
|
65
|
+
], {
|
|
66
|
+
cwd: this.cwd,
|
|
67
|
+
env,
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
// Handle stderr (debug/error output)
|
|
71
|
+
this.process.stderr?.on('data', (data) => {
|
|
72
|
+
if (this.debug) {
|
|
73
|
+
process.stderr.write(`[worker-${this.id}] ${data}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Set up line reader for stdout (JSON messages)
|
|
77
|
+
this.rl = readline.createInterface({
|
|
78
|
+
input: this.process.stdout,
|
|
79
|
+
terminal: false,
|
|
80
|
+
});
|
|
81
|
+
this.rl.on('line', (line) => this.handleMessage(line));
|
|
82
|
+
this.process.on('error', (err) => {
|
|
83
|
+
this.log(`Process error: ${err.message}`);
|
|
84
|
+
this.handleExit(1);
|
|
85
|
+
});
|
|
86
|
+
this.process.on('exit', (code) => {
|
|
87
|
+
this.log(`Process exited with code ${code}`);
|
|
88
|
+
this.handleExit(code ?? 1);
|
|
89
|
+
});
|
|
90
|
+
// Wait for ready signal
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
const timeoutMs = 120_000; // allow more time for Vitest init (coverage, large projects)
|
|
93
|
+
const timeout = setTimeout(() => {
|
|
94
|
+
reject(new Error(`Worker ${this.id} did not become ready in time (${timeoutMs}ms)`));
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
this.once('ready', () => {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
this.once('error', (err) => {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
reject(err);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
handleMessage(line) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (!trimmed)
|
|
109
|
+
return;
|
|
110
|
+
// Only attempt to parse JSON lines; ignore any other stdout noise
|
|
111
|
+
if (!trimmed.startsWith('{')) {
|
|
112
|
+
if (this.debug)
|
|
113
|
+
this.log(`Non-JSON stdout: ${trimmed}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let msg;
|
|
117
|
+
try {
|
|
118
|
+
msg = JSON.parse(trimmed);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
this.log(`Invalid JSON from worker: ${line}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (msg.type === 'ready') {
|
|
125
|
+
this.ready = true;
|
|
126
|
+
this.emit('ready');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (msg.type === 'result') {
|
|
130
|
+
if (this.pendingTask) {
|
|
131
|
+
const { resolve, timeoutHandle } = this.pendingTask;
|
|
132
|
+
if (timeoutHandle)
|
|
133
|
+
clearTimeout(timeoutHandle);
|
|
134
|
+
this.pendingTask = null;
|
|
135
|
+
resolve({
|
|
136
|
+
killed: msg.killed ?? true,
|
|
137
|
+
durationMs: msg.durationMs ?? 0,
|
|
138
|
+
error: msg.error,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (msg.type === 'shutdown') {
|
|
144
|
+
this.emit('shutdown');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
handleExit(code) {
|
|
149
|
+
this.ready = false;
|
|
150
|
+
if (this.pendingTask && !this.shuttingDown) {
|
|
151
|
+
const { reject, timeoutHandle } = this.pendingTask;
|
|
152
|
+
if (timeoutHandle)
|
|
153
|
+
clearTimeout(timeoutHandle);
|
|
154
|
+
this.pendingTask = null;
|
|
155
|
+
reject(new Error(`Worker exited unexpectedly with code ${code}`));
|
|
156
|
+
}
|
|
157
|
+
this.emit('exit', code);
|
|
158
|
+
}
|
|
159
|
+
isReady() {
|
|
160
|
+
return this.ready && this.process !== null && !this.shuttingDown;
|
|
161
|
+
}
|
|
162
|
+
isBusy() {
|
|
163
|
+
return this.pendingTask !== null;
|
|
164
|
+
}
|
|
165
|
+
async run(mutant, tests, timeoutMs = 10_000) {
|
|
166
|
+
if (!this.isReady()) {
|
|
167
|
+
throw new Error(`Worker ${this.id} is not ready`);
|
|
168
|
+
}
|
|
169
|
+
if (this.isBusy()) {
|
|
170
|
+
throw new Error(`Worker ${this.id} is busy`);
|
|
171
|
+
}
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const timeoutHandle = setTimeout(() => {
|
|
174
|
+
if (this.pendingTask) {
|
|
175
|
+
this.pendingTask = null;
|
|
176
|
+
// Kill and restart the worker on timeout
|
|
177
|
+
this.kill();
|
|
178
|
+
resolve({ killed: true, durationMs: timeoutMs, error: 'timeout' });
|
|
179
|
+
}
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
this.pendingTask = { resolve, reject, timeoutHandle };
|
|
182
|
+
const request = JSON.stringify({ type: 'run', mutant, tests });
|
|
183
|
+
this.process.stdin.write(request + '\n');
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async shutdown() {
|
|
187
|
+
if (!this.process || this.shuttingDown)
|
|
188
|
+
return;
|
|
189
|
+
this.shuttingDown = true;
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
const timeout = setTimeout(() => {
|
|
192
|
+
this.kill();
|
|
193
|
+
resolve();
|
|
194
|
+
}, 5000);
|
|
195
|
+
this.once('shutdown', () => {
|
|
196
|
+
clearTimeout(timeout);
|
|
197
|
+
resolve();
|
|
198
|
+
});
|
|
199
|
+
this.process.stdin.write(JSON.stringify({ type: 'shutdown' }) + '\n');
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
kill() {
|
|
203
|
+
if (this.process) {
|
|
204
|
+
try {
|
|
205
|
+
this.process.kill('SIGKILL');
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Ignore
|
|
209
|
+
}
|
|
210
|
+
this.process = null;
|
|
211
|
+
}
|
|
212
|
+
this.ready = false;
|
|
213
|
+
}
|
|
214
|
+
log(msg) {
|
|
215
|
+
if (this.debug) {
|
|
216
|
+
console.error(`[VitestWorker-${this.id}] ${msg}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export class VitestPool {
|
|
221
|
+
constructor(options) {
|
|
222
|
+
this.workers = [];
|
|
223
|
+
this.availableWorkers = [];
|
|
224
|
+
this.waitingTasks = [];
|
|
225
|
+
this.initialized = false;
|
|
226
|
+
this.shuttingDown = false;
|
|
227
|
+
this.options = {
|
|
228
|
+
cwd: options.cwd,
|
|
229
|
+
concurrency: options.concurrency,
|
|
230
|
+
vitestConfig: options.vitestConfig,
|
|
231
|
+
timeoutMs: options.timeoutMs ?? 10_000,
|
|
232
|
+
debug: options.debug ?? false,
|
|
233
|
+
createWorker: options.createWorker,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
async init() {
|
|
237
|
+
if (this.initialized)
|
|
238
|
+
return;
|
|
239
|
+
this.log(`Initializing pool with ${this.options.concurrency} workers`);
|
|
240
|
+
const startPromises = [];
|
|
241
|
+
for (let i = 0; i < this.options.concurrency; i++) {
|
|
242
|
+
const worker = this.options.createWorker?.(`w${i}`, {
|
|
243
|
+
cwd: this.options.cwd,
|
|
244
|
+
vitestConfig: this.options.vitestConfig,
|
|
245
|
+
debug: this.options.debug,
|
|
246
|
+
}) ??
|
|
247
|
+
new VitestWorker(`w${i}`, this.options.cwd, this.options.vitestConfig, this.options.debug);
|
|
248
|
+
worker.on('exit', () => {
|
|
249
|
+
if (!this.shuttingDown) {
|
|
250
|
+
this.handleWorkerExit(worker);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
this.workers.push(worker);
|
|
254
|
+
startPromises.push(worker.start().then(() => {
|
|
255
|
+
this.availableWorkers.push(worker);
|
|
256
|
+
this.log(`Worker ${worker.id} ready`);
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
await Promise.all(startPromises);
|
|
260
|
+
this.initialized = true;
|
|
261
|
+
this.log('Pool initialized');
|
|
262
|
+
}
|
|
263
|
+
handleWorkerExit(worker) {
|
|
264
|
+
// Remove from available list
|
|
265
|
+
const availIdx = this.availableWorkers.indexOf(worker);
|
|
266
|
+
if (availIdx >= 0) {
|
|
267
|
+
this.availableWorkers.splice(availIdx, 1);
|
|
268
|
+
}
|
|
269
|
+
// Try to restart the worker
|
|
270
|
+
this.log(`Worker ${worker.id} exited, attempting restart`);
|
|
271
|
+
const newWorker = this.options.createWorker?.(worker.id, {
|
|
272
|
+
cwd: this.options.cwd,
|
|
273
|
+
vitestConfig: this.options.vitestConfig,
|
|
274
|
+
debug: this.options.debug,
|
|
275
|
+
}) ??
|
|
276
|
+
new VitestWorker(worker.id, this.options.cwd, this.options.vitestConfig, this.options.debug);
|
|
277
|
+
const idx = this.workers.indexOf(worker);
|
|
278
|
+
if (idx >= 0) {
|
|
279
|
+
this.workers[idx] = newWorker;
|
|
280
|
+
}
|
|
281
|
+
newWorker.on('exit', () => {
|
|
282
|
+
if (!this.shuttingDown) {
|
|
283
|
+
this.handleWorkerExit(newWorker);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
newWorker.start()
|
|
287
|
+
.then(() => {
|
|
288
|
+
this.releaseWorker(newWorker);
|
|
289
|
+
this.log(`Worker ${newWorker.id} restarted`);
|
|
290
|
+
})
|
|
291
|
+
.catch((err) => {
|
|
292
|
+
this.log(`Failed to restart worker ${newWorker.id}: ${err}`);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async acquireWorker() {
|
|
296
|
+
// Try to get an available worker
|
|
297
|
+
const worker = this.availableWorkers.shift();
|
|
298
|
+
if (worker) {
|
|
299
|
+
return worker;
|
|
300
|
+
}
|
|
301
|
+
// Wait for one to become available
|
|
302
|
+
return new Promise((resolve) => {
|
|
303
|
+
this.waitingTasks.push(resolve);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
releaseWorker(worker) {
|
|
307
|
+
// If someone is waiting, give them the worker directly
|
|
308
|
+
const waiting = this.waitingTasks.shift();
|
|
309
|
+
if (waiting) {
|
|
310
|
+
waiting(worker);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Otherwise return to the pool
|
|
314
|
+
if (worker.isReady()) {
|
|
315
|
+
this.availableWorkers.push(worker);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async run(mutant, tests) {
|
|
319
|
+
if (!this.initialized) {
|
|
320
|
+
throw new Error('Pool not initialized. Call init() first.');
|
|
321
|
+
}
|
|
322
|
+
if (this.shuttingDown) {
|
|
323
|
+
throw new Error('Pool is shutting down');
|
|
324
|
+
}
|
|
325
|
+
const worker = await this.acquireWorker();
|
|
326
|
+
try {
|
|
327
|
+
const result = await worker.run(mutant, tests, this.options.timeoutMs);
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
this.releaseWorker(worker);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async shutdown() {
|
|
335
|
+
if (this.shuttingDown)
|
|
336
|
+
return;
|
|
337
|
+
this.shuttingDown = true;
|
|
338
|
+
this.log('Shutting down pool');
|
|
339
|
+
await Promise.all(this.workers.map((w) => w.shutdown()));
|
|
340
|
+
this.workers = [];
|
|
341
|
+
this.availableWorkers = [];
|
|
342
|
+
this.initialized = false;
|
|
343
|
+
this.log('Pool shut down');
|
|
344
|
+
}
|
|
345
|
+
log(msg) {
|
|
346
|
+
if (this.options.debug) {
|
|
347
|
+
console.error(`[VitestPool] ${msg}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Run a single mutation using the pool.
|
|
353
|
+
* Convenience function for integration with orchestrator.
|
|
354
|
+
*/
|
|
355
|
+
export async function runWithPool(pool, mutant, tests) {
|
|
356
|
+
try {
|
|
357
|
+
const result = await pool.run(mutant, [...tests]);
|
|
358
|
+
if (result.error === 'timeout') {
|
|
359
|
+
return { status: 'timeout', durationMs: result.durationMs, error: result.error };
|
|
360
|
+
}
|
|
361
|
+
if (result.error && !result.killed) {
|
|
362
|
+
return { status: 'error', durationMs: result.durationMs, error: result.error };
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
status: result.killed ? 'killed' : 'escaped',
|
|
366
|
+
durationMs: result.durationMs,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
return {
|
|
371
|
+
status: 'error',
|
|
372
|
+
durationMs: 0,
|
|
373
|
+
error: err instanceof Error ? err.message : String(err),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Vitest worker process.
|
|
3
|
+
*
|
|
4
|
+
* This worker stays alive and receives mutation tasks via stdin,
|
|
5
|
+
* using Vitest's programmatic API to rerun tests without process restart.
|
|
6
|
+
*
|
|
7
|
+
* Communication protocol (JSON-RPC over stdin/stdout):
|
|
8
|
+
*
|
|
9
|
+
* Request: { "type": "run", "mutant": { file, code, id, name }, "tests": string[] }
|
|
10
|
+
* Response: { "type": "result", "killed": boolean, "durationMs": number }
|
|
11
|
+
*
|
|
12
|
+
* Request: { "type": "shutdown" }
|
|
13
|
+
* Response: { "type": "shutdown", "ok": true }
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Vitest worker process.
|
|
3
|
+
*
|
|
4
|
+
* This worker stays alive and receives mutation tasks via stdin,
|
|
5
|
+
* using Vitest's programmatic API to rerun tests without process restart.
|
|
6
|
+
*
|
|
7
|
+
* Communication protocol (JSON-RPC over stdin/stdout):
|
|
8
|
+
*
|
|
9
|
+
* Request: { "type": "run", "mutant": { file, code, id, name }, "tests": string[] }
|
|
10
|
+
* Response: { "type": "result", "killed": boolean, "durationMs": number }
|
|
11
|
+
*
|
|
12
|
+
* Request: { "type": "shutdown" }
|
|
13
|
+
* Response: { "type": "shutdown", "ok": true }
|
|
14
|
+
*/
|
|
15
|
+
import * as readline from 'node:readline';
|
|
16
|
+
import { createVitestWorkerRuntime } from '../../adapters/vitest/worker-runtime.js';
|
|
17
|
+
// Global state for redirect - shared with the plugin via globalThis
|
|
18
|
+
// Type is declared in pool-plugin.ts
|
|
19
|
+
globalThis.__mutineer_redirect__ = { from: null, to: null };
|
|
20
|
+
function send(response) {
|
|
21
|
+
console.log(JSON.stringify(response));
|
|
22
|
+
}
|
|
23
|
+
function log(msg) {
|
|
24
|
+
if (process.env.MUTINEER_DEBUG !== '1')
|
|
25
|
+
return;
|
|
26
|
+
console.error(`[vitest-worker] ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const workerId = process.env.MUTINEER_WORKER_ID ?? 'unknown';
|
|
30
|
+
const cwd = process.env.MUTINEER_CWD ?? process.cwd();
|
|
31
|
+
const vitestConfigPath = process.env.MUTINEER_VITEST_CONFIG;
|
|
32
|
+
log(`Starting worker ${workerId} in ${cwd}`);
|
|
33
|
+
const runtime = createVitestWorkerRuntime({
|
|
34
|
+
workerId,
|
|
35
|
+
cwd,
|
|
36
|
+
vitestConfigPath,
|
|
37
|
+
debug: process.env.MUTINEER_DEBUG === '1',
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await runtime.init();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error('[vitest-worker] Failed to initialize Vitest:', err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// Signal ready
|
|
47
|
+
send({ type: 'ready', workerId });
|
|
48
|
+
// Process requests from stdin
|
|
49
|
+
const rl = readline.createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
terminal: false,
|
|
52
|
+
});
|
|
53
|
+
for await (const line of rl) {
|
|
54
|
+
if (!line.trim())
|
|
55
|
+
continue;
|
|
56
|
+
let request;
|
|
57
|
+
try {
|
|
58
|
+
request = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log(`Invalid JSON: ${line}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (request.type === 'shutdown') {
|
|
65
|
+
log('Shutting down');
|
|
66
|
+
await runtime.shutdown();
|
|
67
|
+
send({ type: 'shutdown', ok: true });
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
if (request.type === 'run') {
|
|
71
|
+
try {
|
|
72
|
+
const { mutant, tests } = request;
|
|
73
|
+
const result = await runtime.run(mutant, tests);
|
|
74
|
+
send({
|
|
75
|
+
type: 'result',
|
|
76
|
+
killed: result.killed,
|
|
77
|
+
durationMs: result.durationMs,
|
|
78
|
+
error: result.error,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// On error, treat as killed (conservative)
|
|
83
|
+
send({
|
|
84
|
+
type: 'result',
|
|
85
|
+
killed: true,
|
|
86
|
+
durationMs: 0,
|
|
87
|
+
error: String(err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
main().catch((err) => {
|
|
94
|
+
console.error('[vitest-worker] Fatal error:', err);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Vitest worker process.
|
|
3
|
+
*
|
|
4
|
+
* This worker stays alive and receives mutation tasks via stdin,
|
|
5
|
+
* using Vitest's programmatic API to rerun tests without process restart.
|
|
6
|
+
*
|
|
7
|
+
* Communication protocol (JSON-RPC over stdin/stdout):
|
|
8
|
+
*
|
|
9
|
+
* Request: { "type": "run", "mutant": { file, code, id, name }, "tests": string[] }
|
|
10
|
+
* Response: { "type": "result", "killed": boolean, "durationMs": number }
|
|
11
|
+
*
|
|
12
|
+
* Request: { "type": "shutdown" }
|
|
13
|
+
* Response: { "type": "shutdown", "ok": true }
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Vitest worker process.
|
|
3
|
+
*
|
|
4
|
+
* This worker stays alive and receives mutation tasks via stdin,
|
|
5
|
+
* using Vitest's programmatic API to rerun tests without process restart.
|
|
6
|
+
*
|
|
7
|
+
* Communication protocol (JSON-RPC over stdin/stdout):
|
|
8
|
+
*
|
|
9
|
+
* Request: { "type": "run", "mutant": { file, code, id, name }, "tests": string[] }
|
|
10
|
+
* Response: { "type": "result", "killed": boolean, "durationMs": number }
|
|
11
|
+
*
|
|
12
|
+
* Request: { "type": "shutdown" }
|
|
13
|
+
* Response: { "type": "shutdown", "ok": true }
|
|
14
|
+
*/
|
|
15
|
+
import * as readline from 'node:readline';
|
|
16
|
+
import { createVitestWorkerRuntime } from '../adapters/vitest-worker-runtime.js';
|
|
17
|
+
// Global state for redirect - shared with the plugin via globalThis
|
|
18
|
+
// Type is declared in pool-plugin.ts
|
|
19
|
+
globalThis.__mutineer_redirect__ = { from: null, to: null };
|
|
20
|
+
function send(response) {
|
|
21
|
+
console.log(JSON.stringify(response));
|
|
22
|
+
}
|
|
23
|
+
function log(msg) {
|
|
24
|
+
if (process.env.MUTINEER_DEBUG !== '1')
|
|
25
|
+
return;
|
|
26
|
+
console.error(`[vitest-worker] ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const workerId = process.env.MUTINEER_WORKER_ID ?? 'unknown';
|
|
30
|
+
const cwd = process.env.MUTINEER_CWD ?? process.cwd();
|
|
31
|
+
const vitestConfigPath = process.env.MUTINEER_VITEST_CONFIG;
|
|
32
|
+
log(`Starting worker ${workerId} in ${cwd}`);
|
|
33
|
+
const runtime = createVitestWorkerRuntime({
|
|
34
|
+
workerId,
|
|
35
|
+
cwd,
|
|
36
|
+
vitestConfigPath,
|
|
37
|
+
debug: process.env.MUTINEER_DEBUG === '1',
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await runtime.init();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error('[vitest-worker] Failed to initialize Vitest:', err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// Signal ready
|
|
47
|
+
send({ type: 'ready', workerId });
|
|
48
|
+
// Process requests from stdin
|
|
49
|
+
const rl = readline.createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
terminal: false,
|
|
52
|
+
});
|
|
53
|
+
for await (const line of rl) {
|
|
54
|
+
if (!line.trim())
|
|
55
|
+
continue;
|
|
56
|
+
let request;
|
|
57
|
+
try {
|
|
58
|
+
request = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log(`Invalid JSON: ${line}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (request.type === 'shutdown') {
|
|
65
|
+
log('Shutting down');
|
|
66
|
+
await runtime.shutdown();
|
|
67
|
+
send({ type: 'shutdown', ok: true });
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
if (request.type === 'run') {
|
|
71
|
+
try {
|
|
72
|
+
const { mutant, tests } = request;
|
|
73
|
+
const result = await runtime.run(mutant, tests);
|
|
74
|
+
send({
|
|
75
|
+
type: 'result',
|
|
76
|
+
killed: result.killed,
|
|
77
|
+
durationMs: result.durationMs,
|
|
78
|
+
error: result.error,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// On error, treat as killed (conservative)
|
|
83
|
+
send({
|
|
84
|
+
type: 'result',
|
|
85
|
+
killed: true,
|
|
86
|
+
durationMs: 0,
|
|
87
|
+
error: String(err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
main().catch((err) => {
|
|
94
|
+
console.error('[vitest-worker] Fatal error:', err);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for test runner adapters.
|
|
3
|
+
*
|
|
4
|
+
* This module provides common functionality used by both Jest and Vitest adapters,
|
|
5
|
+
* including mutant file path generation and redirect state management.
|
|
6
|
+
*/
|
|
7
|
+
export { getMutantFilePath } from './mutant-paths.js';
|
|
8
|
+
export { setRedirect, getRedirect, clearRedirect, initializeRedirectState, } from './redirect-state.js';
|
|
9
|
+
export type { RedirectConfig } from './redirect-state.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for test runner adapters.
|
|
3
|
+
*
|
|
4
|
+
* This module provides common functionality used by both Jest and Vitest adapters,
|
|
5
|
+
* including mutant file path generation and redirect state management.
|
|
6
|
+
*/
|
|
7
|
+
export { getMutantFilePath } from './mutant-paths.js';
|
|
8
|
+
export { setRedirect, getRedirect, clearRedirect, initializeRedirectState, } from './redirect-state.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for managing mutant file paths.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate a file path for a mutant file in the __mutineer__ directory.
|
|
6
|
+
*
|
|
7
|
+
* @param originalFile - Path to the original source file
|
|
8
|
+
* @param mutantId - Unique identifier for the mutant
|
|
9
|
+
* @returns Absolute path where the mutant file should be written
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* getMutantFilePath('/src/foo.ts', 'mutant#42')
|
|
13
|
+
* // Returns: '/src/__mutineer__/foo_42.ts'
|
|
14
|
+
*/
|
|
15
|
+
export declare function getMutantFilePath(originalFile: string, mutantId: string): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for managing mutant file paths.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
/**
|
|
7
|
+
* Generate a file path for a mutant file in the __mutineer__ directory.
|
|
8
|
+
*
|
|
9
|
+
* @param originalFile - Path to the original source file
|
|
10
|
+
* @param mutantId - Unique identifier for the mutant
|
|
11
|
+
* @returns Absolute path where the mutant file should be written
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* getMutantFilePath('/src/foo.ts', 'mutant#42')
|
|
15
|
+
* // Returns: '/src/__mutineer__/foo_42.ts'
|
|
16
|
+
*/
|
|
17
|
+
export function getMutantFilePath(originalFile, mutantId) {
|
|
18
|
+
const dir = path.dirname(originalFile);
|
|
19
|
+
const ext = path.extname(originalFile);
|
|
20
|
+
const basename = path.basename(originalFile, ext);
|
|
21
|
+
const mutineerDir = path.join(dir, '__mutineer__');
|
|
22
|
+
if (!fs.existsSync(mutineerDir)) {
|
|
23
|
+
fs.mkdirSync(mutineerDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const idMatch = mutantId.match(/#(\d+)$/);
|
|
26
|
+
const suffix = idMatch
|
|
27
|
+
? idMatch[1]
|
|
28
|
+
: mutantId.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 20);
|
|
29
|
+
return path.join(mutineerDir, `${basename}_${suffix}${ext}`);
|
|
30
|
+
}
|