@mutineerjs/mutineer 0.2.3 → 0.2.4
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/core/__tests__/module.spec.js +66 -3
- package/dist/core/__tests__/sfc.spec.d.ts +1 -0
- package/dist/core/__tests__/sfc.spec.js +76 -0
- package/dist/core/__tests__/variant-utils.spec.d.ts +1 -0
- package/dist/core/__tests__/variant-utils.spec.js +93 -0
- package/dist/runner/__tests__/args.spec.d.ts +1 -0
- package/dist/runner/__tests__/args.spec.js +225 -0
- package/dist/runner/__tests__/cache.spec.d.ts +1 -0
- package/dist/runner/__tests__/cache.spec.js +180 -0
- package/dist/runner/__tests__/changed.spec.d.ts +1 -0
- package/dist/runner/__tests__/changed.spec.js +227 -0
- package/dist/runner/__tests__/cleanup.spec.d.ts +1 -0
- package/dist/runner/__tests__/cleanup.spec.js +41 -0
- package/dist/runner/__tests__/config.spec.d.ts +1 -0
- package/dist/runner/__tests__/config.spec.js +71 -0
- package/dist/runner/__tests__/coverage-resolver.spec.d.ts +1 -0
- package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
- package/dist/runner/__tests__/pool-executor.spec.d.ts +1 -0
- package/dist/runner/__tests__/pool-executor.spec.js +213 -0
- package/dist/runner/__tests__/tasks.spec.d.ts +1 -0
- package/dist/runner/__tests__/tasks.spec.js +95 -0
- package/dist/runner/__tests__/variants.spec.d.ts +1 -0
- package/dist/runner/__tests__/variants.spec.js +259 -0
- package/dist/runner/args.d.ts +5 -0
- package/dist/runner/args.js +7 -0
- package/dist/runner/config.js +2 -2
- package/dist/runner/coverage-resolver.d.ts +21 -0
- package/dist/runner/coverage-resolver.js +96 -0
- package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/pool.spec.js +212 -0
- package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
- package/dist/runner/orchestrator.js +43 -295
- package/dist/runner/pool-executor.d.ts +17 -0
- package/dist/runner/pool-executor.js +143 -0
- package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
- package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
- package/dist/runner/tasks.d.ts +12 -0
- package/dist/runner/tasks.js +25 -0
- package/dist/runner/variants.d.ts +17 -2
- package/dist/runner/variants.js +33 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +4 -0
- package/dist/utils/__tests__/logger.spec.d.ts +1 -0
- package/dist/utils/__tests__/logger.spec.js +61 -0
- package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
- package/dist/utils/__tests__/normalizePath.spec.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadCoverageData, loadPerTestCoverageData, } from '../utils/coverage.js';
|
|
3
|
+
import { isCoverageRequestedInArgs } from './vitest/index.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
const log = createLogger('coverage-resolver');
|
|
6
|
+
/**
|
|
7
|
+
* Resolve all coverage-related configuration from CLI options, config, and adapter detection.
|
|
8
|
+
* Returns a unified resolution object used by the orchestrator.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveCoverageConfig(opts, cfg, adapter, cliArgs) {
|
|
11
|
+
const coverageConfig = await adapter.detectCoverageConfig();
|
|
12
|
+
const wantsPerTestCoverageFromConfig = coverageConfig.perTestEnabled;
|
|
13
|
+
const coveragePreference = cfg.coverage;
|
|
14
|
+
const wantsCoverageRun = coveragePreference === true
|
|
15
|
+
? true
|
|
16
|
+
: coveragePreference === false
|
|
17
|
+
? false
|
|
18
|
+
: isCoverageRequestedInArgs([...cliArgs]) || coverageConfig.coverageEnabled;
|
|
19
|
+
// Load pre-existing coverage data if provided
|
|
20
|
+
let coverageData = null;
|
|
21
|
+
if (opts.coverageFilePath) {
|
|
22
|
+
log.info(`Loading coverage data from ${opts.coverageFilePath}...`);
|
|
23
|
+
coverageData = await loadCoverageData(opts.coverageFilePath, process.cwd());
|
|
24
|
+
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
25
|
+
}
|
|
26
|
+
const needsCoverageFromBaseline = opts.wantsOnlyCoveredLines && !coverageData;
|
|
27
|
+
const hasCoverageProviderInstalled = adapter.hasCoverageProvider();
|
|
28
|
+
const rawPerTestCoverage = opts.wantsPerTestCoverage ||
|
|
29
|
+
wantsPerTestCoverageFromConfig ||
|
|
30
|
+
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
31
|
+
const wantsPerTestCoverage = opts.runner === 'jest' ? false : rawPerTestCoverage;
|
|
32
|
+
if (opts.runner === 'jest' && rawPerTestCoverage) {
|
|
33
|
+
log.warn('Per-test coverage is not supported for Jest; continuing without per-test coverage.');
|
|
34
|
+
}
|
|
35
|
+
if (needsCoverageFromBaseline && !hasCoverageProviderInstalled) {
|
|
36
|
+
log.warn('The "onlyCoveredLines" option requires a coverage provider to generate coverage data.');
|
|
37
|
+
log.warn('Please install the appropriate coverage package (or disable onlyCoveredLines).');
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return {
|
|
40
|
+
coverageData: null,
|
|
41
|
+
perTestCoverage: null,
|
|
42
|
+
enableCoverageForBaseline: false,
|
|
43
|
+
wantsPerTestCoverage: false,
|
|
44
|
+
needsCoverageFromBaseline,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (opts.wantsOnlyCoveredLines &&
|
|
48
|
+
coverageData &&
|
|
49
|
+
!hasCoverageProviderInstalled) {
|
|
50
|
+
log.warn('The "onlyCoveredLines" option is enabled, but no coverage provider is installed.');
|
|
51
|
+
log.warn('Running baseline tests without injecting per-test coverage; existing coverageFile will be used for filtering.');
|
|
52
|
+
}
|
|
53
|
+
const enableCoverageForBaseline = needsCoverageFromBaseline ||
|
|
54
|
+
wantsPerTestCoverage ||
|
|
55
|
+
wantsCoverageRun ||
|
|
56
|
+
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
57
|
+
return {
|
|
58
|
+
coverageData,
|
|
59
|
+
perTestCoverage: null,
|
|
60
|
+
enableCoverageForBaseline,
|
|
61
|
+
wantsPerTestCoverage,
|
|
62
|
+
needsCoverageFromBaseline,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Load coverage data produced during the baseline run.
|
|
67
|
+
* Mutates and returns an updated CoverageResolution.
|
|
68
|
+
*/
|
|
69
|
+
export async function loadCoverageAfterBaseline(resolution, cwd) {
|
|
70
|
+
let { coverageData, perTestCoverage } = resolution;
|
|
71
|
+
if (resolution.needsCoverageFromBaseline) {
|
|
72
|
+
const defaultCoveragePath = path.join(cwd, 'coverage', 'coverage-final.json');
|
|
73
|
+
log.info(`Loading coverage data from ${defaultCoveragePath}...`);
|
|
74
|
+
try {
|
|
75
|
+
coverageData = await loadCoverageData(defaultCoveragePath, cwd);
|
|
76
|
+
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
log.warn(`Warning: Could not load coverage data: ${msg}`);
|
|
81
|
+
log.warn('Continuing without coverage filtering.');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (resolution.wantsPerTestCoverage) {
|
|
85
|
+
const reportsDir = path.join(cwd, 'coverage');
|
|
86
|
+
log.info('Loading per-test coverage data...');
|
|
87
|
+
perTestCoverage = await loadPerTestCoverageData(reportsDir, cwd);
|
|
88
|
+
if (!perTestCoverage) {
|
|
89
|
+
log.warn('Per-test coverage data not found. Continuing without per-test test pruning.');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
log.info(`Loaded per-test coverage for ${perTestCoverage.size} tests`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { ...resolution, coverageData, perTestCoverage };
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { JestPool, runWithJestPool } from '../pool.js';
|
|
3
|
+
// We'll use the createWorker option to inject mock workers instead of forking processes
|
|
4
|
+
function makeMockWorker(id) {
|
|
5
|
+
let readyResolve = null;
|
|
6
|
+
const worker = {
|
|
7
|
+
id,
|
|
8
|
+
_ready: true,
|
|
9
|
+
_busy: false,
|
|
10
|
+
_mockResult: { killed: true, durationMs: 10 },
|
|
11
|
+
on: vi.fn(),
|
|
12
|
+
once: vi.fn(),
|
|
13
|
+
isReady: vi.fn(() => worker._ready),
|
|
14
|
+
isBusy: vi.fn(() => worker._busy),
|
|
15
|
+
start: vi.fn(async () => {
|
|
16
|
+
// Simulate worker startup
|
|
17
|
+
}),
|
|
18
|
+
run: vi.fn(async () => worker._mockResult),
|
|
19
|
+
shutdown: vi.fn(async () => { }),
|
|
20
|
+
kill: vi.fn(),
|
|
21
|
+
emit: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
return worker;
|
|
24
|
+
}
|
|
25
|
+
const dummyMutant = {
|
|
26
|
+
id: 'test#1',
|
|
27
|
+
name: 'flipEQ',
|
|
28
|
+
file: '/src/foo.ts',
|
|
29
|
+
code: 'mutated',
|
|
30
|
+
line: 1,
|
|
31
|
+
col: 0,
|
|
32
|
+
};
|
|
33
|
+
describe('JestPool', () => {
|
|
34
|
+
it('throws if run is called before init', async () => {
|
|
35
|
+
const pool = new JestPool({ cwd: '/tmp', concurrency: 1 });
|
|
36
|
+
await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialized');
|
|
37
|
+
});
|
|
38
|
+
it('initializes with the specified concurrency', async () => {
|
|
39
|
+
const workers = [];
|
|
40
|
+
const pool = new JestPool({
|
|
41
|
+
cwd: '/tmp',
|
|
42
|
+
concurrency: 2,
|
|
43
|
+
createWorker: (id) => {
|
|
44
|
+
const w = makeMockWorker(id);
|
|
45
|
+
workers.push(w);
|
|
46
|
+
return w;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
await pool.init();
|
|
50
|
+
expect(workers).toHaveLength(2);
|
|
51
|
+
expect(workers[0].start).toHaveBeenCalled();
|
|
52
|
+
expect(workers[1].start).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
it('does not re-initialize if already initialized', async () => {
|
|
55
|
+
const workers = [];
|
|
56
|
+
const pool = new JestPool({
|
|
57
|
+
cwd: '/tmp',
|
|
58
|
+
concurrency: 1,
|
|
59
|
+
createWorker: (id) => {
|
|
60
|
+
const w = makeMockWorker(id);
|
|
61
|
+
workers.push(w);
|
|
62
|
+
return w;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
await pool.init();
|
|
66
|
+
await pool.init(); // second call should be no-op
|
|
67
|
+
expect(workers).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
it('runs a mutant via a worker', async () => {
|
|
70
|
+
const workers = [];
|
|
71
|
+
const pool = new JestPool({
|
|
72
|
+
cwd: '/tmp',
|
|
73
|
+
concurrency: 1,
|
|
74
|
+
createWorker: (id) => {
|
|
75
|
+
const w = makeMockWorker(id);
|
|
76
|
+
w._mockResult = { killed: true, durationMs: 50 };
|
|
77
|
+
workers.push(w);
|
|
78
|
+
return w;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
await pool.init();
|
|
82
|
+
const result = await pool.run(dummyMutant, ['test.ts']);
|
|
83
|
+
expect(result.killed).toBe(true);
|
|
84
|
+
expect(result.durationMs).toBe(50);
|
|
85
|
+
});
|
|
86
|
+
it('shuts down all workers', async () => {
|
|
87
|
+
const workers = [];
|
|
88
|
+
const pool = new JestPool({
|
|
89
|
+
cwd: '/tmp',
|
|
90
|
+
concurrency: 2,
|
|
91
|
+
createWorker: (id) => {
|
|
92
|
+
const w = makeMockWorker(id);
|
|
93
|
+
workers.push(w);
|
|
94
|
+
return w;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
await pool.init();
|
|
98
|
+
await pool.shutdown();
|
|
99
|
+
for (const w of workers) {
|
|
100
|
+
expect(w.shutdown).toHaveBeenCalled();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it('throws if run is called after shutdown', async () => {
|
|
104
|
+
const pool = new JestPool({
|
|
105
|
+
cwd: '/tmp',
|
|
106
|
+
concurrency: 1,
|
|
107
|
+
createWorker: (id) => makeMockWorker(id),
|
|
108
|
+
});
|
|
109
|
+
await pool.init();
|
|
110
|
+
await pool.shutdown();
|
|
111
|
+
// After shutdown, initialized is set to false, so "not initialized" check fires first
|
|
112
|
+
await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialized');
|
|
113
|
+
});
|
|
114
|
+
it('does not double-shutdown', async () => {
|
|
115
|
+
const workers = [];
|
|
116
|
+
const pool = new JestPool({
|
|
117
|
+
cwd: '/tmp',
|
|
118
|
+
concurrency: 1,
|
|
119
|
+
createWorker: (id) => {
|
|
120
|
+
const w = makeMockWorker(id);
|
|
121
|
+
workers.push(w);
|
|
122
|
+
return w;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
await pool.init();
|
|
126
|
+
await pool.shutdown();
|
|
127
|
+
await pool.shutdown(); // should not throw
|
|
128
|
+
expect(workers[0].shutdown).toHaveBeenCalledTimes(1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('runWithJestPool', () => {
|
|
132
|
+
it('maps killed result correctly', async () => {
|
|
133
|
+
const pool = new JestPool({
|
|
134
|
+
cwd: '/tmp',
|
|
135
|
+
concurrency: 1,
|
|
136
|
+
createWorker: (id) => {
|
|
137
|
+
const w = makeMockWorker(id);
|
|
138
|
+
w._mockResult = { killed: true, durationMs: 10 };
|
|
139
|
+
return w;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
await pool.init();
|
|
143
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
144
|
+
expect(result.status).toBe('killed');
|
|
145
|
+
expect(result.durationMs).toBe(10);
|
|
146
|
+
await pool.shutdown();
|
|
147
|
+
});
|
|
148
|
+
it('maps escaped result correctly', async () => {
|
|
149
|
+
const pool = new JestPool({
|
|
150
|
+
cwd: '/tmp',
|
|
151
|
+
concurrency: 1,
|
|
152
|
+
createWorker: (id) => {
|
|
153
|
+
const w = makeMockWorker(id);
|
|
154
|
+
w._mockResult = { killed: false, durationMs: 20 };
|
|
155
|
+
return w;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
await pool.init();
|
|
159
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
160
|
+
expect(result.status).toBe('escaped');
|
|
161
|
+
await pool.shutdown();
|
|
162
|
+
});
|
|
163
|
+
it('maps timeout error correctly', async () => {
|
|
164
|
+
const pool = new JestPool({
|
|
165
|
+
cwd: '/tmp',
|
|
166
|
+
concurrency: 1,
|
|
167
|
+
createWorker: (id) => {
|
|
168
|
+
const w = makeMockWorker(id);
|
|
169
|
+
w._mockResult = { killed: true, durationMs: 5000, error: 'timeout' };
|
|
170
|
+
return w;
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
await pool.init();
|
|
174
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
175
|
+
expect(result.status).toBe('timeout');
|
|
176
|
+
expect(result.error).toBe('timeout');
|
|
177
|
+
await pool.shutdown();
|
|
178
|
+
});
|
|
179
|
+
it('maps non-timeout error with !killed to error status', async () => {
|
|
180
|
+
const pool = new JestPool({
|
|
181
|
+
cwd: '/tmp',
|
|
182
|
+
concurrency: 1,
|
|
183
|
+
createWorker: (id) => {
|
|
184
|
+
const w = makeMockWorker(id);
|
|
185
|
+
w._mockResult = { killed: false, durationMs: 10, error: 'crash' };
|
|
186
|
+
return w;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
await pool.init();
|
|
190
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
191
|
+
expect(result.status).toBe('error');
|
|
192
|
+
expect(result.error).toBe('crash');
|
|
193
|
+
await pool.shutdown();
|
|
194
|
+
});
|
|
195
|
+
it('handles pool.run throwing an error', async () => {
|
|
196
|
+
const pool = new JestPool({
|
|
197
|
+
cwd: '/tmp',
|
|
198
|
+
concurrency: 1,
|
|
199
|
+
createWorker: (id) => {
|
|
200
|
+
const w = makeMockWorker(id);
|
|
201
|
+
w.run = vi.fn().mockRejectedValue(new Error('pool exploded'));
|
|
202
|
+
return w;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
await pool.init();
|
|
206
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
207
|
+
expect(result.status).toBe('error');
|
|
208
|
+
expect(result.error).toBe('pool exploded');
|
|
209
|
+
expect(result.durationMs).toBe(0);
|
|
210
|
+
await pool.shutdown();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { JestWorkerRuntime, createJestWorkerRuntime } from '../worker-runtime.js';
|
|
3
|
+
// Mock the shared utilities
|
|
4
|
+
vi.mock('../../shared/index.js', () => ({
|
|
5
|
+
getMutantFilePath: vi.fn((file, id) => `/tmp/__mutineer__/mutant_${id}.ts`),
|
|
6
|
+
setRedirect: vi.fn(),
|
|
7
|
+
clearRedirect: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
// Mock fs sync operations
|
|
10
|
+
const writeFileSyncMock = vi.fn();
|
|
11
|
+
const rmSyncMock = vi.fn();
|
|
12
|
+
vi.mock('node:fs', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
existsSync: vi.fn(() => true),
|
|
15
|
+
writeFileSync: (...args) => writeFileSyncMock(...args),
|
|
16
|
+
rmSync: (...args) => rmSyncMock(...args),
|
|
17
|
+
},
|
|
18
|
+
existsSync: vi.fn(() => true),
|
|
19
|
+
writeFileSync: (...args) => writeFileSyncMock(...args),
|
|
20
|
+
rmSync: (...args) => rmSyncMock(...args),
|
|
21
|
+
}));
|
|
22
|
+
// Mock @jest/core
|
|
23
|
+
const mockRunCLI = vi.fn();
|
|
24
|
+
vi.mock('@jest/core', () => ({
|
|
25
|
+
runCLI: (...args) => mockRunCLI(...args),
|
|
26
|
+
}));
|
|
27
|
+
describe('JestWorkerRuntime', () => {
|
|
28
|
+
let runtime;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
runtime = new JestWorkerRuntime({
|
|
32
|
+
workerId: 'w0',
|
|
33
|
+
cwd: '/project',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// Clean up env vars
|
|
38
|
+
delete process.env.MUTINEER_REDIRECT_FROM;
|
|
39
|
+
delete process.env.MUTINEER_REDIRECT_TO;
|
|
40
|
+
});
|
|
41
|
+
it('init and shutdown are no-ops', async () => {
|
|
42
|
+
await expect(runtime.init()).resolves.toBeUndefined();
|
|
43
|
+
await expect(runtime.shutdown()).resolves.toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
it('runs a mutant and returns killed when tests fail', async () => {
|
|
46
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
47
|
+
results: {
|
|
48
|
+
success: false,
|
|
49
|
+
numTotalTests: 3,
|
|
50
|
+
testResults: [{ failureMessage: 'Expected true to be false' }],
|
|
51
|
+
},
|
|
52
|
+
globalConfig: {},
|
|
53
|
+
});
|
|
54
|
+
const result = await runtime.run({
|
|
55
|
+
id: 'foo.ts#1',
|
|
56
|
+
name: 'flipEQ',
|
|
57
|
+
file: '/project/src/foo.ts',
|
|
58
|
+
code: 'mutated code',
|
|
59
|
+
line: 1,
|
|
60
|
+
col: 0,
|
|
61
|
+
}, ['/project/tests/foo.test.ts']);
|
|
62
|
+
expect(result.killed).toBe(true);
|
|
63
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
64
|
+
expect(result.error).toBe('Expected true to be false');
|
|
65
|
+
});
|
|
66
|
+
it('returns not killed when tests pass', async () => {
|
|
67
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
68
|
+
results: {
|
|
69
|
+
success: true,
|
|
70
|
+
numTotalTests: 3,
|
|
71
|
+
testResults: [],
|
|
72
|
+
},
|
|
73
|
+
globalConfig: {},
|
|
74
|
+
});
|
|
75
|
+
const result = await runtime.run({
|
|
76
|
+
id: 'foo.ts#1',
|
|
77
|
+
name: 'flipEQ',
|
|
78
|
+
file: '/project/src/foo.ts',
|
|
79
|
+
code: 'mutated code',
|
|
80
|
+
line: 1,
|
|
81
|
+
col: 0,
|
|
82
|
+
}, ['/project/tests/foo.test.ts']);
|
|
83
|
+
expect(result.killed).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('returns killed on runCLI error', async () => {
|
|
86
|
+
mockRunCLI.mockRejectedValueOnce(new Error('Jest crashed'));
|
|
87
|
+
const result = await runtime.run({
|
|
88
|
+
id: 'foo.ts#1',
|
|
89
|
+
name: 'flipEQ',
|
|
90
|
+
file: '/project/src/foo.ts',
|
|
91
|
+
code: 'mutated code',
|
|
92
|
+
line: 1,
|
|
93
|
+
col: 0,
|
|
94
|
+
}, ['/project/tests/foo.test.ts']);
|
|
95
|
+
expect(result.killed).toBe(true);
|
|
96
|
+
expect(result.error).toBe('Jest crashed');
|
|
97
|
+
});
|
|
98
|
+
it('writes the mutant file and cleans up after run', async () => {
|
|
99
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
100
|
+
results: { success: true, testResults: [] },
|
|
101
|
+
globalConfig: {},
|
|
102
|
+
});
|
|
103
|
+
const { getMutantFilePath, setRedirect, clearRedirect } = await import('../../shared/index.js');
|
|
104
|
+
await runtime.run({
|
|
105
|
+
id: 'foo.ts#1',
|
|
106
|
+
name: 'flipEQ',
|
|
107
|
+
file: '/project/src/foo.ts',
|
|
108
|
+
code: 'mutated code',
|
|
109
|
+
line: 1,
|
|
110
|
+
col: 0,
|
|
111
|
+
}, ['/project/tests/foo.test.ts']);
|
|
112
|
+
expect(getMutantFilePath).toHaveBeenCalled();
|
|
113
|
+
expect(writeFileSyncMock).toHaveBeenCalled();
|
|
114
|
+
expect(setRedirect).toHaveBeenCalled();
|
|
115
|
+
expect(clearRedirect).toHaveBeenCalled();
|
|
116
|
+
expect(rmSyncMock).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
it('uses jest config when provided', async () => {
|
|
119
|
+
const runtimeWithConfig = new JestWorkerRuntime({
|
|
120
|
+
workerId: 'w0',
|
|
121
|
+
cwd: '/project',
|
|
122
|
+
jestConfigPath: 'jest.config.ts',
|
|
123
|
+
});
|
|
124
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
125
|
+
results: { success: true, testResults: [] },
|
|
126
|
+
globalConfig: {},
|
|
127
|
+
});
|
|
128
|
+
await runtimeWithConfig.run({
|
|
129
|
+
id: 'foo.ts#1',
|
|
130
|
+
name: 'flipEQ',
|
|
131
|
+
file: '/project/src/foo.ts',
|
|
132
|
+
code: 'mutated code',
|
|
133
|
+
line: 1,
|
|
134
|
+
col: 0,
|
|
135
|
+
}, ['/project/tests/foo.test.ts']);
|
|
136
|
+
const callArgs = mockRunCLI.mock.calls[0][0];
|
|
137
|
+
expect(callArgs.config).toBe('jest.config.ts');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('createJestWorkerRuntime', () => {
|
|
141
|
+
it('returns a JestWorkerRuntime instance', () => {
|
|
142
|
+
const runtime = createJestWorkerRuntime({
|
|
143
|
+
workerId: 'w1',
|
|
144
|
+
cwd: '/project',
|
|
145
|
+
});
|
|
146
|
+
expect(runtime).toBeInstanceOf(JestWorkerRuntime);
|
|
147
|
+
});
|
|
148
|
+
});
|