@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,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Test Runner Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the TestRunnerAdapter interface for Vitest.
|
|
5
|
+
* Handles baseline test runs, mutant execution via worker pool,
|
|
6
|
+
* and coverage configuration detection.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { VitestPool } from '../pool/index.js';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the Vitest CLI entry point.
|
|
16
|
+
*/
|
|
17
|
+
function resolveVitestPath() {
|
|
18
|
+
try {
|
|
19
|
+
return require.resolve('vitest/vitest.mjs');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
const pkgJson = require.resolve('vitest/package.json');
|
|
23
|
+
return path.join(path.dirname(pkgJson), 'vitest.mjs');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Strip mutineer-specific CLI args that shouldn't be passed to Vitest.
|
|
28
|
+
*/
|
|
29
|
+
function stripMutineerArgs(args) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const consumeNext = new Set([
|
|
32
|
+
'--concurrency',
|
|
33
|
+
'--progress',
|
|
34
|
+
'--min-kill-percent',
|
|
35
|
+
'--config',
|
|
36
|
+
'-c',
|
|
37
|
+
'--coverage-file',
|
|
38
|
+
]);
|
|
39
|
+
const dropExact = new Set([
|
|
40
|
+
'-m',
|
|
41
|
+
'--mutate',
|
|
42
|
+
'--changed',
|
|
43
|
+
'--changed-with-deps',
|
|
44
|
+
'--only-covered-lines',
|
|
45
|
+
'--per-test-coverage',
|
|
46
|
+
'--perTestCoverage',
|
|
47
|
+
]);
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
const a = args[i];
|
|
50
|
+
if (dropExact.has(a))
|
|
51
|
+
continue;
|
|
52
|
+
if (consumeNext.has(a)) {
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (a.startsWith('--min-kill-percent='))
|
|
57
|
+
continue;
|
|
58
|
+
if (a.startsWith('--config=') || a.startsWith('-c='))
|
|
59
|
+
continue;
|
|
60
|
+
out.push(a);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Ensure the Vitest config arg is included if specified.
|
|
66
|
+
*/
|
|
67
|
+
function ensureConfigArg(args, vitestConfig, cwd) {
|
|
68
|
+
if (!vitestConfig)
|
|
69
|
+
return args;
|
|
70
|
+
if (args.some((a) => a === '--config' || a === '-c' || a.startsWith('--config=') || a.startsWith('-c='))) {
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
const resolved = cwd ? path.resolve(cwd, vitestConfig) : vitestConfig;
|
|
74
|
+
return [...args, '--config', resolved];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build Vitest CLI arguments for the given mode.
|
|
78
|
+
*/
|
|
79
|
+
function buildVitestArgs(args, mode) {
|
|
80
|
+
const result = [...args];
|
|
81
|
+
if (!result.includes('run') && !result.includes('--run'))
|
|
82
|
+
result.unshift('run');
|
|
83
|
+
if (!result.some((a) => a.startsWith('--watch')))
|
|
84
|
+
result.push('--watch=false');
|
|
85
|
+
if (!result.some((a) => a.startsWith('--passWithNoTests')))
|
|
86
|
+
result.push('--passWithNoTests');
|
|
87
|
+
if (mode === 'baseline-with-coverage') {
|
|
88
|
+
if (!result.some((a) => a.startsWith('--coverage'))) {
|
|
89
|
+
result.push('--coverage.enabled=true', '--coverage.reporter=json');
|
|
90
|
+
}
|
|
91
|
+
if (!result.some((a) => a.startsWith('--coverage.perTest='))) {
|
|
92
|
+
result.push('--coverage.perTest=true');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Vitest adapter implementation.
|
|
99
|
+
*/
|
|
100
|
+
export class VitestAdapter {
|
|
101
|
+
constructor(options) {
|
|
102
|
+
this.name = 'vitest';
|
|
103
|
+
this.pool = null;
|
|
104
|
+
this.baseArgs = [];
|
|
105
|
+
this.options = options;
|
|
106
|
+
this.vitestPath = resolveVitestPath();
|
|
107
|
+
// Prepare base args by stripping mutineer-specific flags
|
|
108
|
+
const stripped = stripMutineerArgs(options.cliArgs);
|
|
109
|
+
this.baseArgs = ensureConfigArg(stripped, options.config.vitestConfig, options.cwd);
|
|
110
|
+
}
|
|
111
|
+
async init(concurrencyOverride) {
|
|
112
|
+
const workerCount = Math.max(1, concurrencyOverride ?? this.options.concurrency);
|
|
113
|
+
this.pool = new VitestPool({
|
|
114
|
+
cwd: this.options.cwd,
|
|
115
|
+
concurrency: workerCount,
|
|
116
|
+
vitestConfig: this.options.config.vitestConfig,
|
|
117
|
+
timeoutMs: this.options.timeoutMs,
|
|
118
|
+
debug: this.options.debug,
|
|
119
|
+
});
|
|
120
|
+
await this.pool.init();
|
|
121
|
+
}
|
|
122
|
+
async runBaseline(tests, options) {
|
|
123
|
+
const mode = options.collectCoverage ? 'baseline-with-coverage' : 'baseline';
|
|
124
|
+
const args = buildVitestArgs(this.baseArgs, mode);
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
const env = { ...process.env };
|
|
127
|
+
env.VITEST_WATCH = 'false';
|
|
128
|
+
if (!env.CI)
|
|
129
|
+
env.CI = '1';
|
|
130
|
+
const child = spawn(process.execPath, [this.vitestPath, ...args, ...tests], {
|
|
131
|
+
cwd: this.options.cwd,
|
|
132
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
133
|
+
env,
|
|
134
|
+
});
|
|
135
|
+
child.on('error', (err) => {
|
|
136
|
+
if (this.options.debug)
|
|
137
|
+
console.error('Failed to spawn vitest process', err);
|
|
138
|
+
resolve(false);
|
|
139
|
+
});
|
|
140
|
+
child.on('exit', (code) => {
|
|
141
|
+
resolve(code === 0);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async runMutant(mutant, tests) {
|
|
146
|
+
if (!this.pool) {
|
|
147
|
+
throw new Error('VitestAdapter not initialized. Call init() first.');
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const result = await this.pool.run(mutant, [...tests]);
|
|
151
|
+
if (result.error === 'timeout') {
|
|
152
|
+
return {
|
|
153
|
+
status: 'timeout',
|
|
154
|
+
durationMs: result.durationMs,
|
|
155
|
+
error: result.error,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (result.error) {
|
|
159
|
+
return {
|
|
160
|
+
status: 'error',
|
|
161
|
+
durationMs: result.durationMs,
|
|
162
|
+
error: result.error,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
status: result.killed ? 'killed' : 'escaped',
|
|
167
|
+
durationMs: result.durationMs,
|
|
168
|
+
error: result.error,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return {
|
|
173
|
+
status: 'error',
|
|
174
|
+
durationMs: 0,
|
|
175
|
+
error: err instanceof Error ? err.message : String(err),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async shutdown() {
|
|
180
|
+
if (this.pool) {
|
|
181
|
+
await this.pool.shutdown();
|
|
182
|
+
this.pool = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
hasCoverageProvider() {
|
|
186
|
+
try {
|
|
187
|
+
require.resolve('@vitest/coverage-v8/package.json', { paths: [this.options.cwd] });
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async detectCoverageConfig() {
|
|
195
|
+
const configPath = this.options.config.vitestConfig;
|
|
196
|
+
if (!configPath) {
|
|
197
|
+
return { perTestEnabled: false, coverageEnabled: false };
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const abs = path.isAbsolute(configPath) ? configPath : path.join(this.options.cwd, configPath);
|
|
201
|
+
const content = await fs.readFile(abs, 'utf8');
|
|
202
|
+
const perTestEnabled = /perTest\s*:\s*true/.test(content);
|
|
203
|
+
let coverageEnabled = false;
|
|
204
|
+
if (!/coverage\s*\.\s*enabled\s*:\s*false/.test(content) && !/coverage\s*:\s*false/.test(content)) {
|
|
205
|
+
coverageEnabled = /coverage\s*:/.test(content);
|
|
206
|
+
}
|
|
207
|
+
return { perTestEnabled, coverageEnabled };
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { perTestEnabled: false, coverageEnabled: false };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if coverage is requested via CLI args.
|
|
216
|
+
*/
|
|
217
|
+
export function isCoverageRequestedInArgs(args) {
|
|
218
|
+
let requested = false;
|
|
219
|
+
let disabled = false;
|
|
220
|
+
const isFalsey = (v) => typeof v === 'string' && /^(false|0|off)$/i.test(v);
|
|
221
|
+
for (let i = 0; i < args.length; i++) {
|
|
222
|
+
const arg = args[i];
|
|
223
|
+
if (arg === '--no-coverage') {
|
|
224
|
+
disabled = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (arg === '--coverage') {
|
|
228
|
+
requested = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (arg === '--coverage.enabled') {
|
|
232
|
+
const next = args[i + 1];
|
|
233
|
+
if (isFalsey(next))
|
|
234
|
+
disabled = true;
|
|
235
|
+
else
|
|
236
|
+
requested = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (arg.startsWith('--coverage.enabled=')) {
|
|
240
|
+
const val = arg.slice('--coverage.enabled='.length);
|
|
241
|
+
if (isFalsey(val))
|
|
242
|
+
disabled = true;
|
|
243
|
+
else
|
|
244
|
+
requested = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg.startsWith('--coverage=')) {
|
|
248
|
+
const val = arg.slice('--coverage='.length);
|
|
249
|
+
if (isFalsey(val))
|
|
250
|
+
disabled = true;
|
|
251
|
+
else
|
|
252
|
+
requested = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (arg.startsWith('--coverage.')) {
|
|
256
|
+
requested = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return requested && !disabled;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Factory function for creating VitestAdapter instances.
|
|
264
|
+
*/
|
|
265
|
+
export function createVitestAdapter(options) {
|
|
266
|
+
return new VitestAdapter(options);
|
|
267
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Argument Parsing
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for parsing command-line arguments.
|
|
5
|
+
* Easy to test without side effects.
|
|
6
|
+
*/
|
|
7
|
+
import type { MutineerConfig } from '../types/config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parsed CLI options for mutation testing.
|
|
10
|
+
*/
|
|
11
|
+
export interface ParsedCliOptions {
|
|
12
|
+
readonly configPath: string | undefined;
|
|
13
|
+
readonly wantsChanged: boolean;
|
|
14
|
+
readonly wantsChangedWithDeps: boolean;
|
|
15
|
+
readonly wantsOnlyCoveredLines: boolean;
|
|
16
|
+
readonly wantsPerTestCoverage: boolean;
|
|
17
|
+
readonly coverageFilePath: string | undefined;
|
|
18
|
+
readonly concurrency: number;
|
|
19
|
+
readonly progressMode: 'bar' | 'list' | 'quiet';
|
|
20
|
+
readonly minKillPercent: number | undefined;
|
|
21
|
+
readonly runner: 'vitest' | 'jest';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse a numeric CLI flag value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseFlagNumber(raw: string, flag: string): number;
|
|
27
|
+
/**
|
|
28
|
+
* Read a numeric flag from CLI args.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readNumberFlag(args: readonly string[], flag: string): number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Read a string flag from CLI args.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readStringFlag(args: readonly string[], flag: string, alias?: string): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Validate a percentage value (0-100).
|
|
37
|
+
*/
|
|
38
|
+
export declare function validatePercent(value: number | undefined, source: string): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Parse concurrency from CLI args or use default.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseConcurrency(args: readonly string[]): number;
|
|
43
|
+
/**
|
|
44
|
+
* Parse progress mode from CLI args.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseProgressMode(args: readonly string[]): 'bar' | 'list' | 'quiet';
|
|
47
|
+
/**
|
|
48
|
+
* Parse all CLI options.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseCliOptions(args: readonly string[], cfg: MutineerConfig): ParsedCliOptions;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Argument Parsing
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for parsing command-line arguments.
|
|
5
|
+
* Easy to test without side effects.
|
|
6
|
+
*/
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
/**
|
|
9
|
+
* Parse a numeric CLI flag value.
|
|
10
|
+
*/
|
|
11
|
+
export function parseFlagNumber(raw, flag) {
|
|
12
|
+
const num = Number(raw);
|
|
13
|
+
if (!Number.isFinite(num)) {
|
|
14
|
+
throw new Error(`Invalid value for ${flag}: ${raw}`);
|
|
15
|
+
}
|
|
16
|
+
return num;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read a numeric flag from CLI args.
|
|
20
|
+
*/
|
|
21
|
+
export function readNumberFlag(args, flag) {
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const arg = args[i];
|
|
24
|
+
if (arg === flag) {
|
|
25
|
+
const value = args[i + 1];
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
throw new Error(`Expected a numeric value after ${flag}`);
|
|
28
|
+
return parseFlagNumber(value, flag);
|
|
29
|
+
}
|
|
30
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
31
|
+
const value = arg.slice(flag.length + 1);
|
|
32
|
+
return parseFlagNumber(value, flag);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read a string flag from CLI args.
|
|
39
|
+
*/
|
|
40
|
+
export function readStringFlag(args, flag, alias) {
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (arg === flag || (alias && arg === alias)) {
|
|
44
|
+
const value = args[i + 1];
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
throw new Error(`Expected a value after ${arg}`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
51
|
+
return arg.slice(flag.length + 1);
|
|
52
|
+
}
|
|
53
|
+
if (alias && arg.startsWith(`${alias}=`)) {
|
|
54
|
+
return arg.slice(alias.length + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate a percentage value (0-100).
|
|
61
|
+
*/
|
|
62
|
+
export function validatePercent(value, source) {
|
|
63
|
+
if (value === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (!Number.isFinite(value)) {
|
|
66
|
+
throw new Error(`Invalid ${source}: expected a number between 0 and 100`);
|
|
67
|
+
}
|
|
68
|
+
if (value < 0 || value > 100) {
|
|
69
|
+
throw new Error(`Invalid ${source}: expected value between 0 and 100 (received ${value})`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse concurrency from CLI args or use default.
|
|
75
|
+
*/
|
|
76
|
+
export function parseConcurrency(args) {
|
|
77
|
+
const concIdx = args.indexOf('--concurrency');
|
|
78
|
+
const userConc = concIdx >= 0 ? Math.max(1, parseInt(args[concIdx + 1] || '', 10) || 0) : 0;
|
|
79
|
+
const defaultConc = Math.max(1, os.cpus().length - 1);
|
|
80
|
+
return userConc || defaultConc;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse progress mode from CLI args.
|
|
84
|
+
*/
|
|
85
|
+
export function parseProgressMode(args) {
|
|
86
|
+
const progIdx = args.indexOf('--progress');
|
|
87
|
+
const modeArg = progIdx >= 0 ? args[progIdx + 1] || 'bar' : 'bar';
|
|
88
|
+
return modeArg === 'list' ? 'list' : modeArg === 'quiet' ? 'quiet' : 'bar';
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse all CLI options.
|
|
92
|
+
*/
|
|
93
|
+
export function parseCliOptions(args, cfg) {
|
|
94
|
+
const configPath = readStringFlag(args, '--config', '-c');
|
|
95
|
+
const wantsChanged = args.includes('--changed');
|
|
96
|
+
const wantsChangedWithDeps = args.includes('--changed-with-deps');
|
|
97
|
+
const wantsOnlyCoveredLines = args.includes('--only-covered-lines') || cfg.onlyCoveredLines === true;
|
|
98
|
+
const wantsPerTestCoverage = args.includes('--per-test-coverage') || cfg.perTestCoverage === true;
|
|
99
|
+
const coverageFilePath = readStringFlag(args, '--coverage-file') ?? cfg.coverageFile;
|
|
100
|
+
const concurrency = parseConcurrency(args);
|
|
101
|
+
const progressMode = parseProgressMode(args);
|
|
102
|
+
const runnerFlag = readStringFlag(args, '--runner');
|
|
103
|
+
const runner = runnerFlag === 'jest' || runnerFlag === 'vitest'
|
|
104
|
+
? runnerFlag
|
|
105
|
+
: cfg.runner === 'jest'
|
|
106
|
+
? 'jest'
|
|
107
|
+
: 'vitest';
|
|
108
|
+
const cliKillPercent = validatePercent(readNumberFlag(args, '--min-kill-percent'), '--min-kill-percent');
|
|
109
|
+
const configKillPercent = validatePercent(cfg.minKillPercent, 'mutineer.config minKillPercent');
|
|
110
|
+
const minKillPercent = cliKillPercent ?? configKillPercent;
|
|
111
|
+
return {
|
|
112
|
+
configPath,
|
|
113
|
+
wantsChanged,
|
|
114
|
+
wantsChangedWithDeps,
|
|
115
|
+
wantsOnlyCoveredLines,
|
|
116
|
+
wantsPerTestCoverage,
|
|
117
|
+
coverageFilePath,
|
|
118
|
+
concurrency,
|
|
119
|
+
progressMode,
|
|
120
|
+
minKillPercent,
|
|
121
|
+
runner,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Management Module
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for managing mutation testing cache.
|
|
5
|
+
* Handles reading, writing, and decoding cache entries.
|
|
6
|
+
*/
|
|
7
|
+
import type { MutantCacheEntry } from '../types/mutant.js';
|
|
8
|
+
/**
|
|
9
|
+
* Clear the cache file at the start of a run.
|
|
10
|
+
*/
|
|
11
|
+
export declare function clearCacheOnStart(cwd: string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Save cache atomically using a temp file + rename.
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveCacheAtomic(cwd: string, cache: Record<string, MutantCacheEntry>): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Decode a cache key into its component parts.
|
|
18
|
+
* Cache keys have the format: testSig:codeSig:file:line,col:mutator
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodeCacheKey(key: string): {
|
|
21
|
+
file: string;
|
|
22
|
+
line: number;
|
|
23
|
+
col: number;
|
|
24
|
+
mutator: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Create a deterministic key for a set of test files.
|
|
28
|
+
*/
|
|
29
|
+
export declare function keyForTests(tests: readonly string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Create a short hash of a string (12 hex chars).
|
|
32
|
+
*/
|
|
33
|
+
export declare function hash(s: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Read the mutant cache from disk.
|
|
36
|
+
* Normalizes both old (string status) and new (object) formats.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readMutantCache(cwd: string): Promise<Record<string, MutantCacheEntry>>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Management Module
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for managing mutation testing cache.
|
|
5
|
+
* Handles reading, writing, and decoding cache entries.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
const CACHE_FILENAME = '.mutate-cache.json';
|
|
11
|
+
/**
|
|
12
|
+
* Clear the cache file at the start of a run.
|
|
13
|
+
*/
|
|
14
|
+
export async function clearCacheOnStart(cwd) {
|
|
15
|
+
const p = path.join(cwd, CACHE_FILENAME);
|
|
16
|
+
try {
|
|
17
|
+
await fs.unlink(p);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Ignore if file doesn't exist
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Save cache atomically using a temp file + rename.
|
|
25
|
+
*/
|
|
26
|
+
export async function saveCacheAtomic(cwd, cache) {
|
|
27
|
+
const p = path.join(cwd, CACHE_FILENAME);
|
|
28
|
+
const tmp = p + '.tmp';
|
|
29
|
+
const json = JSON.stringify(cache, null, 2);
|
|
30
|
+
await fs.writeFile(tmp, json, 'utf8');
|
|
31
|
+
await fs.rename(tmp, p);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Decode a cache key into its component parts.
|
|
35
|
+
* Cache keys have the format: testSig:codeSig:file:line,col:mutator
|
|
36
|
+
*/
|
|
37
|
+
export function decodeCacheKey(key) {
|
|
38
|
+
let file = key;
|
|
39
|
+
let line = 0;
|
|
40
|
+
let col = 0;
|
|
41
|
+
let mutator = 'unknown';
|
|
42
|
+
try {
|
|
43
|
+
const lastColon = key.lastIndexOf(':');
|
|
44
|
+
if (lastColon === -1)
|
|
45
|
+
return { file, line, col, mutator };
|
|
46
|
+
mutator = key.slice(lastColon + 1);
|
|
47
|
+
let rest = key.slice(0, lastColon);
|
|
48
|
+
const positionColon = rest.lastIndexOf(':');
|
|
49
|
+
if (positionColon === -1)
|
|
50
|
+
return { file, line, col, mutator };
|
|
51
|
+
const posRaw = rest.slice(positionColon + 1);
|
|
52
|
+
rest = rest.slice(0, positionColon);
|
|
53
|
+
const [lineStr, colStr] = posRaw.split(',');
|
|
54
|
+
const maybeLine = parseInt(lineStr, 10);
|
|
55
|
+
const maybeCol = parseInt(colStr, 10);
|
|
56
|
+
if (Number.isFinite(maybeLine))
|
|
57
|
+
line = maybeLine;
|
|
58
|
+
if (Number.isFinite(maybeCol))
|
|
59
|
+
col = maybeCol;
|
|
60
|
+
const firstColon = rest.indexOf(':');
|
|
61
|
+
if (firstColon === -1)
|
|
62
|
+
return { file, line, col, mutator };
|
|
63
|
+
const restAfterFirst = rest.slice(firstColon + 1);
|
|
64
|
+
const secondColon = restAfterFirst.indexOf(':');
|
|
65
|
+
if (secondColon === -1)
|
|
66
|
+
return { file, line, col, mutator };
|
|
67
|
+
file = restAfterFirst.slice(secondColon + 1) || file;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// fall through and return best-effort data
|
|
71
|
+
}
|
|
72
|
+
return { file, line, col, mutator };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a deterministic key for a set of test files.
|
|
76
|
+
*/
|
|
77
|
+
export function keyForTests(tests) {
|
|
78
|
+
return JSON.stringify([...tests].sort());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create a short hash of a string (12 hex chars).
|
|
82
|
+
*/
|
|
83
|
+
export function hash(s) {
|
|
84
|
+
return crypto.createHash('sha1').update(s).digest('hex').slice(0, 12);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the mutant cache from disk.
|
|
88
|
+
* Normalizes both old (string status) and new (object) formats.
|
|
89
|
+
*/
|
|
90
|
+
export async function readMutantCache(cwd) {
|
|
91
|
+
const p = path.join(cwd, CACHE_FILENAME);
|
|
92
|
+
try {
|
|
93
|
+
const data = await fs.readFile(p, 'utf8');
|
|
94
|
+
const raw = JSON.parse(data);
|
|
95
|
+
const normalised = {};
|
|
96
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
97
|
+
const decoded = decodeCacheKey(key);
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
normalised[key] = { ...decoded, status: value };
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (value && typeof value === 'object' && 'status' in value) {
|
|
103
|
+
const entry = value;
|
|
104
|
+
const status = entry.status ?? 'skipped';
|
|
105
|
+
const file = entry.file ?? decoded.file;
|
|
106
|
+
const line = entry.line ?? decoded.line;
|
|
107
|
+
const col = entry.col ?? decoded.col;
|
|
108
|
+
const mutator = entry.mutator ?? decoded.mutator;
|
|
109
|
+
normalised[key] = { status, file, line, col, mutator };
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return normalised;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ChangedFilesOptions {
|
|
2
|
+
includeDeps?: boolean;
|
|
3
|
+
baseRef?: string;
|
|
4
|
+
quiet?: boolean;
|
|
5
|
+
/** Max depth for dependency resolution (default: 1, meaning direct imports only) */
|
|
6
|
+
maxDepth?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* List all changed files in a Git repository.
|
|
10
|
+
*
|
|
11
|
+
* Returns files that are:
|
|
12
|
+
* - Committed but not yet in baseRef (compared to baseRef...HEAD)
|
|
13
|
+
* - Modified but not yet committed (compared to HEAD)
|
|
14
|
+
* - Untracked but not ignored
|
|
15
|
+
*
|
|
16
|
+
* When includeDeps is true, also includes local dependencies of changed files.
|
|
17
|
+
*
|
|
18
|
+
* @param cwd - Working directory (will search for repo root if needed)
|
|
19
|
+
* @param options - Configuration options
|
|
20
|
+
* @returns Array of absolute paths to changed files
|
|
21
|
+
*/
|
|
22
|
+
export declare function listChangedFiles(cwd: string, options?: ChangedFilesOptions): string[];
|