Skip to content

Commit 208c45a

Browse files
committed
Add unit tests for coverage tools
1 parent b2165d9 commit 208c45a

File tree

2 files changed

+654
-0
lines changed

2 files changed

+654
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/**
2+
* Tests for get_coverage_report tool
3+
* Covers happy-path, target filtering, showFiles, and failure paths
4+
*/
5+
6+
import { describe, it, expect } from 'vitest';
7+
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
8+
import { schema, handler, get_coverage_reportLogic } from '../get_coverage_report.ts';
9+
10+
const sampleTargets = [
11+
{ name: 'MyApp.app', coveredLines: 100, executableLines: 200, lineCoverage: 0.5 },
12+
{ name: 'Core', coveredLines: 50, executableLines: 500, lineCoverage: 0.1 },
13+
{ name: 'MyAppTests.xctest', coveredLines: 30, executableLines: 30, lineCoverage: 1.0 },
14+
];
15+
16+
const sampleTargetsWithFiles = [
17+
{
18+
name: 'MyApp.app',
19+
coveredLines: 100,
20+
executableLines: 200,
21+
lineCoverage: 0.5,
22+
files: [
23+
{ name: 'AppDelegate.swift', path: '/src/AppDelegate.swift', coveredLines: 10, executableLines: 50, lineCoverage: 0.2 },
24+
{ name: 'ViewModel.swift', path: '/src/ViewModel.swift', coveredLines: 90, executableLines: 150, lineCoverage: 0.6 },
25+
],
26+
},
27+
{
28+
name: 'Core',
29+
coveredLines: 50,
30+
executableLines: 500,
31+
lineCoverage: 0.1,
32+
files: [
33+
{ name: 'Service.swift', path: '/src/Service.swift', coveredLines: 0, executableLines: 300, lineCoverage: 0 },
34+
{ name: 'Model.swift', path: '/src/Model.swift', coveredLines: 50, executableLines: 200, lineCoverage: 0.25 },
35+
],
36+
},
37+
];
38+
39+
describe('get_coverage_report', () => {
40+
describe('Export Validation', () => {
41+
it('should export get_coverage_reportLogic function', () => {
42+
expect(typeof get_coverage_reportLogic).toBe('function');
43+
});
44+
45+
it('should export handler function', () => {
46+
expect(typeof handler).toBe('function');
47+
});
48+
49+
it('should export schema with expected keys', () => {
50+
expect(Object.keys(schema)).toContain('xcresultPath');
51+
expect(Object.keys(schema)).toContain('target');
52+
expect(Object.keys(schema)).toContain('showFiles');
53+
});
54+
});
55+
56+
describe('Command Generation', () => {
57+
it('should use --only-targets when showFiles is false', async () => {
58+
const commands: string[][] = [];
59+
const mockExecutor = createMockExecutor({
60+
success: true,
61+
output: JSON.stringify(sampleTargets),
62+
onExecute: (command) => { commands.push(command); },
63+
});
64+
65+
await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
66+
67+
expect(commands).toHaveLength(1);
68+
expect(commands[0]).toContain('--only-targets');
69+
expect(commands[0]).toContain('--json');
70+
expect(commands[0]).toContain('/tmp/test.xcresult');
71+
});
72+
73+
it('should omit --only-targets when showFiles is true', async () => {
74+
const commands: string[][] = [];
75+
const mockExecutor = createMockExecutor({
76+
success: true,
77+
output: JSON.stringify(sampleTargetsWithFiles),
78+
onExecute: (command) => { commands.push(command); },
79+
});
80+
81+
await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: true }, mockExecutor);
82+
83+
expect(commands).toHaveLength(1);
84+
expect(commands[0]).not.toContain('--only-targets');
85+
});
86+
});
87+
88+
describe('Happy Path', () => {
89+
it('should return coverage report with all targets sorted by coverage', async () => {
90+
const mockExecutor = createMockExecutor({
91+
success: true,
92+
output: JSON.stringify(sampleTargets),
93+
});
94+
95+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
96+
97+
expect(result.isError).toBeUndefined();
98+
expect(result.content).toHaveLength(1);
99+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
100+
expect(text).toContain('Code Coverage Report');
101+
expect(text).toContain('Overall: 24.7%');
102+
expect(text).toContain('180/730 lines');
103+
// Should be sorted ascending: Core (10%), MyApp (50%), Tests (100%)
104+
const coreIdx = text.indexOf('Core');
105+
const appIdx = text.indexOf('MyApp.app');
106+
const testIdx = text.indexOf('MyAppTests.xctest');
107+
expect(coreIdx).toBeLessThan(appIdx);
108+
expect(appIdx).toBeLessThan(testIdx);
109+
});
110+
111+
it('should include nextStepParams with xcresultPath', async () => {
112+
const mockExecutor = createMockExecutor({
113+
success: true,
114+
output: JSON.stringify(sampleTargets),
115+
});
116+
117+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
118+
119+
expect(result.nextStepParams).toEqual({
120+
get_file_coverage: { xcresultPath: '/tmp/test.xcresult' },
121+
});
122+
});
123+
124+
it('should handle nested targets format', async () => {
125+
const nestedData = { targets: sampleTargets };
126+
const mockExecutor = createMockExecutor({
127+
success: true,
128+
output: JSON.stringify(nestedData),
129+
});
130+
131+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
132+
133+
expect(result.isError).toBeUndefined();
134+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
135+
expect(text).toContain('Core: 10.0%');
136+
expect(text).toContain('MyApp.app: 50.0%');
137+
});
138+
});
139+
140+
describe('Target Filtering', () => {
141+
it('should filter targets by substring match', async () => {
142+
const mockExecutor = createMockExecutor({
143+
success: true,
144+
output: JSON.stringify(sampleTargets),
145+
});
146+
147+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', target: 'MyApp', showFiles: false }, mockExecutor);
148+
149+
expect(result.isError).toBeUndefined();
150+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
151+
expect(text).toContain('MyApp.app');
152+
expect(text).toContain('MyAppTests.xctest');
153+
expect(text).not.toMatch(/^\s+Core:/m);
154+
});
155+
156+
it('should filter case-insensitively', async () => {
157+
const mockExecutor = createMockExecutor({
158+
success: true,
159+
output: JSON.stringify(sampleTargets),
160+
});
161+
162+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', target: 'core', showFiles: false }, mockExecutor);
163+
164+
expect(result.isError).toBeUndefined();
165+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
166+
expect(text).toContain('Core: 10.0%');
167+
});
168+
169+
it('should return error when no targets match filter', async () => {
170+
const mockExecutor = createMockExecutor({
171+
success: true,
172+
output: JSON.stringify(sampleTargets),
173+
});
174+
175+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', target: 'NonExistent', showFiles: false }, mockExecutor);
176+
177+
expect(result.isError).toBe(true);
178+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
179+
expect(text).toContain('No targets found matching "NonExistent"');
180+
});
181+
});
182+
183+
describe('showFiles', () => {
184+
it('should include per-file breakdown under each target', async () => {
185+
const mockExecutor = createMockExecutor({
186+
success: true,
187+
output: JSON.stringify(sampleTargetsWithFiles),
188+
});
189+
190+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: true }, mockExecutor);
191+
192+
expect(result.isError).toBeUndefined();
193+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
194+
expect(text).toContain('AppDelegate.swift: 20.0%');
195+
expect(text).toContain('ViewModel.swift: 60.0%');
196+
expect(text).toContain('Service.swift: 0.0%');
197+
expect(text).toContain('Model.swift: 25.0%');
198+
});
199+
200+
it('should sort files by coverage ascending within each target', async () => {
201+
const mockExecutor = createMockExecutor({
202+
success: true,
203+
output: JSON.stringify(sampleTargetsWithFiles),
204+
});
205+
206+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: true }, mockExecutor);
207+
208+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
209+
// Under MyApp.app: AppDelegate (20%) before ViewModel (60%)
210+
const appDelegateIdx = text.indexOf('AppDelegate.swift');
211+
const viewModelIdx = text.indexOf('ViewModel.swift');
212+
expect(appDelegateIdx).toBeLessThan(viewModelIdx);
213+
});
214+
});
215+
216+
describe('Failure Paths', () => {
217+
it('should return error when xccov command fails', async () => {
218+
const mockExecutor = createMockExecutor({
219+
success: false,
220+
error: 'Failed to load result bundle',
221+
});
222+
223+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/bad.xcresult', showFiles: false }, mockExecutor);
224+
225+
expect(result.isError).toBe(true);
226+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
227+
expect(text).toContain('Failed to get coverage report');
228+
expect(text).toContain('Failed to load result bundle');
229+
});
230+
231+
it('should return error when JSON parsing fails', async () => {
232+
const mockExecutor = createMockExecutor({
233+
success: true,
234+
output: 'not valid json',
235+
});
236+
237+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
238+
239+
expect(result.isError).toBe(true);
240+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
241+
expect(text).toContain('Failed to parse coverage JSON output');
242+
});
243+
244+
it('should return error when data format is unexpected', async () => {
245+
const mockExecutor = createMockExecutor({
246+
success: true,
247+
output: JSON.stringify({ unexpected: 'format' }),
248+
});
249+
250+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
251+
252+
expect(result.isError).toBe(true);
253+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
254+
expect(text).toContain('Unexpected coverage data format');
255+
});
256+
257+
it('should return error when targets array is empty', async () => {
258+
const mockExecutor = createMockExecutor({
259+
success: true,
260+
output: JSON.stringify([]),
261+
});
262+
263+
const result = await get_coverage_reportLogic({ xcresultPath: '/tmp/test.xcresult', showFiles: false }, mockExecutor);
264+
265+
expect(result.isError).toBe(true);
266+
const text = result.content[0].type === 'text' ? result.content[0].text : '';
267+
expect(text).toContain('No coverage data found');
268+
});
269+
});
270+
});

0 commit comments

Comments
 (0)