From becae71770346377dfb12661343cde33f3d7b3b5 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 1 Dec 2024 15:40:07 +0100 Subject: [PATCH 1/6] fix(coverage): special functions have no name --- Cargo.lock | 1 + crates/evm/coverage/src/analysis.rs | 15 ++-- crates/forge/src/coverage.rs | 5 +- crates/forge/tests/cli/coverage.rs | 106 +++++++++++++++++----------- crates/test-utils/Cargo.toml | 1 + crates/test-utils/src/util.rs | 22 +++++- 6 files changed, 98 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0dafa83f97c..3f8fa5bb58ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4173,6 +4173,7 @@ dependencies = [ "regex", "serde_json", "snapbox", + "tempfile", "tokio", "tracing", "tracing-subscriber", diff --git a/crates/evm/coverage/src/analysis.rs b/crates/evm/coverage/src/analysis.rs index 69bd73075a3f..07b9160350c4 100644 --- a/crates/evm/coverage/src/analysis.rs +++ b/crates/evm/coverage/src/analysis.rs @@ -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) } @@ -498,10 +502,7 @@ fn has_statements(node: &Node) -> bool { NodeType::TryStatement | NodeType::VariableDeclarationStatement | NodeType::WhileStatement => true, - _ => { - let statements: Vec = node.attribute("statements").unwrap_or_default(); - !statements.is_empty() - } + _ => node.attribute::>("statements").is_some_and(|s| !s.is_empty()), } } diff --git a/crates/forge/src/coverage.rs b/crates/forge/src/coverage.rs index e1236fa2a40d..14fc5e7be802 100644 --- a/crates/forge/src/coverage.rs +++ b/crates/forge/src/coverage.rs @@ -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 => { diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 060a603715f0..00578c481dcc 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -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| { @@ -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 | |-------------------|---------------|---------------|---------------|---------------| @@ -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| { @@ -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 @@ -391,7 +389,7 @@ BRF:0 BRH:0 end_of_record -"#]] +"#]], ); }); @@ -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 | @@ -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 | |-------------------|---------------|---------------|---------------|---------------| @@ -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); +} diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 11b7800e6bba..efe6d288ff9e 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -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 diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index f887a40ce7a1..3910f96324ea 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -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, @@ -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. @@ -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. From 2e65cce074c4bab39971d2477e8352455119970c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 1 Dec 2024 15:40:14 +0100 Subject: [PATCH 2/6] test: don't to_string --- crates/forge/tests/cli/coverage.rs | 109 ++++++++++------------------- crates/test-utils/src/util.rs | 2 +- 2 files changed, 39 insertions(+), 72 deletions(-) diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 00578c481dcc..eb1379df493d 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -219,43 +219,38 @@ contract AContractTest is DSTest { .unwrap(); // Assert 50% branch coverage for assert failure. - cmd.arg("coverage") - .args(["--mt".to_string(), "testAssertRevertBranch".to_string()]) - .assert_success() - .stdout_eq(str![[r#" + cmd.arg("coverage").args(["--mt", "testAssertRevertBranch"]).assert_success().stdout_eq(str![ + [r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| | src/AContract.sol | 66.67% (2/3) | 50.00% (1/2) | 50.00% (1/2) | 100.00% (1/1) | | Total | 66.67% (2/3) | 50.00% (1/2) | 50.00% (1/2) | 100.00% (1/1) | -"#]]); +"#] + ]); // Assert 50% branch coverage for proper assert. - cmd.forge_fuse() - .arg("coverage") - .args(["--mt".to_string(), "testAssertBranch".to_string()]) - .assert_success() - .stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").args(["--mt", "testAssertBranch"]).assert_success().stdout_eq( + str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|--------------|---------------| | src/AContract.sol | 100.00% (3/3) | 100.00% (2/2) | 50.00% (1/2) | 100.00% (1/1) | | Total | 100.00% (3/3) | 100.00% (2/2) | 50.00% (1/2) | 100.00% (1/1) | -"#]]); +"#]], + ); // Assert 100% coverage (assert properly covered). - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| | src/AContract.sol | 100.00% (3/3) | 100.00% (2/2) | 100.00% (2/2) | 100.00% (1/1) | | Total | 100.00% (3/3) | 100.00% (2/2) | 100.00% (2/2) | 100.00% (1/1) | -"#]], - ); +"#]]); }); forgetest!(test_require_coverage, |prj, cmd| { @@ -300,10 +295,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert 50% branch coverage if only revert tested. - cmd.arg("coverage") - .args(["--mt".to_string(), "testRequireRevert".to_string()]) - .assert_success() - .stdout_eq(str![[r#" + cmd.arg("coverage").args(["--mt", "testRequireRevert"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|--------------|---------------| @@ -315,7 +307,7 @@ contract AContractTest is DSTest { // Assert 50% branch coverage if only happy path tested. cmd.forge_fuse() .arg("coverage") - .args(["--mt".to_string(), "testRequireNoRevert".to_string()]) + .args(["--mt", "testRequireNoRevert"]) .assert_success() .stdout_eq(str![[r#" ... @@ -327,16 +319,14 @@ contract AContractTest is DSTest { "#]]); // Assert 100% branch coverage. - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| | src/AContract.sol | 100.00% (2/2) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) | | Total | 100.00% (2/2) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) | -"#]], - ); +"#]]); }); forgetest!(test_line_hit_not_doubled, |prj, cmd| { @@ -609,10 +599,7 @@ contract FooTest is DSTest { // Assert no coverage for single path branch. 2 branches (parent and child) not covered. cmd.arg("coverage") - .args([ - "--nmt".to_string(), - "test_single_path_child_branch|test_single_path_parent_branch".to_string(), - ]) + .args(["--nmt", "test_single_path_child_branch|test_single_path_parent_branch"]) .assert_success() .stdout_eq(str![[r#" ... @@ -626,7 +613,7 @@ contract FooTest is DSTest { // Assert no coverage for single path child branch. 1 branch (child) not covered. cmd.forge_fuse() .arg("coverage") - .args(["--nmt".to_string(), "test_single_path_child_branch".to_string()]) + .args(["--nmt", "test_single_path_child_branch"]) .assert_success() .stdout_eq(str![[r#" ... @@ -638,16 +625,14 @@ contract FooTest is DSTest { "#]]); // Assert 100% coverage. - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|-----------------|---------------| | src/Foo.sol | 100.00% (36/36) | 100.00% (30/30) | 100.00% (16/16) | 100.00% (9/9) | | Total | 100.00% (36/36) | 100.00% (30/30) | 100.00% (16/16) | 100.00% (9/9) | -"#]], - ); +"#]]); }); forgetest!(test_function_call_coverage, |prj, cmd| { @@ -711,7 +696,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert 100% coverage. - cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|-----------------|---------------|---------------|---------------| @@ -810,28 +795,24 @@ contract FooTest is DSTest { .unwrap(); // Assert coverage not 100% for happy paths only. - cmd.arg("coverage").args(["--mt".to_string(), "happy".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.arg("coverage").args(["--mt", "happy"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|----------------|----------------|--------------|---------------| | src/Foo.sol | 75.00% (15/20) | 66.67% (14/21) | 83.33% (5/6) | 100.00% (5/5) | | Total | 75.00% (15/20) | 66.67% (14/21) | 83.33% (5/6) | 100.00% (5/5) | -"#]], - ); +"#]]); // Assert 100% branch coverage (including clauses without body). - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|---------------|---------------| | src/Foo.sol | 100.00% (20/20) | 100.00% (21/21) | 100.00% (6/6) | 100.00% (5/5) | | Total | 100.00% (20/20) | 100.00% (21/21) | 100.00% (6/6) | 100.00% (5/5) | -"#]], - ); +"#]]); }); forgetest!(test_yul_coverage, |prj, cmd| { @@ -927,16 +908,14 @@ contract FooTest is DSTest { ) .unwrap(); - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|---------------|---------------| | src/Foo.sol | 100.00% (30/30) | 100.00% (40/40) | 100.00% (1/1) | 100.00% (7/7) | | Total | 100.00% (30/30) | 100.00% (40/40) | 100.00% (1/1) | 100.00% (7/7) | -"#]], - ); +"#]]); }); forgetest!(test_misc_coverage, |prj, cmd| { @@ -1019,16 +998,14 @@ contract FooTest is DSTest { ) .unwrap(); - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|---------------|---------------|---------------| | src/Foo.sol | 100.00% (12/12) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (4/4) | | Total | 100.00% (12/12) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (4/4) | -"#]], - ); +"#]]); }); // https://github.com/foundry-rs/foundry/issues/8605 @@ -1075,10 +1052,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert 50% coverage for true branches. - cmd.arg("coverage") - .args(["--mt".to_string(), "testTrueCoverage".to_string()]) - .assert_success() - .stdout_eq(str![[r#" + cmd.arg("coverage").args(["--mt", "testTrueCoverage"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| @@ -1090,7 +1064,7 @@ contract AContractTest is DSTest { // Assert 50% coverage for false branches. cmd.forge_fuse() .arg("coverage") - .args(["--mt".to_string(), "testFalseCoverage".to_string()]) + .args(["--mt", "testFalseCoverage"]) .assert_success() .stdout_eq(str![[r#" ... @@ -1102,16 +1076,14 @@ contract AContractTest is DSTest { "#]]); // Assert 100% coverage (true/false branches properly covered). - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| | src/AContract.sol | 100.00% (5/5) | 100.00% (4/4) | 100.00% (4/4) | 100.00% (1/1) | | Total | 100.00% (5/5) | 100.00% (4/4) | 100.00% (4/4) | 100.00% (1/1) | -"#]], - ); +"#]]); }); // https://github.com/foundry-rs/foundry/issues/8604 @@ -1164,10 +1136,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert 50% coverage for true branches. - cmd.arg("coverage") - .args(["--mt".to_string(), "testTrueCoverage".to_string()]) - .assert_success() - .stdout_eq(str![[r#" + cmd.arg("coverage").args(["--mt", "testTrueCoverage"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| @@ -1179,7 +1148,7 @@ contract AContractTest is DSTest { // Assert 50% coverage for false branches. cmd.forge_fuse() .arg("coverage") - .args(["--mt".to_string(), "testFalseCoverage".to_string()]) + .args(["--mt", "testFalseCoverage"]) .assert_success() .stdout_eq(str![[r#" ... @@ -1191,16 +1160,14 @@ contract AContractTest is DSTest { "#]]); // Assert 100% coverage (true/false branches properly covered). - cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( - str![[r#" + cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| | src/AContract.sol | 100.00% (5/5) | 100.00% (5/5) | 100.00% (2/2) | 100.00% (1/1) | | Total | 100.00% (5/5) | 100.00% (5/5) | 100.00% (2/2) | 100.00% (1/1) | -"#]], - ); +"#]]); }); forgetest!(test_identical_bytecodes, |prj, cmd| { @@ -1262,7 +1229,7 @@ contract AContractTest is DSTest { ) .unwrap(); - cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|-----------------|---------------|---------------|---------------| @@ -1312,7 +1279,7 @@ contract AContractTest is DSTest { ) .unwrap(); - cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -1355,7 +1322,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert there's only one function (`increment`) reported. - cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 3910f96324ea..a86304923ffe 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -902,7 +902,7 @@ impl TestCommand { assert_data_eq!(actual, expected); } - /// Runs the command and asserts that it **failed** nothing was printed to stdout. + /// Runs the command and asserts that it **succeeded** nothing was printed to stdout. #[track_caller] pub fn assert_empty_stdout(&mut self) { self.assert_success().stdout_eq(Data::new()); From a9cef82ac225a49ef350ee0cb564adcab4923f54 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:52:05 +0100 Subject: [PATCH 3/6] test: rm --summary which is not --report=summary --- crates/forge/tests/cli/coverage.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index eb1379df493d..9f0cceafdc0b 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -70,7 +70,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert 100% coverage (init function coverage called in setUp is accounted). - cmd.arg("coverage").arg("--summary").assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -243,7 +243,7 @@ contract AContractTest is DSTest { ); // Assert 100% coverage (assert properly covered). - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -319,7 +319,7 @@ contract AContractTest is DSTest { "#]]); // Assert 100% branch coverage. - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -625,7 +625,7 @@ contract FooTest is DSTest { "#]]); // Assert 100% coverage. - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|-----------------|---------------| @@ -696,7 +696,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert 100% coverage. - cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|-----------------|---------------|---------------|---------------| @@ -805,7 +805,7 @@ contract FooTest is DSTest { "#]]); // Assert 100% branch coverage (including clauses without body). - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|---------------|---------------| @@ -908,7 +908,7 @@ contract FooTest is DSTest { ) .unwrap(); - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|---------------|---------------| @@ -998,7 +998,7 @@ contract FooTest is DSTest { ) .unwrap(); - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|---------------|---------------|---------------| @@ -1076,7 +1076,7 @@ contract AContractTest is DSTest { "#]]); // Assert 100% coverage (true/false branches properly covered). - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -1160,7 +1160,7 @@ contract AContractTest is DSTest { "#]]); // Assert 100% coverage (true/false branches properly covered). - cmd.forge_fuse().arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -1229,7 +1229,7 @@ contract AContractTest is DSTest { ) .unwrap(); - cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|-----------------|---------------|---------------|---------------| @@ -1279,7 +1279,7 @@ contract AContractTest is DSTest { ) .unwrap(); - cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| @@ -1322,7 +1322,7 @@ contract AContractTest is DSTest { .unwrap(); // Assert there's only one function (`increment`) reported. - cmd.arg("coverage").args(["--summary"]).assert_success().stdout_eq(str![[r#" + cmd.arg("coverage").assert_success().stdout_eq(str![[r#" ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| From 8fb268f7b934f809177438915036a02d9f2efc6d Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:10:57 +0100 Subject: [PATCH 4/6] test: add regression test for #9437 --- crates/forge/tests/cli/coverage.rs | 69 ++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 9f0cceafdc0b..4d447fa7f2fc 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -1,6 +1,11 @@ -use foundry_test_utils::{snapbox::IntoData, TestCommand}; - -forgetest_init!(basic_coverage, |prj, cmd| { +use foundry_common::fs; +use foundry_test_utils::{ + snapbox::{Data, IntoData}, + TestCommand, TestProject, +}; +use std::path::Path; + +fn basic_coverage_base(prj: TestProject, mut cmd: TestCommand) { cmd.args(["coverage", "--report=lcov", "--report=summary"]).assert_success().stdout_eq(str![[ r#" [COMPILING_FILES] with [SOLC_VERSION] @@ -24,7 +29,63 @@ Wrote LCOV report. "# ]]); - assert!(prj.root().join("lcov.info").exists(), "lcov.info was not created"); + + let lcov = prj.root().join("lcov.info"); + assert!(lcov.exists(), "lcov.info was not created"); + assert_data_eq!(Data::read_from(&lcov, None), str![[r#" +TN: +SF:script/Counter.s.sol +DA:10,0 +FN:10,10,CounterScript.setUp +FNDA:0,CounterScript.setUp +DA:12,0 +FN:12,18,CounterScript.run +FNDA:0,CounterScript.run +DA:13,0 +DA:15,0 +DA:17,0 +FNF:2 +FNH:0 +LF:5 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/Counter.sol +DA:7,258 +FN:7,9,Counter.setNumber +FNDA:258,Counter.setNumber +DA:8,258 +DA:11,1 +FN:11,13,Counter.increment +FNDA:1,Counter.increment +DA:12,1 +FNF:2 +FNH:2 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record + +"#]]); +} + +forgetest_init!(basic_coverage, |prj, cmd| { + basic_coverage_base(prj, cmd); +}); + +forgetest_init!(basic_coverage_crlf, |prj, cmd| { + // Manually replace `\n` with `\r\n` in the source file. + let make_crlf = |path: &Path| { + fs::write(path, fs::read_to_string(path).unwrap().replace('\n', "\r\n")).unwrap() + }; + make_crlf(&prj.paths().sources.join("Counter.sol")); + make_crlf(&prj.paths().scripts.join("Counter.s.sol")); + + // Should have identical stdout and lcov output. + basic_coverage_base(prj, cmd); }); forgetest!(test_setup_coverage, |prj, cmd| { From 14fe7a4e0620d4274ba2daaf3a41bac4d3d000f6 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:33:04 +0100 Subject: [PATCH 5/6] fmt --- crates/forge/tests/cli/coverage.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 4d447fa7f2fc..9b0569da6ff6 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -32,7 +32,9 @@ Wrote LCOV report. let lcov = prj.root().join("lcov.info"); assert!(lcov.exists(), "lcov.info was not created"); - assert_data_eq!(Data::read_from(&lcov, None), str![[r#" + assert_data_eq!( + Data::read_from(&lcov, None), + str![[r#" TN: SF:script/Counter.s.sol DA:10,0 @@ -69,7 +71,8 @@ BRF:0 BRH:0 end_of_record -"#]]); +"#]] + ); } forgetest_init!(basic_coverage, |prj, cmd| { From f6e3c7d9e07db6ffb31c5143a74107e1b889ebc2 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:16:42 +0100 Subject: [PATCH 6/6] docs --- crates/forge/bin/cmd/coverage.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 10d67d825b30..3a7d43436999 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -310,9 +310,10 @@ impl CoverageArgs { } } -// TODO: HTML -#[derive(Clone, Debug, ValueEnum)] +/// Coverage reports to generate. +#[derive(Clone, Debug, Default, ValueEnum)] pub enum CoverageReportKind { + #[default] Summary, Lcov, Debug,