Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: add code coverage checking script #59

Draft
wants to merge 1 commit into
base: entry-point-v0.6
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ jobs:
- name: Setup environment
uses: ./.github/actions/setup

# TODO report and check coverage
- name: Run Test Coverage
run: yarn coverage
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ via_ir = true
auto_detect_solc = false
auto_detect_remappings = false
deny_warnings = true
no_match_coverage = "test/|script/"

[fuzz]
runs = 1024
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"lint": "solhint -w 0 -c .solhint-src.json './src/**/*.sol' && solhint -w 0 -c .solhint-test.json './test/**/*.sol' && solhint -w 0 -c .solhint-script.json './script/**/*.sol'",
"test": "forge test -vv",
"gasreport": "forge test --gas-report > gas/reports/gas-report.txt",
"coverage": "forge coverage --ir-minimum",
"coverage": "forge coverage --ir-minimum > temp-ci_yarn_check-coverage.out && node script/test/checkCoverage.js temp-ci_yarn_check-coverage.out && rm temp-ci_yarn_check-coverage.out",
"format:check": "forge fmt --check",
"format:write": "forge fmt"
},
Expand Down
103 changes: 103 additions & 0 deletions script/test/checkCoverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const fs = require('fs');

const COVERAGE_TABLE_HEADER = "| File | % Lines | % Statements | % Branches | % Funcs |";
const COVERAGE_TABLE_HEADER_SEPARATOR = "|--------------------------------------------------------------------------------|--------------------|--------------------|------------------|------------------|";
const COVERAGE_TABLE_TOTAL_ROW_NAME = 'Total';
const COVERAGE_TABLE_COLUMN_DELIM = '|';

// Matches expressions like "12.25%"
const COVERAGE_TABLE_COVERAGE_PECENTAGE_REGEXP = /[\d\.]+%/;

const MIN_REQUIRED_LINE_COVERAGE_PERCENTAGE = 90;
const MIN_REQUIRED_STATEMENT_COVERAGE_PERCENTAGE = 90;
const MIN_REQUIRED_BRANCH_COVERAGE_PERCENTAGE = 90;
const MIN_REQUIRED_FUNCTION_COVERAGE_PERCENTAGE = 90;

const NUM_COLUMNS = COVERAGE_TABLE_HEADER.split(COVERAGE_TABLE_COLUMN_DELIM).length - 2;

function parsePercentage(rawCoveragePercentText) {
const numericDecimalText = COVERAGE_TABLE_COVERAGE_PECENTAGE_REGEXP.exec(rawCoveragePercentText)[0].slice(0, -1);
return parseFloat(numericDecimalText);
}

function parseCoverageTableRow(rawRowText) {
let rowParts = rawRowText.split(COVERAGE_TABLE_COLUMN_DELIM);
if (rowParts.length - 2 != NUM_COLUMNS) {
return null
}

rowParts = rowParts.slice(1, -1);
return {
fileName: rowParts[0].trim(),
lineCoveragePercent: parsePercentage(rowParts[1]),
statementCoveragePercent: parsePercentage(rowParts[2]),
branchCoveragePercent: parsePercentage(rowParts[3]),
functionCoveragePercent: parsePercentage(rowParts[4]),
}
}

function getFormattedCoverageTableRowsTest(coverageTableRows) {
return COVERAGE_TABLE_HEADER + '\n'
+ COVERAGE_TABLE_HEADER_SEPARATOR + '\n'
+ coverageTableRows.join('\n') + '\n';
}

(async function main() {
const coverateReportFileName = process.argv[2];
const coverageReportRawText = fs.readFileSync(coverateReportFileName, "utf8");

let coverageTableBodyRaw = "";
try {
coverageTableBodyRaw = coverageReportRawText.split(COVERAGE_TABLE_HEADER)[1];
} catch (error) {
console.error("Unexpected coverage report format");
console.error(error);
process.exit(1);
}

const belowThresholdFiles = [];
const aboveThresholdFiles = [];
let totalCoverageRow = "";
const coverageTableRows = coverageTableBodyRaw.split("\n").slice(3);

for (const coverageTableRowRaw of coverageTableRows) {
const coverageRow = parseCoverageTableRow(coverageTableRowRaw);
if (!coverageRow) {
continue;
}

// Check minimum required coverage percentages
if (coverageRow.fileName == COVERAGE_TABLE_TOTAL_ROW_NAME) {
totalCoverageRow = coverageTableRowRaw;
} else if (coverageRow.lineCoveragePercent < MIN_REQUIRED_LINE_COVERAGE_PERCENTAGE ||
coverageRow.statementCoveragePercent < MIN_REQUIRED_STATEMENT_COVERAGE_PERCENTAGE ||
coverageRow.branchCoveragePercent < MIN_REQUIRED_BRANCH_COVERAGE_PERCENTAGE ||
coverageRow.functionCoveragePercent < MIN_REQUIRED_FUNCTION_COVERAGE_PERCENTAGE) {

belowThresholdFiles.push(coverageTableRowRaw);
} else {
aboveThresholdFiles.push(coverageTableRowRaw);
}
}

// Print coverage breakdown details
console.log("Total coverage: ");
console.log(getFormattedCoverageTableRowsTest([totalCoverageRow]));

if (belowThresholdFiles.length > 0) {
console.log("Found files below coverage threshold: ");
console.log(getFormattedCoverageTableRowsTest(belowThresholdFiles));
} else {
console.log("All source code files meet minimum coverage requirements.");
}
if (aboveThresholdFiles.length > 0) {
console.log("Files above coverage threshold: ");
console.log(getFormattedCoverageTableRowsTest(aboveThresholdFiles));
}

// Fail if any files found below the minimum coverage threshold
if (belowThresholdFiles.length > 0) {
// TODO: uncomment line once source code coverages have been bumped up
// process.exit(2);
}
})();
Loading