Skip to content

Commit

Permalink
Merge pull request #2583 from HHS/TTAHUB-3782/summarize-coverage
Browse files Browse the repository at this point in the history
[TTAHUB-3782] summarize-coverage
  • Loading branch information
GarrettEHill authored Jan 13, 2025
2 parents ac225ce + 3095f83 commit 90c8be5
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 192 deletions.
8 changes: 8 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,14 @@ jobs:
echo "Not a PR build. Skipping coverage check."
fi
when: always
- run:
name: Summarize coverage
command: |
chmod +x ./tools/summarize-coverageCLI.js
node ./tools/summarize-coverageCLI.js \
./coverage/coverage-final.json \
90
when: always
- run:
name: Compress coverage artifacts
command: tar -cvzf backend-coverage-artifacts.tar coverage/
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,10 @@
],
"coverageThreshold": {
"global": {
"statements": 75,
"functions": 75,
"branches": 75,
"lines": 75
"statements": 90,
"functions": 88,
"branches": 80,
"lines": 90
}
},
"setupFiles": [
Expand Down
3 changes: 2 additions & 1 deletion src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
},
"include": [
"./**/*.js",
"./**/*.ts"
"./**/*.ts",
"../tools/**/*.js"
]
}

148 changes: 69 additions & 79 deletions tools/check-coverage.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// src/tools/check-coverage.js

/* eslint-disable no-console */
/* eslint-disable no-plusplus */
/* eslint-disable no-await-in-loop */
const fs = require('fs');
const path = require('path');
const simpleGit = require('simple-git');
// eslint-disable-next-line import/no-extraneous-dependencies
const { createCoverageMap } = require('istanbul-lib-coverage');
const simpleGit = require('simple-git');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');

// Configuration
const argv = yargs(hideBin(process.argv))
const { argv } = yargs(hideBin(process.argv))
.option('coverage-file', {
alias: 'c',
type: 'string',
Expand Down Expand Up @@ -40,7 +42,7 @@ const argv = yargs(hideBin(process.argv))
default: 'json',
})
.help()
.alias('help', 'h').argv;
.alias('help', 'h');

