Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix/feat(coverage): add --lcov-version #9462

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions crates/forge/bin/cmd/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -42,6 +42,18 @@ pub struct CoverageArgs {
#[arg(long, value_enum, default_value = "summary")]
report: Vec<CoverageReportKind>,

/// 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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -404,3 +416,31 @@ impl BytecodeData {
)
}
}

fn parse_lcov_version(s: &str) -> Result<Version, String> {
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));
}
}
22 changes: 18 additions & 4 deletions crates/forge/src/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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());

Expand All @@ -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}")?;
Expand Down
102 changes: 94 additions & 8 deletions crates/forge/tests/cli/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

"#]],
);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
}
Loading