Skip to content
Merged
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 > 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
121 changes: 121 additions & 0 deletions script/test/checkCoverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2025 Circle Internet Group, Inc. All rights reserved.

* SPDX-License-Identifier: GPL-3.0-or-later

* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.

* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.

* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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);
}
})();
4 changes: 2 additions & 2 deletions test/msca/6900/v0.7/UpgradableMSCAFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ contract UpgradableMSCAFactoryTest is TestUtils {
// nonce key is 0
uint256 acctNonce = entryPoint.getNonce(sender, 0);
// start with balance
vm.deal(sender, 1 ether);
vm.deal(sender, 10 ether);
testLiquidityPool.mint(sender, 2000000);
address recipient = address(0x9005Be081B8EC2A31258878409E88675Cd791376);
// execute ERC20 token contract
Expand All @@ -165,7 +165,7 @@ contract UpgradableMSCAFactoryTest is TestUtils {
vm.toString(initCode),
vm.toString(executeCallData),
83353,
1028650,
10028650,
45484,
516219199704,
1130000000,
Expand Down
10 changes: 6 additions & 4 deletions test/msca/6900/v0.7/WalletStorageV1Lib.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -191,28 +191,30 @@ contract WalletStorageV1LibTest is TestUtils {

function testBulkGetPlugins() public {
// try out different limits, even bigger than totalPlugins
// we can't set the limit too high for unoptimized runs such as forge coverage
for (uint256 limit = 1; limit <= 10; limit++) {
// 4 plugins
bulkAddAndGetPlugins(new TestCircleMSCA(entryPoint, pluginManager), 4, limit);
}
for (uint256 limit = 1; limit <= 55; limit++) {
for (uint256 limit = 1; limit <= 25; limit++) {
bulkAddAndGetPlugins(new TestCircleMSCA(entryPoint, pluginManager), 50, limit);
}
for (uint256 limit = 1; limit <= 56; limit++) {
for (uint256 limit = 1; limit <= 26; limit++) {
bulkAddAndGetPlugins(new TestCircleMSCA(entryPoint, pluginManager), 50, limit);
}
}

function testBulkGetPreUserOpValidationHooks() public {
// try out different limits, even bigger than totalHooks
// we can't set the limit too high for unoptimized runs such as forge coverage
for (uint256 limit = 1; limit <= 10; limit++) {
// 4 plugins
bulkAddAndGetPreUserOpValidationHooks(new TestCircleMSCA(entryPoint, pluginManager), 4, limit);
}
for (uint256 limit = 1; limit <= 55; limit++) {
for (uint256 limit = 1; limit <= 25; limit++) {
bulkAddAndGetPreUserOpValidationHooks(new TestCircleMSCA(entryPoint, pluginManager), 50, limit);
}
for (uint256 limit = 1; limit <= 56; limit++) {
for (uint256 limit = 1; limit <= 26; limit++) {
bulkAddAndGetPreUserOpValidationHooks(new TestCircleMSCA(entryPoint, pluginManager), 50, limit);
}
}
Expand Down
Loading