const COVERAGE_FILE = path.resolve(__dirname, argv['coverage-file']);
const BASE_BRANCH = 'main';
Expand All @@ -60,12 +62,10 @@ async function fetchBaseBranch() {
* @param {string} [directoryFilter] - The directory to filter files by (optional).
*/
async function getModifiedLines(directoryFilter = ['src/', 'tools/', 'packages/common/']) {
// eslint-disable-next-line no-console
console.log('getModifiedLines:', directoryFilter);

const git = simpleGit();
const diffFiles = await git.diff(['--name-only', `${BASE_BRANCH}...HEAD`]);
// eslint-disable-next-line no-console
console.log('getModifiedLines:\n', diffFiles);

// Filter files based on the file extension and optional directory
Expand All @@ -81,28 +81,28 @@ async function getModifiedLines(directoryFilter = ['src/', 'tools/', 'packages/c
files = files.filter((file) => directoryFilter.some((directory) => file.startsWith(directory)));
}

// eslint-disable-next-line no-console
console.log('files:', files);

const modifiedLines = {};

// eslint-disable-next-line no-restricted-syntax
for (const file of files) {
// Log the file being processed
// eslint-disable-next-line no-console
console.log('getModifiedLines:', file);
const diff = await git.diff(['-U0', `${BASE_BRANCH}...HEAD`, '--', file]);
const regex = /@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/g;
let match;

// eslint-disable-next-line no-cond-assign
while ((match = regex.exec(diff)) !== null) {
const startLine = parseInt(match[1], 10);
const lineCount = match[2] ? parseInt(match[2], 10) : 1;

if (!modifiedLines[file]) {
modifiedLines[file] = new Set();
}

for (let i = startLine; i < startLine + lineCount; i++) {
// eslint-disable-next-line no-console
console.log(i);
modifiedLines[file].add(i);
}
Expand All @@ -124,14 +124,12 @@ function loadCoverage(coverageFile = COVERAGE_FILE) {
try {
if (!fs.existsSync(coverageFile)) {
const errorMessage = `Coverage file not found at ${coverageFile}`;
// eslint-disable-next-line no-console
console.error(errorMessage);
throw new Error(errorMessage);
}

const coverageData = JSON.parse(fs.readFileSync(coverageFile, 'utf8'));
const coverageMap = createCoverageMap(coverageData);
return coverageMap;
return createCoverageMap(coverageData);
} catch (error) {
throw new Error(`Failed to parse coverage data at ${coverageFile}`);
}
Expand Down Expand Up @@ -177,6 +175,33 @@ function intersectLocationWithLines(loc, overlappingLines) {
return { start: newStart, end: newEnd };
}

function groupIntoRanges(lines) {
const ranges = [];
if (lines.length === 0) {
return ranges;
}

lines.sort((a, b) => a - b);
let start = lines[0];
let end = lines[0];

for (let i = 1; i < lines.length; i++) {
if (lines[i] === end + 1) {
// Contiguous line
end = lines[i];
} else {
// Not contiguous, save the previous range
ranges.push({ start, end });
start = lines[i];
end = lines[i];
}
}
// Push the last range
ranges.push({ start, end });

return ranges;
}

/**
* Check if modified lines are covered.
*/
Expand All @@ -195,7 +220,8 @@ function checkCoverage(modifiedLines, coverageMap) {
}
} catch (e) {
const ranges = groupIntoRanges(lines);
console.log('checkCoverage:',ranges);
console.log('checkCoverage:', ranges);
// eslint-disable-next-line max-len
uncovered[relativeFile] = uncovered[relativeFile] || { statements: [], functions: [], branches: [] };
ranges.forEach(({ start, end }) => {
uncovered[relativeFile].statements.push({
Expand All @@ -212,7 +238,7 @@ function checkCoverage(modifiedLines, coverageMap) {
Object.entries(fileCoverage.statementMap).forEach(([id, loc]) => {
const statementLines = getLinesFromLocation(loc);
const overlappingLines = linesIntersect(lines, statementLines);
console.log('checkCoverage:',overlappingLines);
console.log('checkCoverage:', overlappingLines);
if (overlappingLines.length > 0 && fileCoverage.s[id] === 0) {
const intersectedLoc = intersectLocationWithLines(loc, overlappingLines);
if (intersectedLoc) {
Expand Down Expand Up @@ -263,9 +289,9 @@ function checkCoverage(modifiedLines, coverageMap) {

// Remove empty file entry if no uncovered items were found
if (
uncovered[relativeFile].statements.length === 0 &&
uncovered[relativeFile].functions.length === 0 &&
uncovered[relativeFile].branches.length === 0
uncovered[relativeFile].statements.length === 0
&& uncovered[relativeFile].functions.length === 0
&& uncovered[relativeFile].branches.length === 0
) {
delete uncovered[relativeFile];
}
Expand All @@ -274,33 +300,6 @@ function checkCoverage(modifiedLines, coverageMap) {
return uncovered;
}

function groupIntoRanges(lines) {
const ranges = [];
if (lines.length === 0) {
return ranges;
}

lines.sort((a, b) => a - b);
let start = lines[0];
let end = lines[0];

for (let i = 1; i < lines.length; i++) {
if (lines[i] === end + 1) {
// Contiguous line
end = lines[i];
} else {
// Not contiguous, save the previous range
ranges.push({ start, end });
start = lines[i];
end = lines[i];
}
}
// Push the last range
ranges.push({ start, end });

return ranges;
}

/**
* Generate an artifact report for uncovered lines.
*/
Expand All @@ -312,7 +311,6 @@ function generateArtifact(uncovered, artifactDir = ARTIFACT_DIR) {
const artifactPath = path.join(artifactDir, 'uncovered-lines.json');

fs.writeFileSync(artifactPath, JSON.stringify(uncovered, null, 2), 'utf-8');
// eslint-disable-next-line no-console
console.log(`JSON artifact generated at ${artifactPath}`);
}

Expand Down Expand Up @@ -372,7 +370,7 @@ function generateHtmlReport(uncovered, artifactDir = ARTIFACT_DIR) {
htmlContent += `<h2>${file}</h2>`;

if (data.statements.length > 0) {
htmlContent += `<h3>Statements</h3>`;
htmlContent += '<h3>Statements</h3>';
htmlContent += `
<table>
<thead>
Expand Down Expand Up @@ -400,7 +398,7 @@ function generateHtmlReport(uncovered, artifactDir = ARTIFACT_DIR) {
}

if (data.functions.length > 0) {
htmlContent += `<h3>Functions</h3>`;
htmlContent += '<h3>Functions</h3>';
htmlContent += `
<table>
<thead>
Expand Down Expand Up @@ -429,9 +427,12 @@ function generateHtmlReport(uncovered, artifactDir = ARTIFACT_DIR) {
`;
}

if (data.branches.length > 0) {
htmlContent += `<h3>Branches</h3>`;
htmlContent += `
if (!(data.branches.length > 0)) {
return;
}

htmlContent += '<h3>Branches</h3>';
htmlContent += `
<table>
<thead>
<tr>
Expand All @@ -443,21 +444,20 @@ function generateHtmlReport(uncovered, artifactDir = ARTIFACT_DIR) {
</thead>
<tbody>
`;
data.branches.forEach((branch) => {
htmlContent += `
data.branches.forEach((branch) => {
htmlContent += `
<tr>
<td>${branch.id}</td>
<td>${branch.locationIndex}</td>
<td>${branch.start.line}</td>
<td>${branch.end.line}</td>
</tr>
`;
});
htmlContent += `
});
htmlContent += `
</tbody>
</table>
`;
}
});

htmlContent += `
Expand All @@ -481,57 +481,50 @@ async function main({
outputFormat = argv['output-format'],
} = {}) {
try {
// eslint-disable-next-line no-console
console.log('Fetching base branch...');
await fetchBaseBranch();

// eslint-disable-next-line no-console
console.log('Identifying modified lines...');
const modifiedLines = await getModifiedLines(directoryFilter);

// eslint-disable-next-line no-console
console.log('Loading coverage data...');
const coverageMap = loadCoverage(coverageFile);

// eslint-disable-next-line no-console
console.log('Checking coverage...');
const uncovered = checkCoverage(modifiedLines, coverageMap);

if (Object.keys(uncovered).length > 0) {
// eslint-disable-next-line no-console
console.log('Uncovered lines detected:', uncovered);
Object.entries(uncovered).forEach(([file, data]) => {
console.error(`- ${file}:`);
if (data.statements.length > 0) {
const lines = data.statements.map((stmt) => stmt.start.line).sort((a, b) => a - b);
const ranges = groupIntoRanges(lines);
const rangeStrings = ranges
.map((range) =>
range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`,
)
.map((range) => (range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`))
.join(', ');
console.log(` Statements: ${rangeStrings}`);
}

if (data.functions.length > 0) {
const lines = data.functions.map((fn) => fn.start.line).sort((a, b) => a - b);
const ranges = groupIntoRanges(lines);
const rangeStrings = ranges
.map((range) =>
range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`,
)
.map((range) => (range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`))
.join(', ');
console.log(` Functions: ${rangeStrings}`);
}
if (data.branches.length > 0) {
const lines = data.branches.map((branch) => branch.start.line).sort((a, b) => a - b);
const ranges = groupIntoRanges(lines);
const rangeStrings = ranges
.map((range) =>
range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`,
)
.join(', ');
console.log(` Branches: ${rangeStrings}`);

if (!(data.branches.length > 0)) {
return;
}

const lines = data.branches.map((branch) => branch.start.line).sort((a, b) => a - b);
const ranges = groupIntoRanges(lines);
const rangeStrings = ranges
.map((range) => (range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`))
.join(', ');
console.log(` Branches: ${rangeStrings}`);
});

// Generate JSON artifact
Expand All @@ -545,20 +538,17 @@ async function main({
}

if (failOnUncovered) {
// eslint-disable-next-line no-console
console.log('Coverage check failed due to uncovered lines.');
process.exit(1);
}
} else {
// eslint-disable-next-line no-console
console.log('All modified lines are covered by tests.');

if (outputFormat.includes('html')) {
generateHtmlReport(uncovered, artifactDir);
}
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error during coverage check:', error);
process.exit(1);
}
Expand Down
Loading

0 comments on commit 90c8be5

Please sign in to comment.