Skip to content

Commit 53fd155

Browse files
committed
PFM-TASK-6308 refactor: improve coverage file detection and reporting for projects
1 parent 1ca22e4 commit 53fd155

2 files changed

Lines changed: 288 additions & 15 deletions

File tree

tools/scripts/run-many/coverage-evaluator.spec.ts

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ jest.mock('fs', () => ({
99
existsSync: jest.fn(),
1010
readFileSync: jest.fn(),
1111
writeFileSync: jest.fn(),
12+
readdirSync: jest.fn(),
13+
statSync: jest.fn(),
1214
}));
1315
jest.mock('path', () => ({
1416
resolve: jest.fn((_, p) => p),
17+
join: jest.fn((...args) => args.join('/')),
1518
}));
1619
jest.mock('@actions/core', () => ({
1720
info: jest.fn(),
@@ -69,12 +72,13 @@ describe('coverage-evaluator', () => {
6972
mockGetProjectThresholds.mockReturnValue({ lines: 80, statements: 75 });
7073

7174
const mockExistsSync = fs.existsSync as jest.Mock;
75+
// All potential coverage file paths return false
7276
mockExistsSync.mockReturnValue(false);
7377

7478
const result = evaluateCoverage(['project-a'], { global: {}, projects: {} });
7579

7680
expect(result).toBe(1);
77-
expect(core.warning).toHaveBeenCalledWith('No coverage report found for project-a at coverage/project-a/coverage-summary.json');
81+
expect(core.warning).toHaveBeenCalledWith('No coverage report found for project-a');
7882

7983
// Verify that the comment shows individual thresholds with "No Data"
8084
const writeFileSyncMock = fs.writeFileSync as jest.Mock;
@@ -94,7 +98,8 @@ describe('coverage-evaluator', () => {
9498
});
9599

96100
const mockExistsSync = fs.existsSync as jest.Mock;
97-
mockExistsSync.mockReturnValue(true);
101+
// First path (specific project path) returns true
102+
mockExistsSync.mockImplementation((path) => path.includes('coverage/project-a/coverage-summary.json'));
98103

99104
const mockReadFileSync = fs.readFileSync as jest.Mock;
100105
mockReadFileSync.mockReturnValue(JSON.stringify({
@@ -132,7 +137,7 @@ describe('coverage-evaluator', () => {
132137
});
133138

134139
const mockExistsSync = fs.existsSync as jest.Mock;
135-
mockExistsSync.mockReturnValue(true);
140+
mockExistsSync.mockImplementation((path) => path.includes('coverage/project-a/coverage-summary.json'));
136141

137142
const mockReadFileSync = fs.readFileSync as jest.Mock;
138143
mockReadFileSync.mockReturnValue(JSON.stringify({
@@ -169,7 +174,7 @@ describe('coverage-evaluator', () => {
169174
});
170175

171176
const mockExistsSync = fs.existsSync as jest.Mock;
172-
mockExistsSync.mockReturnValue(true);
177+
mockExistsSync.mockImplementation((path) => path.includes('coverage/project-a/coverage-summary.json'));
173178

174179
const mockReadFileSync = fs.readFileSync as jest.Mock;
175180
mockReadFileSync.mockImplementation(() => {
@@ -179,7 +184,7 @@ describe('coverage-evaluator', () => {
179184
const result = evaluateCoverage(['project-a'], { global: {}, projects: {} });
180185

181186
expect(result).toBe(1);
182-
expect(core.error).toHaveBeenCalledWith('Error processing coverage for project-a: Test error');
187+
expect(core.error).toHaveBeenCalledWith('Error reading coverage file coverage/project-a/coverage-summary.json: Test error');
183188

184189
// Verify that the comment shows individual thresholds with "No Data"
185190
const writeFileSyncMock = fs.writeFileSync as jest.Mock;
@@ -200,7 +205,7 @@ describe('coverage-evaluator', () => {
200205
});
201206

202207
const mockExistsSync = fs.existsSync as jest.Mock;
203-
mockExistsSync.mockReturnValue(true);
208+
mockExistsSync.mockImplementation((path) => path.includes('coverage/project-a/coverage-summary.json'));
204209

205210
const mockReadFileSync = fs.readFileSync as jest.Mock;
206211
mockReadFileSync.mockReturnValue(JSON.stringify({
@@ -231,7 +236,7 @@ describe('coverage-evaluator', () => {
231236
mockGetProjectThresholds.mockReturnValue({});
232237

233238
const mockExistsSync = fs.existsSync as jest.Mock;
234-
mockExistsSync.mockReturnValue(true);
239+
mockExistsSync.mockImplementation((path) => path.includes('coverage/project-a/coverage-summary.json'));
235240

236241
const mockReadFileSync = fs.readFileSync as jest.Mock;
237242
mockReadFileSync.mockReturnValue(JSON.stringify({
@@ -250,6 +255,77 @@ describe('coverage-evaluator', () => {
250255
expect(core.info).toHaveBeenCalledWith('Project project-a passed all coverage thresholds');
251256
});
252257

258+
it('should find coverage files in alternative locations', () => {
259+
const mockGetProjectThresholds = getProjectThresholds as jest.Mock;
260+
mockGetProjectThresholds.mockReturnValue({
261+
lines: 80,
262+
statements: 80,
263+
functions: 75,
264+
branches: 70
265+
});
266+
267+
const mockExistsSync = fs.existsSync as jest.Mock;
268+
// First path fails, second path (apps/) succeeds
269+
mockExistsSync.mockImplementation((path) => {
270+
return path.includes('coverage/apps/project-a/coverage-summary.json');
271+
});
272+
273+
const mockReadFileSync = fs.readFileSync as jest.Mock;
274+
mockReadFileSync.mockReturnValue(JSON.stringify({
275+
total: {
276+
lines: { pct: 85 },
277+
statements: { pct: 85 },
278+
functions: { pct: 80 },
279+
branches: { pct: 75 }
280+
}
281+
}));
282+
283+
const result = evaluateCoverage(['project-a'], { global: {}, projects: {} });
284+
285+
expect(result).toBe(0);
286+
expect(core.info).toHaveBeenCalledWith('Found coverage file for project-a at: coverage/apps/project-a/coverage-summary.json');
287+
expect(core.info).toHaveBeenCalledWith('Project project-a passed all coverage thresholds');
288+
});
289+
290+
it('should handle global coverage file with project-specific data', () => {
291+
const mockGetProjectThresholds = getProjectThresholds as jest.Mock;
292+
mockGetProjectThresholds.mockReturnValue({
293+
lines: 80,
294+
statements: 80,
295+
functions: 75,
296+
branches: 70
297+
});
298+
299+
const mockExistsSync = fs.existsSync as jest.Mock;
300+
// Only the global coverage file exists
301+
mockExistsSync.mockImplementation((path) => {
302+
return path.includes('coverage/coverage-summary.json');
303+
});
304+
305+
const mockReadFileSync = fs.readFileSync as jest.Mock;
306+
// Global file contains data for multiple projects
307+
mockReadFileSync.mockReturnValue(JSON.stringify({
308+
'project-a': {
309+
lines: { pct: 85 },
310+
statements: { pct: 85 },
311+
functions: { pct: 80 },
312+
branches: { pct: 75 }
313+
},
314+
'project-b': {
315+
lines: { pct: 90 },
316+
statements: { pct: 90 },
317+
functions: { pct: 85 },
318+
branches: { pct: 80 }
319+
}
320+
}));
321+
322+
const result = evaluateCoverage(['project-a'], { global: {}, projects: {} });
323+
324+
expect(result).toBe(0);
325+
expect(core.info).toHaveBeenCalledWith('Using project-specific coverage for project-a from global file');
326+
expect(core.info).toHaveBeenCalledWith('Project project-a passed all coverage thresholds');
327+
});
328+
253329
it('should count multiple failures correctly', () => {
254330
// First project passes
255331
// Second project is skipped
@@ -283,7 +359,7 @@ describe('coverage-evaluator', () => {
283359
if (path.includes('project-d')) {
284360
return false; // No coverage file for project-d
285361
}
286-
return true;
362+
return path.includes('project-a') || path.includes('project-c');
287363
});
288364

289365
const mockReadFileSync = fs.readFileSync as jest.Mock;
@@ -342,6 +418,32 @@ describe('coverage-evaluator', () => {
342418
// Artifact URL is included
343419
expect(comment).toContain('📊 [View Detailed HTML Coverage Reports](https://example.com/artifact)');
344420
});
421+
422+
it('should list available coverage files when debugging', () => {
423+
const mockGetProjectThresholds = getProjectThresholds as jest.Mock;
424+
mockGetProjectThresholds.mockReturnValue({ lines: 80 });
425+
426+
const mockExistsSync = fs.existsSync as jest.Mock;
427+
mockExistsSync.mockImplementation((path) => {
428+
// Coverage directory exists, but no project-specific files
429+
return path === 'coverage';
430+
});
431+
432+
const mockReaddirSync = fs.readdirSync as jest.Mock;
433+
const mockStatSync = fs.statSync as jest.Mock;
434+
435+
// Mock directory structure
436+
mockReaddirSync.mockReturnValue(['other-project', 'coverage-summary.json']);
437+
mockStatSync.mockImplementation((path) => ({
438+
isDirectory: () => path.includes('other-project')
439+
}));
440+
441+
const result = evaluateCoverage(['project-a'], { global: {}, projects: {} });
442+
443+
expect(result).toBe(1);
444+
expect(core.warning).toHaveBeenCalledWith('No coverage report found for project-a');
445+
expect(core.warning).toHaveBeenCalledWith('No coverage file found for project-a. Available coverage files:');
446+
});
345447
});
346448

347449
describe('generateEmptyCoverageReport', () => {

0 commit comments

Comments
 (0)