@mutineerjs/mutineer 0.6.0 → 0.7.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/dist/bin/mutineer.d.ts +1 -1
- package/dist/bin/mutineer.js +3 -1
- package/dist/mutators/__tests__/operator.spec.js +97 -1
- package/dist/mutators/__tests__/registry.spec.js +8 -0
- package/dist/mutators/operator.d.ts +8 -0
- package/dist/mutators/operator.js +58 -1
- package/dist/mutators/registry.js +9 -1
- package/dist/mutators/utils.d.ts +2 -0
- package/dist/mutators/utils.js +58 -1
- package/dist/runner/__tests__/args.spec.js +57 -1
- package/dist/runner/__tests__/cache.spec.js +65 -8
- package/dist/runner/__tests__/cleanup.spec.js +30 -0
- package/dist/runner/__tests__/coverage-resolver.spec.js +2 -0
- package/dist/runner/__tests__/discover.spec.js +128 -0
- package/dist/runner/__tests__/orchestrator.spec.js +167 -2
- package/dist/runner/__tests__/pool-executor.spec.js +60 -1
- package/dist/runner/args.d.ts +13 -0
- package/dist/runner/args.js +27 -0
- package/dist/runner/cache.d.ts +19 -3
- package/dist/runner/cache.js +14 -7
- package/dist/runner/cleanup.d.ts +3 -1
- package/dist/runner/cleanup.js +18 -1
- package/dist/runner/coverage-resolver.js +1 -1
- package/dist/runner/discover.d.ts +1 -1
- package/dist/runner/discover.js +30 -20
- package/dist/runner/orchestrator.d.ts +1 -0
- package/dist/runner/orchestrator.js +22 -8
- package/dist/runner/pool-executor.d.ts +5 -0
- package/dist/runner/pool-executor.js +15 -4
- package/dist/runner/vitest/__tests__/adapter.spec.js +41 -0
- package/dist/runner/vitest/adapter.js +13 -9
- package/dist/types/config.d.ts +2 -0
- package/dist/utils/__tests__/summary.spec.js +43 -1
- package/dist/utils/summary.d.ts +18 -0
- package/dist/utils/summary.js +25 -0
- package/package.json +2 -1
package/dist/runner/discover.js
CHANGED
|
@@ -200,7 +200,7 @@ async function createResolver(rootAbs, exts) {
|
|
|
200
200
|
return createNodeResolver();
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
203
|
+
export async function autoDiscoverTargetsAndTests(root, cfg, onProgress) {
|
|
204
204
|
const rootAbs = path.resolve(root);
|
|
205
205
|
const sourceRoots = toArray(cfg.source ?? 'src').map((s) => path.resolve(rootAbs, s));
|
|
206
206
|
const exts = new Set(toArray(cfg.extensions ?? EXT_DEFAULT));
|
|
@@ -219,6 +219,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
219
219
|
if (!tests.length)
|
|
220
220
|
return { targets: [], testMap: new Map(), directTestMap: new Map() };
|
|
221
221
|
const testSet = new Set(tests.map((t) => normalizePath(t)));
|
|
222
|
+
onProgress?.(`Found ${tests.length} test file(s), resolving imports...`);
|
|
222
223
|
// 2) Create resolver (Vite if available, otherwise Node-based fallback)
|
|
223
224
|
const { resolve, cleanup } = await createResolver(rootAbs, exts);
|
|
224
225
|
const targets = new Map();
|
|
@@ -226,6 +227,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
226
227
|
const directTestMap = new Map();
|
|
227
228
|
const contentCache = new Map();
|
|
228
229
|
const resolveCache = new Map(); // key: importer\0spec -> resolved id
|
|
230
|
+
const childrenCache = new Map(); // key: normalized file -> resolved child abs paths
|
|
229
231
|
async function crawl(absFile, depth, seen, currentTestAbs) {
|
|
230
232
|
if (depth > MAX_CRAWL_DEPTH)
|
|
231
233
|
return; // sane guard for huge graphs
|
|
@@ -266,27 +268,34 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
266
268
|
}
|
|
267
269
|
if (!code)
|
|
268
270
|
return;
|
|
269
|
-
// find import specs and resolve relative to absFile
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
resolveCache.
|
|
271
|
+
// find import specs and resolve relative to absFile, memoized per file
|
|
272
|
+
let children = childrenCache.get(key);
|
|
273
|
+
if (children === undefined) {
|
|
274
|
+
const resolved = [];
|
|
275
|
+
for (const spec of extractImportSpecs(code)) {
|
|
276
|
+
if (!spec)
|
|
277
|
+
continue;
|
|
278
|
+
const cacheKey = `${absFile}\0${spec}`;
|
|
279
|
+
let resolvedId = resolveCache.get(cacheKey);
|
|
280
|
+
if (!resolvedId) {
|
|
281
|
+
resolvedId = await resolve(spec, absFile);
|
|
282
|
+
resolveCache.set(cacheKey, resolvedId);
|
|
283
|
+
}
|
|
284
|
+
// vite ids could be URLs; ensure we turn into absolute disk path when possible
|
|
285
|
+
const next = path.isAbsolute(resolvedId)
|
|
286
|
+
? resolvedId
|
|
287
|
+
: normalizePath(path.resolve(rootAbs, resolvedId));
|
|
288
|
+
// skip node_modules and virtual ids
|
|
289
|
+
if (next.includes('/node_modules/'))
|
|
290
|
+
continue;
|
|
291
|
+
if (!path.isAbsolute(next))
|
|
292
|
+
continue;
|
|
293
|
+
resolved.push(next);
|
|
278
294
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
? resolved
|
|
282
|
-
: normalizePath(path.resolve(rootAbs, resolved));
|
|
283
|
-
// skip node_modules and virtual ids
|
|
284
|
-
if (next.includes('/node_modules/'))
|
|
285
|
-
continue;
|
|
286
|
-
if (!path.isAbsolute(next))
|
|
287
|
-
continue;
|
|
288
|
-
await crawl(next, depth + 1, seen, currentTestAbs);
|
|
295
|
+
childrenCache.set(key, resolved);
|
|
296
|
+
children = resolved;
|
|
289
297
|
}
|
|
298
|
+
await Promise.all(children.map((next) => crawl(next, depth + 1, seen, currentTestAbs)));
|
|
290
299
|
}
|
|
291
300
|
try {
|
|
292
301
|
await Promise.all(tests.map(async (testAbs) => {
|
|
@@ -312,6 +321,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
312
321
|
await crawl(abs, 0, seen, testAbs);
|
|
313
322
|
}
|
|
314
323
|
}));
|
|
324
|
+
onProgress?.(`Discovery complete: ${targets.size} source file(s), ${tests.length} test file(s)`);
|
|
315
325
|
return { targets: Array.from(targets.values()), testMap, directTestMap };
|
|
316
326
|
}
|
|
317
327
|
finally {
|
|
@@ -9,5 +9,6 @@
|
|
|
9
9
|
* 5. Execute mutants via worker pool
|
|
10
10
|
* 6. Report results
|
|
11
11
|
*/
|
|
12
|
+
export declare function parseMutantTimeoutMs(raw: string | undefined): number;
|
|
12
13
|
export { readMutantCache } from './cache.js';
|
|
13
14
|
export declare function runOrchestrator(cliArgs: string[], cwd: string): Promise<void>;
|
|
@@ -26,11 +26,11 @@ import { prepareTasks } from './tasks.js';
|
|
|
26
26
|
import { executePool } from './pool-executor.js';
|
|
27
27
|
const log = createLogger('orchestrator');
|
|
28
28
|
// Per-mutant test timeout (ms). Can be overridden with env MUTINEER_MUTANT_TIMEOUT_MS
|
|
29
|
-
|
|
30
|
-
const raw = process.env.MUTINEER_MUTANT_TIMEOUT_MS;
|
|
29
|
+
export function parseMutantTimeoutMs(raw) {
|
|
31
30
|
const n = raw ? Number(raw) : NaN;
|
|
32
31
|
return Number.isFinite(n) && n > 0 ? n : 30_000;
|
|
33
|
-
}
|
|
32
|
+
}
|
|
33
|
+
const MUTANT_TIMEOUT_MS = parseMutantTimeoutMs(process.env.MUTINEER_MUTANT_TIMEOUT_MS);
|
|
34
34
|
// Re-export readMutantCache for external use
|
|
35
35
|
export { readMutantCache } from './cache.js';
|
|
36
36
|
export async function runOrchestrator(cliArgs, cwd) {
|
|
@@ -38,7 +38,7 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
38
38
|
const cfgPath = extractConfigPath(cliArgs);
|
|
39
39
|
const cfg = await loadMutineerConfig(cwd, cfgPath);
|
|
40
40
|
const opts = parseCliOptions(cliArgs, cfg);
|
|
41
|
-
await clearCacheOnStart(cwd);
|
|
41
|
+
await clearCacheOnStart(cwd, opts.shard);
|
|
42
42
|
// Create test runner adapter
|
|
43
43
|
const adapter = (opts.runner === 'jest' ? createJestAdapter : createVitestAdapter)({
|
|
44
44
|
cwd,
|
|
@@ -66,8 +66,9 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
66
66
|
}))
|
|
67
67
|
: null;
|
|
68
68
|
// 4. Discover targets and tests
|
|
69
|
-
const cache = await readMutantCache(cwd);
|
|
70
|
-
|
|
69
|
+
const cache = await readMutantCache(cwd, opts.shard);
|
|
70
|
+
log.info('Discovering tests...');
|
|
71
|
+
const discovered = await autoDiscoverTargetsAndTests(cwd, cfg, (msg) => log.info(msg));
|
|
71
72
|
const { testMap, directTestMap } = discovered;
|
|
72
73
|
const targets = cfg.targets?.length
|
|
73
74
|
? [...cfg.targets]
|
|
@@ -104,7 +105,8 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
if (!baselineTests.length) {
|
|
107
|
-
log.
|
|
108
|
+
log.error('No tests found for the selected targets. Ensure your source files are covered by at least one test file.');
|
|
109
|
+
process.exitCode = 1;
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
112
|
// 5. Run baseline tests (with coverage if needed for filtering)
|
|
@@ -137,7 +139,17 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
137
139
|
return;
|
|
138
140
|
}
|
|
139
141
|
// 8. Prepare tasks and execute via worker pool
|
|
140
|
-
|
|
142
|
+
let tasks = prepareTasks(variants, updatedCoverage.perTestCoverage, directTestMap);
|
|
143
|
+
// Apply shard filter if requested
|
|
144
|
+
if (opts.shard) {
|
|
145
|
+
const { index, total } = opts.shard;
|
|
146
|
+
tasks = tasks.filter((_, i) => i % total === index - 1);
|
|
147
|
+
log.info(`Shard ${index}/${total}: running ${tasks.length} mutant(s)`);
|
|
148
|
+
if (tasks.length === 0) {
|
|
149
|
+
log.info('No mutants assigned to this shard. Exiting.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
141
153
|
await executePool({
|
|
142
154
|
tasks,
|
|
143
155
|
adapter,
|
|
@@ -145,6 +157,8 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
145
157
|
concurrency: opts.concurrency,
|
|
146
158
|
progressMode: opts.progressMode,
|
|
147
159
|
minKillPercent: opts.minKillPercent,
|
|
160
|
+
reportFormat: opts.reportFormat,
|
|
148
161
|
cwd,
|
|
162
|
+
shard: opts.shard,
|
|
149
163
|
});
|
|
150
164
|
}
|
|
@@ -8,7 +8,12 @@ export interface PoolExecutionOptions {
|
|
|
8
8
|
concurrency: number;
|
|
9
9
|
progressMode: 'bar' | 'list' | 'quiet';
|
|
10
10
|
minKillPercent?: number;
|
|
11
|
+
reportFormat?: 'text' | 'json';
|
|
11
12
|
cwd: string;
|
|
13
|
+
shard?: {
|
|
14
|
+
index: number;
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
19
|
* Execute all mutant tasks through the worker pool.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { render } from 'ink';
|
|
3
4
|
import { createElement } from 'react';
|
|
4
5
|
import { Progress } from '../utils/progress.js';
|
|
5
|
-
import { computeSummary, printSummary } from '../utils/summary.js';
|
|
6
|
+
import { computeSummary, printSummary, buildJsonReport, } from '../utils/summary.js';
|
|
6
7
|
import { saveCacheAtomic } from './cache.js';
|
|
7
8
|
import { cleanupMutineerDirs } from './cleanup.js';
|
|
8
9
|
import { PoolSpinner } from '../utils/PoolSpinner.js';
|
|
@@ -29,7 +30,18 @@ export async function executePool(opts) {
|
|
|
29
30
|
const durationMs = Date.now() - mutationStartTime;
|
|
30
31
|
progress.finish();
|
|
31
32
|
const summary = computeSummary(cache);
|
|
32
|
-
|
|
33
|
+
if (opts.reportFormat === 'json') {
|
|
34
|
+
const report = buildJsonReport(summary, cache, durationMs);
|
|
35
|
+
const shardSuffix = opts.shard
|
|
36
|
+
? `-shard-${opts.shard.index}-of-${opts.shard.total}`
|
|
37
|
+
: '';
|
|
38
|
+
const outPath = path.join(opts.cwd, `mutineer-report${shardSuffix}.json`);
|
|
39
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
40
|
+
log.info(`JSON report written to ${path.relative(process.cwd(), outPath)}`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
printSummary(summary, cache, durationMs);
|
|
44
|
+
}
|
|
33
45
|
if (opts.minKillPercent !== undefined) {
|
|
34
46
|
const killRateString = summary.killRate.toFixed(2);
|
|
35
47
|
const thresholdString = opts.minKillPercent.toFixed(2);
|
|
@@ -68,7 +80,6 @@ export async function executePool(opts) {
|
|
|
68
80
|
let nextIdx = 0;
|
|
69
81
|
async function processTask(task) {
|
|
70
82
|
const { v, tests, key, directTests } = task;
|
|
71
|
-
log.debug('Cache ' + JSON.stringify(cache));
|
|
72
83
|
const cached = cache[key];
|
|
73
84
|
if (cached) {
|
|
74
85
|
progress.update(cached.status);
|
|
@@ -159,7 +170,7 @@ export async function executePool(opts) {
|
|
|
159
170
|
for (let i = 0; i < workerCount; i++)
|
|
160
171
|
workers.push(worker());
|
|
161
172
|
await Promise.all(workers);
|
|
162
|
-
await saveCacheAtomic(cwd, cache);
|
|
173
|
+
await saveCacheAtomic(cwd, cache, opts.shard);
|
|
163
174
|
}
|
|
164
175
|
finally {
|
|
165
176
|
process.removeAllListeners('SIGINT');
|
|
@@ -122,6 +122,21 @@ describe('Vitest adapter', () => {
|
|
|
122
122
|
expect(argStr).toContain('--coverage.thresholds.branches=0');
|
|
123
123
|
expect(argStr).toContain('--coverage.thresholds.statements=0');
|
|
124
124
|
});
|
|
125
|
+
it('strips --shard= flag from vitest args', async () => {
|
|
126
|
+
const adapter = makeAdapter({ cliArgs: ['--shard=1/4'] });
|
|
127
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
128
|
+
on: (evt, cb) => {
|
|
129
|
+
if (evt === 'exit')
|
|
130
|
+
cb(0);
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
await adapter.runBaseline(['test-a'], {
|
|
134
|
+
collectCoverage: false,
|
|
135
|
+
perTestCoverage: false,
|
|
136
|
+
});
|
|
137
|
+
const args = spawnMock.mock.calls[0][1];
|
|
138
|
+
expect(args.join(' ')).not.toContain('--shard');
|
|
139
|
+
});
|
|
125
140
|
it('detects coverage config from vitest config file', async () => {
|
|
126
141
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-vitest-'));
|
|
127
142
|
const cfgPath = path.join(tmp, 'vitest.config.ts');
|
|
@@ -140,6 +155,32 @@ describe('Vitest adapter', () => {
|
|
|
140
155
|
}
|
|
141
156
|
});
|
|
142
157
|
});
|
|
158
|
+
describe('hasCoverageProvider', () => {
|
|
159
|
+
it('returns true when @vitest/coverage-v8 is resolvable', () => {
|
|
160
|
+
const adapter = makeAdapter({ cwd: process.cwd() });
|
|
161
|
+
// coverage-v8 is installed as a devDependency, so this must resolve
|
|
162
|
+
expect(adapter.hasCoverageProvider()).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
it('returns false when neither provider is resolvable', () => {
|
|
165
|
+
const adapter = makeAdapter({ cwd: '/tmp' });
|
|
166
|
+
expect(adapter.hasCoverageProvider()).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('returns true when @vitest/coverage-istanbul is resolvable', () => {
|
|
169
|
+
const adapter = makeAdapter({ cwd: process.cwd() });
|
|
170
|
+
const origResolve = require.resolve;
|
|
171
|
+
const resolveStub = vi
|
|
172
|
+
.spyOn(require, 'resolve')
|
|
173
|
+
.mockImplementation((id, opts) => {
|
|
174
|
+
if (String(id).includes('coverage-v8'))
|
|
175
|
+
throw new Error('not found');
|
|
176
|
+
if (String(id).includes('coverage-istanbul'))
|
|
177
|
+
return '/fake/path';
|
|
178
|
+
return origResolve(id, opts);
|
|
179
|
+
});
|
|
180
|
+
expect(adapter.hasCoverageProvider()).toBe(true);
|
|
181
|
+
resolveStub.mockRestore();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
143
184
|
describe('isCoverageRequestedInArgs', () => {
|
|
144
185
|
it('detects enabled coverage flags', () => {
|
|
145
186
|
expect(isCoverageRequestedInArgs(['--coverage'])).toBe(true);
|
|
@@ -37,6 +37,7 @@ function stripMutineerArgs(args) {
|
|
|
37
37
|
'--config',
|
|
38
38
|
'-c',
|
|
39
39
|
'--coverage-file',
|
|
40
|
+
'--shard',
|
|
40
41
|
]);
|
|
41
42
|
const dropExact = new Set([
|
|
42
43
|
'-m',
|
|
@@ -59,6 +60,8 @@ function stripMutineerArgs(args) {
|
|
|
59
60
|
continue;
|
|
60
61
|
if (a.startsWith('--config=') || a.startsWith('-c='))
|
|
61
62
|
continue;
|
|
63
|
+
if (a.startsWith('--shard='))
|
|
64
|
+
continue;
|
|
62
65
|
out.push(a);
|
|
63
66
|
}
|
|
64
67
|
return out;
|
|
@@ -196,15 +199,16 @@ export class VitestAdapter {
|
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
hasCoverageProvider() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
const packages = ['@vitest/coverage-v8', '@vitest/coverage-istanbul'];
|
|
203
|
+
return packages.some((pkg) => {
|
|
204
|
+
try {
|
|
205
|
+
require.resolve(`${pkg}/package.json`, { paths: [this.options.cwd] });
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
208
212
|
}
|
|
209
213
|
async detectCoverageConfig() {
|
|
210
214
|
const configPath = this.options.config.vitestConfig;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -40,4 +40,6 @@ export interface MutineerConfig {
|
|
|
40
40
|
readonly perTestCoverage?: boolean;
|
|
41
41
|
/** Per-mutant test timeout in milliseconds (default: 30000) */
|
|
42
42
|
readonly timeout?: number;
|
|
43
|
+
/** Output report format: 'text' (default) or 'json' (writes mutineer-report.json) */
|
|
44
|
+
readonly report?: 'text' | 'json';
|
|
43
45
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { computeSummary, printSummary, summarise } from '../summary.js';
|
|
2
|
+
import { computeSummary, printSummary, summarise, buildJsonReport, } from '../summary.js';
|
|
3
3
|
/** Strip ANSI escape codes for clean text assertions */
|
|
4
4
|
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
|
|
5
5
|
function makeEntry(overrides) {
|
|
@@ -97,6 +97,48 @@ describe('summary', () => {
|
|
|
97
97
|
expect(lines.some((l) => l.includes('↳'))).toBe(false);
|
|
98
98
|
logSpy.mockRestore();
|
|
99
99
|
});
|
|
100
|
+
it('buildJsonReport includes schemaVersion, timestamp, summary, and mutants', () => {
|
|
101
|
+
const cache = {
|
|
102
|
+
a: makeEntry({ status: 'killed', file: '/tmp/a.ts', mutator: 'flip' }),
|
|
103
|
+
b: makeEntry({ status: 'escaped', file: '/tmp/b.ts', mutator: 'wrap' }),
|
|
104
|
+
};
|
|
105
|
+
const summary = computeSummary(cache);
|
|
106
|
+
const report = buildJsonReport(summary, cache, 1000);
|
|
107
|
+
expect(report.schemaVersion).toBe(1);
|
|
108
|
+
expect(typeof report.timestamp).toBe('string');
|
|
109
|
+
expect(report.durationMs).toBe(1000);
|
|
110
|
+
expect(report.summary).toEqual(summary);
|
|
111
|
+
expect(report.mutants).toHaveLength(2);
|
|
112
|
+
});
|
|
113
|
+
it('buildJsonReport mutant entries have required fields', () => {
|
|
114
|
+
const cache = {
|
|
115
|
+
a: makeEntry({
|
|
116
|
+
status: 'escaped',
|
|
117
|
+
file: '/tmp/a.ts',
|
|
118
|
+
mutator: 'flip',
|
|
119
|
+
originalSnippet: 'a === b',
|
|
120
|
+
mutatedSnippet: 'a !== b',
|
|
121
|
+
coveringTests: ['/tmp/a.spec.ts'],
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
const summary = computeSummary(cache);
|
|
125
|
+
const report = buildJsonReport(summary, cache);
|
|
126
|
+
const mutant = report.mutants[0];
|
|
127
|
+
expect(mutant.file).toBe('/tmp/a.ts');
|
|
128
|
+
expect(mutant.status).toBe('escaped');
|
|
129
|
+
expect(mutant.mutator).toBe('flip');
|
|
130
|
+
expect(mutant.originalSnippet).toBe('a === b');
|
|
131
|
+
expect(mutant.mutatedSnippet).toBe('a !== b');
|
|
132
|
+
expect(mutant.coveringTests).toEqual(['/tmp/a.spec.ts']);
|
|
133
|
+
});
|
|
134
|
+
it('buildJsonReport omits optional fields when absent', () => {
|
|
135
|
+
const cache = { a: makeEntry({ status: 'killed' }) };
|
|
136
|
+
const summary = computeSummary(cache);
|
|
137
|
+
const report = buildJsonReport(summary, cache);
|
|
138
|
+
expect('durationMs' in report).toBe(false);
|
|
139
|
+
expect('originalSnippet' in report.mutants[0]).toBe(false);
|
|
140
|
+
expect('coveringTests' in report.mutants[0]).toBe(false);
|
|
141
|
+
});
|
|
100
142
|
it('summarise returns summary and prints', () => {
|
|
101
143
|
const cache = { a: makeEntry({ status: 'killed' }) };
|
|
102
144
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
package/dist/utils/summary.d.ts
CHANGED
|
@@ -9,4 +9,22 @@ export interface Summary {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function computeSummary(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
|
|
11
11
|
export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): void;
|
|
12
|
+
export interface JsonMutant {
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly line: number;
|
|
15
|
+
readonly col: number;
|
|
16
|
+
readonly mutator: string;
|
|
17
|
+
readonly status: string;
|
|
18
|
+
readonly originalSnippet?: string;
|
|
19
|
+
readonly mutatedSnippet?: string;
|
|
20
|
+
readonly coveringTests?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export interface JsonReport {
|
|
23
|
+
readonly schemaVersion: 1;
|
|
24
|
+
readonly timestamp: string;
|
|
25
|
+
readonly durationMs?: number;
|
|
26
|
+
readonly summary: Summary;
|
|
27
|
+
readonly mutants: JsonMutant[];
|
|
28
|
+
}
|
|
29
|
+
export declare function buildJsonReport(summary: Summary, cache: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): JsonReport;
|
|
12
30
|
export declare function summarise(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
|
package/dist/utils/summary.js
CHANGED
|
@@ -115,6 +115,31 @@ export function printSummary(summary, cache, durationMs) {
|
|
|
115
115
|
}
|
|
116
116
|
console.log(chalk.dim(SEPARATOR) + '\n');
|
|
117
117
|
}
|
|
118
|
+
export function buildJsonReport(summary, cache, durationMs) {
|
|
119
|
+
const mutants = Object.values(cache).map((entry) => ({
|
|
120
|
+
file: entry.file,
|
|
121
|
+
line: entry.line,
|
|
122
|
+
col: entry.col,
|
|
123
|
+
mutator: entry.mutator,
|
|
124
|
+
status: entry.status,
|
|
125
|
+
...(entry.originalSnippet !== undefined && {
|
|
126
|
+
originalSnippet: entry.originalSnippet,
|
|
127
|
+
}),
|
|
128
|
+
...(entry.mutatedSnippet !== undefined && {
|
|
129
|
+
mutatedSnippet: entry.mutatedSnippet,
|
|
130
|
+
}),
|
|
131
|
+
...(entry.coveringTests !== undefined && {
|
|
132
|
+
coveringTests: entry.coveringTests,
|
|
133
|
+
}),
|
|
134
|
+
}));
|
|
135
|
+
return {
|
|
136
|
+
schemaVersion: 1,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
...(durationMs !== undefined && { durationMs }),
|
|
139
|
+
summary,
|
|
140
|
+
mutants,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
118
143
|
export function summarise(cache) {
|
|
119
144
|
const s = computeSummary(cache);
|
|
120
145
|
printSummary(s, cache);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutineerjs/mutineer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A fast, targeted mutation testing framework for JavaScript and TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
101
102
|
"@commitlint/cli": "^20.4.3",
|
|
102
103
|
"@commitlint/config-conventional": "^20.4.3",
|
|
103
104
|
"@types/babel__traverse": "^7.28.0",
|