@mutineerjs/mutineer 0.9.0 → 0.11.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/README.md +52 -47
- package/dist/__tests__/index.spec.js +8 -0
- package/dist/bin/__tests__/mutineer.spec.js +7 -7
- package/dist/bin/mutineer.d.ts +1 -1
- package/dist/bin/mutineer.js +7 -4
- package/dist/core/__tests__/schemata.spec.js +62 -0
- package/dist/core/__tests__/sfc.spec.js +41 -1
- package/dist/core/schemata.js +15 -21
- package/dist/core/sfc.js +0 -4
- package/dist/core/variant-utils.js +0 -4
- package/dist/mutators/__tests__/utils.spec.js +65 -1
- package/dist/mutators/operator.js +13 -27
- package/dist/mutators/return-value.js +3 -7
- package/dist/mutators/utils.d.ts +2 -2
- package/dist/mutators/utils.js +59 -96
- package/dist/runner/__tests__/args.spec.js +8 -4
- package/dist/runner/__tests__/cache.spec.js +24 -0
- package/dist/runner/__tests__/changed.spec.js +75 -0
- package/dist/runner/__tests__/config.spec.js +50 -1
- package/dist/runner/__tests__/coverage-resolver.spec.js +88 -1
- package/dist/runner/__tests__/discover.spec.js +179 -0
- package/dist/runner/__tests__/orchestrator.spec.js +336 -11
- package/dist/runner/__tests__/pool-executor.spec.js +77 -0
- package/dist/runner/__tests__/ts-checker-worker.spec.d.ts +1 -0
- package/dist/runner/__tests__/ts-checker-worker.spec.js +66 -0
- package/dist/runner/__tests__/ts-checker.spec.js +89 -2
- package/dist/runner/args.d.ts +1 -1
- package/dist/runner/args.js +2 -2
- package/dist/runner/config.js +3 -4
- package/dist/runner/coverage-resolver.js +2 -1
- package/dist/runner/discover.js +2 -2
- package/dist/runner/jest/__tests__/adapter.spec.js +169 -0
- package/dist/runner/jest/__tests__/pool.spec.js +223 -1
- package/dist/runner/jest/adapter.js +3 -45
- package/dist/runner/jest/pool.js +4 -10
- package/dist/runner/jest/worker-runtime.js +2 -1
- package/dist/runner/orchestrator.js +8 -7
- package/dist/runner/pool-executor.js +7 -12
- package/dist/runner/shared/__tests__/strip-mutineer-args.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/strip-mutineer-args.spec.js +104 -0
- package/dist/runner/shared/__tests__/worker-script.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/worker-script.spec.js +32 -0
- package/dist/runner/shared/index.d.ts +4 -0
- package/dist/runner/shared/index.js +2 -0
- package/dist/runner/shared/pending-task.d.ts +9 -0
- package/dist/runner/shared/pending-task.js +1 -0
- package/dist/runner/shared/strip-mutineer-args.d.ts +11 -0
- package/dist/runner/shared/strip-mutineer-args.js +47 -0
- package/dist/runner/shared/worker-script.d.ts +5 -0
- package/dist/runner/shared/worker-script.js +12 -0
- package/dist/runner/ts-checker-worker.d.ts +10 -1
- package/dist/runner/ts-checker-worker.js +27 -25
- package/dist/runner/ts-checker.d.ts +6 -0
- package/dist/runner/ts-checker.js +1 -1
- package/dist/runner/vitest/__tests__/adapter.spec.js +294 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +28 -1
- package/dist/runner/vitest/__tests__/pool.spec.js +711 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +116 -1
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +81 -0
- package/dist/runner/vitest/adapter.js +14 -46
- package/dist/runner/vitest/plugin.js +1 -7
- package/dist/runner/vitest/pool.js +6 -19
- package/dist/runner/vitest/redirect-loader.js +3 -1
- package/dist/runner/vitest/worker-runtime.js +16 -1
- package/dist/runner/vitest/worker.mjs +1 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/mutant.d.ts +3 -0
- package/dist/utils/__tests__/PoolSpinner.spec.d.ts +1 -0
- package/dist/utils/__tests__/PoolSpinner.spec.js +15 -0
- package/dist/utils/__tests__/coverage.spec.js +89 -0
- package/dist/utils/__tests__/logger.spec.js +9 -0
- package/dist/utils/__tests__/progress.spec.js +38 -0
- package/dist/utils/__tests__/summary.spec.js +70 -31
- package/dist/utils/coverage.js +3 -4
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/summary.d.ts +2 -3
- package/dist/utils/summary.js +5 -6
- package/package.json +1 -1
- package/dist/utils/CompileErrors.d.ts +0 -7
- package/dist/utils/CompileErrors.js +0 -24
- package/dist/utils/__tests__/CompileErrors.spec.js +0 -96
- /package/dist/{utils/__tests__/CompileErrors.spec.d.ts → __tests__/index.spec.d.ts} +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const COMMON_CONSUME_NEXT = new Set([
|
|
2
|
+
'--concurrency',
|
|
3
|
+
'--progress',
|
|
4
|
+
'--min-kill-percent',
|
|
5
|
+
'--config',
|
|
6
|
+
'-c',
|
|
7
|
+
'--coverage-file',
|
|
8
|
+
'--report',
|
|
9
|
+
]);
|
|
10
|
+
const COMMON_DROP_EXACT = new Set([
|
|
11
|
+
'-m',
|
|
12
|
+
'--mutate',
|
|
13
|
+
'--changed',
|
|
14
|
+
'--changed-with-imports',
|
|
15
|
+
'--full',
|
|
16
|
+
'--only-covered-lines',
|
|
17
|
+
'--per-test-coverage',
|
|
18
|
+
'--perTestCoverage',
|
|
19
|
+
]);
|
|
20
|
+
const COMMON_DROP_PREFIXES = ['--min-kill-percent=', '--config=', '-c='];
|
|
21
|
+
/**
|
|
22
|
+
* Strip mutineer-specific CLI args that shouldn't be passed to the underlying
|
|
23
|
+
* test runner. Callers may supply runner-specific extras via options.
|
|
24
|
+
*/
|
|
25
|
+
export function stripMutineerArgs(args, options = {}) {
|
|
26
|
+
const consumeNext = options.extraConsumeNext
|
|
27
|
+
? new Set([...COMMON_CONSUME_NEXT, ...options.extraConsumeNext])
|
|
28
|
+
: COMMON_CONSUME_NEXT;
|
|
29
|
+
const extraPrefixes = options.extraPrefixes ?? [];
|
|
30
|
+
const allPrefixes = extraPrefixes.length
|
|
31
|
+
? [...COMMON_DROP_PREFIXES, ...extraPrefixes]
|
|
32
|
+
: COMMON_DROP_PREFIXES;
|
|
33
|
+
const out = [];
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
const a = args[i];
|
|
36
|
+
if (COMMON_DROP_EXACT.has(a))
|
|
37
|
+
continue;
|
|
38
|
+
if (consumeNext.has(a)) {
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (allPrefixes.some((p) => a.startsWith(p)))
|
|
43
|
+
continue;
|
|
44
|
+
out.push(a);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Find a worker/loader script by checking .js -> .mjs -> .ts extension fallback.
|
|
5
|
+
* Handles compiled (.js), bundled (.mjs), and source (.ts) environments.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveWorkerScript(dir, basename) {
|
|
8
|
+
const js = path.join(dir, `${basename}.js`);
|
|
9
|
+
const mjs = path.join(dir, `${basename}.mjs`);
|
|
10
|
+
const ts = path.join(dir, `${basename}.ts`);
|
|
11
|
+
return fs.existsSync(js) ? js : fs.existsSync(mjs) ? mjs : ts;
|
|
12
|
+
}
|
|
@@ -2,4 +2,13 @@
|
|
|
2
2
|
* Worker thread entry point for parallel TypeScript type checking.
|
|
3
3
|
* Receives one file group, runs baseline + per-variant diagnose, posts results.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
/** Stable fingerprint for a diagnostic. */
|
|
7
|
+
export declare function diagnosticKey(d: ts.Diagnostic): string;
|
|
8
|
+
/** Create a compiler host that serves `code` for `targetPath`. */
|
|
9
|
+
export declare function makeHost(options: ts.CompilerOptions, targetPath: string, code: string): ts.CompilerHost;
|
|
10
|
+
/** Run semantic diagnostics for `code` in `targetPath`. */
|
|
11
|
+
export declare function diagnose(options: ts.CompilerOptions, targetPath: string, code: string, oldProgram: ts.Program | undefined): {
|
|
12
|
+
program: ts.Program;
|
|
13
|
+
keys: Set<string>;
|
|
14
|
+
};
|
|
@@ -7,11 +7,11 @@ import ts from 'typescript';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
/** Stable fingerprint for a diagnostic. */
|
|
10
|
-
function diagnosticKey(d) {
|
|
10
|
+
export function diagnosticKey(d) {
|
|
11
11
|
return `${d.code}:${d.start ?? -1}`;
|
|
12
12
|
}
|
|
13
13
|
/** Create a compiler host that serves `code` for `targetPath`. */
|
|
14
|
-
function makeHost(options, targetPath, code) {
|
|
14
|
+
export function makeHost(options, targetPath, code) {
|
|
15
15
|
const host = ts.createCompilerHost(options);
|
|
16
16
|
const orig = host.getSourceFile.bind(host);
|
|
17
17
|
host.getSourceFile = (fileName, langOrOpts) => {
|
|
@@ -23,7 +23,7 @@ function makeHost(options, targetPath, code) {
|
|
|
23
23
|
return host;
|
|
24
24
|
}
|
|
25
25
|
/** Run semantic diagnostics for `code` in `targetPath`. */
|
|
26
|
-
function diagnose(options, targetPath, code, oldProgram) {
|
|
26
|
+
export function diagnose(options, targetPath, code, oldProgram) {
|
|
27
27
|
const host = makeHost(options, targetPath, code);
|
|
28
28
|
const program = ts.createProgram({
|
|
29
29
|
rootNames: [targetPath],
|
|
@@ -39,28 +39,30 @@ function diagnose(options, targetPath, code, oldProgram) {
|
|
|
39
39
|
const keys = new Set(program.getSemanticDiagnostics(sourceFile).map(diagnosticKey));
|
|
40
40
|
return { program, keys };
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const { program: baseProgram, keys: baselineKeys } = diagnose(options, resolvedPath, originalCode, undefined);
|
|
52
|
-
let prevProgram = baseProgram;
|
|
53
|
-
const compileErrorIds = [];
|
|
54
|
-
for (const variant of variants) {
|
|
55
|
-
const { program: mutProgram, keys: mutantKeys } = diagnose(options, resolvedPath, variant.code, prevProgram);
|
|
56
|
-
prevProgram = mutProgram;
|
|
57
|
-
let newErrors = 0;
|
|
58
|
-
for (const key of mutantKeys) {
|
|
59
|
-
if (!baselineKeys.has(key))
|
|
60
|
-
newErrors++;
|
|
42
|
+
if (workerData) {
|
|
43
|
+
const { options, filePath, variants } = workerData;
|
|
44
|
+
const resolvedPath = path.resolve(filePath);
|
|
45
|
+
let originalCode = '';
|
|
46
|
+
try {
|
|
47
|
+
originalCode = fs.readFileSync(resolvedPath, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// empty baseline — all mutant errors count as new
|
|
61
51
|
}
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
const { program: baseProgram, keys: baselineKeys } = diagnose(options, resolvedPath, originalCode, undefined);
|
|
53
|
+
let prevProgram = baseProgram;
|
|
54
|
+
const compileErrorIds = [];
|
|
55
|
+
for (const variant of variants) {
|
|
56
|
+
const { program: mutProgram, keys: mutantKeys } = diagnose(options, resolvedPath, variant.code, prevProgram);
|
|
57
|
+
prevProgram = mutProgram;
|
|
58
|
+
let newErrors = 0;
|
|
59
|
+
for (const key of mutantKeys) {
|
|
60
|
+
if (!baselineKeys.has(key))
|
|
61
|
+
newErrors++;
|
|
62
|
+
}
|
|
63
|
+
if (newErrors > 0) {
|
|
64
|
+
compileErrorIds.push(variant.id);
|
|
65
|
+
}
|
|
64
66
|
}
|
|
67
|
+
parentPort.postMessage({ compileErrorIds });
|
|
65
68
|
}
|
|
66
|
-
parentPort.postMessage({ compileErrorIds });
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
import ts from 'typescript';
|
|
16
16
|
import type { MutineerConfig } from '../types/config.js';
|
|
17
17
|
import type { Variant } from '../types/mutant.js';
|
|
18
|
+
/**
|
|
19
|
+
* Compiler options used for all type checks: fully isolated, zero I/O.
|
|
20
|
+
* We read strict/noImplicitAny/etc from the user's tsconfig (to catch the
|
|
21
|
+
* same errors they care about), but always override lib/resolve settings.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveCompilerOptions(tsconfig: string | undefined, cwd: string): ts.CompilerOptions;
|
|
18
24
|
/**
|
|
19
25
|
* Run type checks for one file group synchronously. Used both by the sync
|
|
20
26
|
* fallback (test/dev environments) and by the worker thread.
|
|
@@ -25,7 +25,7 @@ const log = createLogger('ts-checker');
|
|
|
25
25
|
* We read strict/noImplicitAny/etc from the user's tsconfig (to catch the
|
|
26
26
|
* same errors they care about), but always override lib/resolve settings.
|
|
27
27
|
*/
|
|
28
|
-
function resolveCompilerOptions(tsconfig, cwd) {
|
|
28
|
+
export function resolveCompilerOptions(tsconfig, cwd) {
|
|
29
29
|
const base = {
|
|
30
30
|
noEmit: true,
|
|
31
31
|
skipLibCheck: true,
|
|
@@ -56,6 +56,30 @@ describe('Vitest adapter', () => {
|
|
|
56
56
|
const res = await adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
57
57
|
expect(res).toEqual({ status: 'killed', durationMs: 10, error: undefined });
|
|
58
58
|
});
|
|
59
|
+
it('includes passingTests in escaped result', async () => {
|
|
60
|
+
const adapter = makeAdapter();
|
|
61
|
+
await adapter.init();
|
|
62
|
+
poolInstance.run.mockResolvedValueOnce({
|
|
63
|
+
killed: false,
|
|
64
|
+
durationMs: 8,
|
|
65
|
+
passingTests: ['Suite > test one', 'Suite > test two'],
|
|
66
|
+
});
|
|
67
|
+
const res = await adapter.runMutant({ id: '2', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
68
|
+
expect(res.status).toBe('escaped');
|
|
69
|
+
expect(res.passingTests).toEqual(['Suite > test one', 'Suite > test two']);
|
|
70
|
+
});
|
|
71
|
+
it('omits passingTests for killed mutants', async () => {
|
|
72
|
+
const adapter = makeAdapter();
|
|
73
|
+
await adapter.init();
|
|
74
|
+
poolInstance.run.mockResolvedValueOnce({
|
|
75
|
+
killed: true,
|
|
76
|
+
durationMs: 5,
|
|
77
|
+
passingTests: ['should not appear'],
|
|
78
|
+
});
|
|
79
|
+
const res = await adapter.runMutant({ id: '3', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
80
|
+
expect(res.status).toBe('killed');
|
|
81
|
+
expect(res.passingTests).toBeUndefined();
|
|
82
|
+
});
|
|
59
83
|
it('maps pool timeout errors to timeout status', async () => {
|
|
60
84
|
const adapter = makeAdapter();
|
|
61
85
|
await adapter.init();
|
|
@@ -137,6 +161,22 @@ describe('Vitest adapter', () => {
|
|
|
137
161
|
const args = spawnMock.mock.calls[0][1];
|
|
138
162
|
expect(args.join(' ')).not.toContain('--shard');
|
|
139
163
|
});
|
|
164
|
+
it('strips --report flag and value from vitest args', async () => {
|
|
165
|
+
const adapter = makeAdapter({ cliArgs: ['--report', 'json'] });
|
|
166
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
167
|
+
on: (evt, cb) => {
|
|
168
|
+
if (evt === 'exit')
|
|
169
|
+
cb(0);
|
|
170
|
+
},
|
|
171
|
+
}));
|
|
172
|
+
await adapter.runBaseline(['test-a'], {
|
|
173
|
+
collectCoverage: false,
|
|
174
|
+
perTestCoverage: false,
|
|
175
|
+
});
|
|
176
|
+
const args = spawnMock.mock.calls[0][1];
|
|
177
|
+
expect(args.join(' ')).not.toContain('--report');
|
|
178
|
+
expect(args.join(' ')).not.toContain('json');
|
|
179
|
+
});
|
|
140
180
|
it('does not write captured output to stdout/stderr on success', async () => {
|
|
141
181
|
const adapter = makeAdapter({ cliArgs: [] });
|
|
142
182
|
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
|
@@ -258,4 +298,258 @@ describe('isCoverageRequestedInArgs', () => {
|
|
|
258
298
|
expect(isCoverageRequestedInArgs(['--coverage.enabled=false'])).toBe(false);
|
|
259
299
|
expect(isCoverageRequestedInArgs(['--no-coverage'])).toBe(false);
|
|
260
300
|
});
|
|
301
|
+
it('handles --coverage.enabled with space-separated value', () => {
|
|
302
|
+
expect(isCoverageRequestedInArgs(['--coverage.enabled', 'false'])).toBe(false);
|
|
303
|
+
expect(isCoverageRequestedInArgs(['--coverage.enabled', 'true'])).toBe(true);
|
|
304
|
+
expect(isCoverageRequestedInArgs(['--coverage.enabled', '0'])).toBe(false);
|
|
305
|
+
expect(isCoverageRequestedInArgs(['--coverage.enabled', 'off'])).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
it('handles --coverage= form', () => {
|
|
308
|
+
expect(isCoverageRequestedInArgs(['--coverage=true'])).toBe(true);
|
|
309
|
+
expect(isCoverageRequestedInArgs(['--coverage=false'])).toBe(false);
|
|
310
|
+
expect(isCoverageRequestedInArgs(['--coverage=0'])).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
it('handles --coverage.something form (other coverage flags)', () => {
|
|
313
|
+
expect(isCoverageRequestedInArgs(['--coverage.reporter=json'])).toBe(true);
|
|
314
|
+
expect(isCoverageRequestedInArgs(['--coverage.all=true'])).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
describe('Vitest adapter additional coverage', () => {
|
|
318
|
+
beforeEach(() => {
|
|
319
|
+
vi.clearAllMocks();
|
|
320
|
+
});
|
|
321
|
+
it('strips --min-kill-percent= args from vitest args', async () => {
|
|
322
|
+
const adapter = makeAdapter({ cliArgs: ['--min-kill-percent=80'] });
|
|
323
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
324
|
+
on: (evt, cb) => {
|
|
325
|
+
if (evt === 'exit')
|
|
326
|
+
cb(0);
|
|
327
|
+
},
|
|
328
|
+
}));
|
|
329
|
+
await adapter.runBaseline(['test-a'], {
|
|
330
|
+
collectCoverage: false,
|
|
331
|
+
perTestCoverage: false,
|
|
332
|
+
});
|
|
333
|
+
const args = spawnMock.mock.calls[0][1];
|
|
334
|
+
expect(args.join(' ')).not.toContain('--min-kill-percent');
|
|
335
|
+
});
|
|
336
|
+
it('strips --config= args from vitest args', async () => {
|
|
337
|
+
const adapter = makeAdapter({ cliArgs: ['--config=./custom.vite.ts'] });
|
|
338
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
339
|
+
on: (evt, cb) => {
|
|
340
|
+
if (evt === 'exit')
|
|
341
|
+
cb(0);
|
|
342
|
+
},
|
|
343
|
+
}));
|
|
344
|
+
await adapter.runBaseline(['test-a'], {
|
|
345
|
+
collectCoverage: false,
|
|
346
|
+
perTestCoverage: false,
|
|
347
|
+
});
|
|
348
|
+
const args = spawnMock.mock.calls[0][1];
|
|
349
|
+
expect(args.join(' ')).not.toContain('--config=./custom.vite.ts');
|
|
350
|
+
});
|
|
351
|
+
it('passes through non-mutineer args unchanged', async () => {
|
|
352
|
+
const adapter = makeAdapter({ cliArgs: ['--reporter=verbose'] });
|
|
353
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
354
|
+
on: (evt, cb) => {
|
|
355
|
+
if (evt === 'exit')
|
|
356
|
+
cb(0);
|
|
357
|
+
},
|
|
358
|
+
}));
|
|
359
|
+
await adapter.runBaseline(['test-a'], {
|
|
360
|
+
collectCoverage: false,
|
|
361
|
+
perTestCoverage: false,
|
|
362
|
+
});
|
|
363
|
+
const args = spawnMock.mock.calls[0][1];
|
|
364
|
+
expect(args).toContain('--reporter=verbose');
|
|
365
|
+
});
|
|
366
|
+
it('handles spawn error event in runBaseline', async () => {
|
|
367
|
+
const adapter = makeAdapter({ cliArgs: [] });
|
|
368
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
369
|
+
stdout: { on: () => { } },
|
|
370
|
+
stderr: { on: () => { } },
|
|
371
|
+
on: (evt, cb) => {
|
|
372
|
+
if (evt === 'error')
|
|
373
|
+
cb(new Error('spawn failed'));
|
|
374
|
+
},
|
|
375
|
+
}));
|
|
376
|
+
const result = await adapter.runBaseline(['test-a'], {
|
|
377
|
+
collectCoverage: false,
|
|
378
|
+
perTestCoverage: false,
|
|
379
|
+
});
|
|
380
|
+
expect(result).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
it('throws when runMutant called before init', async () => {
|
|
383
|
+
const adapter = makeAdapter();
|
|
384
|
+
await expect(adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t'])).rejects.toThrow('VitestAdapter not initialised');
|
|
385
|
+
});
|
|
386
|
+
it('shuts down pool on shutdown()', async () => {
|
|
387
|
+
const adapter = makeAdapter();
|
|
388
|
+
await adapter.init();
|
|
389
|
+
await adapter.shutdown();
|
|
390
|
+
expect(poolInstance?.shutdown).toHaveBeenCalledTimes(1);
|
|
391
|
+
});
|
|
392
|
+
it('does nothing on shutdown() when pool is null', async () => {
|
|
393
|
+
const adapter = makeAdapter();
|
|
394
|
+
await expect(adapter.shutdown()).resolves.toBeUndefined();
|
|
395
|
+
});
|
|
396
|
+
it('detectCoverageConfig returns false defaults when no vitestConfig', async () => {
|
|
397
|
+
const adapter = makeAdapter({ config: {} });
|
|
398
|
+
const result = await adapter.detectCoverageConfig();
|
|
399
|
+
expect(result).toEqual({ perTestEnabled: false, coverageEnabled: false });
|
|
400
|
+
});
|
|
401
|
+
it('detectCoverageConfig returns false defaults when config file missing', async () => {
|
|
402
|
+
const adapter = makeAdapter({
|
|
403
|
+
config: { vitestConfig: '/nonexistent/vitest.config.ts' },
|
|
404
|
+
});
|
|
405
|
+
const result = await adapter.detectCoverageConfig();
|
|
406
|
+
expect(result).toEqual({ perTestEnabled: false, coverageEnabled: false });
|
|
407
|
+
});
|
|
408
|
+
it('does not prepend "run" if already present in args', async () => {
|
|
409
|
+
const adapter = makeAdapter({ cliArgs: ['run'] });
|
|
410
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
411
|
+
on: (evt, cb) => {
|
|
412
|
+
if (evt === 'exit')
|
|
413
|
+
cb(0);
|
|
414
|
+
},
|
|
415
|
+
}));
|
|
416
|
+
await adapter.runBaseline(['test-a'], {
|
|
417
|
+
collectCoverage: false,
|
|
418
|
+
perTestCoverage: false,
|
|
419
|
+
});
|
|
420
|
+
const args = spawnMock.mock.calls[0][1];
|
|
421
|
+
const runCount = args.filter((a) => a === 'run').length;
|
|
422
|
+
expect(runCount).toBe(1);
|
|
423
|
+
});
|
|
424
|
+
it('does not add --watch=false if --watch flag already in args', async () => {
|
|
425
|
+
const adapter = makeAdapter({ cliArgs: ['--watch=false'] });
|
|
426
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
427
|
+
on: (evt, cb) => {
|
|
428
|
+
if (evt === 'exit')
|
|
429
|
+
cb(0);
|
|
430
|
+
},
|
|
431
|
+
}));
|
|
432
|
+
await adapter.runBaseline(['test-a'], {
|
|
433
|
+
collectCoverage: false,
|
|
434
|
+
perTestCoverage: false,
|
|
435
|
+
});
|
|
436
|
+
const args = spawnMock.mock.calls[0][1];
|
|
437
|
+
const watchCount = args.filter((a) => a.startsWith('--watch')).length;
|
|
438
|
+
expect(watchCount).toBe(1);
|
|
439
|
+
});
|
|
440
|
+
it('does not add --passWithNoTests if already in args', async () => {
|
|
441
|
+
const adapter = makeAdapter({ cliArgs: ['--passWithNoTests'] });
|
|
442
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
443
|
+
on: (evt, cb) => {
|
|
444
|
+
if (evt === 'exit')
|
|
445
|
+
cb(0);
|
|
446
|
+
},
|
|
447
|
+
}));
|
|
448
|
+
await adapter.runBaseline(['test-a'], {
|
|
449
|
+
collectCoverage: false,
|
|
450
|
+
perTestCoverage: false,
|
|
451
|
+
});
|
|
452
|
+
const args = spawnMock.mock.calls[0][1];
|
|
453
|
+
const count = args.filter((a) => a === '--passWithNoTests').length;
|
|
454
|
+
expect(count).toBe(1);
|
|
455
|
+
});
|
|
456
|
+
it('does not add coverage flags if --coverage already in args during baseline-with-coverage', async () => {
|
|
457
|
+
const adapter = makeAdapter({
|
|
458
|
+
cliArgs: ['--coverage.reporter=html', '--coverage.perTest=true'],
|
|
459
|
+
});
|
|
460
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
461
|
+
on: (evt, cb) => {
|
|
462
|
+
if (evt === 'exit')
|
|
463
|
+
cb(0);
|
|
464
|
+
},
|
|
465
|
+
}));
|
|
466
|
+
await adapter.runBaseline(['test-a'], {
|
|
467
|
+
collectCoverage: true,
|
|
468
|
+
perTestCoverage: true,
|
|
469
|
+
});
|
|
470
|
+
const args = spawnMock.mock.calls[0][1];
|
|
471
|
+
// --coverage.enabled=true should NOT be added since --coverage.reporter is already present
|
|
472
|
+
expect(args).not.toContain('--coverage.enabled=true');
|
|
473
|
+
// --coverage.perTest=true should NOT be added again
|
|
474
|
+
const perTestCount = args.filter((a) => a.startsWith('--coverage.perTest=')).length;
|
|
475
|
+
expect(perTestCount).toBe(1);
|
|
476
|
+
});
|
|
477
|
+
it('does not override CI env var when already set', async () => {
|
|
478
|
+
const originalCI = process.env.CI;
|
|
479
|
+
process.env.CI = 'existing-ci-value';
|
|
480
|
+
try {
|
|
481
|
+
const adapter = makeAdapter({ cliArgs: [] });
|
|
482
|
+
let capturedEnv;
|
|
483
|
+
spawnMock.mockImplementationOnce((_, __, opts) => {
|
|
484
|
+
capturedEnv = opts.env;
|
|
485
|
+
return {
|
|
486
|
+
on: (evt, cb) => {
|
|
487
|
+
if (evt === 'exit')
|
|
488
|
+
cb(0);
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
await adapter.runBaseline(['test-a'], {
|
|
493
|
+
collectCoverage: false,
|
|
494
|
+
perTestCoverage: false,
|
|
495
|
+
});
|
|
496
|
+
expect(capturedEnv?.CI).toBe('existing-ci-value');
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
if (originalCI === undefined)
|
|
500
|
+
delete process.env.CI;
|
|
501
|
+
else
|
|
502
|
+
process.env.CI = originalCI;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
it('runBaseline with non-zero exit and no chunks does not write output', async () => {
|
|
506
|
+
const adapter = makeAdapter({ cliArgs: [] });
|
|
507
|
+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
|
508
|
+
const stderrWrite = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
509
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
510
|
+
stdout: { on: () => { } },
|
|
511
|
+
stderr: { on: () => { } },
|
|
512
|
+
on: (evt, cb) => {
|
|
513
|
+
if (evt === 'exit')
|
|
514
|
+
cb(1);
|
|
515
|
+
},
|
|
516
|
+
}));
|
|
517
|
+
const ok = await adapter.runBaseline(['test-a'], {
|
|
518
|
+
collectCoverage: false,
|
|
519
|
+
perTestCoverage: false,
|
|
520
|
+
});
|
|
521
|
+
expect(ok).toBe(false);
|
|
522
|
+
expect(stdoutWrite).not.toHaveBeenCalled();
|
|
523
|
+
expect(stderrWrite).not.toHaveBeenCalled();
|
|
524
|
+
stdoutWrite.mockRestore();
|
|
525
|
+
stderrWrite.mockRestore();
|
|
526
|
+
});
|
|
527
|
+
it('returns error status on pool throw with non-Error', async () => {
|
|
528
|
+
const adapter = makeAdapter();
|
|
529
|
+
await adapter.init();
|
|
530
|
+
poolInstance.run.mockRejectedValueOnce('string error');
|
|
531
|
+
const res = await adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
532
|
+
expect(res).toEqual({
|
|
533
|
+
status: 'error',
|
|
534
|
+
durationMs: 0,
|
|
535
|
+
error: 'string error',
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
it('detectCoverageConfig returns false when coverage is disabled in config', async () => {
|
|
539
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-vitest-'));
|
|
540
|
+
const cfgPath = path.join(tmp, 'vitest.config.ts');
|
|
541
|
+
// content with "coverage: false" matches the exclusion regex
|
|
542
|
+
await fs.writeFile(cfgPath, '// coverage: false');
|
|
543
|
+
try {
|
|
544
|
+
const adapter = makeAdapter({
|
|
545
|
+
cwd: tmp,
|
|
546
|
+
config: { vitestConfig: cfgPath },
|
|
547
|
+
});
|
|
548
|
+
const result = await adapter.detectCoverageConfig();
|
|
549
|
+
expect(result.coverageEnabled).toBe(false);
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
261
555
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import fssync from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -156,6 +156,33 @@ describe('poolMutineerPlugin', () => {
|
|
|
156
156
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
|
+
it('returns null and logs error when readFileSync throws on redirect', async () => {
|
|
160
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-plugin-err-'));
|
|
161
|
+
const fromPath = path.join(tmpDir, 'source.ts');
|
|
162
|
+
const mutatedPath = path.join(tmpDir, 'mutated.ts');
|
|
163
|
+
globalThis.__mutineer_redirect__ = {
|
|
164
|
+
from: fromPath,
|
|
165
|
+
to: mutatedPath,
|
|
166
|
+
};
|
|
167
|
+
const readSpy = vi
|
|
168
|
+
.spyOn(fssync, 'readFileSync')
|
|
169
|
+
.mockImplementationOnce(() => {
|
|
170
|
+
throw new Error('disk error');
|
|
171
|
+
});
|
|
172
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
173
|
+
try {
|
|
174
|
+
const load = getLoadFn();
|
|
175
|
+
const result = load?.(fromPath);
|
|
176
|
+
expect(result).toBeNull();
|
|
177
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to read mutant file'));
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
readSpy.mockRestore();
|
|
181
|
+
consoleSpy.mockRestore();
|
|
182
|
+
globalThis.__mutineer_redirect__ = undefined;
|
|
183
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
159
186
|
describe('config hook', () => {
|
|
160
187
|
it('returns null when MUTINEER_ACTIVE_ID_FILE is not set', () => {
|
|
161
188
|
const config = getConfigFn();
|