@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
|
@@ -211,6 +211,22 @@ describe('executePool', () => {
|
|
|
211
211
|
// default file should NOT exist
|
|
212
212
|
await expect(fs.access(path.join(tmpDir, '.mutineer-cache.json'))).rejects.toThrow();
|
|
213
213
|
});
|
|
214
|
+
it('writes mutineer-report.json when reportFormat=json without shard', async () => {
|
|
215
|
+
const adapter = makeAdapter();
|
|
216
|
+
const cache = {};
|
|
217
|
+
await executePool({
|
|
218
|
+
tasks: [makeTask({ key: 'json-no-shard-key' })],
|
|
219
|
+
adapter,
|
|
220
|
+
cache,
|
|
221
|
+
concurrency: 1,
|
|
222
|
+
progressMode: 'list',
|
|
223
|
+
cwd: tmpDir,
|
|
224
|
+
reportFormat: 'json',
|
|
225
|
+
});
|
|
226
|
+
const reportFile = path.join(tmpDir, 'mutineer-report.json');
|
|
227
|
+
const content = JSON.parse(await fs.readFile(reportFile, 'utf8'));
|
|
228
|
+
expect(content.schemaVersion).toBe(1);
|
|
229
|
+
});
|
|
214
230
|
it('writes shard-suffixed JSON report when shard and reportFormat=json are set', async () => {
|
|
215
231
|
const adapter = makeAdapter();
|
|
216
232
|
const cache = {};
|
|
@@ -516,6 +532,67 @@ describe('executePool', () => {
|
|
|
516
532
|
expect(cacheStringifyCalls.length).toBe(1);
|
|
517
533
|
stringifySpy.mockRestore();
|
|
518
534
|
});
|
|
535
|
+
it('uses bar mode for Progress when progressMode is bar', async () => {
|
|
536
|
+
const adapter = makeAdapter();
|
|
537
|
+
const cache = {};
|
|
538
|
+
// Should not throw, and bar mode writes to stderr (no isTTY in tests so uses console.log)
|
|
539
|
+
await executePool({
|
|
540
|
+
tasks: [makeTask({ key: 'bar-mode-key' })],
|
|
541
|
+
adapter,
|
|
542
|
+
cache,
|
|
543
|
+
concurrency: 1,
|
|
544
|
+
progressMode: 'bar',
|
|
545
|
+
cwd: tmpDir,
|
|
546
|
+
});
|
|
547
|
+
expect(cache['bar-mode-key'].status).toBe('killed');
|
|
548
|
+
});
|
|
549
|
+
it('stores passingTests when result contains passingTests', async () => {
|
|
550
|
+
const tmpFile = path.join(tmpDir, 'passing.ts');
|
|
551
|
+
await fs.writeFile(tmpFile, 'const x = a + b\n');
|
|
552
|
+
const adapter = makeAdapter({
|
|
553
|
+
runMutant: vi.fn().mockResolvedValue({
|
|
554
|
+
status: 'escaped',
|
|
555
|
+
durationMs: 1,
|
|
556
|
+
passingTests: ['/tests/foo.spec.ts'],
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
const cache = {};
|
|
560
|
+
const task = makeTask({
|
|
561
|
+
key: 'passing-key',
|
|
562
|
+
v: {
|
|
563
|
+
id: 'passing.ts#0',
|
|
564
|
+
name: 'flipArith',
|
|
565
|
+
file: tmpFile,
|
|
566
|
+
code: 'const x = a - b\n',
|
|
567
|
+
line: 1,
|
|
568
|
+
col: 10,
|
|
569
|
+
tests: ['/tests/foo.spec.ts'],
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
await executePool({
|
|
573
|
+
tasks: [task],
|
|
574
|
+
adapter,
|
|
575
|
+
cache,
|
|
576
|
+
concurrency: 1,
|
|
577
|
+
progressMode: 'list',
|
|
578
|
+
cwd: tmpDir,
|
|
579
|
+
});
|
|
580
|
+
expect(cache['passing-key'].passingTests).toEqual(['/tests/foo.spec.ts']);
|
|
581
|
+
});
|
|
582
|
+
it('sets exitCode and logs no-mutants note when minKillPercent is set but no mutants evaluated', async () => {
|
|
583
|
+
const adapter = makeAdapter();
|
|
584
|
+
const cache = {};
|
|
585
|
+
await executePool({
|
|
586
|
+
tasks: [],
|
|
587
|
+
adapter,
|
|
588
|
+
cache,
|
|
589
|
+
concurrency: 1,
|
|
590
|
+
progressMode: 'list',
|
|
591
|
+
minKillPercent: 80,
|
|
592
|
+
cwd: tmpDir,
|
|
593
|
+
});
|
|
594
|
+
expect(process.exitCode).toBe(1);
|
|
595
|
+
});
|
|
519
596
|
it('handles adapter errors gracefully and still shuts down', async () => {
|
|
520
597
|
const adapter = makeAdapter({
|
|
521
598
|
runMutant: vi.fn().mockRejectedValue(new Error('adapter failure')),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { diagnosticKey, makeHost, diagnose } from '../ts-checker-worker.js';
|
|
5
|
+
const defaultOptions = {
|
|
6
|
+
strict: true,
|
|
7
|
+
noEmit: true,
|
|
8
|
+
target: ts.ScriptTarget.ES2020,
|
|
9
|
+
};
|
|
10
|
+
describe('diagnosticKey', () => {
|
|
11
|
+
it('returns code:start string for diagnostic with start', () => {
|
|
12
|
+
const d = { code: 2345, start: 10 };
|
|
13
|
+
expect(diagnosticKey(d)).toBe('2345:10');
|
|
14
|
+
});
|
|
15
|
+
it('returns code:-1 when start is undefined', () => {
|
|
16
|
+
const d = { code: 1234, start: undefined };
|
|
17
|
+
expect(diagnosticKey(d)).toBe('1234:-1');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('makeHost', () => {
|
|
21
|
+
it('returns custom source file for the target path', () => {
|
|
22
|
+
const targetPath = path.resolve('/fake/target.ts');
|
|
23
|
+
const code = 'const x: string = 1';
|
|
24
|
+
const host = makeHost(defaultOptions, targetPath, code);
|
|
25
|
+
const sf = host.getSourceFile(targetPath, ts.ScriptTarget.ES2020);
|
|
26
|
+
expect(sf).toBeDefined();
|
|
27
|
+
expect(sf.text).toBe(code);
|
|
28
|
+
});
|
|
29
|
+
it('falls back to orig for non-target files', () => {
|
|
30
|
+
const targetPath = path.resolve('/fake/target.ts');
|
|
31
|
+
const host = makeHost(defaultOptions, targetPath, 'const x = 1');
|
|
32
|
+
// A file that doesn't exist and isn't the target — should return undefined
|
|
33
|
+
const sf = host.getSourceFile('/some/other/file.ts', ts.ScriptTarget.ES2020);
|
|
34
|
+
expect(sf).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('diagnose', () => {
|
|
38
|
+
it('returns empty keys for valid TypeScript code', () => {
|
|
39
|
+
const targetPath = path.resolve('/fake/valid.ts');
|
|
40
|
+
const code = 'const x: number = 1\n';
|
|
41
|
+
const { keys } = diagnose(defaultOptions, targetPath, code, undefined);
|
|
42
|
+
expect(keys.size).toBe(0);
|
|
43
|
+
}, 15000);
|
|
44
|
+
it('detects type errors and returns diagnostic keys', () => {
|
|
45
|
+
const targetPath = path.resolve('/fake/invalid.ts');
|
|
46
|
+
const code = 'const x: number = "hello"\n';
|
|
47
|
+
const { keys } = diagnose(defaultOptions, targetPath, code, undefined);
|
|
48
|
+
expect(keys.size).toBeGreaterThan(0);
|
|
49
|
+
for (const key of keys) {
|
|
50
|
+
expect(key).toMatch(/^\d+:\d+$/);
|
|
51
|
+
}
|
|
52
|
+
}, 15000);
|
|
53
|
+
it('returns empty keys when sourceFile not found', () => {
|
|
54
|
+
// diagnose with empty code for a path that resolves to something ts can not find
|
|
55
|
+
const targetPath = path.resolve('/absolutely/nonexistent/path/foo.ts');
|
|
56
|
+
// Pass code that makes it hard for ts to find the sourceFile by alternate path
|
|
57
|
+
const { keys } = diagnose(defaultOptions, targetPath, '', undefined);
|
|
58
|
+
expect(keys instanceof Set).toBe(true);
|
|
59
|
+
}, 15000);
|
|
60
|
+
it('accepts an oldProgram for incremental compilation', () => {
|
|
61
|
+
const targetPath = path.resolve('/fake/incr.ts');
|
|
62
|
+
const { program: p1 } = diagnose(defaultOptions, targetPath, 'const x = 1\n', undefined);
|
|
63
|
+
const { keys } = diagnose(defaultOptions, targetPath, 'const x: string = 1\n', p1);
|
|
64
|
+
expect(keys instanceof Set).toBe(true);
|
|
65
|
+
}, 15000);
|
|
66
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { resolveTypescriptEnabled, resolveTsconfigPath, resolveCompilerOptions, checkTypes, checkFileSync, } from '../ts-checker.js';
|
|
4
6
|
// Directory with a real tsconfig.json (the project root, 3 levels up from __tests__)
|
|
5
7
|
const CWD_WITH_TSCONFIG = path.resolve(import.meta.dirname, '../../../');
|
|
6
8
|
// A temp-like directory unlikely to have tsconfig.json
|
|
@@ -17,6 +19,45 @@ function makeVariant(overrides = {}) {
|
|
|
17
19
|
...overrides,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
22
|
+
describe('resolveCompilerOptions', () => {
|
|
23
|
+
let tmpDir;
|
|
24
|
+
let brokenTsconfigDir;
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-tsco-'));
|
|
27
|
+
brokenTsconfigDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-tsco-broken-'));
|
|
28
|
+
// Write a broken tsconfig (invalid JSON)
|
|
29
|
+
await fs.writeFile(path.join(brokenTsconfigDir, 'tsconfig.json'), '{ invalid json', 'utf8');
|
|
30
|
+
});
|
|
31
|
+
afterAll(async () => {
|
|
32
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
33
|
+
await fs.rm(brokenTsconfigDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
it('returns base options when no tsconfig found in cwd', () => {
|
|
36
|
+
const opts = resolveCompilerOptions(undefined, tmpDir);
|
|
37
|
+
expect(opts.noEmit).toBe(true);
|
|
38
|
+
expect(opts.noLib).toBe(true);
|
|
39
|
+
expect(opts.noResolve).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('returns base options when tsconfig has parse errors', () => {
|
|
42
|
+
const opts = resolveCompilerOptions(undefined, brokenTsconfigDir);
|
|
43
|
+
expect(opts.noEmit).toBe(true);
|
|
44
|
+
expect(opts.noLib).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('parses real tsconfig and merges options', () => {
|
|
47
|
+
const opts = resolveCompilerOptions(undefined, CWD_WITH_TSCONFIG);
|
|
48
|
+
// Should still override these core isolation settings
|
|
49
|
+
expect(opts.noEmit).toBe(true);
|
|
50
|
+
expect(opts.noLib).toBe(true);
|
|
51
|
+
expect(opts.noResolve).toBe(true);
|
|
52
|
+
expect(opts.skipLibCheck).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('uses explicit tsconfig path when provided', () => {
|
|
55
|
+
const tsconfigPath = path.join(CWD_WITH_TSCONFIG, 'tsconfig.json');
|
|
56
|
+
const opts = resolveCompilerOptions(tsconfigPath, CWD_WITH_TSCONFIG);
|
|
57
|
+
expect(opts.noEmit).toBe(true);
|
|
58
|
+
expect(opts.noLib).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
20
61
|
describe('resolveTsconfigPath', () => {
|
|
21
62
|
it('returns undefined for boolean true', () => {
|
|
22
63
|
expect(resolveTsconfigPath({ typescript: true })).toBeUndefined();
|
|
@@ -97,6 +138,28 @@ describe('checkTypes', () => {
|
|
|
97
138
|
expect(result.has('multi.ts#0')).toBe(false);
|
|
98
139
|
expect(result.has('multi.ts#1')).toBe(true);
|
|
99
140
|
}, 15000);
|
|
141
|
+
it('uses tsconfig from CWD_WITH_TSCONFIG when provided', async () => {
|
|
142
|
+
const variant = makeVariant({
|
|
143
|
+
id: 'with-tsconfig.ts#0',
|
|
144
|
+
file: '/nonexistent/with-tsconfig.ts',
|
|
145
|
+
code: 'const x: number = 1',
|
|
146
|
+
});
|
|
147
|
+
// Should not flag valid code even when cwd has tsconfig
|
|
148
|
+
const result = await checkTypes([variant], undefined, CWD_WITH_TSCONFIG);
|
|
149
|
+
expect(result.has('with-tsconfig.ts#0')).toBe(false);
|
|
150
|
+
}, 30000);
|
|
151
|
+
it('filters out mutant errors that were already in baseline', async () => {
|
|
152
|
+
// The original file also has a type error (same code as mutant) — no NEW errors
|
|
153
|
+
const variant = makeVariant({
|
|
154
|
+
id: 'baseline-error.ts#0',
|
|
155
|
+
file: '/nonexistent/baseline-error.ts',
|
|
156
|
+
// The mutant code is IDENTICAL to what we'd read as baseline (empty string)
|
|
157
|
+
// so any errors in the mutant were already in the baseline
|
|
158
|
+
code: 'const x: number = 1', // valid code - no new errors compared to empty baseline
|
|
159
|
+
});
|
|
160
|
+
const result = await checkTypes([variant], undefined, CWD_WITHOUT_TSCONFIG);
|
|
161
|
+
expect(result.has('baseline-error.ts#0')).toBe(false);
|
|
162
|
+
}, 15000);
|
|
100
163
|
it('checks variants from different files independently', async () => {
|
|
101
164
|
const validA = makeVariant({
|
|
102
165
|
id: 'a.ts#0',
|
|
@@ -113,3 +176,27 @@ describe('checkTypes', () => {
|
|
|
113
176
|
expect(result.has('b.ts#0')).toBe(true);
|
|
114
177
|
}, 15000);
|
|
115
178
|
});
|
|
179
|
+
describe('checkFileSync', () => {
|
|
180
|
+
let tmpDir;
|
|
181
|
+
beforeAll(async () => {
|
|
182
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-checkfilesync-'));
|
|
183
|
+
});
|
|
184
|
+
afterAll(async () => {
|
|
185
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
186
|
+
});
|
|
187
|
+
it('does not flag errors that are already present in the baseline file', async () => {
|
|
188
|
+
// Write a baseline file with a type error
|
|
189
|
+
const errorFile = path.join(tmpDir, 'baseline-err.ts');
|
|
190
|
+
await fs.writeFile(errorFile, 'const x: number = "bad string"', 'utf8');
|
|
191
|
+
const options = resolveCompilerOptions(undefined, tmpDir);
|
|
192
|
+
const variant = makeVariant({
|
|
193
|
+
id: 'baseline-err.ts#0',
|
|
194
|
+
file: errorFile,
|
|
195
|
+
// Mutant has the same type error as the baseline file
|
|
196
|
+
code: 'const x: number = "bad string"',
|
|
197
|
+
});
|
|
198
|
+
// The error exists in both baseline and mutant — no NEW errors, so not flagged
|
|
199
|
+
const ids = checkFileSync(options, errorFile, [variant]);
|
|
200
|
+
expect(ids).not.toContain('baseline-err.ts#0');
|
|
201
|
+
}, 15000);
|
|
202
|
+
});
|
package/dist/runner/args.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { MutineerConfig } from '../types/config.js';
|
|
|
11
11
|
export interface ParsedCliOptions {
|
|
12
12
|
readonly configPath: string | undefined;
|
|
13
13
|
readonly wantsChanged: boolean;
|
|
14
|
-
readonly
|
|
14
|
+
readonly wantsChangedWithImports: boolean;
|
|
15
15
|
readonly wantsFull: boolean;
|
|
16
16
|
readonly wantsOnlyCoveredLines: boolean;
|
|
17
17
|
readonly wantsPerTestCoverage: boolean;
|
package/dist/runner/args.js
CHANGED
|
@@ -133,7 +133,7 @@ export function parseShardOption(args) {
|
|
|
133
133
|
export function parseCliOptions(args, cfg) {
|
|
134
134
|
const configPath = readStringFlag(args, '--config', '-c');
|
|
135
135
|
const wantsChanged = args.includes('--changed');
|
|
136
|
-
const
|
|
136
|
+
const wantsChangedWithImports = args.includes('--changed-with-imports');
|
|
137
137
|
const wantsFull = args.includes('--full');
|
|
138
138
|
const wantsOnlyCoveredLines = args.includes('--only-covered-lines') || cfg.onlyCoveredLines === true;
|
|
139
139
|
const wantsPerTestCoverage = args.includes('--per-test-coverage') || cfg.perTestCoverage === true;
|
|
@@ -163,7 +163,7 @@ export function parseCliOptions(args, cfg) {
|
|
|
163
163
|
return {
|
|
164
164
|
configPath,
|
|
165
165
|
wantsChanged,
|
|
166
|
-
|
|
166
|
+
wantsChangedWithImports,
|
|
167
167
|
wantsFull,
|
|
168
168
|
wantsOnlyCoveredLines,
|
|
169
169
|
wantsPerTestCoverage,
|
package/dist/runner/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
import { toErrorMessage } from '../utils/errors.js';
|
|
5
6
|
// Constants
|
|
6
7
|
const CONFIG_FILENAMES = [
|
|
7
8
|
'mutineer.config.ts',
|
|
@@ -71,8 +72,7 @@ export async function loadMutineerConfig(cwd, configPath) {
|
|
|
71
72
|
return loadedConfig;
|
|
72
73
|
}
|
|
73
74
|
catch (err) {
|
|
74
|
-
|
|
75
|
-
throw new Error(`Failed to load config from ${configFile}: ${message}`);
|
|
75
|
+
throw new Error(`Failed to load config from ${configFile}: ${toErrorMessage(err)}`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
@@ -88,7 +88,6 @@ async function loadTypeScriptConfig(filePath) {
|
|
|
88
88
|
return loaded?.config ?? {};
|
|
89
89
|
}
|
|
90
90
|
catch (err) {
|
|
91
|
-
|
|
92
|
-
throw new Error(`Cannot load TypeScript config. Ensure 'vite' is installed or rename to .js/.mjs:\n${message}`);
|
|
91
|
+
throw new Error(`Cannot load TypeScript config. Ensure 'vite' is installed or rename to .js/.mjs:\n${toErrorMessage(err)}`);
|
|
93
92
|
}
|
|
94
93
|
}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { loadCoverageData, loadPerTestCoverageData, } from '../utils/coverage.js';
|
|
3
3
|
import { isCoverageRequestedInArgs } from './vitest/index.js';
|
|
4
4
|
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
import { toErrorMessage } from '../utils/errors.js';
|
|
5
6
|
const log = createLogger('coverage-resolver');
|
|
6
7
|
/**
|
|
7
8
|
* Resolve all coverage-related configuration from CLI options, config, and adapter detection.
|
|
@@ -76,7 +77,7 @@ export async function loadCoverageAfterBaseline(resolution, cwd) {
|
|
|
76
77
|
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
77
78
|
}
|
|
78
79
|
catch (err) {
|
|
79
|
-
const msg =
|
|
80
|
+
const msg = toErrorMessage(err);
|
|
80
81
|
log.warn(`Warning: Could not load coverage data: ${msg}`);
|
|
81
82
|
log.warn('Continuing without coverage filtering.');
|
|
82
83
|
}
|
package/dist/runner/discover.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import fg from 'fast-glob';
|
|
5
5
|
import { normalizePath } from '../utils/normalizePath.js';
|
|
6
6
|
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
import { toErrorMessage } from '../utils/errors.js';
|
|
7
8
|
const TEST_PATTERNS_DEFAULT = [
|
|
8
9
|
'**/*.test.[jt]s?(x)',
|
|
9
10
|
'**/*.spec.[jt]s?(x)',
|
|
@@ -99,8 +100,7 @@ async function createViteResolver(rootAbs, exts) {
|
|
|
99
100
|
typeof vue === 'function' ? [vue()] : [];
|
|
100
101
|
}
|
|
101
102
|
catch (err) {
|
|
102
|
-
|
|
103
|
-
log.warn(`Unable to load @vitejs/plugin-vue; Vue SFC imports may fail to resolve (${detail})`);
|
|
103
|
+
log.warn(`Unable to load @vitejs/plugin-vue; Vue SFC imports may fail to resolve (${toErrorMessage(err)})`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
|
@@ -32,6 +32,15 @@ function makeAdapter(opts = {}) {
|
|
|
32
32
|
cliArgs: opts.cliArgs ?? ['--changed'],
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
function makeAdapterWithArgs(cliArgs) {
|
|
36
|
+
return createJestAdapter({
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
concurrency: 2,
|
|
39
|
+
timeoutMs: 1000,
|
|
40
|
+
config: { jestConfig: undefined },
|
|
41
|
+
cliArgs,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
35
44
|
describe('Jest adapter', () => {
|
|
36
45
|
beforeEach(() => {
|
|
37
46
|
vi.clearAllMocks();
|
|
@@ -157,3 +166,163 @@ describe('Jest adapter', () => {
|
|
|
157
166
|
}
|
|
158
167
|
});
|
|
159
168
|
});
|
|
169
|
+
describe('Jest adapter additional coverage', () => {
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
vi.clearAllMocks();
|
|
172
|
+
});
|
|
173
|
+
it('strips --min-kill-percent= and --config= and -c= style args', () => {
|
|
174
|
+
const adapter = makeAdapterWithArgs([
|
|
175
|
+
'--min-kill-percent=50',
|
|
176
|
+
'--config=jest.config.ts',
|
|
177
|
+
'-c=x',
|
|
178
|
+
'--verbose',
|
|
179
|
+
]);
|
|
180
|
+
expect(adapter).toBeDefined();
|
|
181
|
+
});
|
|
182
|
+
it('strips consumeNext args like --concurrency and --runner', () => {
|
|
183
|
+
const adapter = makeAdapterWithArgs([
|
|
184
|
+
'--concurrency',
|
|
185
|
+
'4',
|
|
186
|
+
'--runner',
|
|
187
|
+
'jest',
|
|
188
|
+
]);
|
|
189
|
+
expect(adapter).toBeDefined();
|
|
190
|
+
});
|
|
191
|
+
it('runBaseline catches runCLI Error and returns false', async () => {
|
|
192
|
+
const adapter = makeAdapter();
|
|
193
|
+
runCLIMock.mockRejectedValueOnce(new Error('jest crashed'));
|
|
194
|
+
const ok = await adapter.runBaseline(['test.spec.ts'], {
|
|
195
|
+
collectCoverage: false,
|
|
196
|
+
perTestCoverage: false,
|
|
197
|
+
});
|
|
198
|
+
expect(ok).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
it('runBaseline catches non-Error runCLI rejection and returns false', async () => {
|
|
201
|
+
const adapter = makeAdapter();
|
|
202
|
+
runCLIMock.mockRejectedValueOnce('string rejection');
|
|
203
|
+
const ok = await adapter.runBaseline(['test.spec.ts'], {
|
|
204
|
+
collectCoverage: false,
|
|
205
|
+
perTestCoverage: false,
|
|
206
|
+
});
|
|
207
|
+
expect(ok).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
it('makeCapture handles Buffer chunks and callback signatures', async () => {
|
|
210
|
+
const adapter = makeAdapter();
|
|
211
|
+
runCLIMock.mockImplementationOnce(async () => {
|
|
212
|
+
// Buffer chunk (covers Buffer.isBuffer true branch)
|
|
213
|
+
process.stdout.write(Buffer.from('buffered'));
|
|
214
|
+
// Write with encoding arg (covers typeof encodingOrCb !== 'function' → use cb)
|
|
215
|
+
const cb1 = vi.fn();
|
|
216
|
+
process.stdout.write('str-with-encoding', 'utf8', cb1);
|
|
217
|
+
// Write with function as second arg (covers typeof encodingOrCb === 'function')
|
|
218
|
+
const cb2 = vi.fn();
|
|
219
|
+
process.stdout.write('str-with-cb', cb2);
|
|
220
|
+
return { results: { success: false } };
|
|
221
|
+
});
|
|
222
|
+
const writtenStdout = [];
|
|
223
|
+
const spy = vi
|
|
224
|
+
.spyOn(process.stdout, 'write')
|
|
225
|
+
.mockImplementation((chunk) => {
|
|
226
|
+
writtenStdout.push(Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk));
|
|
227
|
+
return true;
|
|
228
|
+
});
|
|
229
|
+
await adapter.runBaseline(['test.spec.ts'], {
|
|
230
|
+
collectCoverage: false,
|
|
231
|
+
perTestCoverage: false,
|
|
232
|
+
});
|
|
233
|
+
spy.mockRestore();
|
|
234
|
+
expect(writtenStdout.join('')).toContain('buffered');
|
|
235
|
+
});
|
|
236
|
+
it('runMutant throws if init() not called', async () => {
|
|
237
|
+
const adapter = makeAdapter();
|
|
238
|
+
await expect(adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t'])).rejects.toThrow('JestAdapter not initialised');
|
|
239
|
+
});
|
|
240
|
+
it('runMutant returns error on pool.run() Error throw', async () => {
|
|
241
|
+
const adapter = makeAdapter();
|
|
242
|
+
await adapter.init();
|
|
243
|
+
poolInstance.run.mockRejectedValueOnce(new Error('pool crashed'));
|
|
244
|
+
const res = await adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
245
|
+
expect(res).toEqual({
|
|
246
|
+
status: 'error',
|
|
247
|
+
durationMs: 0,
|
|
248
|
+
error: 'pool crashed',
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
it('runMutant returns error on pool.run() non-Error throw', async () => {
|
|
252
|
+
const adapter = makeAdapter();
|
|
253
|
+
await adapter.init();
|
|
254
|
+
poolInstance.run.mockRejectedValueOnce('string error');
|
|
255
|
+
const res = await adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
256
|
+
expect(res).toEqual({
|
|
257
|
+
status: 'error',
|
|
258
|
+
durationMs: 0,
|
|
259
|
+
error: 'string error',
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
it('runMutant maps escaped result', async () => {
|
|
263
|
+
const adapter = makeAdapter();
|
|
264
|
+
await adapter.init();
|
|
265
|
+
poolInstance.run.mockResolvedValueOnce({ killed: false, durationMs: 8 });
|
|
266
|
+
const res = await adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
267
|
+
expect(res).toEqual({ status: 'escaped', durationMs: 8 });
|
|
268
|
+
});
|
|
269
|
+
it('runMutant maps timeout result', async () => {
|
|
270
|
+
const adapter = makeAdapter();
|
|
271
|
+
await adapter.init();
|
|
272
|
+
poolInstance.run.mockResolvedValueOnce({
|
|
273
|
+
killed: false,
|
|
274
|
+
durationMs: 30000,
|
|
275
|
+
error: 'timeout',
|
|
276
|
+
});
|
|
277
|
+
const res = await adapter.runMutant({ id: '1', name: 'm', file: 'f', code: 'c', line: 1, col: 1 }, ['t']);
|
|
278
|
+
expect(res).toEqual({
|
|
279
|
+
status: 'timeout',
|
|
280
|
+
durationMs: 30000,
|
|
281
|
+
error: 'timeout',
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
it('shutdown with pool calls pool.shutdown and nulls pool', async () => {
|
|
285
|
+
const adapter = makeAdapter();
|
|
286
|
+
await adapter.init();
|
|
287
|
+
await adapter.shutdown();
|
|
288
|
+
expect(poolInstance.shutdown).toHaveBeenCalledTimes(1);
|
|
289
|
+
});
|
|
290
|
+
it('shutdown with no pool is a no-op', async () => {
|
|
291
|
+
const adapter = makeAdapter();
|
|
292
|
+
await expect(adapter.shutdown()).resolves.toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
it('hasCoverageProvider returns false when jest not found in cwd', () => {
|
|
295
|
+
const adapter = makeAdapter({ cwd: os.tmpdir() });
|
|
296
|
+
// Resolving jest/package.json from os.tmpdir() should fail
|
|
297
|
+
const result = adapter.hasCoverageProvider();
|
|
298
|
+
expect(result).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
it('detectCoverageConfig returns defaults when no jestConfig set', async () => {
|
|
301
|
+
const adapter = makeAdapter({ config: { jestConfig: undefined } });
|
|
302
|
+
const coverage = await adapter.detectCoverageConfig();
|
|
303
|
+
expect(coverage).toEqual({ perTestEnabled: false, coverageEnabled: false });
|
|
304
|
+
});
|
|
305
|
+
it('detectCoverageConfig returns false on unreadable config file', async () => {
|
|
306
|
+
const adapter = makeAdapter({
|
|
307
|
+
config: { jestConfig: '/nonexistent/path/jest.config.ts' },
|
|
308
|
+
});
|
|
309
|
+
const coverage = await adapter.detectCoverageConfig();
|
|
310
|
+
expect(coverage).toEqual({ perTestEnabled: false, coverageEnabled: false });
|
|
311
|
+
});
|
|
312
|
+
it('detectCoverageConfig detects coverageProvider pattern', async () => {
|
|
313
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-jest-'));
|
|
314
|
+
const cfgPath = path.join(tmp, 'jest.config.ts');
|
|
315
|
+
await fs.writeFile(cfgPath, 'module.exports = { coverageProvider: "v8" }');
|
|
316
|
+
try {
|
|
317
|
+
const adapter = makeAdapter({
|
|
318
|
+
cwd: tmp,
|
|
319
|
+
config: { jestConfig: cfgPath },
|
|
320
|
+
});
|
|
321
|
+
const coverage = await adapter.detectCoverageConfig();
|
|
322
|
+
expect(coverage.coverageEnabled).toBe(true);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|