@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,105 @@
|
|
|
1
|
+
import { createVitest } from 'vitest/node';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { poolMutineerPlugin } from './plugin.js';
|
|
5
|
+
import { getMutantFilePath, setRedirect, clearRedirect, } from '../shared/index.js';
|
|
6
|
+
import { createLogger } from '../../utils/logger.js';
|
|
7
|
+
const log = createLogger('vitest-runtime');
|
|
8
|
+
export class VitestWorkerRuntime {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.vitest = null;
|
|
12
|
+
}
|
|
13
|
+
async init() {
|
|
14
|
+
try {
|
|
15
|
+
this.vitest = await createVitest('test', {
|
|
16
|
+
watch: true,
|
|
17
|
+
reporters: ['dot'],
|
|
18
|
+
silent: true,
|
|
19
|
+
pool: 'forks',
|
|
20
|
+
bail: 1,
|
|
21
|
+
...(this.options.vitestConfigPath
|
|
22
|
+
? { config: this.options.vitestConfigPath }
|
|
23
|
+
: {}),
|
|
24
|
+
}, {
|
|
25
|
+
plugins: [poolMutineerPlugin()],
|
|
26
|
+
});
|
|
27
|
+
await this.vitest.init();
|
|
28
|
+
log.debug(`Vitest initialized for worker ${this.options.workerId}`);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
log.error(`Failed to initialize Vitest: ${err}`);
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async shutdown() {
|
|
36
|
+
if (!this.vitest)
|
|
37
|
+
return;
|
|
38
|
+
await this.vitest.close();
|
|
39
|
+
this.vitest = null;
|
|
40
|
+
}
|
|
41
|
+
async run(mutant, tests) {
|
|
42
|
+
if (!this.vitest) {
|
|
43
|
+
throw new Error('Vitest runtime not initialized');
|
|
44
|
+
}
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
const mutantPath = getMutantFilePath(mutant.file, mutant.id);
|
|
48
|
+
fs.writeFileSync(mutantPath, mutant.code, 'utf8');
|
|
49
|
+
log.debug(`Wrote mutant to ${mutantPath}`);
|
|
50
|
+
setRedirect({
|
|
51
|
+
from: path.resolve(mutant.file),
|
|
52
|
+
to: mutantPath,
|
|
53
|
+
});
|
|
54
|
+
this.vitest.invalidateFile(mutant.file);
|
|
55
|
+
log.debug(`Invalidated ${mutant.file}`);
|
|
56
|
+
const specs = [];
|
|
57
|
+
for (const testFile of tests) {
|
|
58
|
+
const spec = this.vitest
|
|
59
|
+
.getProjectByName('')
|
|
60
|
+
?.createSpecification(testFile);
|
|
61
|
+
if (spec)
|
|
62
|
+
specs.push(spec);
|
|
63
|
+
}
|
|
64
|
+
if (specs.length === 0) {
|
|
65
|
+
return {
|
|
66
|
+
killed: false,
|
|
67
|
+
durationMs: Date.now() - start,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
log.debug(`Running ${specs.length} test specs`);
|
|
71
|
+
const results = await this.vitest.runTestSpecifications(specs);
|
|
72
|
+
const requestedModules = new Set(specs.map((s) => s.moduleId));
|
|
73
|
+
const relevantModules = results.testModules.filter((mod) => requestedModules.has(mod.moduleId));
|
|
74
|
+
const modulesForDecision = relevantModules.length
|
|
75
|
+
? relevantModules
|
|
76
|
+
: results.testModules;
|
|
77
|
+
const killed = modulesForDecision.some((mod) => !mod.ok());
|
|
78
|
+
return {
|
|
79
|
+
killed,
|
|
80
|
+
durationMs: Date.now() - start,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
killed: true,
|
|
86
|
+
durationMs: Date.now() - start,
|
|
87
|
+
error: err instanceof Error ? err.message : String(err),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
// Clear redirect and clean up temp file
|
|
92
|
+
const mutantPath = getMutantFilePath(mutant.file, mutant.id);
|
|
93
|
+
clearRedirect();
|
|
94
|
+
try {
|
|
95
|
+
fs.rmSync(mutantPath, { force: true });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function createVitestWorkerRuntime(options) {
|
|
104
|
+
return new VitestWorkerRuntime(options);
|
|
105
|
+
}
|
|
@@ -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,92 @@
|
|
|
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 './worker-runtime.js';
|
|
17
|
+
import { createLogger } from '../../utils/logger.js';
|
|
18
|
+
const log = createLogger('vitest-worker');
|
|
19
|
+
// Global state for redirect - shared with the plugin via globalThis
|
|
20
|
+
// Type is declared in pool-plugin.ts
|
|
21
|
+
globalThis.__mutineer_redirect__ = { from: null, to: null };
|
|
22
|
+
function send(response) {
|
|
23
|
+
console.log(JSON.stringify(response));
|
|
24
|
+
}
|
|
25
|
+
async function main() {
|
|
26
|
+
const workerId = process.env.MUTINEER_WORKER_ID ?? 'unknown';
|
|
27
|
+
const cwd = process.env.MUTINEER_CWD ?? process.cwd();
|
|
28
|
+
const vitestConfigPath = process.env.MUTINEER_VITEST_CONFIG;
|
|
29
|
+
log.debug(`Starting worker ${workerId} in ${cwd}`);
|
|
30
|
+
const runtime = createVitestWorkerRuntime({
|
|
31
|
+
workerId,
|
|
32
|
+
cwd,
|
|
33
|
+
vitestConfigPath,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
await runtime.init();
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
log.error(`Failed to initialize Vitest: ${err}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Signal ready
|
|
43
|
+
send({ type: 'ready', workerId });
|
|
44
|
+
// Process requests from stdin
|
|
45
|
+
const rl = readline.createInterface({
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
terminal: false,
|
|
48
|
+
});
|
|
49
|
+
for await (const line of rl) {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
let request;
|
|
53
|
+
try {
|
|
54
|
+
request = JSON.parse(line);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
log.debug(`Invalid JSON: ${line}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (request.type === 'shutdown') {
|
|
61
|
+
log.debug('Shutting down');
|
|
62
|
+
await runtime.shutdown();
|
|
63
|
+
send({ type: 'shutdown', ok: true });
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (request.type === 'run') {
|
|
67
|
+
try {
|
|
68
|
+
const { mutant, tests } = request;
|
|
69
|
+
const result = await runtime.run(mutant, tests);
|
|
70
|
+
send({
|
|
71
|
+
type: 'result',
|
|
72
|
+
killed: result.killed,
|
|
73
|
+
durationMs: result.durationMs,
|
|
74
|
+
error: result.error,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// On error, treat as killed (conservative)
|
|
79
|
+
send({
|
|
80
|
+
type: 'result',
|
|
81
|
+
killed: true,
|
|
82
|
+
durationMs: 0,
|
|
83
|
+
error: String(err),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
main().catch((err) => {
|
|
90
|
+
log.error(`Fatal error: ${err}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MutineerConfig } from './config.js';
|
|
2
|
+
import type { MutantResult } from './mutant.js';
|
|
3
|
+
export type { MutantCacheEntry, MutantResult, MutantStatus } from './mutant.js';
|
|
4
|
+
export type DiffLineType = 'context' | 'add' | 'remove';
|
|
5
|
+
export interface DiffLine {
|
|
6
|
+
readonly type: DiffLineType;
|
|
7
|
+
readonly leftNumber?: number;
|
|
8
|
+
readonly rightNumber?: number;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DiffResponse {
|
|
12
|
+
readonly lines: readonly DiffLine[];
|
|
13
|
+
}
|
|
14
|
+
export interface ApiResponse {
|
|
15
|
+
readonly mutants: readonly MutantResult[];
|
|
16
|
+
}
|
|
17
|
+
export interface ConfigResponse {
|
|
18
|
+
readonly config?: MutineerConfig;
|
|
19
|
+
readonly error?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface MutateTargetObject {
|
|
2
|
+
readonly file: string;
|
|
3
|
+
/** Auto-detected from file extension if omitted (.vue = 'vue:script-setup', else 'module') */
|
|
4
|
+
readonly kind?: 'vue:script-setup' | 'module';
|
|
5
|
+
}
|
|
6
|
+
/** Target can be a simple path string or an object with file and optional kind */
|
|
7
|
+
export type MutateTarget = string | MutateTargetObject;
|
|
8
|
+
export interface MutineerConfig {
|
|
9
|
+
readonly targets?: readonly MutateTarget[];
|
|
10
|
+
/** Mutator names to include (e.g., ['flipStrictEQ', 'andToOr']) */
|
|
11
|
+
readonly include?: readonly string[];
|
|
12
|
+
/** Mutator names to exclude (e.g., ['relaxGE']) */
|
|
13
|
+
readonly exclude?: readonly string[];
|
|
14
|
+
/** Glob patterns for paths to exclude from mutation (e.g., ['admin/**']) */
|
|
15
|
+
readonly excludePaths?: readonly string[];
|
|
16
|
+
readonly maxMutantsPerFile?: number;
|
|
17
|
+
readonly source?: string | readonly string[];
|
|
18
|
+
readonly baseRef?: string;
|
|
19
|
+
readonly testPatterns?: readonly string[];
|
|
20
|
+
readonly extensions?: readonly string[];
|
|
21
|
+
readonly autoDiscover?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Control how Vitest output is handled for mutant runs:
|
|
24
|
+
* - 'mute' (default) suppresses all output
|
|
25
|
+
* - 'minimal' echoes only pass/fail summaries
|
|
26
|
+
* - 'inherit' streams full Vitest output to the CLI
|
|
27
|
+
*/
|
|
28
|
+
readonly mutantOutput?: 'mute' | 'minimal' | 'inherit';
|
|
29
|
+
readonly minKillPercent?: number;
|
|
30
|
+
/** Preferred test runner (defaults to vitest) */
|
|
31
|
+
readonly runner?: 'vitest' | 'jest';
|
|
32
|
+
readonly vitestConfig?: string;
|
|
33
|
+
readonly jestConfig?: string;
|
|
34
|
+
/** Max depth for dependency resolution with --changed-with-deps (default: 1) */
|
|
35
|
+
readonly dependencyDepth?: number;
|
|
36
|
+
/** Path to coverage JSON file (Istanbul format, e.g., coverage/coverage-final.json) */
|
|
37
|
+
readonly coverageFile?: string;
|
|
38
|
+
/** Only mutate lines that are covered by tests (requires coverageFile) */
|
|
39
|
+
readonly onlyCoveredLines?: boolean;
|
|
40
|
+
/** Request a coverage-instrumented baseline run (implies per-test coverage if available) */
|
|
41
|
+
readonly coverage?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Enable per-test coverage collection during the baseline run.
|
|
44
|
+
* When enabled, Mutineer will try to run only the tests that actually cover a mutated line.
|
|
45
|
+
* Requires Vitest coverage with perTest support.
|
|
46
|
+
*/
|
|
47
|
+
readonly perTestCoverage?: boolean;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all public types from a single entry point for easier imports
|
|
5
|
+
* and better tree-shaking.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import type { MutineerConfig, MutantStatus } from '../types/index.js'
|
|
9
|
+
*/
|
|
10
|
+
export type { MutineerConfig, MutateTarget } from './config.js';
|
|
11
|
+
export type { MutantStatus, MutantRunStatus, MutantCacheEntry, MutantResult, MutantRunSummary, MutantRunResult, MutantPayload, MutantDescriptor, MutantLocation, Variant, } from './mutant.js';
|
|
12
|
+
export type { MutationVariant } from '../core/types.js';
|
|
13
|
+
export { defineMutineerConfig } from '../index.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all public types from a single entry point for easier imports
|
|
5
|
+
* and better tree-shaking.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import type { MutineerConfig, MutantStatus } from '../types/index.js'
|
|
9
|
+
*/
|
|
10
|
+
// Re-export for convenience
|
|
11
|
+
export { defineMutineerConfig } from '../index.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mutant-related type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the shapes used across the runner and adapters so we
|
|
5
|
+
* don't duplicate unions or object shapes in multiple modules.
|
|
6
|
+
*/
|
|
7
|
+
export type MutantStatus = 'killed' | 'escaped' | 'skipped' | 'error' | 'timeout';
|
|
8
|
+
export type MutantRunStatus = MutantStatus;
|
|
9
|
+
export interface MutantLocation {
|
|
10
|
+
readonly file: string;
|
|
11
|
+
readonly line: number;
|
|
12
|
+
readonly col: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MutantDescriptor extends MutantLocation {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly code: string;
|
|
18
|
+
}
|
|
19
|
+
/** Payload passed to workers/pools for execution. */
|
|
20
|
+
export type MutantPayload = MutantDescriptor;
|
|
21
|
+
/** Variant with attached test files. */
|
|
22
|
+
export interface Variant extends MutantDescriptor {
|
|
23
|
+
readonly tests: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
export interface MutantCacheEntry extends MutantLocation {
|
|
26
|
+
readonly status: MutantStatus;
|
|
27
|
+
readonly mutator: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MutantResult extends MutantCacheEntry {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly relativePath: string;
|
|
32
|
+
}
|
|
33
|
+
/** Low-level execution result returned by a worker. */
|
|
34
|
+
export interface MutantRunSummary {
|
|
35
|
+
readonly killed: boolean;
|
|
36
|
+
readonly durationMs: number;
|
|
37
|
+
readonly error?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Normalised result returned by adapters/orchestrator. */
|
|
40
|
+
export interface MutantRunResult {
|
|
41
|
+
readonly status: MutantRunStatus;
|
|
42
|
+
readonly durationMs: number;
|
|
43
|
+
readonly error?: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
export function PoolSpinner({ message }) {
|
|
5
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProgressBarProps {
|
|
2
|
+
total: number;
|
|
3
|
+
done: number;
|
|
4
|
+
killed: number;
|
|
5
|
+
escaped: number;
|
|
6
|
+
errors: number;
|
|
7
|
+
timeouts: number;
|
|
8
|
+
skipped: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function ProgressBar({ total, done, killed, escaped, errors, timeouts, skipped, width, }: ProgressBarProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function ProgressBar({ total, done, killed, escaped, errors, timeouts, skipped, width = 40, }) {
|
|
4
|
+
const ratio = total === 0 ? 1 : Math.min(done / total, 1);
|
|
5
|
+
const filled = Math.round(ratio * width);
|
|
6
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
|
|
7
|
+
const pct = Math.round(ratio * 100);
|
|
8
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { children: ["mutants ", done, "/", total, " [", bar, "] ", pct, "%", ' '] }), _jsxs(Text, { color: "green", children: ["killed=", killed] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "red", children: ["escaped=", escaped] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: ["errors=", errors] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: ["timeouts=", timeouts] }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["skipped=", skipped] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { loadCoverageData, isLineCovered, getFileCoverageStats, loadPerTestCoverageData, } from '../coverage.js';
|
|
6
|
+
describe('coverage utilities', () => {
|
|
7
|
+
it('loads coverage data and reports covered lines', async () => {
|
|
8
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
9
|
+
const covPath = path.join(tmpDir, 'coverage-final.json');
|
|
10
|
+
const filePath = path.join(tmpDir, 'src', 'file.ts');
|
|
11
|
+
const data = {
|
|
12
|
+
[filePath]: {
|
|
13
|
+
path: filePath,
|
|
14
|
+
statementMap: {
|
|
15
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 2, column: 0 } },
|
|
16
|
+
'1': { start: { line: 5, column: 0 }, end: { line: 5, column: 10 } },
|
|
17
|
+
},
|
|
18
|
+
s: { '0': 1, '1': 0 },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
22
|
+
await fs.writeFile(covPath, JSON.stringify(data), 'utf8');
|
|
23
|
+
try {
|
|
24
|
+
const coverage = await loadCoverageData(covPath, tmpDir);
|
|
25
|
+
expect(isLineCovered(coverage, filePath, 1)).toBe(true);
|
|
26
|
+
expect(isLineCovered(coverage, filePath, 2)).toBe(true);
|
|
27
|
+
expect(isLineCovered(coverage, filePath, 3)).toBe(false);
|
|
28
|
+
expect(isLineCovered(coverage, filePath, 5)).toBe(false);
|
|
29
|
+
const stats = getFileCoverageStats(coverage, filePath);
|
|
30
|
+
expect(stats?.count).toBe(2);
|
|
31
|
+
expect(stats?.lines.has(1)).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
it('returns null stats for missing file', () => {
|
|
38
|
+
const coverage = { coveredLines: new Map() };
|
|
39
|
+
expect(getFileCoverageStats(coverage, '/nope.ts')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
it('loads per-test coverage data from various shapes', async () => {
|
|
42
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
43
|
+
const reportsDir = path.join(tmpDir, 'coverage');
|
|
44
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
45
|
+
const testFile = path.join(tmpDir, 'tests', 'foo.spec.ts');
|
|
46
|
+
const srcFile = path.join(tmpDir, 'src', 'foo.ts');
|
|
47
|
+
// Write per-test-coverage.json (format A)
|
|
48
|
+
const formatA = {
|
|
49
|
+
tests: {
|
|
50
|
+
[testFile]: {
|
|
51
|
+
files: {
|
|
52
|
+
[srcFile]: { lines: [1, 2, 3] },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
await fs.writeFile(path.join(reportsDir, 'per-test-coverage.json'), JSON.stringify(formatA), 'utf8');
|
|
58
|
+
// Also drop a tmp file with format B to ensure fallback works when main file is absent
|
|
59
|
+
const tmpSub = path.join(reportsDir, 'tmp');
|
|
60
|
+
await fs.mkdir(tmpSub, { recursive: true });
|
|
61
|
+
const formatB = {
|
|
62
|
+
[testFile]: {
|
|
63
|
+
[srcFile]: [4, 5],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
await fs.writeFile(path.join(tmpSub, 'extra.json'), JSON.stringify(formatB), 'utf8');
|
|
67
|
+
try {
|
|
68
|
+
const map = await loadPerTestCoverageData(reportsDir, tmpDir);
|
|
69
|
+
expect(map).not.toBeNull();
|
|
70
|
+
const perTest = map.get(testFile);
|
|
71
|
+
expect(perTest).toBeDefined();
|
|
72
|
+
const lines = perTest.get(srcFile);
|
|
73
|
+
expect(lines).toBeDefined();
|
|
74
|
+
// Format A lines present
|
|
75
|
+
expect(lines.has(1)).toBe(true);
|
|
76
|
+
expect(lines.has(3)).toBe(true);
|
|
77
|
+
// Fallback not used yet
|
|
78
|
+
expect(lines.has(4)).toBe(false);
|
|
79
|
+
// Remove primary file to force fallback loading from tmp/extra.json
|
|
80
|
+
await fs.rm(path.join(reportsDir, 'per-test-coverage.json'));
|
|
81
|
+
const fallback = await loadPerTestCoverageData(reportsDir, tmpDir);
|
|
82
|
+
expect(fallback).not.toBeNull();
|
|
83
|
+
const fallbackLines = fallback.get(testFile).get(srcFile);
|
|
84
|
+
expect(fallbackLines.has(4)).toBe(true);
|
|
85
|
+
expect(fallbackLines.has(5)).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { Progress } from '../progress.js';
|
|
3
|
+
describe('Progress', () => {
|
|
4
|
+
let originalConsole;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
originalConsole = {
|
|
7
|
+
log: console.log,
|
|
8
|
+
info: console.info,
|
|
9
|
+
warn: console.warn,
|
|
10
|
+
error: console.error,
|
|
11
|
+
};
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
console.log = originalConsole.log;
|
|
16
|
+
console.info = originalConsole.info;
|
|
17
|
+
console.warn = originalConsole.warn;
|
|
18
|
+
console.error = originalConsole.error;
|
|
19
|
+
});
|
|
20
|
+
it('logs run/update/finish messages in list mode and tracks counts', () => {
|
|
21
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
22
|
+
const progress = new Progress(5, { mode: 'list' });
|
|
23
|
+
progress.start();
|
|
24
|
+
progress.update('killed');
|
|
25
|
+
progress.update('escaped');
|
|
26
|
+
progress.update('skipped');
|
|
27
|
+
progress.update('error');
|
|
28
|
+
progress.update('timeout');
|
|
29
|
+
progress.finish();
|
|
30
|
+
const logs = logSpy.mock.calls.map((args) => args.join(' '));
|
|
31
|
+
expect(logs[0]).toContain('running 5 mutants');
|
|
32
|
+
expect(logs.some((l) => l.includes('mutant 1/5 killed'))).toBe(true);
|
|
33
|
+
expect(logs.some((l) => l.includes('mutant 2/5 escaped'))).toBe(true);
|
|
34
|
+
expect(logs.some((l) => l.includes('mutant 3/5 skipped'))).toBe(true);
|
|
35
|
+
expect(logs.some((l) => l.includes('mutant 4/5 error'))).toBe(true);
|
|
36
|
+
expect(logs.some((l) => l.includes('mutant 5/5 timeout'))).toBe(true);
|
|
37
|
+
const lastLog = logs[logs.length - 1] ?? '';
|
|
38
|
+
expect(lastLog).toContain('killed=1');
|
|
39
|
+
expect(lastLog).toContain('errors=1');
|
|
40
|
+
expect(lastLog).toContain('timeouts=1');
|
|
41
|
+
});
|
|
42
|
+
it('is tolerant to finish before start and clamps negative totals', () => {
|
|
43
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
44
|
+
const progress = new Progress(-5, { mode: 'list' });
|
|
45
|
+
progress.finish(); // no-op
|
|
46
|
+
progress.start();
|
|
47
|
+
progress.finish();
|
|
48
|
+
expect(logSpy).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { computeSummary, printSummary, summarise } from '../summary.js';
|
|
3
|
+
/** Strip ANSI escape codes for clean text assertions */
|
|
4
|
+
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
|
|
5
|
+
function makeEntry(overrides) {
|
|
6
|
+
return {
|
|
7
|
+
mutator: 'flip',
|
|
8
|
+
file: '/tmp/file.ts',
|
|
9
|
+
line: 1,
|
|
10
|
+
col: 1,
|
|
11
|
+
status: 'killed',
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
describe('summary', () => {
|
|
16
|
+
it('computes totals and kill rate', () => {
|
|
17
|
+
const cache = {
|
|
18
|
+
a: makeEntry({ status: 'killed' }),
|
|
19
|
+
b: makeEntry({ status: 'escaped' }),
|
|
20
|
+
c: makeEntry({ status: 'skipped' }),
|
|
21
|
+
};
|
|
22
|
+
const s = computeSummary(cache);
|
|
23
|
+
expect(s).toEqual({
|
|
24
|
+
total: 3,
|
|
25
|
+
killed: 1,
|
|
26
|
+
escaped: 1,
|
|
27
|
+
skipped: 1,
|
|
28
|
+
evaluated: 2,
|
|
29
|
+
killRate: 50,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('prints a friendly summary with sections', () => {
|
|
33
|
+
const cache = {
|
|
34
|
+
a: makeEntry({ status: 'killed', file: '/tmp/a.ts' }),
|
|
35
|
+
b: makeEntry({ status: 'escaped', file: '/tmp/b.ts', mutator: 'wrap' }),
|
|
36
|
+
};
|
|
37
|
+
const summary = computeSummary(cache);
|
|
38
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
39
|
+
printSummary(summary, cache, 1500);
|
|
40
|
+
const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
|
|
41
|
+
expect(lines.some((l) => l.includes('Killed Mutants'))).toBe(true);
|
|
42
|
+
expect(lines.some((l) => l.includes('Escaped Mutants'))).toBe(true);
|
|
43
|
+
expect(lines.some((l) => l.includes('Duration: 1.50s'))).toBe(true);
|
|
44
|
+
logSpy.mockRestore();
|
|
45
|
+
});
|
|
46
|
+
it('summarise returns summary and prints', () => {
|
|
47
|
+
const cache = { a: makeEntry({ status: 'killed' }) };
|
|
48
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
49
|
+
const s = summarise(cache);
|
|
50
|
+
expect(s.total).toBe(1);
|
|
51
|
+
expect(logSpy).toHaveBeenCalled();
|
|
52
|
+
logSpy.mockRestore();
|
|
53
|
+
});
|
|
54
|
+
});
|