Skip to content

Commit 5f10cf9

Browse files
committed
feat(utils): truncate paths with shared prefix and/or suffix
1 parent f680f5e commit 5f10cf9

File tree

4 files changed

+128
-16
lines changed

4 files changed

+128
-16
lines changed

packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
pluralizeToken,
1515
readTextFile,
1616
toUnixNewlines,
17+
truncatePaths,
1718
} from '@code-pushup/utils';
1819
import type { CoverageResult, CoverageType } from '../../config.js';
1920
import { ALL_COVERAGE_TYPES } from '../../constants.js';
@@ -169,7 +170,8 @@ function getCoverageStatsFromLcovRecord<T extends CoverageType>(
169170
}
170171

171172
function logLcovRecords(recordsPerReport: Record<string, LCOVRecord[]>): void {
172-
const reportsCount = Object.keys(recordsPerReport).length;
173+
const reportPaths = Object.keys(recordsPerReport);
174+
const reportsCount = reportPaths.length;
173175
const sourceFilesCount = new Set(
174176
Object.values(recordsPerReport)
175177
.flat()
@@ -183,11 +185,13 @@ function logLcovRecords(recordsPerReport: Record<string, LCOVRecord[]>): void {
183185
return;
184186
}
185187

188+
const truncatedPaths = truncatePaths(reportPaths);
189+
186190
logger.newline();
187191
logger.debug(
188192
formatAsciiTable({
189193
columns: [
190-
{ key: 'reportPath', label: 'LCOV report', align: 'left' },
194+
{ key: 'report', label: 'LCOV report', align: 'left' },
191195
{ key: 'filesCount', label: 'Files', align: 'right' },
192196
...ALL_COVERAGE_TYPES.map(
193197
(type): TableColumnObject => ({
@@ -197,20 +201,22 @@ function logLcovRecords(recordsPerReport: Record<string, LCOVRecord[]>): void {
197201
}),
198202
),
199203
],
200-
// TODO: truncate report paths (replace shared segments with ellipsis)
201-
rows: Object.entries(recordsPerReport).map(([reportPath, records]) => {
202-
const groups = groupLcovRecordsByCoverageType(
203-
records,
204-
ALL_COVERAGE_TYPES,
205-
);
206-
const stats: Record<CoverageType, string> = objectFromEntries(
207-
objectToEntries(groups).map(([type, files = []]) => [
208-
type,
209-
formatCoverageSum(files),
210-
]),
211-
);
212-
return { reportPath, filesCount: records.length, ...stats };
213-
}),
204+
rows: Object.entries(recordsPerReport).map(
205+
([reportPath, records], idx) => {
206+
const groups = groupLcovRecordsByCoverageType(
207+
records,
208+
ALL_COVERAGE_TYPES,
209+
);
210+
const stats: Record<CoverageType, string> = objectFromEntries(
211+
objectToEntries(groups).map(([type, files = []]) => [
212+
type,
213+
formatCoverageSum(files),
214+
]),
215+
);
216+
const report = truncatedPaths[idx] ?? reportPath;
217+
return { report, filesCount: records.length, ...stats };
218+
},
219+
),
214220
}),
215221
);
216222
logger.newline();
@@ -227,6 +233,10 @@ function formatCoverageSum(files: FileCoverage[]): string {
227233
{ covered: 0, total: 0 },
228234
);
229235

236+
if (total === 0) {
237+
return 'n/a';
238+
}
239+
230240
const percentage = (covered / total) * 100;
231241
return `${percentage.toFixed(1)}%`;
232242
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export {
4343
readJsonFile,
4444
readTextFile,
4545
removeDirectoryIfExists,
46+
truncatePaths,
4647
type CrawlFileSystemOptions,
4748
} from './lib/file-system.js';
4849
export { filterItemRefsBy } from './lib/filter.js';

packages/utils/src/lib/file-system.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,46 @@ export function splitFilePath(filePath: string): SplitFilePath {
180180
}
181181
return { folders, file };
182182
}
183+
184+
export function truncatePaths(paths: string[]): string[] {
185+
const segmentedPaths = paths
186+
.map(splitFilePath)
187+
.map(({ folders, file }): string[] => [...folders, file]);
188+
189+
const first = segmentedPaths[0];
190+
const others = segmentedPaths.slice(1);
191+
if (!first) {
192+
return paths;
193+
}
194+
195+
/* eslint-disable functional/no-let,functional/no-loop-statements,unicorn/no-for-loop */
196+
let offsetLeft = 0;
197+
let offsetRight = 0;
198+
for (let left = 0; left < first.length; left++) {
199+
if (others.every(segments => segments[left] === first[left])) {
200+
offsetLeft++;
201+
} else {
202+
break;
203+
}
204+
}
205+
for (let right = 1; right <= first.length; right++) {
206+
if (others.every(segments => segments.at(-right) === first.at(-right))) {
207+
offsetRight++;
208+
} else {
209+
break;
210+
}
211+
}
212+
/* eslint-enable functional/no-let,functional/no-loop-statements,unicorn/no-for-loop */
213+
214+
return segmentedPaths.map(segments => {
215+
const uniqueSegments = segments.slice(
216+
offsetLeft,
217+
offsetRight > 0 ? -offsetRight : undefined,
218+
);
219+
return path.join(
220+
offsetLeft > 0 ? '…' : '',
221+
...uniqueSegments,
222+
offsetRight > 0 ? '…' : '',
223+
);
224+
});
225+
}

packages/utils/src/lib/file-system.unit.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
findNearestFile,
1313
projectToFilename,
1414
splitFilePath,
15+
truncatePaths,
1516
} from './file-system.js';
1617

1718
describe('ensureDirectoryExists', () => {
@@ -268,3 +269,60 @@ describe('splitFilePath', () => {
268269
});
269270
});
270271
});
272+
273+
describe('truncatePaths', () => {
274+
it('should replace shared path prefix with ellipsis', () => {
275+
expect(
276+
truncatePaths([
277+
path.join('dist', 'packages', 'cli'),
278+
path.join('dist', 'packages', 'core'),
279+
path.join('dist', 'packages', 'utils'),
280+
]),
281+
).toEqual([
282+
path.join('…', 'cli'),
283+
path.join('…', 'core'),
284+
path.join('…', 'utils'),
285+
]);
286+
});
287+
288+
it('should replace shared path suffix with ellipsis', () => {
289+
expect(
290+
truncatePaths([
291+
path.join('e2e', 'cli-e2e', 'coverage', 'lcov.info'),
292+
path.join('packages', 'cli', 'coverage', 'lcov.info'),
293+
path.join('packages', 'core', 'coverage', 'lcov.info'),
294+
]),
295+
).toEqual([
296+
path.join('e2e', 'cli-e2e', '…'),
297+
path.join('packages', 'cli', '…'),
298+
path.join('packages', 'core', '…'),
299+
]);
300+
});
301+
302+
it('should replace shared path prefix and suffix at once', () => {
303+
expect(
304+
truncatePaths([
305+
path.join('coverage', 'packages', 'cli', 'int-tests', 'lcov.info'),
306+
path.join('coverage', 'packages', 'cli', 'unit-tests', 'lcov.info'),
307+
path.join('coverage', 'packages', 'core', 'int-tests', 'lcov.info'),
308+
path.join('coverage', 'packages', 'core', 'unit-tests', 'lcov.info'),
309+
path.join('coverage', 'packages', 'utils', 'unit-tests', 'lcov.info'),
310+
]),
311+
).toEqual([
312+
path.join('…', 'cli', 'int-tests', '…'),
313+
path.join('…', 'cli', 'unit-tests', '…'),
314+
path.join('…', 'core', 'int-tests', '…'),
315+
path.join('…', 'core', 'unit-tests', '…'),
316+
path.join('…', 'utils', 'unit-tests', '…'),
317+
]);
318+
});
319+
320+
it('should leave unique paths unchanged', () => {
321+
const paths = [
322+
path.join('e2e', 'cli-e2e'),
323+
path.join('packages', 'cli'),
324+
path.join('packages', 'core'),
325+
];
326+
expect(truncatePaths(paths)).toEqual(paths);
327+
});
328+
});

0 commit comments

Comments
 (0)