From e5dbb7a320c2b871c4a4a1006ad3c15a08fcf17b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:16:59 +0200 Subject: [PATCH] fix/feat(coverage): add --lcov-version (#9462) feat(coverage): add --lcov-version --- crates/forge/bin/cmd/coverage.rs | 44 ++++++++++++- crates/forge/src/coverage.rs | 22 +++++-- crates/forge/tests/cli/coverage.rs | 102 ++++++++++++++++++++++++++--- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index d995e764df5b..9178c27bea5f 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -23,7 +23,7 @@ use foundry_compilers::{ }; use foundry_config::{Config, SolcReq}; use rayon::prelude::*; -use semver::Version; +use semver::{Version, VersionReq}; use std::{ io, path::{Path, PathBuf}, @@ -42,6 +42,18 @@ pub struct CoverageArgs { #[arg(long, value_enum, default_value = "summary")] report: Vec, + /// The version of the LCOV "tracefile" format to use. + /// + /// Format: `MAJOR[.MINOR]`. + /// + /// Main differences: + /// - `1.x`: The original v1 format. + /// - `2.0`: Adds support for "line end" numbers for functions. LCOV 2.1 and onwards may emit + /// an error if this option is not provided. + /// - `2.2`: Changes the format of functions. + #[arg(long, default_value = "1.16", value_parser = parse_lcov_version)] + lcov_version: Version, + /// Enable viaIR with minimum optimization /// /// This can fix most of the "stack too deep" errors while resulting a @@ -295,7 +307,7 @@ impl CoverageArgs { let path = root.join(self.report_file.as_deref().unwrap_or("lcov.info".as_ref())); let mut file = io::BufWriter::new(fs::create_file(path)?); - LcovReporter::new(&mut file).report(&report) + LcovReporter::new(&mut file, self.lcov_version.clone()).report(&report) } CoverageReportKind::Bytecode => { let destdir = root.join("bytecode-coverage"); @@ -404,3 +416,31 @@ impl BytecodeData { ) } } + +fn parse_lcov_version(s: &str) -> Result { + let vr = VersionReq::parse(&format!("={s}")).map_err(|e| e.to_string())?; + let [c] = &vr.comparators[..] else { + return Err("invalid version".to_string()); + }; + if c.op != semver::Op::Exact { + return Err("invalid version".to_string()); + } + if !c.pre.is_empty() { + return Err("pre-releases are not supported".to_string()); + } + Ok(Version::new(c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lcov_version() { + assert_eq!(parse_lcov_version("0").unwrap(), Version::new(0, 0, 0)); + assert_eq!(parse_lcov_version("1").unwrap(), Version::new(1, 0, 0)); + assert_eq!(parse_lcov_version("1.0").unwrap(), Version::new(1, 0, 0)); + assert_eq!(parse_lcov_version("1.1").unwrap(), Version::new(1, 1, 0)); + assert_eq!(parse_lcov_version("1.11").unwrap(), Version::new(1, 11, 0)); + } +} diff --git a/crates/forge/src/coverage.rs b/crates/forge/src/coverage.rs index f73b17da532d..813480b40523 100644 --- a/crates/forge/src/coverage.rs +++ b/crates/forge/src/coverage.rs @@ -4,6 +4,7 @@ use alloy_primitives::map::HashMap; use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, Color, Row, Table}; use evm_disassembler::disassemble_bytes; use foundry_common::fs; +use semver::Version; use std::{ collections::hash_map, io::Write, @@ -83,17 +84,19 @@ fn format_cell(hits: usize, total: usize) -> Cell { /// [tracefile format]: https://man.archlinux.org/man/geninfo.1.en#TRACEFILE_FORMAT pub struct LcovReporter<'a> { out: &'a mut (dyn Write + 'a), + version: Version, } impl<'a> LcovReporter<'a> { /// Create a new LCOV reporter. - pub fn new(out: &'a mut (dyn Write + 'a)) -> Self { - Self { out } + pub fn new(out: &'a mut (dyn Write + 'a), version: Version) -> Self { + Self { out, version } } } impl CoverageReporter for LcovReporter<'_> { fn report(self, report: &CoverageReport) -> eyre::Result<()> { + let mut fn_index = 0usize; for (path, items) in report.items_by_file() { let summary = CoverageSummary::from_items(items.iter().copied()); @@ -108,8 +111,19 @@ impl CoverageReporter for LcovReporter<'_> { match item.kind { CoverageItemKind::Function { ref name } => { let name = format!("{}.{name}", item.loc.contract_name); - writeln!(self.out, "FN:{line},{end_line},{name}")?; - writeln!(self.out, "FNDA:{hits},{name}")?; + if self.version >= Version::new(2, 2, 0) { + // v2.2 changed the FN format. + writeln!(self.out, "FNL:{fn_index},{line},{end_line}")?; + writeln!(self.out, "FNA:{fn_index},{hits},{name}")?; + fn_index += 1; + } else if self.version >= Version::new(2, 0, 0) { + // v2.0 added end_line to FN. + writeln!(self.out, "FN:{line},{end_line},{name}")?; + writeln!(self.out, "FNDA:{hits},{name}")?; + } else { + writeln!(self.out, "FN:{line},{name}")?; + writeln!(self.out, "FNDA:{hits},{name}")?; + } } CoverageItemKind::Line => { writeln!(self.out, "DA:{line},{hits}")?; diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 6ffc6c2b323a..8c6cbc19c54c 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -32,8 +32,52 @@ 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), + let default_lcov = str![[r#" +TN: +SF:script/Counter.s.sol +DA:10,0 +FN:10,CounterScript.setUp +FNDA:0,CounterScript.setUp +DA:12,0 +FN:12,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,Counter.setNumber +FNDA:258,Counter.setNumber +DA:8,258 +DA:11,1 +FN:11,Counter.increment +FNDA:1,Counter.increment +DA:12,1 +FNF:2 +FNH:2 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record + +"#]]; + assert_data_eq!(Data::read_from(&lcov, None), default_lcov.clone()); + assert_lcov( + cmd.forge_fuse().args(["coverage", "--report=lcov", "--lcov-version=1"]), + default_lcov, + ); + + assert_lcov( + cmd.forge_fuse().args(["coverage", "--report=lcov", "--lcov-version=2"]), str![[r#" TN: SF:script/Counter.s.sol @@ -71,7 +115,49 @@ BRF:0 BRH:0 end_of_record -"#]] +"#]], + ); + + assert_lcov( + cmd.forge_fuse().args(["coverage", "--report=lcov", "--lcov-version=2.2"]), + str![[r#" +TN: +SF:script/Counter.s.sol +DA:10,0 +FNL:0,10,10 +FNA:0,0,CounterScript.setUp +DA:12,0 +FNL:1,12,18 +FNA:1,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 +FNL:2,7,9 +FNA:2,258,Counter.setNumber +DA:8,258 +DA:11,1 +FNL:3,11,13 +FNA:3,1,Counter.increment +DA:12,1 +FNF:2 +FNH:2 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record + +"#]], ); } @@ -432,7 +518,7 @@ contract AContractTest is DSTest { TN: SF:src/AContract.sol DA:7,1 -FN:7,9,AContract.foo +FN:7,AContract.foo FNDA:1,AContract.foo DA:8,1 FNF:1 @@ -1397,7 +1483,7 @@ contract AContractTest is DSTest { TN: SF:src/AContract.sol DA:9,1 -FN:9,9,AContract.increment +FN:9,AContract.increment FNDA:1,AContract.increment FNF:1 FNH:1 @@ -1466,11 +1552,11 @@ contract AContractTest is DSTest { TN: SF:src/AContract.sol DA:7,1 -FN:7,9,AContract.constructor +FN:7,AContract.constructor FNDA:1,AContract.constructor DA:8,1 DA:11,1 -FN:11,13,AContract.receive +FN:11,AContract.receive FNDA:1,AContract.receive DA:12,1 FNF:2 @@ -1530,5 +1616,5 @@ contract AContract { #[track_caller] fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) { - cmd.args(["--report=lcov", "--report-file"]).assert_file(data); + cmd.args(["--report=lcov", "--report-file"]).assert_file(data.into_data()); }