Skip to content

Commit

Permalink
fix(coverage): special functions have no name
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniPopes committed Dec 1, 2024
1 parent ac81a53 commit becae71
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 52 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions crates/evm/coverage/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,20 @@ impl<'a> ContractVisitor<'a> {
fn visit_function_definition(&mut self, node: &Node) -> eyre::Result<()> {
let Some(body) = &node.body else { return Ok(()) };

let kind: String =
node.attribute("kind").ok_or_else(|| eyre::eyre!("Function has no kind"))?;

let name: String =
node.attribute("name").ok_or_else(|| eyre::eyre!("Function has no name"))?;
let kind: String =
node.attribute("kind").ok_or_else(|| eyre::eyre!("Function has no kind"))?;

// Do not add coverage item for constructors without statements.
if kind == "constructor" && !has_statements(body) {
return Ok(())
}

// `fallback`, `receive`, and `constructor` functions have an empty `name`.
// Use the `kind` itself as the name.
let name = if name.is_empty() { kind } else { name };

self.push_item_kind(CoverageItemKind::Function { name }, &node.src);
self.visit_block(body)
}
Expand Down Expand Up @@ -498,10 +502,7 @@ fn has_statements(node: &Node) -> bool {
NodeType::TryStatement |
NodeType::VariableDeclarationStatement |
NodeType::WhileStatement => true,
_ => {
let statements: Vec<Node> = node.attribute("statements").unwrap_or_default();
!statements.is_empty()
}
_ => node.attribute::<Vec<Node>>("statements").is_some_and(|s| !s.is_empty()),
}
}

Expand Down
5 changes: 3 additions & 2 deletions crates/forge/src/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ impl CoverageReporter for LcovReporter<'_> {

for item in items {
let line = item.loc.lines.start;
let line_end = item.loc.lines.end - 1;
// `lines` is half-open, so we need to subtract 1 to get the last included line.
let end_line = item.loc.lines.end - 1;
let hits = item.hits;
match item.kind {
CoverageItemKind::Function { ref name } => {
let name = format!("{}.{name}", item.loc.contract_name);
writeln!(self.out, "FN:{line},{line_end},{name}")?;
writeln!(self.out, "FN:{line},{end_line},{name}")?;
writeln!(self.out, "FNDA:{hits},{name}")?;
}
CoverageItemKind::Line => {
Expand Down
106 changes: 66 additions & 40 deletions crates/forge/tests/cli/coverage.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
use foundry_test_utils::{assert_data_eq, str};
use foundry_test_utils::{snapbox::IntoData, TestCommand};

forgetest!(basic_coverage, |_prj, cmd| {
cmd.args(["coverage"]);
cmd.assert_success();
});

forgetest!(report_file_coverage, |prj, cmd| {
cmd.arg("coverage").args([
"--report".to_string(),
"lcov".to_string(),
"--report-file".to_string(),
prj.root().join("lcov.info").to_str().unwrap().to_string(),
]);
cmd.assert_success();
forgetest_init!(basic_coverage, |prj, cmd| {
cmd.args(["coverage", "--report=lcov", "--report=summary"]).assert_success().stdout_eq(str![[
r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
Analysing contracts...
Running tests...
Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS])
[PASS] test_Increment() ([GAS])
Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED]
Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests)
Wrote LCOV report.
| File | % Lines | % Statements | % Branches | % Funcs |
|----------------------|---------------|---------------|---------------|---------------|
| script/Counter.s.sol | 0.00% (0/5) | 0.00% (0/3) | 100.00% (0/0) | 0.00% (0/2) |
| src/Counter.sol | 100.00% (4/4) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) |
| Total | 44.44% (4/9) | 40.00% (2/5) | 100.00% (0/0) | 50.00% (2/4) |
"#
]]);
assert!(prj.root().join("lcov.info").exists(), "lcov.info was not created");
});

