@boyingliu01/xp-gate 0.14.8 → 0.14.9
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/bin/xp-gate.js +23 -2
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/upgrade-skill-sync.test.js +65 -0
- package/lib/check.js +115 -20
- package/lib/doctor.js +63 -0
- package/lib/gate-runner.js +209 -0
- package/lib/shared-utils.js +1 -1
- package/lib/sprint-status.js +3 -1
- package/lib/update-hooks.js +1 -1
- package/lib/upgrade.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/init-baseline.test.ts +347 -0
- package/mutation/__tests__/mutmut-runner.test.ts +561 -0
- package/mutation/__tests__/runners-index.test.ts +134 -0
- package/mutation/__tests__/stryker-runner.test.ts +455 -0
- package/mutation/__tests__/update-baseline.test.ts +393 -0
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-MUT-004 Stryker mutation runner
|
|
3
|
+
* @intent Verify StrykerRunner registration, availability checks, Stryker report parsing,
|
|
4
|
+
* timeout handling, spawn errors, and per-file breakdown
|
|
5
|
+
* @covers AC-MUT-004, AC-MUT-006
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { spawn, spawnSync, type ChildProcess } from 'child_process';
|
|
10
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { tmpdir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
vi.mock('child_process', () => ({
|
|
15
|
+
spawn: vi.fn(),
|
|
16
|
+
spawnSync: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('fs', async () => {
|
|
20
|
+
const actual = await vi.importActual<typeof import('fs')>('fs');
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
existsSync: vi.fn(actual.existsSync),
|
|
24
|
+
readFileSync: vi.fn(actual.readFileSync),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('StrykerRunner', () => {
|
|
29
|
+
let StrykerRunner: typeof import('../runners/stryker-runner').StrykerRunner;
|
|
30
|
+
let tmpDir: string;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
tmpDir = join(tmpdir(), `xp-gate-stryker-test-${Date.now()}`);
|
|
35
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
36
|
+
StrykerRunner = (await import('../runners/stryker-runner')).StrykerRunner;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── name and extensions ──────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('name and extensions', () => {
|
|
42
|
+
it('should have name "Stryker"', () => {
|
|
43
|
+
const runner = new StrykerRunner();
|
|
44
|
+
expect(runner.name).toBe('Stryker');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle .ts and .tsx extensions', () => {
|
|
48
|
+
const runner = new StrykerRunner();
|
|
49
|
+
expect(runner.extensions).toContain('ts');
|
|
50
|
+
expect(runner.extensions).toContain('tsx');
|
|
51
|
+
expect(runner.extensions).not.toContain('go');
|
|
52
|
+
expect(runner.extensions).not.toContain('py');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── isAvailable ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe('isAvailable', () => {
|
|
59
|
+
it('should return true when npx stryker is available (exit 0)', async () => {
|
|
60
|
+
vi.mocked(spawnSync).mockReturnValue({
|
|
61
|
+
status: 0,
|
|
62
|
+
output: [],
|
|
63
|
+
pid: 0,
|
|
64
|
+
signal: null,
|
|
65
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
66
|
+
|
|
67
|
+
const runner = new StrykerRunner();
|
|
68
|
+
const result = await runner.isAvailable();
|
|
69
|
+
expect(result).toBe(true);
|
|
70
|
+
expect(spawnSync).toHaveBeenCalledWith(
|
|
71
|
+
'npx',
|
|
72
|
+
['stryker', '--version'],
|
|
73
|
+
expect.objectContaining({ stdio: 'pipe', timeout: 5000 }),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return false when npx stryker is not installed (non-zero exit)', async () => {
|
|
78
|
+
vi.mocked(spawnSync).mockReturnValue({
|
|
79
|
+
status: 1,
|
|
80
|
+
output: [],
|
|
81
|
+
pid: 0,
|
|
82
|
+
signal: null,
|
|
83
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
84
|
+
|
|
85
|
+
const runner = new StrykerRunner();
|
|
86
|
+
const result = await runner.isAvailable();
|
|
87
|
+
expect(result).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return false when spawnSync throws', async () => {
|
|
91
|
+
vi.mocked(spawnSync).mockImplementation(() => {
|
|
92
|
+
throw new Error('ENOENT');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const runner = new StrykerRunner();
|
|
96
|
+
const result = await runner.isAvailable();
|
|
97
|
+
expect(result).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── run ──────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe('run', () => {
|
|
104
|
+
it('should spawn npx stryker run with correct args and parse valid report', async () => {
|
|
105
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
106
|
+
const mockChild = {
|
|
107
|
+
stdout: { on: vi.fn() },
|
|
108
|
+
stderr: { on: vi.fn() },
|
|
109
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
110
|
+
if (event === 'close') closeCb = cb;
|
|
111
|
+
}),
|
|
112
|
+
kill: vi.fn(),
|
|
113
|
+
killed: false,
|
|
114
|
+
};
|
|
115
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
116
|
+
|
|
117
|
+
writeFileSync(join(tmpDir, 'stryker.prepush.conf.json'), '{}');
|
|
118
|
+
writeFileSync(join(tmpDir, '.stryker-report.json'), JSON.stringify({
|
|
119
|
+
mutationScore: 75,
|
|
120
|
+
nrOfMutants: 40,
|
|
121
|
+
nrOfKilledMutants: 30,
|
|
122
|
+
nrOfSurvivedMutants: 10,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const runner = new StrykerRunner();
|
|
126
|
+
const promise = runner.run({
|
|
127
|
+
files: ['src/a.ts', 'src/b.tsx'],
|
|
128
|
+
timeoutMs: 60000,
|
|
129
|
+
cwd: tmpDir,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
closeCb!(0);
|
|
133
|
+
const result = await promise;
|
|
134
|
+
|
|
135
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
136
|
+
'npx',
|
|
137
|
+
[
|
|
138
|
+
'stryker', 'run',
|
|
139
|
+
'--config', join(tmpDir, 'stryker.prepush.conf.json'),
|
|
140
|
+
'--mutate', 'src/a.ts',
|
|
141
|
+
'--mutate', 'src/b.tsx',
|
|
142
|
+
],
|
|
143
|
+
expect.objectContaining({ cwd: tmpDir, stdio: 'pipe', shell: false }),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(result.timedOut).toBe(false);
|
|
147
|
+
expect(result.report).not.toBeNull();
|
|
148
|
+
expect(result.report!.mutationScore).toBe(75);
|
|
149
|
+
expect(result.report!.nrOfMutants).toBe(40);
|
|
150
|
+
expect(result.report!.nrOfKilledMutants).toBe(30);
|
|
151
|
+
expect(result.report!.nrOfSurvivedMutants).toBe(10);
|
|
152
|
+
expect(result.error).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should omit --config when stryker.prepush.conf.json does not exist', async () => {
|
|
156
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
157
|
+
const mockChild = {
|
|
158
|
+
stdout: { on: vi.fn() },
|
|
159
|
+
stderr: { on: vi.fn() },
|
|
160
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
161
|
+
if (event === 'close') closeCb = cb;
|
|
162
|
+
}),
|
|
163
|
+
kill: vi.fn(),
|
|
164
|
+
killed: false,
|
|
165
|
+
};
|
|
166
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
167
|
+
|
|
168
|
+
const runner = new StrykerRunner();
|
|
169
|
+
const promise = runner.run({
|
|
170
|
+
files: ['src/a.ts'],
|
|
171
|
+
timeoutMs: 60000,
|
|
172
|
+
cwd: tmpDir,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
closeCb!(0);
|
|
176
|
+
await promise;
|
|
177
|
+
|
|
178
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
179
|
+
'npx',
|
|
180
|
+
['stryker', 'run', '--mutate', 'src/a.ts'],
|
|
181
|
+
expect.objectContaining({ cwd: tmpDir }),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should return timedOut=true when child is killed (close code null)', async () => {
|
|
186
|
+
const mockChild = {
|
|
187
|
+
stdout: { on: vi.fn() },
|
|
188
|
+
stderr: { on: vi.fn() },
|
|
189
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
190
|
+
if (event === 'close') setTimeout(() => cb(null), 10);
|
|
191
|
+
}),
|
|
192
|
+
kill: vi.fn(),
|
|
193
|
+
killed: false,
|
|
194
|
+
};
|
|
195
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
196
|
+
|
|
197
|
+
const runner = new StrykerRunner();
|
|
198
|
+
const result = await runner.run({
|
|
199
|
+
files: ['src/a.ts'],
|
|
200
|
+
timeoutMs: 100,
|
|
201
|
+
cwd: tmpDir,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result.timedOut).toBe(true);
|
|
205
|
+
expect(result.report).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should return error when spawn fails (error event)', async () => {
|
|
209
|
+
const mockChild = {
|
|
210
|
+
stdout: { on: vi.fn() },
|
|
211
|
+
stderr: { on: vi.fn() },
|
|
212
|
+
on: vi.fn((event: string, cb: (err: Error) => void) => {
|
|
213
|
+
if (event === 'error') setTimeout(() => cb(new Error('spawn ENOENT')), 10);
|
|
214
|
+
}),
|
|
215
|
+
kill: vi.fn(),
|
|
216
|
+
killed: false,
|
|
217
|
+
};
|
|
218
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
219
|
+
|
|
220
|
+
const runner = new StrykerRunner();
|
|
221
|
+
const result = await runner.run({
|
|
222
|
+
files: ['src/a.ts'],
|
|
223
|
+
timeoutMs: 60000,
|
|
224
|
+
cwd: tmpDir,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result.report).toBeNull();
|
|
228
|
+
expect(result.timedOut).toBe(false);
|
|
229
|
+
expect(result.error).toBe('spawn ENOENT');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should include stderr as error on non-zero exit code', async () => {
|
|
233
|
+
let stderrCb: ((d: Buffer) => void) | null = null;
|
|
234
|
+
const mockChild = {
|
|
235
|
+
stdout: { on: vi.fn() },
|
|
236
|
+
stderr: {
|
|
237
|
+
on: vi.fn((_event: string, cb: (d: Buffer) => void) => {
|
|
238
|
+
stderrCb = cb;
|
|
239
|
+
}),
|
|
240
|
+
},
|
|
241
|
+
on: vi.fn((event: string, cb: (code: number) => void) => {
|
|
242
|
+
if (event === 'close') {
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
if (stderrCb) stderrCb(Buffer.from('mutation score below threshold'));
|
|
245
|
+
cb(1);
|
|
246
|
+
}, 10);
|
|
247
|
+
}
|
|
248
|
+
}),
|
|
249
|
+
kill: vi.fn(),
|
|
250
|
+
killed: false,
|
|
251
|
+
};
|
|
252
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
253
|
+
|
|
254
|
+
writeFileSync(join(tmpDir, '.stryker-report.json'), JSON.stringify({
|
|
255
|
+
mutationScore: 40,
|
|
256
|
+
nrOfMutants: 50,
|
|
257
|
+
nrOfKilledMutants: 20,
|
|
258
|
+
nrOfSurvivedMutants: 30,
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
const runner = new StrykerRunner();
|
|
262
|
+
const result = await runner.run({
|
|
263
|
+
files: ['src/a.ts'],
|
|
264
|
+
timeoutMs: 60000,
|
|
265
|
+
cwd: tmpDir,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(result.timedOut).toBe(false);
|
|
269
|
+
expect(result.report).not.toBeNull();
|
|
270
|
+
expect(result.report!.mutationScore).toBe(40);
|
|
271
|
+
expect(result.error).toContain('mutation score below threshold');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should return null report when .stryker-report.json does not exist', async () => {
|
|
275
|
+
const mockChild = {
|
|
276
|
+
stdout: { on: vi.fn() },
|
|
277
|
+
stderr: { on: vi.fn() },
|
|
278
|
+
on: vi.fn((event: string, cb: (code: number) => void) => {
|
|
279
|
+
if (event === 'close') setTimeout(() => cb(0), 10);
|
|
280
|
+
}),
|
|
281
|
+
kill: vi.fn(),
|
|
282
|
+
killed: false,
|
|
283
|
+
};
|
|
284
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
285
|
+
|
|
286
|
+
const runner = new StrykerRunner();
|
|
287
|
+
const result = await runner.run({
|
|
288
|
+
files: ['src/a.ts'],
|
|
289
|
+
timeoutMs: 60000,
|
|
290
|
+
cwd: tmpDir,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(result.report).toBeNull();
|
|
294
|
+
expect(result.timedOut).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── Report parsing ────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
describe('report parsing', () => {
|
|
301
|
+
it('should parse valid Stryker JSON report', async () => {
|
|
302
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
303
|
+
const mockChild = {
|
|
304
|
+
stdout: { on: vi.fn() },
|
|
305
|
+
stderr: { on: vi.fn() },
|
|
306
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
307
|
+
if (event === 'close') closeCb = cb;
|
|
308
|
+
}),
|
|
309
|
+
kill: vi.fn(),
|
|
310
|
+
killed: false,
|
|
311
|
+
};
|
|
312
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
313
|
+
|
|
314
|
+
writeFileSync(join(tmpDir, '.stryker-report.json'), JSON.stringify({
|
|
315
|
+
mutationScore: 88.5,
|
|
316
|
+
nrOfMutants: 100,
|
|
317
|
+
nrOfKilledMutants: 88,
|
|
318
|
+
nrOfSurvivedMutants: 12,
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
const runner = new StrykerRunner();
|
|
322
|
+
const promise = runner.run({
|
|
323
|
+
files: ['src/a.ts'],
|
|
324
|
+
timeoutMs: 60000,
|
|
325
|
+
cwd: tmpDir,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
closeCb!(0);
|
|
329
|
+
const result = await promise;
|
|
330
|
+
|
|
331
|
+
expect(result.report).not.toBeNull();
|
|
332
|
+
expect(result.report!.mutationScore).toBe(88.5);
|
|
333
|
+
expect(result.report!.nrOfMutants).toBe(100);
|
|
334
|
+
expect(result.report!.nrOfKilledMutants).toBe(88);
|
|
335
|
+
expect(result.report!.nrOfSurvivedMutants).toBe(12);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should return null report for invalid JSON', async () => {
|
|
339
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
340
|
+
const mockChild = {
|
|
341
|
+
stdout: { on: vi.fn() },
|
|
342
|
+
stderr: { on: vi.fn() },
|
|
343
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
344
|
+
if (event === 'close') closeCb = cb;
|
|
345
|
+
}),
|
|
346
|
+
kill: vi.fn(),
|
|
347
|
+
killed: false,
|
|
348
|
+
};
|
|
349
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
350
|
+
|
|
351
|
+
writeFileSync(join(tmpDir, '.stryker-report.json'), 'not valid json {{{');
|
|
352
|
+
|
|
353
|
+
const runner = new StrykerRunner();
|
|
354
|
+
const promise = runner.run({
|
|
355
|
+
files: ['src/a.ts'],
|
|
356
|
+
timeoutMs: 60000,
|
|
357
|
+
cwd: tmpDir,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
closeCb!(0);
|
|
361
|
+
const result = await promise;
|
|
362
|
+
|
|
363
|
+
expect(result.report).toBeNull();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should default missing numeric fields to 0 (asNumber fallback)', async () => {
|
|
367
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
368
|
+
const mockChild = {
|
|
369
|
+
stdout: { on: vi.fn() },
|
|
370
|
+
stderr: { on: vi.fn() },
|
|
371
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
372
|
+
if (event === 'close') closeCb = cb;
|
|
373
|
+
}),
|
|
374
|
+
kill: vi.fn(),
|
|
375
|
+
killed: false,
|
|
376
|
+
};
|
|
377
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
378
|
+
|
|
379
|
+
writeFileSync(join(tmpDir, '.stryker-report.json'), JSON.stringify({
|
|
380
|
+
someOtherField: 'value',
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
const runner = new StrykerRunner();
|
|
384
|
+
const promise = runner.run({
|
|
385
|
+
files: ['src/a.ts'],
|
|
386
|
+
timeoutMs: 60000,
|
|
387
|
+
cwd: tmpDir,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
closeCb!(0);
|
|
391
|
+
const result = await promise;
|
|
392
|
+
|
|
393
|
+
// asNumber defaults non-number values to 0; isNaN(0) is false → report is non-null
|
|
394
|
+
expect(result.report).not.toBeNull();
|
|
395
|
+
expect(result.report!.mutationScore).toBe(0);
|
|
396
|
+
expect(result.report!.nrOfMutants).toBe(0);
|
|
397
|
+
expect(result.report!.nrOfKilledMutants).toBe(0);
|
|
398
|
+
expect(result.report!.nrOfSurvivedMutants).toBe(0);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should parse per-file breakdown from files object', async () => {
|
|
402
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
403
|
+
const mockChild = {
|
|
404
|
+
stdout: { on: vi.fn() },
|
|
405
|
+
stderr: { on: vi.fn() },
|
|
406
|
+
on: vi.fn((event: string, cb: (code: number | null) => void) => {
|
|
407
|
+
if (event === 'close') closeCb = cb;
|
|
408
|
+
}),
|
|
409
|
+
kill: vi.fn(),
|
|
410
|
+
killed: false,
|
|
411
|
+
};
|
|
412
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
413
|
+
|
|
414
|
+
writeFileSync(join(tmpDir, '.stryker-report.json'), JSON.stringify({
|
|
415
|
+
mutationScore: 70,
|
|
416
|
+
nrOfMutants: 30,
|
|
417
|
+
nrOfKilledMutants: 21,
|
|
418
|
+
nrOfSurvivedMutants: 9,
|
|
419
|
+
files: {
|
|
420
|
+
'src/a.ts': {
|
|
421
|
+
mutationScore: 80,
|
|
422
|
+
nrOfMutants: 10,
|
|
423
|
+
nrOfKilledMutants: 8,
|
|
424
|
+
nrOfSurvivedMutants: 2,
|
|
425
|
+
},
|
|
426
|
+
'src/b.tsx': {
|
|
427
|
+
mutationScore: 60,
|
|
428
|
+
nrOfMutants: 20,
|
|
429
|
+
nrOfKilledMutants: 12,
|
|
430
|
+
nrOfSurvivedMutants: 8,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
}));
|
|
434
|
+
|
|
435
|
+
const runner = new StrykerRunner();
|
|
436
|
+
const promise = runner.run({
|
|
437
|
+
files: ['src/a.ts', 'src/b.tsx'],
|
|
438
|
+
timeoutMs: 60000,
|
|
439
|
+
cwd: tmpDir,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
closeCb!(0);
|
|
443
|
+
const result = await promise;
|
|
444
|
+
|
|
445
|
+
expect(result.report).not.toBeNull();
|
|
446
|
+
expect(result.report!.files).toBeDefined();
|
|
447
|
+
expect(result.report!.files!['src/a.ts'].mutationScore).toBe(80);
|
|
448
|
+
expect(result.report!.files!['src/a.ts'].nrOfMutants).toBe(10);
|
|
449
|
+
expect(result.report!.files!['src/a.ts'].nrOfKilledMutants).toBe(8);
|
|
450
|
+
expect(result.report!.files!['src/a.ts'].nrOfSurvivedMutants).toBe(2);
|
|
451
|
+
expect(result.report!.files!['src/b.tsx'].mutationScore).toBe(60);
|
|
452
|
+
expect(result.report!.files!['src/b.tsx'].nrOfMutants).toBe(20);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|