From 7628c02776c5b080c1e1fd509cd781fa02f554f3 Mon Sep 17 00:00:00 2001 From: Ashutosh Ukey Date: Tue, 24 Dec 2024 20:01:16 +0000 Subject: [PATCH] ci: add code coverage checking script --- .github/workflows/coverage.yml | 3 +- foundry.toml | 1 + package.json | 2 +- script/test/checkCoverage.js | 103 +++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 script/test/checkCoverage.js diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f9e29d9..c509768 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,4 +20,5 @@ jobs: - name: Setup environment uses: ./.github/actions/setup - # TODO report and check coverage + - name: Run Test Coverage + run: yarn coverage diff --git a/foundry.toml b/foundry.toml index c496664..c8c3980 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 diff --git a/package.json b/package.json index 8a197d8..d6aa6ea 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/script/test/checkCoverage.js b/script/test/checkCoverage.js new file mode 100644 index 0000000..43817e0 --- /dev/null +++ b/script/test/checkCoverage.js @@ -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); + } +})(); \ No newline at end of file