Skip to content

Commit dad3901

Browse files
authored
feat(coverage): report try-catch coverage (#8448)
1 parent b98590c commit dad3901

File tree

2 files changed

+148
-9
lines changed

2 files changed

+148
-9
lines changed

crates/evm/coverage/src/analysis.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,8 @@ impl<'a> ContractVisitor<'a> {
126126
});
127127
Ok(())
128128
}
129-
130129
// Skip placeholder statements as they are never referenced in source maps.
131130
NodeType::PlaceholderStatement => Ok(()),
132-
133131
// Return with eventual subcall
134132
NodeType::Return => {
135133
self.push_item(CoverageItem {
@@ -142,7 +140,6 @@ impl<'a> ContractVisitor<'a> {
142140
}
143141
Ok(())
144142
}
145-
146143
// Variable declaration
147144
NodeType::VariableDeclarationStatement => {
148145
self.push_item(CoverageItem {
@@ -160,7 +157,7 @@ impl<'a> ContractVisitor<'a> {
160157
self.visit_expression(
161158
&node
162159
.attribute("condition")
163-
.ok_or_else(|| eyre::eyre!("if statement had no condition"))?,
160+
.ok_or_else(|| eyre::eyre!("while statement had no condition"))?,
164161
)?;
165162

166163
let body = node
@@ -199,7 +196,7 @@ impl<'a> ContractVisitor<'a> {
199196
self.visit_expression(
200197
&node
201198
.attribute("condition")
202-
.ok_or_else(|| eyre::eyre!("while statement had no condition"))?,
199+
.ok_or_else(|| eyre::eyre!("if statement had no condition"))?,
203200
)?;
204201

205202
let true_body: Node = node
@@ -300,15 +297,44 @@ impl<'a> ContractVisitor<'a> {
300297

301298
Ok(())
302299
}
303-
// Try-catch statement
300+
// Try-catch statement. Coverage is reported for expression, for each clause and their
301+
// bodies (if any).
304302
NodeType::TryStatement => {
305-
// TODO: Clauses
306-
// TODO: This is branching, right?
307303
self.visit_expression(
308304
&node
309305
.attribute("externalCall")
310306
.ok_or_else(|| eyre::eyre!("try statement had no call"))?,
311-
)
307+
)?;
308+
309+
// Add coverage for each Try-catch clause.
310+
for clause in node
311+
.attribute::<Vec<Node>>("clauses")
312+
.ok_or_else(|| eyre::eyre!("try statement had no clause"))?
313+
{
314+
// Add coverage for clause statement.
315+
self.push_item(CoverageItem {
316+
kind: CoverageItemKind::Statement,
317+
loc: self.source_location_for(&clause.src),
318+
hits: 0,
319+
});
320+
self.visit_statement(&clause)?;
321+
322+
// Add coverage for clause body only if it is not empty.
323+
if let Some(block) = clause.attribute::<Node>("block") {
324+
let statements: Vec<Node> =
325+
block.attribute("statements").unwrap_or_default();
326+
if !statements.is_empty() {
327+
self.push_item(CoverageItem {
328+
kind: CoverageItemKind::Statement,
329+
loc: self.source_location_for(&block.src),
330+
hits: 0,
331+
});
332+
self.visit_block(&block)?;
333+
}
334+
}
335+
}
336+
337+
Ok(())
312338
}
313339
_ => {
314340
warn!("unexpected node type, expected a statement: {:?}", node.node_type);

crates/forge/tests/cli/coverage.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,3 +588,116 @@ contract AContractTest is DSTest {
588588
589589
"#]]);
590590
});
591+
592+
forgetest!(test_try_catch_coverage, |prj, cmd| {
593+
prj.insert_ds_test();
594+
prj.add_source(
595+
"Foo.sol",
596+
r#"
597+
contract Foo {
598+
address public owner;
599+
600+
constructor(address _owner) {
601+
require(_owner != address(0), "invalid address");
602+
assert(_owner != 0x0000000000000000000000000000000000000001);
603+
owner = _owner;
604+
}
605+
606+
function myFunc(uint256 x) public pure returns (string memory) {
607+
require(x != 0, "require failed");
608+
return "my func was called";
609+
}
610+
}
611+
612+
contract Bar {
613+
event Log(string message);
614+
event LogBytes(bytes data);
615+
616+
Foo public foo;
617+
618+
constructor() {
619+
foo = new Foo(msg.sender);
620+
}
621+
622+
function tryCatchExternalCall(uint256 _i) public {
623+
try foo.myFunc(_i) returns (string memory result) {
624+
emit Log(result);
625+
} catch {
626+
emit Log("external call failed");
627+
}
628+
}
629+
630+
function tryCatchNewContract(address _owner) public {
631+
try new Foo(_owner) returns (Foo foo_) {
632+
emit Log("Foo created");
633+
} catch Error(string memory reason) {
634+
emit Log(reason);
635+
} catch (bytes memory reason) {}
636+
}
637+
}
638+
"#,
639+
)
640+
.unwrap();
641+
642+
prj.add_source(
643+
"FooTest.sol",
644+
r#"
645+
import "./test.sol";
646+
import {Bar, Foo} from "./Foo.sol";
647+
648+
interface Vm {
649+
function expectRevert() external;
650+
}
651+
652+
contract FooTest is DSTest {
653+
Vm constant vm = Vm(HEVM_ADDRESS);
654+
655+
function test_happy_foo_coverage() external {
656+
vm.expectRevert();
657+
Foo foo = new Foo(address(0));
658+
vm.expectRevert();
659+
foo = new Foo(address(1));
660+
foo = new Foo(address(2));
661+
}
662+
663+
function test_happy_path_coverage() external {
664+
Bar bar = new Bar();
665+
bar.tryCatchNewContract(0x0000000000000000000000000000000000000002);
666+
bar.tryCatchExternalCall(1);
667+
}
668+
669+
function test_coverage() external {
670+
Bar bar = new Bar();
671+
bar.tryCatchNewContract(0x0000000000000000000000000000000000000000);
672+
bar.tryCatchNewContract(0x0000000000000000000000000000000000000001);
673+
bar.tryCatchExternalCall(0);
674+
}
675+
}
676+
"#,
677+
)
678+
.unwrap();
679+
680+
// Assert coverage not 100% for happy paths only.
681+
cmd.arg("coverage").args(["--mt".to_string(), "happy".to_string()]).assert_success().stdout_eq(
682+
str![[r#"
683+
...
684+
| File | % Lines | % Statements | % Branches | % Funcs |
685+
|-------------|----------------|----------------|---------------|---------------|
686+
| src/Foo.sol | 66.67% (10/15) | 66.67% (14/21) | 100.00% (4/4) | 100.00% (5/5) |
687+
| Total | 66.67% (10/15) | 66.67% (14/21) | 100.00% (4/4) | 100.00% (5/5) |
688+
689+
"#]],
690+
);
691+
692+
// Assert 100% branch coverage (including clauses without body).
693+
cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(
694+
str![[r#"
695+
...
696+
| File | % Lines | % Statements | % Branches | % Funcs |
697+
|-------------|-----------------|-----------------|---------------|---------------|
698+
| src/Foo.sol | 100.00% (15/15) | 100.00% (21/21) | 100.00% (4/4) | 100.00% (5/5) |
699+
| Total | 100.00% (15/15) | 100.00% (21/21) | 100.00% (4/4) | 100.00% (5/5) |
700+
701+
"#]],
702+
);
703+
});

0 commit comments

Comments
 (0)