Skip to content

Commit 5902a6f

Browse files
authored
fix(coverage): require should be branch (#8433)
1 parent 547e975 commit 5902a6f

File tree

2 files changed

+100
-9
lines changed

2 files changed

+100
-9
lines changed

crates/evm/coverage/src/analysis.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,17 +325,42 @@ impl<'a> ContractVisitor<'a> {
325325
// tupleexpression
326326
// yulfunctioncall
327327
match node.node_type {
328-
NodeType::Assignment |
329-
NodeType::UnaryOperation |
330-
NodeType::FunctionCall |
331-
NodeType::Conditional => {
328+
NodeType::Assignment | NodeType::UnaryOperation | NodeType::Conditional => {
332329
self.push_item(CoverageItem {
333330
kind: CoverageItemKind::Statement,
334331
loc: self.source_location_for(&node.src),
335332
hits: 0,
336333
});
337334
Ok(())
338335
}
336+
NodeType::FunctionCall => {
337+
self.push_item(CoverageItem {
338+
kind: CoverageItemKind::Statement,
339+
loc: self.source_location_for(&node.src),
340+
hits: 0,
341+
});
342+
343+
let expr: Option<Node> = node.attribute("expression");
344+
if let Some(NodeType::Identifier) = expr.as_ref().map(|expr| &expr.node_type) {
345+
// Might be a require call, add branch coverage.
346+
let name: Option<String> = expr.and_then(|expr| expr.attribute("name"));
347+
if let Some("require") = name.as_deref() {
348+
let branch_id = self.branch_id;
349+
self.branch_id += 1;
350+
self.push_item(CoverageItem {
351+
kind: CoverageItemKind::Branch { branch_id, path_id: 0 },
352+
loc: self.source_location_for(&node.src),
353+
hits: 0,
354+
});
355+
self.push_item(CoverageItem {
356+
kind: CoverageItemKind::Branch { branch_id, path_id: 1 },
357+
loc: self.source_location_for(&node.src),
358+
hits: 0,
359+
});
360+
}
361+
}
362+
Ok(())
363+
}
339364
NodeType::BinaryOperation => {
340365
self.push_item(CoverageItem {
341366
kind: CoverageItemKind::Statement,

crates/forge/tests/cli/coverage.rs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,14 @@ contract BContractTest is DSTest {
167167
"#]]);
168168
});
169169

170-
forgetest!(test_assert_require_coverage, |prj, cmd| {
170+
forgetest!(test_assert_coverage, |prj, cmd| {
171171
prj.insert_ds_test();
172172
prj.add_source(
173173
"AContract.sol",
174174
r#"
175175
contract AContract {
176176
function checkA() external pure returns (bool) {
177177
assert(10 > 2);
178-
require(10 > 2, "true");
179178
return true;
180179
}
181180
}
@@ -200,17 +199,84 @@ contract AContractTest is DSTest {
200199
)
201200
.unwrap();
202201

203-
// Assert 100% coverage (assert and require properly covered).
202+
// Assert 100% coverage (assert properly covered).
204203
cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#"
205204
...
206205
| File | % Lines | % Statements | % Branches | % Funcs |
207206
|-------------------|---------------|---------------|---------------|---------------|
208-
| src/AContract.sol | 100.00% (3/3) | 100.00% (3/3) | 100.00% (0/0) | 100.00% (1/1) |
209-
| Total | 100.00% (3/3) | 100.00% (3/3) | 100.00% (0/0) | 100.00% (1/1) |
207+
| src/AContract.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (1/1) |
208+
| Total | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (1/1) |
210209
211210
"#]]);
212211
});
213212

213+
forgetest!(test_require_coverage, |prj, cmd| {
214+
prj.insert_ds_test();
215+
prj.add_source(
216+
"AContract.sol",
217+
r#"
218+
contract AContract {
219+
function checkRequire(bool doNotRevert) public view {
220+
require(doNotRevert, "reverted");
221+
}
222+
}
223+
"#,
224+
)
225+
.unwrap();
226+
227+
prj.add_source(
228+
"AContractTest.sol",
229+
r#"
230+
import "./test.sol";
231+
import {AContract} from "./AContract.sol";
232+
233+
interface Vm {
234+
function expectRevert(bytes calldata revertData) external;
235+
}
236+
237+
contract AContractTest is DSTest {
238+
Vm constant vm = Vm(HEVM_ADDRESS);
239+
function testRequireRevert() external {
240+
AContract a = new AContract();
241+
vm.expectRevert(abi.encodePacked("reverted"));
242+
a.checkRequire(false);
243+
}
244+
245+
function testRequireNoRevert() external {
246+
AContract a = new AContract();
247+
a.checkRequire(true);
248+
}
249+
}
250+
"#,
251+
)
252+
.unwrap();
253+
254+
// Assert 50% branch coverage if only revert tested.
255+
cmd.arg("coverage")
256+
.args(["--mt".to_string(), "testRequireRevert".to_string()])
257+
.assert_success()
258+
.stdout_eq(str![[r#"
259+
...
260+
| File | % Lines | % Statements | % Branches | % Funcs |
261+
|-------------------|---------------|---------------|--------------|---------------|
262+
| src/AContract.sol | 100.00% (1/1) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) |
263+
| Total | 100.00% (1/1) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) |
264+
265+
"#]]);
266+
267+
// Assert 100% branch coverage.
268+
cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(
269+
str![[r#"
270+
...
271+
| File | % Lines | % Statements | % Branches | % Funcs |
272+
|-------------------|---------------|---------------|---------------|---------------|
273+
| src/AContract.sol | 100.00% (1/1) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) |
274+
| Total | 100.00% (1/1) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) |
275+
276+
"#]],
277+
);
278+
});
279+
214280
forgetest!(test_line_hit_not_doubled, |prj, cmd| {
215281
prj.insert_ds_test();
216282
prj.add_source(

0 commit comments

Comments
 (0)