Frequently Asked Questions
Continuous Integration (CI)
It is a best practice to run your coverage job separately from your standard test job in CI. The instrumentation process modifies your contracts and uses high gas limits, which can mask real-world gas issues or introduce unexpected behavior.
Examples:
- GitHub Actions and CodeCov (OpenZeppelin)
- CircleCI and Coveralls with parallelized jobs (SetProtocol)
Coveralls vs. Codecov
We recommend Coveralls for projects that require accurate branch coverage reporting for Solidity. While both are excellent services, Coveralls has been observed to provide better out-of-the-box branch coverage visualization for Solidity require
and if
statements.
Running out of Memory
For large projects, the Node.js process might exceed its default memory limit during the compilation of instrumented contracts. You can increase the memory allocation for the Hardhat process as follows:
node --max-old-space-size=4096 ./node_modules/.bin/hardhat coverage
If you still encounter memory-related errors from solc
(the Solidity compiler), such as RuntimeError: memory access out of bounds
, you can reduce the instrumentation footprint by disabling statement coverage in your .solcover.js
:
module.exports = {
measureStatementCoverage: false
};
This will still provide line, branch, and function coverage.
Running out of Time
Tests run significantly slower under coverage. If you have long-running tests, they might hit Mocha's default timeout. You can disable timeouts for the coverage task in .solcover.js
:
module.exports = {
mocha: {
enableTimeouts: false
}
};
Running out of Stack ("Stack Too Deep")
Stack-too-deep errors can occur in large, complex projects, especially those using ABI encoder V2 or Solidity >= 0.8.x. This happens because solidity-coverage
disables the solc
optimizer to trace code execution correctly, but some projects require the optimizer to be enabled to compile.
Here are several workarounds to try in your .solcover.js
:
Workaround #1: Enable the Yul optimizer.
module.exports = {
configureYulOptimizer: true
};
Workaround #2: Configure specific Yul optimizer details.
module.exports = {
configureYulOptimizer: true,
solcOptimizerDetails: {
peephole: false,
inliner: false,
jumpdestRemover: false,
orderLiterals: true, // Important for stack issues
deduplicate: false,
cse: false,
constantOptimizer: false,
yul: false
}
};
Workaround #3: Use an alternative Yul configuration.
module.exports = {
configureYulOptimizer: true,
solcOptimizerDetails: {
yul: true,
yulDetails: {
optimizerSteps: ""
},
}
};
Notes on Gas Distortion
solidity-coverage
injects statements into your code, which increases the gas cost of execution. Be aware of the following:
- Gas usage reports and simulations will not be accurate.
- Tests with hardcoded gas costs may fail.
- Contract logic that depends on precise gas usage (e.g., within
gasleft()
constraints) may fail.
It is recommended to use estimateGas
or default gas settings in your tests to make them more resilient. Relying on specific gas costs is often considered an anti-pattern, as EVM gas costs can change between forks.
Notes on Branch Coverage
solidity-coverage
treats require
and assert
statements as code branches. This is because they conditionally control the execution flow: if the condition is true, execution continues; if false, it reverts.
- If a
require
orassert
is marked with anI
(if) in the coverage report, it means the condition was never true during tests. - If it is marked with an
E
(else), it means the condition was never false.
To achieve 100% branch coverage, your tests must cover both the success and failure cases for each of these checks.