From 7a23a5cf851b991bfd2fde32d4f088319bbc1183 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 30 Nov 2024 15:47:18 +0100 Subject: [PATCH] fix(coverage): clean ups, use normalized source code for locations (#9438) * feat(coverage): add function line end to LCOV, clean ups * fix(coverage): store normalized source code * fix(coverage): add a Line item for functions too * test: update snapshots * clean --- crates/evm/coverage/src/analysis.rs | 56 ++++++------ crates/evm/coverage/src/anchors.rs | 8 +- crates/evm/coverage/src/lib.rs | 134 ++++++++++++++++------------ crates/forge/bin/cmd/coverage.rs | 21 +++-- crates/forge/src/coverage.rs | 56 ++++++------ crates/forge/tests/cli/coverage.rs | 133 +++++++++++++-------------- 6 files changed, 219 insertions(+), 189 deletions(-) diff --git a/crates/evm/coverage/src/analysis.rs b/crates/evm/coverage/src/analysis.rs index c18ba823b2d0..69bd73075a3f 100644 --- a/crates/evm/coverage/src/analysis.rs +++ b/crates/evm/coverage/src/analysis.rs @@ -1,7 +1,10 @@ use super::{CoverageItem, CoverageItemKind, SourceLocation}; use alloy_primitives::map::HashMap; use foundry_common::TestFunctionExt; -use foundry_compilers::artifacts::ast::{self, Ast, Node, NodeType}; +use foundry_compilers::artifacts::{ + ast::{self, Ast, Node, NodeType}, + Source, +}; use rayon::prelude::*; use std::sync::Arc; @@ -19,7 +22,7 @@ pub struct ContractVisitor<'a> { /// The current branch ID branch_id: usize, /// Stores the last line we put in the items collection to ensure we don't push duplicate lines - last_line: usize, + last_line: u32, /// Coverage items pub items: Vec, @@ -47,23 +50,20 @@ impl<'a> ContractVisitor<'a> { } fn visit_function_definition(&mut self, node: &Node) -> eyre::Result<()> { - let name: String = - node.attribute("name").ok_or_else(|| eyre::eyre!("Function has no name"))?; + let Some(body) = &node.body else { return Ok(()) }; let kind: String = node.attribute("kind").ok_or_else(|| eyre::eyre!("Function has no kind"))?; - match &node.body { - Some(body) => { - // Do not add coverage item for constructors without statements. - if kind == "constructor" && !has_statements(body) { - return Ok(()) - } - self.push_item_kind(CoverageItemKind::Function { name }, &node.src); - self.visit_block(body) - } - _ => Ok(()), + let name: String = + node.attribute("name").ok_or_else(|| eyre::eyre!("Function has no name"))?; + + // Do not add coverage item for constructors without statements. + if kind == "constructor" && !has_statements(body) { + return Ok(()) } + self.push_item_kind(CoverageItemKind::Function { name }, &node.src); + self.visit_block(body) } fn visit_modifier_or_yul_fn_definition(&mut self, node: &Node) -> eyre::Result<()> { @@ -454,30 +454,34 @@ impl<'a> ContractVisitor<'a> { /// collection (plus additional coverage line if item is a statement). fn push_item_kind(&mut self, kind: CoverageItemKind, src: &ast::LowFidelitySourceLocation) { let item = CoverageItem { kind, loc: self.source_location_for(src), hits: 0 }; - // Push a line item if we haven't already - if matches!(item.kind, CoverageItemKind::Statement | CoverageItemKind::Branch { .. }) && - self.last_line < item.loc.line - { + + // Push a line item if we haven't already. + debug_assert!(!matches!(item.kind, CoverageItemKind::Line)); + if self.last_line < item.loc.lines.start { self.items.push(CoverageItem { kind: CoverageItemKind::Line, loc: item.loc.clone(), hits: 0, }); - self.last_line = item.loc.line; + self.last_line = item.loc.lines.start; } self.items.push(item); } fn source_location_for(&self, loc: &ast::LowFidelitySourceLocation) -> SourceLocation { - let loc_start = - self.source.char_indices().map(|(i, _)| i).nth(loc.start).unwrap_or_default(); + let bytes_start = loc.start as u32; + let bytes_end = (loc.start + loc.length.unwrap_or(0)) as u32; + let bytes = bytes_start..bytes_end; + + let start_line = self.source[..bytes.start as usize].lines().count() as u32; + let n_lines = self.source[bytes.start as usize..bytes.end as usize].lines().count() as u32; + let lines = start_line..start_line + n_lines; SourceLocation { source_id: self.source_id, contract_name: self.contract_name.clone(), - start: loc.start as u32, - length: loc.length.map(|x| x as u32), - line: self.source[..loc_start].lines().count(), + bytes, + lines, } } } @@ -556,7 +560,7 @@ impl<'a> SourceAnalyzer<'a> { .attribute("name") .ok_or_else(|| eyre::eyre!("Contract has no name"))?; - let mut visitor = ContractVisitor::new(source_id, source, &name); + let mut visitor = ContractVisitor::new(source_id, &source.content, &name); visitor.visit_contract(node)?; let mut items = visitor.items; @@ -590,7 +594,7 @@ pub struct SourceFiles<'a> { #[derive(Debug)] pub struct SourceFile<'a> { /// The source code. - pub source: String, + pub source: Source, /// The AST of the source code. pub ast: &'a Ast, } diff --git a/crates/evm/coverage/src/anchors.rs b/crates/evm/coverage/src/anchors.rs index 6643524d61de..ee723d95cc5c 100644 --- a/crates/evm/coverage/src/anchors.rs +++ b/crates/evm/coverage/src/anchors.rs @@ -177,14 +177,14 @@ fn is_in_source_range(element: &SourceElement, location: &SourceLocation) -> boo } // Needed because some source ranges in the source map mark the entire contract... - let is_within_start = element.offset() >= location.start; + let is_within_start = element.offset() >= location.bytes.start; if !is_within_start { return false; } - let start_of_ranges = location.start.max(element.offset()); - let end_of_ranges = (location.start + location.length.unwrap_or_default()) - .min(element.offset() + element.length()); + let start_of_ranges = location.bytes.start.max(element.offset()); + let end_of_ranges = + (location.bytes.start + location.len()).min(element.offset() + element.length()); let within_ranges = start_of_ranges <= end_of_ranges; if !within_ranges { return false; diff --git a/crates/evm/coverage/src/lib.rs b/crates/evm/coverage/src/lib.rs index ad4ab53e3cf1..8d5575631f84 100644 --- a/crates/evm/coverage/src/lib.rs +++ b/crates/evm/coverage/src/lib.rs @@ -18,7 +18,7 @@ use semver::Version; use std::{ collections::BTreeMap, fmt::Display, - ops::{AddAssign, Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, sync::Arc, }; @@ -82,40 +82,29 @@ impl CoverageReport { self.anchors.extend(anchors); } - /// Get coverage summaries by source file path. - pub fn summary_by_file(&self) -> impl Iterator { - let mut summaries = BTreeMap::new(); - - for (version, items) in self.items.iter() { - for item in items { - let Some(path) = - self.source_paths.get(&(version.clone(), item.loc.source_id)).cloned() - else { - continue; - }; - *summaries.entry(path).or_default() += item; - } - } - - summaries.into_iter() + /// Returns an iterator over coverage summaries by source file path. + pub fn summary_by_file(&self) -> impl Iterator { + self.by_file(|summary: &mut CoverageSummary, item| summary.add_item(item)) } - /// Get coverage items by source file path. - pub fn items_by_source(&self) -> impl Iterator)> { - let mut items_by_source: BTreeMap<_, Vec<_>> = BTreeMap::new(); + /// Returns an iterator over coverage items by source file path. + pub fn items_by_file(&self) -> impl Iterator)> { + self.by_file(|list: &mut Vec<_>, item| list.push(item)) + } - for (version, items) in self.items.iter() { + fn by_file<'a, T: Default>( + &'a self, + mut f: impl FnMut(&mut T, &'a CoverageItem), + ) -> impl Iterator { + let mut by_file: BTreeMap<&Path, T> = BTreeMap::new(); + for (version, items) in &self.items { for item in items { - let Some(path) = - self.source_paths.get(&(version.clone(), item.loc.source_id)).cloned() - else { - continue; - }; - items_by_source.entry(path).or_default().push(item.clone()); + let key = (version.clone(), item.loc.source_id); + let Some(path) = self.source_paths.get(&key) else { continue }; + f(by_file.entry(path).or_default(), item); } } - - items_by_source.into_iter() + by_file.into_iter() } /// Processes data from a [`HitMap`] and sets hit counts for coverage items in this coverage @@ -345,30 +334,34 @@ impl Display for CoverageItem { } } +/// A source location. #[derive(Clone, Debug)] pub struct SourceLocation { /// The source ID. pub source_id: usize, /// The contract this source range is in. pub contract_name: Arc, - /// Start byte in the source code. - pub start: u32, - /// Number of bytes in the source code. - pub length: Option, - /// The line in the source code. - pub line: usize, + /// Byte range. + pub bytes: Range, + /// Line range. Indices are 1-based. + pub lines: Range, } impl Display for SourceLocation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "source ID {}, line {}, chars {}-{}", - self.source_id, - self.line, - self.start, - self.length.map_or(self.start, |length| self.start + length) - ) + write!(f, "source ID {}, lines {:?}, bytes {:?}", self.source_id, self.lines, self.bytes) + } +} + +impl SourceLocation { + /// Returns the length of the byte range. + pub fn len(&self) -> u32 { + self.bytes.len() as u32 + } + + /// Returns true if the byte range is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 } } @@ -393,21 +386,43 @@ pub struct CoverageSummary { pub function_hits: usize, } -impl AddAssign<&Self> for CoverageSummary { - fn add_assign(&mut self, other: &Self) { - self.line_count += other.line_count; - self.line_hits += other.line_hits; - self.statement_count += other.statement_count; - self.statement_hits += other.statement_hits; - self.branch_count += other.branch_count; - self.branch_hits += other.branch_hits; - self.function_count += other.function_count; - self.function_hits += other.function_hits; +impl CoverageSummary { + /// Creates a new, empty coverage summary. + pub fn new() -> Self { + Self::default() + } + + /// Creates a coverage summary from a collection of coverage items. + pub fn from_items<'a>(items: impl IntoIterator) -> Self { + let mut summary = Self::default(); + summary.add_items(items); + summary } -} -impl AddAssign<&CoverageItem> for CoverageSummary { - fn add_assign(&mut self, item: &CoverageItem) { + /// Adds another coverage summary to this one. + pub fn merge(&mut self, other: &Self) { + let Self { + line_count, + line_hits, + statement_count, + statement_hits, + branch_count, + branch_hits, + function_count, + function_hits, + } = self; + *line_count += other.line_count; + *line_hits += other.line_hits; + *statement_count += other.statement_count; + *statement_hits += other.statement_hits; + *branch_count += other.branch_count; + *branch_hits += other.branch_hits; + *function_count += other.function_count; + *function_hits += other.function_hits; + } + + /// Adds a coverage item to this summary. + pub fn add_item(&mut self, item: &CoverageItem) { match item.kind { CoverageItemKind::Line => { self.line_count += 1; @@ -435,4 +450,11 @@ impl AddAssign<&CoverageItem> for CoverageSummary { } } } + + /// Adds multiple coverage items to this summary. + pub fn add_items<'a>(&mut self, items: impl IntoIterator) { + for item in items { + self.add_item(item); + } + } } diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 4ca111a56740..10d67d825b30 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -16,13 +16,16 @@ use forge::{ use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED}; use foundry_common::{compile::ProjectCompiler, fs}; use foundry_compilers::{ - artifacts::{sourcemap::SourceMap, CompactBytecode, CompactDeployedBytecode, SolcLanguage}, + artifacts::{ + sourcemap::SourceMap, CompactBytecode, CompactDeployedBytecode, SolcLanguage, Source, + }, Artifact, ArtifactId, Project, ProjectCompileOutput, }; use foundry_config::{Config, SolcReq}; use rayon::prelude::*; use semver::Version; use std::{ + io, path::{Path, PathBuf}, sync::Arc, }; @@ -153,7 +156,7 @@ impl CoverageArgs { let source = SourceFile { ast, - source: fs::read_to_string(&file) + source: Source::read(&file) .wrap_err("Could not read source code for analysis")?, }; versioned_sources @@ -290,19 +293,15 @@ impl CoverageArgs { match report_kind { CoverageReportKind::Summary => SummaryReporter::default().report(&report), CoverageReportKind::Lcov => { - if let Some(report_file) = self.report_file { - return LcovReporter::new(&mut fs::create_file(root.join(report_file))?) - .report(&report) - } else { - return LcovReporter::new(&mut fs::create_file(root.join("lcov.info"))?) - .report(&report) - } + 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) } CoverageReportKind::Bytecode => { let destdir = root.join("bytecode-coverage"); fs::create_dir_all(&destdir)?; - BytecodeReporter::new(root.clone(), destdir).report(&report)?; - Ok(()) + BytecodeReporter::new(root.clone(), destdir).report(&report) } CoverageReportKind::Debug => DebugReporter.report(&report), }?; diff --git a/crates/forge/src/coverage.rs b/crates/forge/src/coverage.rs index de8d0a8aa853..e1236fa2a40d 100644 --- a/crates/forge/src/coverage.rs +++ b/crates/forge/src/coverage.rs @@ -50,7 +50,7 @@ impl SummaryReporter { impl CoverageReporter for SummaryReporter { fn report(mut self, report: &CoverageReport) -> eyre::Result<()> { for (path, summary) in report.summary_by_file() { - self.total += &summary; + self.total.merge(&summary); self.add_row(path.display(), summary); } @@ -77,66 +77,68 @@ fn format_cell(hits: usize, total: usize) -> Cell { cell } +/// Writes the coverage report in [LCOV]'s [tracefile format]. +/// +/// [LCOV]: https://github.com/linux-test-project/lcov +/// [tracefile format]: https://man.archlinux.org/man/geninfo.1.en#TRACEFILE_FORMAT pub struct LcovReporter<'a> { - /// Destination buffer - destination: &'a mut (dyn Write + 'a), + out: &'a mut (dyn Write + 'a), } impl<'a> LcovReporter<'a> { - pub fn new(destination: &'a mut (dyn Write + 'a)) -> Self { - Self { destination } + /// Create a new LCOV reporter. + pub fn new(out: &'a mut (dyn Write + 'a)) -> Self { + Self { out } } } impl CoverageReporter for LcovReporter<'_> { fn report(self, report: &CoverageReport) -> eyre::Result<()> { - for (file, items) in report.items_by_source() { - let summary = items.iter().fold(CoverageSummary::default(), |mut summary, item| { - summary += item; - summary - }); + for (path, items) in report.items_by_file() { + let summary = CoverageSummary::from_items(items.iter().copied()); - writeln!(self.destination, "TN:")?; - writeln!(self.destination, "SF:{}", file.display())?; + writeln!(self.out, "TN:")?; + writeln!(self.out, "SF:{}", path.display())?; for item in items { - let line = item.loc.line; + let line = item.loc.lines.start; + let line_end = item.loc.lines.end - 1; let hits = item.hits; match item.kind { - CoverageItemKind::Function { name } => { + CoverageItemKind::Function { ref name } => { let name = format!("{}.{name}", item.loc.contract_name); - writeln!(self.destination, "FN:{line},{name}")?; - writeln!(self.destination, "FNDA:{hits},{name}")?; + writeln!(self.out, "FN:{line},{line_end},{name}")?; + writeln!(self.out, "FNDA:{hits},{name}")?; } CoverageItemKind::Line => { - writeln!(self.destination, "DA:{line},{hits}")?; + writeln!(self.out, "DA:{line},{hits}")?; } CoverageItemKind::Branch { branch_id, path_id, .. } => { writeln!( - self.destination, + self.out, "BRDA:{line},{branch_id},{path_id},{}", if hits == 0 { "-".to_string() } else { hits.to_string() } )?; } // Statements are not in the LCOV format. // We don't add them in order to avoid doubling line hits. - _ => {} + CoverageItemKind::Statement { .. } => {} } } // Function summary - writeln!(self.destination, "FNF:{}", summary.function_count)?; - writeln!(self.destination, "FNH:{}", summary.function_hits)?; + writeln!(self.out, "FNF:{}", summary.function_count)?; + writeln!(self.out, "FNH:{}", summary.function_hits)?; // Line summary - writeln!(self.destination, "LF:{}", summary.line_count)?; - writeln!(self.destination, "LH:{}", summary.line_hits)?; + writeln!(self.out, "LF:{}", summary.line_count)?; + writeln!(self.out, "LH:{}", summary.line_hits)?; // Branch summary - writeln!(self.destination, "BRF:{}", summary.branch_count)?; - writeln!(self.destination, "BRH:{}", summary.branch_hits)?; + writeln!(self.out, "BRF:{}", summary.branch_count)?; + writeln!(self.out, "BRH:{}", summary.branch_hits)?; - writeln!(self.destination, "end_of_record")?; + writeln!(self.out, "end_of_record")?; } sh_println!("Wrote LCOV report.")?; @@ -150,7 +152,7 @@ pub struct DebugReporter; impl CoverageReporter for DebugReporter { fn report(self, report: &CoverageReport) -> eyre::Result<()> { - for (path, items) in report.items_by_source() { + for (path, items) in report.items_by_file() { sh_println!("Uncovered for {}:", path.display())?; items.iter().for_each(|item| { if item.hits == 0 { diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 65900c592ba5..060a603715f0 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -62,8 +62,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | -| Total | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | +| src/AContract.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) | "#]]); }); @@ -161,8 +161,8 @@ contract BContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/BContract.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | -| Total | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | +| 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) | "#]]); }); @@ -218,8 +218,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| -| src/AContract.sol | 50.00% (1/2) | 50.00% (1/2) | 50.00% (1/2) | 100.00% (1/1) | -| Total | 50.00% (1/2) | 50.00% (1/2) | 50.00% (1/2) | 100.00% (1/1) | +| 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) | "#]]); @@ -232,8 +232,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|--------------|---------------| -| src/AContract.sol | 100.00% (2/2) | 100.00% (2/2) | 50.00% (1/2) | 100.00% (1/1) | -| Total | 100.00% (2/2) | 100.00% (2/2) | 50.00% (1/2) | 100.00% (1/1) | +| 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) | "#]]); @@ -243,8 +243,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (2/2) | 100.00% (1/1) | -| Total | 100.00% (2/2) | 100.00% (2/2) | 100.00% (2/2) | 100.00% (1/1) | +| 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) | "#]], ); @@ -299,8 +299,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|--------------|---------------| -| src/AContract.sol | 100.00% (1/1) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | -| Total | 100.00% (1/1) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | +| src/AContract.sol | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | +| Total | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | "#]]); @@ -313,8 +313,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|--------------|---------------| -| src/AContract.sol | 100.00% (1/1) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | -| Total | 100.00% (1/1) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | +| src/AContract.sol | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | +| Total | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) | "#]]); @@ -324,8 +324,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (1/1) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) | -| Total | 100.00% (1/1) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) | +| 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) | "#]], ); @@ -376,18 +376,21 @@ contract AContractTest is DSTest { // 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(), - str![[r#"TN: + str![[r#" +TN: SF:src/AContract.sol -FN:7,AContract.foo +DA:7,1 +FN:7,9,AContract.foo FNDA:1,AContract.foo DA:8,1 FNF:1 FNH:1 -LF:1 -LH:1 +LF:2 +LH:2 BRF:0 BRH:0 -end[..] +end_of_record + "#]] ); }); @@ -617,8 +620,8 @@ contract FooTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|----------------|----------------|----------------|---------------| -| src/Foo.sol | 88.89% (24/27) | 90.00% (27/30) | 87.50% (14/16) | 100.00% (9/9) | -| Total | 88.89% (24/27) | 90.00% (27/30) | 87.50% (14/16) | 100.00% (9/9) | +| src/Foo.sol | 91.67% (33/36) | 90.00% (27/30) | 87.50% (14/16) | 100.00% (9/9) | +| Total | 91.67% (33/36) | 90.00% (27/30) | 87.50% (14/16) | 100.00% (9/9) | "#]]); @@ -631,8 +634,8 @@ contract FooTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|----------------|----------------|----------------|---------------| -| src/Foo.sol | 96.30% (26/27) | 96.67% (29/30) | 93.75% (15/16) | 100.00% (9/9) | -| Total | 96.30% (26/27) | 96.67% (29/30) | 93.75% (15/16) | 100.00% (9/9) | +| src/Foo.sol | 97.22% (35/36) | 96.67% (29/30) | 93.75% (15/16) | 100.00% (9/9) | +| Total | 97.22% (35/36) | 96.67% (29/30) | 93.75% (15/16) | 100.00% (9/9) | "#]]); @@ -642,8 +645,8 @@ contract FooTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|-----------------|---------------| -| src/Foo.sol | 100.00% (27/27) | 100.00% (30/30) | 100.00% (16/16) | 100.00% (9/9) | -| Total | 100.00% (27/27) | 100.00% (30/30) | 100.00% (16/16) | 100.00% (9/9) | +| 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) | "#]], ); @@ -713,10 +716,10 @@ contract AContractTest is DSTest { // constructor calls are not included). cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#" ... -| File | % Lines | % Statements | % Branches | % Funcs | -|-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (9/9) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (5/5) | -| Total | 100.00% (9/9) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (5/5) | +| File | % Lines | % Statements | % Branches | % Funcs | +|-------------------|-----------------|---------------|---------------|---------------| +| src/AContract.sol | 100.00% (14/14) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (5/5) | +| Total | 100.00% (14/14) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (5/5) | "#]]); }); @@ -815,8 +818,8 @@ contract FooTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|----------------|----------------|--------------|---------------| -| src/Foo.sol | 66.67% (10/15) | 66.67% (14/21) | 83.33% (5/6) | 100.00% (5/5) | -| Total | 66.67% (10/15) | 66.67% (14/21) | 83.33% (5/6) | 100.00% (5/5) | +| 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) | "#]], ); @@ -827,8 +830,8 @@ contract FooTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|---------------|---------------| -| src/Foo.sol | 100.00% (15/15) | 100.00% (21/21) | 100.00% (6/6) | 100.00% (5/5) | -| Total | 100.00% (15/15) | 100.00% (21/21) | 100.00% (6/6) | 100.00% (5/5) | +| 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) | "#]], ); @@ -932,8 +935,8 @@ contract FooTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------|-----------------|-----------------|---------------|---------------| -| src/Foo.sol | 100.00% (23/23) | 100.00% (40/40) | 100.00% (1/1) | 100.00% (7/7) | -| Total | 100.00% (23/23) | 100.00% (40/40) | 100.00% (1/1) | 100.00% (7/7) | +| 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) | "#]], ); @@ -1022,10 +1025,10 @@ contract FooTest is DSTest { cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq( str![[r#" ... -| File | % Lines | % Statements | % Branches | % Funcs | -|-------------|---------------|---------------|---------------|---------------| -| src/Foo.sol | 100.00% (8/8) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (4/4) | -| Total | 100.00% (8/8) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (4/4) | +| 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) | "#]], ); @@ -1082,8 +1085,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| -| src/AContract.sol | 50.00% (2/4) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | -| Total | 50.00% (2/4) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | +| src/AContract.sol | 60.00% (3/5) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | +| Total | 60.00% (3/5) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | "#]]); @@ -1096,8 +1099,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| -| src/AContract.sol | 50.00% (2/4) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | -| Total | 50.00% (2/4) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | +| src/AContract.sol | 60.00% (3/5) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | +| Total | 60.00% (3/5) | 50.00% (2/4) | 50.00% (2/4) | 100.00% (1/1) | "#]]); @@ -1107,8 +1110,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (4/4) | 100.00% (4/4) | 100.00% (4/4) | 100.00% (1/1) | -| Total | 100.00% (4/4) | 100.00% (4/4) | 100.00% (4/4) | 100.00% (1/1) | +| 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) | "#]], ); @@ -1171,8 +1174,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| -| src/AContract.sol | 75.00% (3/4) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | -| Total | 75.00% (3/4) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | +| src/AContract.sol | 80.00% (4/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | +| Total | 80.00% (4/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | "#]]); @@ -1185,8 +1188,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|--------------|--------------|--------------|---------------| -| src/AContract.sol | 50.00% (2/4) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | -| Total | 50.00% (2/4) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | +| src/AContract.sol | 60.00% (3/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | +| Total | 60.00% (3/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) | "#]]); @@ -1196,8 +1199,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (4/4) | 100.00% (5/5) | 100.00% (2/2) | 100.00% (1/1) | -| Total | 100.00% (4/4) | 100.00% (5/5) | 100.00% (2/2) | 100.00% (1/1) | +| 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) | "#]], ); @@ -1264,10 +1267,10 @@ contract AContractTest is DSTest { cmd.arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(str![[r#" ... -| File | % Lines | % Statements | % Branches | % Funcs | -|-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (9/9) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (3/3) | -| Total | 100.00% (9/9) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (3/3) | +| File | % Lines | % Statements | % Branches | % Funcs | +|-------------------|-----------------|---------------|---------------|---------------| +| src/AContract.sol | 100.00% (12/12) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (3/3) | +| Total | 100.00% (12/12) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (3/3) | "#]]); }); @@ -1316,8 +1319,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | -| Total | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | +| src/AContract.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) | "#]]); }); @@ -1359,8 +1362,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (0/0) | 100.00% (0/0) | 100.00% (0/0) | 100.00% (1/1) | -| Total | 100.00% (0/0) | 100.00% (0/0) | 100.00% (0/0) | 100.00% (1/1) | +| src/AContract.sol | 100.00% (1/1) | 100.00% (0/0) | 100.00% (0/0) | 100.00% (1/1) | +| Total | 100.00% (1/1) | 100.00% (0/0) | 100.00% (0/0) | 100.00% (1/1) | "#]]); }); @@ -1408,8 +1411,8 @@ contract AContractTest is DSTest { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|---------------|---------------|---------------|---------------| -| src/AContract.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | -| Total | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | +| src/AContract.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) | "#]]); }); @@ -1442,8 +1445,8 @@ contract AContract { ... | File | % Lines | % Statements | % Branches | % Funcs | |-------------------|-------------|--------------|---------------|-------------| -| src/AContract.sol | 0.00% (0/4) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/1) | -| Total | 0.00% (0/4) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/1) | +| src/AContract.sol | 0.00% (0/5) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/1) | +| Total | 0.00% (0/5) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/1) | "#]]); });