forgetest!(test_setup_coverage, |prj, cmd| {
Expand Down Expand Up @@ -58,7 +70,7 @@ contract AContractTest is DSTest {
.unwrap();

// Assert 100% coverage (init function coverage called in setUp is accounted).
cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#"
cmd.arg("coverage").arg("--summary").assert_success().stdout_eq(str![[r#"
...
| File | % Lines | % Statements | % Branches | % Funcs |
|-------------------|---------------|---------------|---------------|---------------|
Expand Down Expand Up @@ -151,20 +163,16 @@ contract BContractTest is DSTest {
.unwrap();

// Assert AContract is not included in report.
cmd.arg("coverage")
.args([
"--no-match-coverage".to_string(),
"AContract".to_string(), // Filter out `AContract`
])
.assert_success()
.stdout_eq(str![[r#"
cmd.arg("coverage").arg("--no-match-coverage=AContract").assert_success().stdout_eq(str![[
r#"
...
| File | % Lines | % Statements | % Branches | % Funcs |
|-------------------|---------------|---------------|---------------|---------------|
| src/BContract.sol | 100.00% (4/4) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) |
| Total | 100.00% (4/4) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) |
"#]]);
"#
]]);
});

forgetest!(test_assert_coverage, |prj, cmd| {
Expand Down Expand Up @@ -363,19 +371,9 @@ contract AContractTest is DSTest {
)
.unwrap();

let lcov_info = prj.root().join("lcov.info");
cmd.arg("coverage").args([
"--report".to_string(),
"lcov".to_string(),
"--report-file".to_string(),
lcov_info.to_str().unwrap().to_string(),
]);
cmd.assert_success();
assert!(lcov_info.exists());

// We want to make sure DA:8,1 is added only once so line hit is not doubled.
assert_data_eq!(
std::fs::read_to_string(lcov_info).unwrap(),
assert_lcov(
cmd.arg("coverage"),
str![[r#"
TN:
SF:src/AContract.sol
Expand All @@ -391,7 +389,7 @@ BRF:0
BRH:0
end_of_record
"#]]
"#]],
);
});

Expand Down Expand Up @@ -712,8 +710,7 @@ contract AContractTest is DSTest {
)
.unwrap();

// Assert 100% coverage and only 9 lines reported (comments, type conversions and struct
// constructor calls are not included).
// Assert 100% coverage.
cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#"
...
| File | % Lines | % Statements | % Branches | % Funcs |
Expand Down Expand Up @@ -1406,8 +1403,32 @@ contract AContractTest is DSTest {
)
.unwrap();

// Assert both constructor and receive functions coverage reported.
cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#"
// Assert both constructor and receive functions coverage reported and appear in LCOV.
assert_lcov(
cmd.arg("coverage"),
str![[r#"
TN:
SF:src/AContract.sol
DA:7,1
FN:7,9,AContract.constructor
FNDA:1,AContract.constructor
DA:8,1
DA:11,1
FN:11,13,AContract.receive
FNDA:1,AContract.receive
DA:12,1
FNF:2
FNH:2
LF:4
LH:4
BRF:0
BRH:0
end_of_record
"#]],
);

cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
| File | % Lines | % Statements | % Branches | % Funcs |
|-------------------|---------------|---------------|---------------|---------------|
Expand Down Expand Up @@ -1450,3 +1471,8 @@ contract AContract {
"#]]);
});

#[track_caller]
fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) {
cmd.args(["--report=lcov", "--report-file"]).assert_file(data);
}
1 change: 1 addition & 0 deletions crates/test-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
rand.workspace = true
snapbox = { version = "0.6", features = ["json", "regex"] }
tempfile.workspace = true

[dev-dependencies]
tokio.workspace = true
Expand Down
22 changes: 19 additions & 3 deletions crates/test-utils/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use foundry_compilers::{
use foundry_config::Config;
use parking_lot::Mutex;
use regex::Regex;
use snapbox::{assert_data_eq, cmd::OutputAssert, str, IntoData};
use snapbox::{assert_data_eq, cmd::OutputAssert, Data, IntoData};
use std::{
env,
ffi::OsStr,
Expand Down Expand Up @@ -905,7 +905,7 @@ impl TestCommand {
/// Runs the command and asserts that it **failed** nothing was printed to stdout.
#[track_caller]
pub fn assert_empty_stdout(&mut self) {
self.assert_success().stdout_eq(str![[r#""#]]);
self.assert_success().stdout_eq(Data::new());
}

/// Runs the command and asserts that it failed.
Expand All @@ -923,7 +923,23 @@ impl TestCommand {
/// Runs the command and asserts that it **failed** nothing was printed to stderr.
#[track_caller]
pub fn assert_empty_stderr(&mut self) {
self.assert_failure().stderr_eq(str![[r#""#]]);
self.assert_failure().stderr_eq(Data::new());
}

/// Runs the command with a temporary file argument and asserts that the contents of the file
/// match the given data.
#[track_caller]
pub fn assert_file(&mut self, data: impl IntoData) {
self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
}

/// Creates a temporary file, passes it to `f`, then asserts that the contents of the file match
/// the given data.
#[track_caller]
pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
f(self, file.path());
assert_data_eq!(Data::read_from(file.path(), None), data);
}

/// Does not apply [`snapbox`] redactions to the command output.
Expand Down

0 comments on commit becae71

Please sign in to comment.