@mutineerjs/mutineer 0.5.1 → 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/README.md +42 -2
- package/dist/bin/__tests__/mutineer.spec.d.ts +1 -0
- package/dist/bin/__tests__/mutineer.spec.js +43 -0
- package/dist/bin/mutineer.d.ts +2 -1
- package/dist/bin/mutineer.js +44 -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 +101 -1
- package/dist/runner/__tests__/cache.spec.js +65 -8
- package/dist/runner/__tests__/changed.spec.js +85 -2
- package/dist/runner/__tests__/cleanup.spec.js +30 -0
- package/dist/runner/__tests__/config.spec.js +2 -13
- package/dist/runner/__tests__/coverage-resolver.spec.js +9 -2
- package/dist/runner/__tests__/discover.spec.js +128 -0
- package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
- package/dist/runner/__tests__/orchestrator.spec.js +306 -0
- package/dist/runner/__tests__/pool-executor.spec.js +60 -1
- package/dist/runner/args.d.ts +18 -0
- package/dist/runner/args.js +40 -0
- package/dist/runner/cache.d.ts +19 -3
- package/dist/runner/cache.js +14 -7
- package/dist/runner/changed.js +15 -43
- package/dist/runner/cleanup.d.ts +3 -1
- package/dist/runner/cleanup.js +18 -1
- package/dist/runner/config.js +1 -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/jest/__tests__/pool.spec.js +41 -0
- package/dist/runner/jest/pool.js +3 -3
- package/dist/runner/orchestrator.d.ts +1 -0
- package/dist/runner/orchestrator.js +38 -9
- 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 +60 -0
- package/dist/runner/vitest/__tests__/pool.spec.js +57 -0
- package/dist/runner/vitest/adapter.js +16 -9
- package/dist/runner/vitest/pool.js +3 -3
- package/dist/types/config.d.ts +4 -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
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { clearCacheOnStart, saveCacheAtomic, decodeCacheKey, keyForTests, hash, readMutantCache, } from '../cache.js';
|
|
5
|
+
import { clearCacheOnStart, saveCacheAtomic, decodeCacheKey, keyForTests, hash, readMutantCache, getCacheFilename, } from '../cache.js';
|
|
6
6
|
let tmpDir;
|
|
7
7
|
beforeEach(async () => {
|
|
8
8
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-cache-'));
|
|
@@ -10,9 +10,19 @@ beforeEach(async () => {
|
|
|
10
10
|
afterEach(async () => {
|
|
11
11
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
12
12
|
});
|
|
13
|
+
describe('getCacheFilename', () => {
|
|
14
|
+
it('returns default filename when no shard', () => {
|
|
15
|
+
expect(getCacheFilename()).toBe('.mutineer-cache.json');
|
|
16
|
+
expect(getCacheFilename(undefined)).toBe('.mutineer-cache.json');
|
|
17
|
+
});
|
|
18
|
+
it('returns shard-namespaced filename when shard provided', () => {
|
|
19
|
+
expect(getCacheFilename({ index: 1, total: 2 })).toBe('.mutineer-cache-shard-1-of-2.json');
|
|
20
|
+
expect(getCacheFilename({ index: 3, total: 4 })).toBe('.mutineer-cache-shard-3-of-4.json');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
13
23
|
describe('clearCacheOnStart', () => {
|
|
14
24
|
it('removes the cache file if it exists', async () => {
|
|
15
|
-
const cacheFile = path.join(tmpDir, '.
|
|
25
|
+
const cacheFile = path.join(tmpDir, '.mutineer-cache.json');
|
|
16
26
|
await fs.writeFile(cacheFile, '{}');
|
|
17
27
|
await clearCacheOnStart(tmpDir);
|
|
18
28
|
await expect(fs.access(cacheFile)).rejects.toThrow();
|
|
@@ -20,6 +30,19 @@ describe('clearCacheOnStart', () => {
|
|
|
20
30
|
it('does not throw if cache file does not exist', async () => {
|
|
21
31
|
await expect(clearCacheOnStart(tmpDir)).resolves.toBeUndefined();
|
|
22
32
|
});
|
|
33
|
+
it('removes shard-specific cache file', async () => {
|
|
34
|
+
const shardFile = path.join(tmpDir, '.mutineer-cache-shard-1-of-2.json');
|
|
35
|
+
await fs.writeFile(shardFile, '{}');
|
|
36
|
+
await clearCacheOnStart(tmpDir, { index: 1, total: 2 });
|
|
37
|
+
await expect(fs.access(shardFile)).rejects.toThrow();
|
|
38
|
+
});
|
|
39
|
+
it('does not remove default cache when shard is specified', async () => {
|
|
40
|
+
const defaultFile = path.join(tmpDir, '.mutineer-cache.json');
|
|
41
|
+
await fs.writeFile(defaultFile, '{}');
|
|
42
|
+
await clearCacheOnStart(tmpDir, { index: 1, total: 2 });
|
|
43
|
+
// default file should still exist
|
|
44
|
+
await expect(fs.access(defaultFile)).resolves.toBeUndefined();
|
|
45
|
+
});
|
|
23
46
|
});
|
|
24
47
|
describe('saveCacheAtomic', () => {
|
|
25
48
|
it('writes cache data to the file', async () => {
|
|
@@ -33,7 +56,7 @@ describe('saveCacheAtomic', () => {
|
|
|
33
56
|
},
|
|
34
57
|
};
|
|
35
58
|
await saveCacheAtomic(tmpDir, cache);
|
|
36
|
-
const content = await fs.readFile(path.join(tmpDir, '.
|
|
59
|
+
const content = await fs.readFile(path.join(tmpDir, '.mutineer-cache.json'), 'utf8');
|
|
37
60
|
expect(JSON.parse(content)).toEqual(cache);
|
|
38
61
|
});
|
|
39
62
|
it('overwrites existing cache', async () => {
|
|
@@ -48,9 +71,25 @@ describe('saveCacheAtomic', () => {
|
|
|
48
71
|
},
|
|
49
72
|
};
|
|
50
73
|
await saveCacheAtomic(tmpDir, newCache);
|
|
51
|
-
const content = await fs.readFile(path.join(tmpDir, '.
|
|
74
|
+
const content = await fs.readFile(path.join(tmpDir, '.mutineer-cache.json'), 'utf8');
|
|
52
75
|
expect(JSON.parse(content)).toEqual(newCache);
|
|
53
76
|
});
|
|
77
|
+
it('writes to shard-named file when shard is provided', async () => {
|
|
78
|
+
const cache = {
|
|
79
|
+
k: {
|
|
80
|
+
status: 'killed',
|
|
81
|
+
file: 'x.ts',
|
|
82
|
+
line: 1,
|
|
83
|
+
col: 0,
|
|
84
|
+
mutator: 'm',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
await saveCacheAtomic(tmpDir, cache, { index: 2, total: 3 });
|
|
88
|
+
const content = await fs.readFile(path.join(tmpDir, '.mutineer-cache-shard-2-of-3.json'), 'utf8');
|
|
89
|
+
expect(JSON.parse(content)).toEqual(cache);
|
|
90
|
+
// default file should NOT exist
|
|
91
|
+
await expect(fs.access(path.join(tmpDir, '.mutineer-cache.json'))).rejects.toThrow();
|
|
92
|
+
});
|
|
54
93
|
});
|
|
55
94
|
describe('decodeCacheKey', () => {
|
|
56
95
|
it('decodes a full cache key', () => {
|
|
@@ -128,6 +167,24 @@ describe('readMutantCache', () => {
|
|
|
128
167
|
const result = await readMutantCache(tmpDir);
|
|
129
168
|
expect(result).toEqual({});
|
|
130
169
|
});
|
|
170
|
+
it('reads from shard-named file when shard is provided', async () => {
|
|
171
|
+
const cache = {
|
|
172
|
+
'k:v:f.ts:1,0:m': {
|
|
173
|
+
status: 'killed',
|
|
174
|
+
file: 'f.ts',
|
|
175
|
+
line: 1,
|
|
176
|
+
col: 0,
|
|
177
|
+
mutator: 'm',
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache-shard-1-of-2.json'), JSON.stringify(cache));
|
|
181
|
+
const result = await readMutantCache(tmpDir, { index: 1, total: 2 });
|
|
182
|
+
expect(result['k:v:f.ts:1,0:m'].status).toBe('killed');
|
|
183
|
+
});
|
|
184
|
+
it('returns empty object when shard file does not exist', async () => {
|
|
185
|
+
const result = await readMutantCache(tmpDir, { index: 2, total: 4 });
|
|
186
|
+
expect(result).toEqual({});
|
|
187
|
+
});
|
|
131
188
|
it('reads and normalizes object-format cache entries', async () => {
|
|
132
189
|
const cache = {
|
|
133
190
|
'testsig:codesig:file.ts:1,0:flip': {
|
|
@@ -138,7 +195,7 @@ describe('readMutantCache', () => {
|
|
|
138
195
|
mutator: 'flip',
|
|
139
196
|
},
|
|
140
197
|
};
|
|
141
|
-
await fs.writeFile(path.join(tmpDir, '.
|
|
198
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache.json'), JSON.stringify(cache));
|
|
142
199
|
const result = await readMutantCache(tmpDir);
|
|
143
200
|
expect(result['testsig:codesig:file.ts:1,0:flip']).toEqual({
|
|
144
201
|
status: 'killed',
|
|
@@ -152,14 +209,14 @@ describe('readMutantCache', () => {
|
|
|
152
209
|
const cache = {
|
|
153
210
|
'testsig:codesig:file.ts:1,0:flip': 'killed',
|
|
154
211
|
};
|
|
155
|
-
await fs.writeFile(path.join(tmpDir, '.
|
|
212
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache.json'), JSON.stringify(cache));
|
|
156
213
|
const result = await readMutantCache(tmpDir);
|
|
157
214
|
const entry = result['testsig:codesig:file.ts:1,0:flip'];
|
|
158
215
|
expect(entry.status).toBe('killed');
|
|
159
216
|
expect(entry.mutator).toBe('flip');
|
|
160
217
|
});
|
|
161
218
|
it('returns empty object for invalid JSON', async () => {
|
|
162
|
-
await fs.writeFile(path.join(tmpDir, '.
|
|
219
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache.json'), 'not json');
|
|
163
220
|
const result = await readMutantCache(tmpDir);
|
|
164
221
|
expect(result).toEqual({});
|
|
165
222
|
});
|
|
@@ -169,7 +226,7 @@ describe('readMutantCache', () => {
|
|
|
169
226
|
status: 'escaped',
|
|
170
227
|
},
|
|
171
228
|
};
|
|
172
|
-
await fs.writeFile(path.join(tmpDir, '.
|
|
229
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache.json'), JSON.stringify(cache));
|
|
173
230
|
const result = await readMutantCache(tmpDir);
|
|
174
231
|
const entry = result['testsig:codesig:file.ts:5,3:mut'];
|
|
175
232
|
expect(entry.status).toBe('escaped');
|
|
@@ -145,11 +145,94 @@ describe('listChangedFiles', () => {
|
|
|
145
145
|
}
|
|
146
146
|
return { status: 0, stdout: '' };
|
|
147
147
|
});
|
|
148
|
-
// File with imports - the import resolution will fail (no real files)
|
|
149
|
-
// but it exercises the parsing code paths
|
|
150
148
|
readFileSyncMock.mockReturnValue('import { bar } from "./bar"\nexport { baz } from "./baz"\nconst x = require("./qux")');
|
|
149
|
+
existsSyncMock.mockImplementation((p) => {
|
|
150
|
+
// Changed file and .ts variants of deps exist
|
|
151
|
+
return [
|
|
152
|
+
'/repo/src/foo.ts',
|
|
153
|
+
'/repo/src/bar.ts',
|
|
154
|
+
'/repo/src/baz.ts',
|
|
155
|
+
'/repo/src/qux.ts',
|
|
156
|
+
].includes(p);
|
|
157
|
+
});
|
|
151
158
|
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
152
159
|
expect(result).toContain('/repo/src/foo.ts');
|
|
160
|
+
expect(result).toContain('/repo/src/bar.ts');
|
|
161
|
+
expect(result).toContain('/repo/src/baz.ts');
|
|
162
|
+
expect(result).toContain('/repo/src/qux.ts');
|
|
163
|
+
});
|
|
164
|
+
it('resolves dep with .ts extension when imported without extension', () => {
|
|
165
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
166
|
+
if (args.includes('--show-toplevel'))
|
|
167
|
+
return { status: 0, stdout: '/repo\n' };
|
|
168
|
+
if (args.includes('main...HEAD'))
|
|
169
|
+
return { status: 0, stdout: 'src/foo.ts\0' };
|
|
170
|
+
return { status: 0, stdout: '' };
|
|
171
|
+
});
|
|
172
|
+
readFileSyncMock.mockReturnValue('import { x } from "./utils"');
|
|
173
|
+
existsSyncMock.mockImplementation((p) => {
|
|
174
|
+
return ['/repo/src/foo.ts', '/repo/src/utils.ts'].includes(p);
|
|
175
|
+
});
|
|
176
|
+
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
177
|
+
expect(result).toContain('/repo/src/utils.ts');
|
|
178
|
+
});
|
|
179
|
+
it('excludes deps outside cwd', () => {
|
|
180
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
181
|
+
if (args.includes('--show-toplevel'))
|
|
182
|
+
return { status: 0, stdout: '/repo\n' };
|
|
183
|
+
if (args.includes('main...HEAD'))
|
|
184
|
+
return { status: 0, stdout: 'src/foo.ts\0' };
|
|
185
|
+
return { status: 0, stdout: '' };
|
|
186
|
+
});
|
|
187
|
+
readFileSyncMock.mockReturnValue('import { x } from "../../outside/dep"');
|
|
188
|
+
existsSyncMock.mockImplementation((p) => {
|
|
189
|
+
return ['/repo/src/foo.ts', '/outside/dep.ts'].includes(p);
|
|
190
|
+
});
|
|
191
|
+
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
192
|
+
expect(result).not.toContain('/outside/dep.ts');
|
|
193
|
+
expect(result).toHaveLength(1);
|
|
194
|
+
});
|
|
195
|
+
it('excludes deps inside node_modules', () => {
|
|
196
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
197
|
+
if (args.includes('--show-toplevel'))
|
|
198
|
+
return { status: 0, stdout: '/repo\n' };
|
|
199
|
+
if (args.includes('main...HEAD'))
|
|
200
|
+
return { status: 0, stdout: 'src/foo.ts\0' };
|
|
201
|
+
return { status: 0, stdout: '' };
|
|
202
|
+
});
|
|
203
|
+
readFileSyncMock.mockReturnValue('import { x } from "./node_modules/pkg/index"');
|
|
204
|
+
existsSyncMock.mockImplementation((p) => {
|
|
205
|
+
return p === '/repo/src/foo.ts' || p.includes('node_modules');
|
|
206
|
+
});
|
|
207
|
+
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
208
|
+
expect(result.some((p) => p.includes('node_modules'))).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
it('resolves direct dep but not transitive dep at maxDepth=1', () => {
|
|
211
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
212
|
+
if (args.includes('--show-toplevel'))
|
|
213
|
+
return { status: 0, stdout: '/repo\n' };
|
|
214
|
+
if (args.includes('main...HEAD'))
|
|
215
|
+
return { status: 0, stdout: 'src/foo.ts\0' };
|
|
216
|
+
return { status: 0, stdout: '' };
|
|
217
|
+
});
|
|
218
|
+
readFileSyncMock.mockImplementation((p) => {
|
|
219
|
+
if (p === '/repo/src/foo.ts')
|
|
220
|
+
return 'import { x } from "./bar"';
|
|
221
|
+
if (p === '/repo/src/bar.ts')
|
|
222
|
+
return 'import { y } from "./baz"';
|
|
223
|
+
return '';
|
|
224
|
+
});
|
|
225
|
+
existsSyncMock.mockImplementation((p) => {
|
|
226
|
+
return [
|
|
227
|
+
'/repo/src/foo.ts',
|
|
228
|
+
'/repo/src/bar.ts',
|
|
229
|
+
'/repo/src/baz.ts',
|
|
230
|
+
].includes(p);
|
|
231
|
+
});
|
|
232
|
+
const result = listChangedFiles('/repo', { includeDeps: true, maxDepth: 1 });
|
|
233
|
+
expect(result).toContain('/repo/src/foo.ts');
|
|
234
|
+
expect(result).toContain('/repo/src/bar.ts');
|
|
235
|
+
expect(result).not.toContain('/repo/src/baz.ts');
|
|
153
236
|
});
|
|
154
237
|
it('skips non-local imports in dependency resolution', () => {
|
|
155
238
|
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
@@ -38,4 +38,34 @@ describe('cleanupMutineerDirs', () => {
|
|
|
38
38
|
const stat = await fs.stat(srcDir);
|
|
39
39
|
expect(stat.isDirectory()).toBe(true);
|
|
40
40
|
});
|
|
41
|
+
it('does not remove cache files by default', async () => {
|
|
42
|
+
const cacheFile = path.join(tmpDir, '.mutineer-cache.json');
|
|
43
|
+
await fs.writeFile(cacheFile, '{}');
|
|
44
|
+
await cleanupMutineerDirs(tmpDir);
|
|
45
|
+
await expect(fs.access(cacheFile)).resolves.toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
it('removes .mutineer-cache*.json files when includeCacheFiles is true', async () => {
|
|
48
|
+
const cacheFile = path.join(tmpDir, '.mutineer-cache.json');
|
|
49
|
+
const shardFile = path.join(tmpDir, '.mutineer-cache-shard-1-of-2.json');
|
|
50
|
+
await fs.writeFile(cacheFile, '{}');
|
|
51
|
+
await fs.writeFile(shardFile, '{}');
|
|
52
|
+
await cleanupMutineerDirs(tmpDir, { includeCacheFiles: true });
|
|
53
|
+
await expect(fs.access(cacheFile)).rejects.toThrow();
|
|
54
|
+
await expect(fs.access(shardFile)).rejects.toThrow();
|
|
55
|
+
});
|
|
56
|
+
it('removes legacy .mutate-cache*.json files for migration when includeCacheFiles is true', async () => {
|
|
57
|
+
const legacyCache = path.join(tmpDir, '.mutate-cache.json');
|
|
58
|
+
const legacyShard = path.join(tmpDir, '.mutate-cache-shard-2-of-4.json');
|
|
59
|
+
await fs.writeFile(legacyCache, '{}');
|
|
60
|
+
await fs.writeFile(legacyShard, '{}');
|
|
61
|
+
await cleanupMutineerDirs(tmpDir, { includeCacheFiles: true });
|
|
62
|
+
await expect(fs.access(legacyCache)).rejects.toThrow();
|
|
63
|
+
await expect(fs.access(legacyShard)).rejects.toThrow();
|
|
64
|
+
});
|
|
65
|
+
it('removes .mutineer-cache*.json.tmp temp files when includeCacheFiles is true', async () => {
|
|
66
|
+
const tmpFile = path.join(tmpDir, '.mutineer-cache.json.tmp');
|
|
67
|
+
await fs.writeFile(tmpFile, '{}');
|
|
68
|
+
await cleanupMutineerDirs(tmpDir, { includeCacheFiles: true });
|
|
69
|
+
await expect(fs.access(tmpFile)).rejects.toThrow();
|
|
70
|
+
});
|
|
41
71
|
});
|
|
@@ -52,20 +52,9 @@ describe('loadMutineerConfig', () => {
|
|
|
52
52
|
await fs.writeFile(configFile, '??? not valid javascript ???');
|
|
53
53
|
await expect(loadMutineerConfig(tmpDir)).rejects.toThrow(/Failed to load config from/);
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
// 1. validateConfig uses `&&` instead of `||`: `typeof config !== 'object' && config === null`
|
|
57
|
-
// Since typeof null === 'object', this condition is always false, so null passes validation.
|
|
58
|
-
// 2. loadModule uses `||` instead of `??`: `mod.default || mod`
|
|
59
|
-
// When default export is null (falsy), it falls back to the module namespace object.
|
|
60
|
-
// Together: null configs pass validation AND get returned as the module namespace.
|
|
61
|
-
it('BUG: null config passes validation due to && vs || logic error', async () => {
|
|
55
|
+
it('throws when config exports null', async () => {
|
|
62
56
|
const configFile = path.join(tmpDir, 'mutineer.config.mjs');
|
|
63
57
|
await fs.writeFile(configFile, 'export default null');
|
|
64
|
-
|
|
65
|
-
// Additionally, loadModule returns { default: null } instead of null
|
|
66
|
-
// because it uses || (which treats null as falsy) instead of ??
|
|
67
|
-
const config = await loadMutineerConfig(tmpDir);
|
|
68
|
-
// Bug: returns the module namespace object instead of throwing
|
|
69
|
-
expect(config).toHaveProperty('default', null);
|
|
58
|
+
await expect(loadMutineerConfig(tmpDir)).rejects.toThrow('does not export a valid configuration object');
|
|
70
59
|
});
|
|
71
60
|
});
|
|
@@ -15,6 +15,9 @@ function makeOpts(overrides = {}) {
|
|
|
15
15
|
progressMode: 'bar',
|
|
16
16
|
minKillPercent: undefined,
|
|
17
17
|
runner: 'vitest',
|
|
18
|
+
timeout: undefined,
|
|
19
|
+
reportFormat: 'text',
|
|
20
|
+
shard: undefined,
|
|
18
21
|
...overrides,
|
|
19
22
|
};
|
|
20
23
|
}
|
|
@@ -62,7 +65,9 @@ describe('resolveCoverageConfig', () => {
|
|
|
62
65
|
});
|
|
63
66
|
it('sets exitCode when onlyCoveredLines is set but no coverage provider', async () => {
|
|
64
67
|
const opts = makeOpts({ wantsOnlyCoveredLines: true });
|
|
65
|
-
const adapter = makeAdapter({
|
|
68
|
+
const adapter = makeAdapter({
|
|
69
|
+
hasCoverageProvider: vi.fn().mockReturnValue(false),
|
|
70
|
+
});
|
|
66
71
|
await resolveCoverageConfig(opts, {}, adapter, []);
|
|
67
72
|
expect(process.exitCode).toBe(1);
|
|
68
73
|
});
|
|
@@ -129,7 +134,9 @@ describe('loadCoverageAfterBaseline', () => {
|
|
|
129
134
|
const coverageJson = {
|
|
130
135
|
'/src/foo.ts': {
|
|
131
136
|
path: '/src/foo.ts',
|
|
132
|
-
statementMap: {
|
|
137
|
+
statementMap: {
|
|
138
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
139
|
+
},
|
|
133
140
|
s: { '0': 1 },
|
|
134
141
|
},
|
|
135
142
|
};
|
|
@@ -114,6 +114,134 @@ describe('autoDiscoverTargetsAndTests', () => {
|
|
|
114
114
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
115
115
|
}
|
|
116
116
|
});
|
|
117
|
+
it('calls onProgress at least twice with informational messages', async () => {
|
|
118
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-progress-'));
|
|
119
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
120
|
+
const moduleFile = path.join(srcDir, 'foo.ts');
|
|
121
|
+
const testFile = path.join(srcDir, 'foo.test.ts');
|
|
122
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
123
|
+
await fs.writeFile(moduleFile, 'export const foo = 1\n', 'utf8');
|
|
124
|
+
const importLine = ['im', 'port { foo } from "./foo"'].join('');
|
|
125
|
+
await fs.writeFile(testFile, `${importLine}\nconsole.log(foo)\n`, 'utf8');
|
|
126
|
+
const messages = [];
|
|
127
|
+
try {
|
|
128
|
+
await autoDiscoverTargetsAndTests(tmpDir, { testPatterns: ['**/*.test.ts'] }, (msg) => messages.push(msg));
|
|
129
|
+
expect(messages.length).toBeGreaterThanOrEqual(2);
|
|
130
|
+
expect(messages.some((m) => m.includes('test file'))).toBe(true);
|
|
131
|
+
expect(messages.some((m) => m.includes('Discovery complete'))).toBe(true);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
it('shared dep imported by 2 tests appears in testMap for both', async () => {
|
|
138
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-shared-'));
|
|
139
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
140
|
+
const sharedDep = path.join(srcDir, 'shared.ts');
|
|
141
|
+
const test1 = path.join(srcDir, 'a.test.ts');
|
|
142
|
+
const test2 = path.join(srcDir, 'b.test.ts');
|
|
143
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
144
|
+
await fs.writeFile(sharedDep, 'export const shared = 1\n', 'utf8');
|
|
145
|
+
const importShared = ['im', 'port { shared } from "./shared"'].join('');
|
|
146
|
+
await fs.writeFile(test1, `${importShared}\n`, 'utf8');
|
|
147
|
+
await fs.writeFile(test2, `${importShared}\n`, 'utf8');
|
|
148
|
+
try {
|
|
149
|
+
const { testMap } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
150
|
+
testPatterns: ['**/*.test.ts'],
|
|
151
|
+
});
|
|
152
|
+
const sharedAbs = normalizePath(sharedDep);
|
|
153
|
+
const test1Abs = normalizePath(test1);
|
|
154
|
+
const test2Abs = normalizePath(test2);
|
|
155
|
+
expect(testMap.get(sharedAbs)?.has(test1Abs)).toBe(true);
|
|
156
|
+
expect(testMap.get(sharedAbs)?.has(test2Abs)).toBe(true);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
it('diamond graph: shared grandchild discovered with no duplicates', async () => {
|
|
163
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-diamond-'));
|
|
164
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
165
|
+
const fileA = path.join(srcDir, 'A.ts');
|
|
166
|
+
const fileB = path.join(srcDir, 'B.ts');
|
|
167
|
+
const fileC = path.join(srcDir, 'C.ts');
|
|
168
|
+
const fileD = path.join(srcDir, 'D.ts');
|
|
169
|
+
const testFile = path.join(srcDir, 'test.test.ts');
|
|
170
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
171
|
+
await fs.writeFile(fileD, 'export const d = 4\n', 'utf8');
|
|
172
|
+
const importD = ['im', 'port { d } from "./D"'].join('');
|
|
173
|
+
await fs.writeFile(fileB, `${importD}\nexport const b = 2\n`, 'utf8');
|
|
174
|
+
await fs.writeFile(fileC, `${importD}\nexport const c = 3\n`, 'utf8');
|
|
175
|
+
const importB = ['im', 'port { b } from "./B"'].join('');
|
|
176
|
+
const importC = ['im', 'port { c } from "./C"'].join('');
|
|
177
|
+
await fs.writeFile(fileA, `${importB}\n${importC}\nexport const a = 1\n`, 'utf8');
|
|
178
|
+
const importA = ['im', 'port { a } from "./A"'].join('');
|
|
179
|
+
await fs.writeFile(testFile, `${importA}\n`, 'utf8');
|
|
180
|
+
try {
|
|
181
|
+
const { testMap } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
182
|
+
testPatterns: ['**/*.test.ts'],
|
|
183
|
+
});
|
|
184
|
+
const dAbs = normalizePath(fileD);
|
|
185
|
+
const testAbs = normalizePath(testFile);
|
|
186
|
+
expect(testMap.get(dAbs)?.has(testAbs)).toBe(true);
|
|
187
|
+
// D should only be in testMap once (Set ensures no duplicates)
|
|
188
|
+
expect(testMap.get(dAbs)?.size).toBe(1);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
it('deep chain: deepest file is discovered correctly', async () => {
|
|
195
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-deep-'));
|
|
196
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
197
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
198
|
+
// chain: test -> f1 -> f2 -> f3 -> f4 -> f5
|
|
199
|
+
const files = Array.from({ length: 5 }, (_, i) => path.join(srcDir, `f${i + 1}.ts`));
|
|
200
|
+
const testFile = path.join(srcDir, 'chain.test.ts');
|
|
201
|
+
await fs.writeFile(files[4], 'export const f5 = 5\n', 'utf8');
|
|
202
|
+
for (let i = 3; i >= 0; i--) {
|
|
203
|
+
const importNext = ['im', `port { f${i + 2} } from "./f${i + 2}"`].join('');
|
|
204
|
+
await fs.writeFile(files[i], `${importNext}\nexport const f${i + 1} = ${i + 1}\n`, 'utf8');
|
|
205
|
+
}
|
|
206
|
+
const importF1 = ['im', 'port { f1 } from "./f1"'].join('');
|
|
207
|
+
await fs.writeFile(testFile, `${importF1}\n`, 'utf8');
|
|
208
|
+
try {
|
|
209
|
+
const { testMap } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
210
|
+
testPatterns: ['**/*.test.ts'],
|
|
211
|
+
});
|
|
212
|
+
const f5Abs = normalizePath(files[4]);
|
|
213
|
+
const testAbs = normalizePath(testFile);
|
|
214
|
+
expect(testMap.get(f5Abs)?.has(testAbs)).toBe(true);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
it('2 tests directly importing same file both appear in directTestMap', async () => {
|
|
221
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-direct2-'));
|
|
222
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
223
|
+
const sharedDep = path.join(srcDir, 'shared.ts');
|
|
224
|
+
const test1 = path.join(srcDir, 'a.test.ts');
|
|
225
|
+
const test2 = path.join(srcDir, 'b.test.ts');
|
|
226
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
227
|
+
await fs.writeFile(sharedDep, 'export const shared = 1\n', 'utf8');
|
|
228
|
+
const importShared = ['im', 'port { shared } from "./shared"'].join('');
|
|
229
|
+
await fs.writeFile(test1, `${importShared}\n`, 'utf8');
|
|
230
|
+
await fs.writeFile(test2, `${importShared}\n`, 'utf8');
|
|
231
|
+
try {
|
|
232
|
+
const { directTestMap } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
233
|
+
testPatterns: ['**/*.test.ts'],
|
|
234
|
+
});
|
|
235
|
+
const sharedAbs = normalizePath(sharedDep);
|
|
236
|
+
const test1Abs = normalizePath(test1);
|
|
237
|
+
const test2Abs = normalizePath(test2);
|
|
238
|
+
expect(directTestMap.get(sharedAbs)?.has(test1Abs)).toBe(true);
|
|
239
|
+
expect(directTestMap.get(sharedAbs)?.has(test2Abs)).toBe(true);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
117
245
|
it('ignores test files when collecting mutate targets', async () => {
|
|
118
246
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-'));
|
|
119
247
|
const srcDir = path.join(tmpDir, 'src');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|