| name | coverage-analysis |
| description | Use when measuring test coverage for Solidity contracts, identifying untested code paths, or establishing coverage requirements. Covers forge coverage, lcov output, visualization, and coverage-driven testing. |
Test Coverage Analysis
Running Coverage
forge coverage
LCOV Output for Visualization
forge coverage --report lcov
genhtml lcov.info -o coverage-report --branch-coverage
open coverage-report/index.html
Coverage Types
| Type | Meaning | Priority |
|---|
| Line | Was this line executed? | Medium |
| Statement | Was this statement executed? | Medium |
| Branch | Were both sides of if/else taken? | High |
| Function | Was this function called? | Low |
Branch coverage is the most important for security — untested branches are where bugs hide.
Filtering Coverage
forge coverage --report lcov
lcov --remove lcov.info 'test/*' 'script/*' 'node_modules/*' \
--output-file lcov-filtered.info
Coverage-Driven Testing Workflow
1. Identify Gaps
forge coverage --report lcov
genhtml lcov.info -o coverage-report --branch-coverage
2. Focus on Uncovered Branches
For each uncovered branch, write a targeted test:
// If coverage shows this branch is untested:
// if (amount > maxDeposit) revert ExceedsMax();
function test_depositExceedsMax() public {
uint256 maxDeposit = vault.maxDeposit(address(this));
vm.expectRevert(abi.encodeWithSelector(ExceedsMax.selector));
vault.deposit(maxDeposit + 1, address(this));
}
3. Common Untested Paths
// Zero-amount edge cases
function test_depositZero() public {
vm.expectRevert("zero amount");
vault.deposit(0, address(this));
}
// Reentrancy guard activation
function test_reentrancyBlocked() public {
attacker.setReentrancy(true);
vm.expectRevert("ReentrancyGuard: reentrant call");
attacker.attack();
}
// Overflow/underflow (checked math reverts)
function test_overflowReverts() public {
vm.expectRevert();
vault.deposit(type(uint256).max, address(this));
}
// Access control all roles
function test_onlyOwnerFunctions() public {
vm.prank(nonOwner);
vm.expectRevert();
vault.setFee(100);
}
// Pause/unpause paths
function test_pausedDeposit() public {
vault.pause();
vm.expectRevert("Pausable: paused");
vault.deposit(1 ether, address(this));
}
CI Integration
- name: Check coverage threshold
run: |
forge coverage --report summary | tee coverage.txt
COVERAGE=$(grep "| Total" coverage.txt | awk '{print $4}' | tr -d '%')
if (( $(echo "$COVERAGE < 85" | bc -l) )); then
echo "Coverage $COVERAGE% is below 85% threshold"
exit 1
fi
Coverage Report in foundry.toml
[profile.default]
no_match_coverage = "test|script|mock"
Coverage Targets by Component
| Component | Minimum | Target |
|---|
| Core protocol logic | 95% branch | 100% branch |
| Token contracts | 90% branch | 100% branch |
| Admin/governance | 85% branch | 95% branch |
| Periphery/helpers | 80% branch | 90% branch |
| View functions | 70% line | 85% line |
Limitations
- Coverage does not measure test quality — 100% coverage doesn't mean 100% correct
- Foundry coverage doesn't handle inline assembly well
- Branch coverage inside
require() chains may not report correctly
- Coverage overhead slows test execution (separate coverage CI step)
- Unreachable code (dead branches) inflates uncovered percentage
Checklist