@boyingliu01/xp-gate 0.11.2 → 0.11.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/adapter-common.sh +135 -55
- package/hooks/adapter-common.sh +135 -55
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/pitest-runner.test.ts +574 -0
- package/mutation/gate-m.ts +138 -94
- package/mutation/runners/index.ts +3 -0
- package/mutation/runners/pitest-runner.ts +290 -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 +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-specification-alignment/AGENTS.md +4 -4
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-MUT-006 PITest mutation runner
|
|
3
|
+
* @intent Verify PitestRunner registration, availability checks, Maven/Gradle branch routing, and JSON report parsing
|
|
4
|
+
* @covers AC-MUT-006, AC-MUT-007
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { execSync, spawn, type ChildProcess } from 'child_process';
|
|
9
|
+
import { mkdirSync } from 'fs';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
vi.mock('child_process', () => ({
|
|
14
|
+
execSync: vi.fn(),
|
|
15
|
+
spawn: vi.fn(),
|
|
16
|
+
spawnSync: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('fs', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import('fs')>();
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
readFileSync: vi.fn((_path: string, _encoding?: string) => {
|
|
24
|
+
return '';
|
|
25
|
+
}),
|
|
26
|
+
existsSync: vi.fn((_path: string) => {
|
|
27
|
+
return false;
|
|
28
|
+
}),
|
|
29
|
+
readdirSync: vi.fn(() => []),
|
|
30
|
+
mkdirSync: vi.fn(),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('PitestRunner', () => {
|
|
35
|
+
let PitestRunner: typeof import('../runners/pitest-runner').PitestRunner;
|
|
36
|
+
let tmpDir: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
tmpDir = join(tmpdir(), `xp-gate-pitest-test-${Date.now()}`);
|
|
41
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
42
|
+
PitestRunner = (await import('../runners/pitest-runner')).PitestRunner;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
try { process.kill(process.pid, 0); } catch { /* noop */ }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('name and extensions', () => {
|
|
50
|
+
it('should have name "pitest"', () => {
|
|
51
|
+
const runner = new PitestRunner();
|
|
52
|
+
expect(runner.name).toBe('pitest');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle .java, .kt, and .kts extensions', () => {
|
|
56
|
+
const runner = new PitestRunner();
|
|
57
|
+
expect(runner.extensions).toContain('java');
|
|
58
|
+
expect(runner.extensions).toContain('kt');
|
|
59
|
+
expect(runner.extensions).toContain('kts');
|
|
60
|
+
expect(runner.extensions).not.toContain('ts');
|
|
61
|
+
expect(runner.extensions).not.toContain('py');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('isAvailable', () => {
|
|
66
|
+
it('should return false when neither Maven nor Gradle is available', async () => {
|
|
67
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
68
|
+
throw new Error('ENOENT');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const runner = new PitestRunner();
|
|
72
|
+
const result = await runner.isAvailable();
|
|
73
|
+
expect(result).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return true when Maven is available and pom.xml has pitest-maven', async () => {
|
|
77
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
78
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
79
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
80
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
81
|
+
'<project><build><plugins><plugin><groupId>org.pitest</groupId><artifactId>pitest-maven</artifactId></plugin></plugins></build></project>',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const runner = new PitestRunner();
|
|
85
|
+
const result = await runner.isAvailable();
|
|
86
|
+
expect(result).toBe(true);
|
|
87
|
+
expect(execSync).toHaveBeenCalledWith('mvn --version', expect.any(Object));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return false when Maven is available but pom.xml lacks pitest plugin', async () => {
|
|
91
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
92
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
93
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
94
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
95
|
+
'<project><build><plugins></plugins></build></project>',
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const runner = new PitestRunner();
|
|
99
|
+
const result = await runner.isAvailable();
|
|
100
|
+
expect(result).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return true when Gradle is available and build.gradle has pitest plugin', async () => {
|
|
104
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
105
|
+
vi.mocked(execSync)
|
|
106
|
+
.mockImplementationOnce(() => {
|
|
107
|
+
throw new Error('mvn not found');
|
|
108
|
+
})
|
|
109
|
+
.mockReturnValueOnce(Buffer.from('Gradle 8.5'));
|
|
110
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
111
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
112
|
+
'plugins { id("info.solidsoft.pitest") version "1.15.0" }',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const runner = new PitestRunner();
|
|
116
|
+
const result = await runner.isAvailable();
|
|
117
|
+
expect(result).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return true when ./gradlew exists with build.gradle.kts containing pitest plugin', async () => {
|
|
121
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
122
|
+
vi.mocked(execSync)
|
|
123
|
+
.mockImplementationOnce(() => {
|
|
124
|
+
throw new Error('mvn not found');
|
|
125
|
+
});
|
|
126
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
127
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
128
|
+
'plugins { id("info.solidsoft.gradle.pitest") version "1.15.0" }',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const runner = new PitestRunner();
|
|
132
|
+
const result = await runner.isAvailable();
|
|
133
|
+
expect(result).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('run - Maven branch', () => {
|
|
138
|
+
it('should run PITest via Maven and parse JSON output', async () => {
|
|
139
|
+
const { existsSync, readFileSync, readdirSync } = await import('fs');
|
|
140
|
+
|
|
141
|
+
vi.mocked(execSync).mockReset();
|
|
142
|
+
vi.mocked(spawn).mockReset();
|
|
143
|
+
vi.mocked(existsSync).mockReset();
|
|
144
|
+
vi.mocked(readFileSync).mockReset();
|
|
145
|
+
vi.mocked(readdirSync).mockReset();
|
|
146
|
+
|
|
147
|
+
vi.mocked(execSync).mockImplementation(() => Buffer.from('Apache Maven 3.9.0'));
|
|
148
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
149
|
+
const path = String(p);
|
|
150
|
+
if (path.endsWith('pom.xml')) return true;
|
|
151
|
+
if (path.includes('pit-reports')) return true;
|
|
152
|
+
if (path.endsWith('mutations.json')) return true;
|
|
153
|
+
return false;
|
|
154
|
+
});
|
|
155
|
+
vi.mocked(readdirSync).mockReturnValue(['202506300000'] as unknown as ReturnType<typeof readdirSync>);
|
|
156
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
157
|
+
mutations: [
|
|
158
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.java' },
|
|
159
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.java' },
|
|
160
|
+
{ status: 'SURVIVED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.java' },
|
|
161
|
+
{ status: 'SURVIVED', mutatedClass: 'com.example.Bar', sourceFile: 'Bar.java' },
|
|
162
|
+
{ status: 'NO_COVERAGE', mutatedClass: 'com.example.Baz', sourceFile: 'Baz.java' },
|
|
163
|
+
],
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
167
|
+
const mockChild = {
|
|
168
|
+
stdout: { on: vi.fn() },
|
|
169
|
+
stderr: { on: vi.fn() },
|
|
170
|
+
on: vi.fn((event, cb) => {
|
|
171
|
+
if (event === 'close') closeCb = cb;
|
|
172
|
+
}),
|
|
173
|
+
kill: vi.fn(),
|
|
174
|
+
killed: false,
|
|
175
|
+
};
|
|
176
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
177
|
+
|
|
178
|
+
const runner = new PitestRunner();
|
|
179
|
+
const promise = runner.run({
|
|
180
|
+
files: ['src/main/java/com/example/Foo.java'],
|
|
181
|
+
timeoutMs: 60000,
|
|
182
|
+
cwd: tmpDir,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
closeCb!(0);
|
|
186
|
+
const result = await promise;
|
|
187
|
+
|
|
188
|
+
expect(spawn).toHaveBeenCalledWith('mvn', [
|
|
189
|
+
'test-compile',
|
|
190
|
+
'org.pitest:pitest-maven:mutationCoverage',
|
|
191
|
+
'-DoutputFormats=JSON',
|
|
192
|
+
], expect.objectContaining({ cwd: tmpDir }));
|
|
193
|
+
|
|
194
|
+
expect(result.timedOut).toBe(false);
|
|
195
|
+
expect(result.report).not.toBeNull();
|
|
196
|
+
expect(result.report!.mutationScore).toBe(40);
|
|
197
|
+
expect(result.report!.nrOfMutants).toBe(5);
|
|
198
|
+
expect(result.report!.nrOfKilledMutants).toBe(2);
|
|
199
|
+
expect(result.report!.nrOfSurvivedMutants).toBe(3);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('run - Gradle branch', () => {
|
|
204
|
+
it('should run PITest via Gradle and parse JSON output', async () => {
|
|
205
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
206
|
+
|
|
207
|
+
vi.mocked(existsSync).mockReset();
|
|
208
|
+
vi.mocked(readFileSync).mockReset();
|
|
209
|
+
vi.mocked(spawn).mockReset();
|
|
210
|
+
vi.mocked(execSync).mockReset();
|
|
211
|
+
|
|
212
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
213
|
+
const path = String(p);
|
|
214
|
+
if (path.includes('pom.xml')) return false;
|
|
215
|
+
if (path.includes('build.gradle')) return true;
|
|
216
|
+
if (path.includes('gradlew')) return true;
|
|
217
|
+
if (path.includes('mutations.json')) return true;
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
222
|
+
const mockChild = {
|
|
223
|
+
stdout: { on: vi.fn() },
|
|
224
|
+
stderr: { on: vi.fn() },
|
|
225
|
+
on: vi.fn((event, cb) => {
|
|
226
|
+
if (event === 'close') closeCb = cb;
|
|
227
|
+
}),
|
|
228
|
+
kill: vi.fn(),
|
|
229
|
+
killed: false,
|
|
230
|
+
};
|
|
231
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
232
|
+
|
|
233
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
234
|
+
mutations: [
|
|
235
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.kt' },
|
|
236
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.kt' },
|
|
237
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.kt' },
|
|
238
|
+
{ status: 'SURVIVED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.kt' },
|
|
239
|
+
],
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
const runner = new PitestRunner();
|
|
243
|
+
const promise = runner.run({
|
|
244
|
+
files: ['src/main/kotlin/com/example/Foo.kt'],
|
|
245
|
+
timeoutMs: 60000,
|
|
246
|
+
cwd: tmpDir,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
closeCb!(0);
|
|
250
|
+
const result = await promise;
|
|
251
|
+
|
|
252
|
+
expect(spawn).toHaveBeenCalledWith(join(tmpDir, 'gradlew'), ['pitest'], {
|
|
253
|
+
cwd: tmpDir,
|
|
254
|
+
stdio: 'pipe',
|
|
255
|
+
shell: false,
|
|
256
|
+
});
|
|
257
|
+
expect(result.timedOut).toBe(false);
|
|
258
|
+
expect(result.report).not.toBeNull();
|
|
259
|
+
expect(result.report!.mutationScore).toBe(75);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should use gradle (not ./gradlew) when gradlew does not exist', async () => {
|
|
263
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
264
|
+
|
|
265
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Gradle 8.5'));
|
|
266
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
267
|
+
const path = String(p);
|
|
268
|
+
if (path.includes('pom.xml')) return false;
|
|
269
|
+
if (path.includes('build.gradle')) return true;
|
|
270
|
+
if (path.includes('gradlew')) return false;
|
|
271
|
+
if (path.includes('mutations.json')) return true;
|
|
272
|
+
return false;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
276
|
+
const mockChild = {
|
|
277
|
+
stdout: { on: vi.fn() },
|
|
278
|
+
stderr: { on: vi.fn() },
|
|
279
|
+
on: vi.fn((event, cb) => {
|
|
280
|
+
if (event === 'close') closeCb = cb;
|
|
281
|
+
}),
|
|
282
|
+
kill: vi.fn(),
|
|
283
|
+
killed: false,
|
|
284
|
+
};
|
|
285
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
286
|
+
|
|
287
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
288
|
+
mutations: [
|
|
289
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.java' },
|
|
290
|
+
],
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
const runner = new PitestRunner();
|
|
294
|
+
const promise = runner.run({
|
|
295
|
+
files: ['src/main/java/com/example/Foo.java'],
|
|
296
|
+
timeoutMs: 60000,
|
|
297
|
+
cwd: tmpDir,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
closeCb!(0);
|
|
301
|
+
const result = await promise;
|
|
302
|
+
|
|
303
|
+
expect(spawn).toHaveBeenCalledWith('gradle', ['pitest'], expect.objectContaining({ cwd: tmpDir }));
|
|
304
|
+
expect(result.report).not.toBeNull();
|
|
305
|
+
expect(result.report!.mutationScore).toBe(100);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('run - error handling', () => {
|
|
310
|
+
it('should return error when no build tool is available', async () => {
|
|
311
|
+
const { existsSync } = await import('fs');
|
|
312
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
313
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
314
|
+
throw new Error('ENOENT');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const runner = new PitestRunner();
|
|
318
|
+
const result = await runner.run({
|
|
319
|
+
files: ['src/main/java/Foo.java'],
|
|
320
|
+
timeoutMs: 60000,
|
|
321
|
+
cwd: tmpDir,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(result.report).toBeNull();
|
|
325
|
+
expect(result.timedOut).toBe(false);
|
|
326
|
+
expect(result.error).toBeTruthy();
|
|
327
|
+
expect(result.error).toContain('No supported build tool');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should return timedOut=true when child process is killed by timeout', async () => {
|
|
331
|
+
const { existsSync } = await import('fs');
|
|
332
|
+
|
|
333
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
334
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
335
|
+
const path = String(p);
|
|
336
|
+
if (path.includes('pom.xml')) return true;
|
|
337
|
+
return false;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const mockChild = {
|
|
341
|
+
stdout: { on: vi.fn() },
|
|
342
|
+
stderr: { on: vi.fn() },
|
|
343
|
+
on: vi.fn((event, cb) => {
|
|
344
|
+
if (event === 'close') setTimeout(() => cb(null), 10);
|
|
345
|
+
}),
|
|
346
|
+
kill: vi.fn(),
|
|
347
|
+
killed: false,
|
|
348
|
+
};
|
|
349
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
350
|
+
|
|
351
|
+
const runner = new PitestRunner();
|
|
352
|
+
const result = await runner.run({
|
|
353
|
+
files: ['src/main/java/Foo.java'],
|
|
354
|
+
timeoutMs: 100,
|
|
355
|
+
cwd: tmpDir,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(result.timedOut).toBe(true);
|
|
359
|
+
expect(result.report).toBeNull();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should return error when spawn fails', async () => {
|
|
363
|
+
const { existsSync } = await import('fs');
|
|
364
|
+
|
|
365
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
366
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
367
|
+
const path = String(p);
|
|
368
|
+
if (path.includes('pom.xml')) return true;
|
|
369
|
+
return false;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const mockChild = {
|
|
373
|
+
stdout: { on: vi.fn() },
|
|
374
|
+
stderr: { on: vi.fn() },
|
|
375
|
+
on: vi.fn((event, cb) => {
|
|
376
|
+
if (event === 'error') setTimeout(() => cb(new Error('spawn ENOENT')), 10);
|
|
377
|
+
}),
|
|
378
|
+
kill: vi.fn(),
|
|
379
|
+
killed: false,
|
|
380
|
+
};
|
|
381
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
382
|
+
|
|
383
|
+
const runner = new PitestRunner();
|
|
384
|
+
const result = await runner.run({
|
|
385
|
+
files: ['src/main/java/Foo.java'],
|
|
386
|
+
timeoutMs: 60000,
|
|
387
|
+
cwd: tmpDir,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(result.report).toBeNull();
|
|
391
|
+
expect(result.timedOut).toBe(false);
|
|
392
|
+
expect(result.error).toBe('spawn ENOENT');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should return null report when mutations.json does not exist', async () => {
|
|
396
|
+
const { existsSync } = await import('fs');
|
|
397
|
+
|
|
398
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
399
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
400
|
+
const path = String(p);
|
|
401
|
+
if (path.includes('pom.xml')) return true;
|
|
402
|
+
if (path.includes('pit-reports') && !path.includes('mutations.json')) return true;
|
|
403
|
+
if (path.includes('mutations.json')) return false;
|
|
404
|
+
return false;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
408
|
+
const mockChild = {
|
|
409
|
+
stdout: { on: vi.fn() },
|
|
410
|
+
stderr: { on: vi.fn() },
|
|
411
|
+
on: vi.fn((event, cb) => {
|
|
412
|
+
if (event === 'close') closeCb = cb;
|
|
413
|
+
}),
|
|
414
|
+
kill: vi.fn(),
|
|
415
|
+
killed: false,
|
|
416
|
+
};
|
|
417
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
418
|
+
|
|
419
|
+
const runner = new PitestRunner();
|
|
420
|
+
const promise = runner.run({
|
|
421
|
+
files: ['src/main/java/Foo.java'],
|
|
422
|
+
timeoutMs: 60000,
|
|
423
|
+
cwd: tmpDir,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
closeCb!(0);
|
|
427
|
+
const result = await promise;
|
|
428
|
+
|
|
429
|
+
expect(result.report).toBeNull();
|
|
430
|
+
expect(result.timedOut).toBe(false);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should return error string when Maven exits with non-zero code', async () => {
|
|
434
|
+
const { existsSync, readFileSync, readdirSync } = await import('fs');
|
|
435
|
+
|
|
436
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
437
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
438
|
+
const path = String(p);
|
|
439
|
+
if (path.includes('pom.xml')) return true;
|
|
440
|
+
if (path.includes('mutations.json')) return true;
|
|
441
|
+
if (path.includes('pit-reports')) return true;
|
|
442
|
+
return false;
|
|
443
|
+
});
|
|
444
|
+
vi.mocked(readdirSync).mockReturnValue(['202506300000'] as unknown as ReturnType<typeof readdirSync>);
|
|
445
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
446
|
+
mutations: [
|
|
447
|
+
{ status: 'KILLED', mutatedClass: 'com.example.Foo', sourceFile: 'Foo.java' },
|
|
448
|
+
],
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
let stderrCalled = false;
|
|
452
|
+
const mockChild = {
|
|
453
|
+
stdout: { on: vi.fn() },
|
|
454
|
+
stderr: {
|
|
455
|
+
on: vi.fn((_event: string, cb: (d: Buffer) => void) => {
|
|
456
|
+
if (!stderrCalled) {
|
|
457
|
+
stderrCalled = true;
|
|
458
|
+
cb(Buffer.from('Build failed: compilation error'));
|
|
459
|
+
}
|
|
460
|
+
}),
|
|
461
|
+
},
|
|
462
|
+
on: vi.fn((event, cb) => {
|
|
463
|
+
if (event === 'close') cb(1);
|
|
464
|
+
}),
|
|
465
|
+
kill: vi.fn(),
|
|
466
|
+
killed: false,
|
|
467
|
+
};
|
|
468
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
469
|
+
|
|
470
|
+
const runner = new PitestRunner();
|
|
471
|
+
const result = await runner.run({
|
|
472
|
+
files: ['src/main/java/Foo.java'],
|
|
473
|
+
timeoutMs: 60000,
|
|
474
|
+
cwd: tmpDir,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expect(result.timedOut).toBe(false);
|
|
478
|
+
expect(result.report).not.toBeNull();
|
|
479
|
+
expect(result.error).toBeTruthy();
|
|
480
|
+
expect(result.error).toContain('Build failed');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should return null report when JSON is unparseable', async () => {
|
|
484
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
485
|
+
|
|
486
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
487
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
488
|
+
const path = String(p);
|
|
489
|
+
if (path.includes('pom.xml')) return true;
|
|
490
|
+
if (path.includes('mutations.json')) return true;
|
|
491
|
+
if (path.includes('pit-reports')) return true;
|
|
492
|
+
return false;
|
|
493
|
+
});
|
|
494
|
+
vi.mocked(readFileSync).mockReturnValue('not valid json {{{');
|
|
495
|
+
|
|
496
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
497
|
+
const mockChild = {
|
|
498
|
+
stdout: { on: vi.fn() },
|
|
499
|
+
stderr: { on: vi.fn() },
|
|
500
|
+
on: vi.fn((event, cb) => {
|
|
501
|
+
if (event === 'close') closeCb = cb;
|
|
502
|
+
}),
|
|
503
|
+
kill: vi.fn(),
|
|
504
|
+
killed: false,
|
|
505
|
+
};
|
|
506
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
507
|
+
|
|
508
|
+
const runner = new PitestRunner();
|
|
509
|
+
const promise = runner.run({
|
|
510
|
+
files: ['src/main/java/Foo.java'],
|
|
511
|
+
timeoutMs: 60000,
|
|
512
|
+
cwd: tmpDir,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
closeCb!(0);
|
|
516
|
+
const result = await promise;
|
|
517
|
+
|
|
518
|
+
expect(result.report).toBeNull();
|
|
519
|
+
expect(result.timedOut).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should return null report when mutations array is empty', async () => {
|
|
523
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
524
|
+
|
|
525
|
+
vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Apache Maven 3.9.0'));
|
|
526
|
+
vi.mocked(existsSync).mockImplementation((p) => {
|
|
527
|
+
const path = String(p);
|
|
528
|
+
if (path.includes('pom.xml')) return true;
|
|
529
|
+
if (path.includes('mutations.json')) return true;
|
|
530
|
+
if (path.includes('pit-reports')) return true;
|
|
531
|
+
return false;
|
|
532
|
+
});
|
|
533
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
534
|
+
mutations: [],
|
|
535
|
+
}));
|
|
536
|
+
|
|
537
|
+
let closeCb: ((code: number | null) => void) | null = null;
|
|
538
|
+
const mockChild = {
|
|
539
|
+
stdout: { on: vi.fn() },
|
|
540
|
+
stderr: { on: vi.fn() },
|
|
541
|
+
on: vi.fn((event, cb) => {
|
|
542
|
+
if (event === 'close') closeCb = cb;
|
|
543
|
+
}),
|
|
544
|
+
kill: vi.fn(),
|
|
545
|
+
killed: false,
|
|
546
|
+
};
|
|
547
|
+
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
|
|
548
|
+
|
|
549
|
+
const runner = new PitestRunner();
|
|
550
|
+
const promise = runner.run({
|
|
551
|
+
files: ['src/main/java/Foo.java'],
|
|
552
|
+
timeoutMs: 60000,
|
|
553
|
+
cwd: tmpDir,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
closeCb!(0);
|
|
557
|
+
const result = await promise;
|
|
558
|
+
|
|
559
|
+
expect(result.report).toBeNull();
|
|
560
|
+
expect(result.timedOut).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe('registration', () => {
|
|
565
|
+
it('should register in runnerRegistry via registerAllRunners', async () => {
|
|
566
|
+
const { runnerRegistry, registerAllRunners } = await import('../runners');
|
|
567
|
+
registerAllRunners();
|
|
568
|
+
const runner = runnerRegistry.get('pitest');
|
|
569
|
+
expect(runner).toBeDefined();
|
|
570
|
+
expect(runner!.name).toBe('pitest');
|
|
571
|
+
expect(runner!.extensions).toContain('java');
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
});
|