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

feat: add option to ignore directories from coverage report #7301

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/common/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ pub trait TestFilter: Send + Sync {
fn matches_path(&self, path: &Path) -> bool;
}

/// Coverage filter.
pub trait CoverageFilter: Send + Sync {
/// Returns whether the file path should be included in coverage report.
fn matches_file_path(&self, path: &Path) -> bool;
}

/// Extension trait for `Function`.
pub trait TestFunctionExt {
/// Returns whether this function should be executed as invariant test.
Expand Down
1 change: 1 addition & 0 deletions crates/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ match_contract = "Foo"
no_match_contract = "Bar"
match_path = "*/Foo*"
no_match_path = "*/Bar*"
no_coverage_path = "*/Baz*"
ffi = false
always_use_create_2_factory = false
# These are the default callers, generated using `address(uint160(uint256(keccak256("foundry default caller"))))`
Expand Down
4 changes: 4 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ pub struct Config {
/// Only run tests in source files that do not match the specified glob pattern.
#[serde(rename = "no_match_path", with = "from_opt_glob")]
pub path_pattern_inverse: Option<globset::Glob>,
/// Only show coverage for files that do not match the specified glob pattern.
#[serde(rename = "no_coverage_path", with = "from_opt_glob")]
pub path_pattern_ignore_coverage: Option<globset::Glob>,
/// Configuration for fuzz testing
pub fuzz: FuzzConfig,
/// Configuration for invariant testing
Expand Down Expand Up @@ -1870,6 +1873,7 @@ impl Default for Config {
contract_pattern_inverse: None,
path_pattern: None,
path_pattern_inverse: None,
path_pattern_ignore_coverage: None,
fuzz: Default::default(),
invariant: Default::default(),
always_use_create_2_factory: false,
Expand Down
31 changes: 29 additions & 2 deletions crates/evm/coverage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ use std::{
fmt::Display,
ops::{AddAssign, Deref, DerefMut},
};
use std::path::Path;

use eyre::{Context, Result};
use foundry_common::CoverageFilter;

pub mod analysis;
pub mod anchors;

mod inspector;

pub use inspector::CoverageCollector;

/// A coverage report.
Expand Down Expand Up @@ -75,7 +78,7 @@ impl CoverageReport {
}

/// Get coverage summaries by source file path
pub fn summary_by_file(&self) -> impl Iterator<Item = (String, CoverageSummary)> {
pub fn summary_by_file(&self) -> impl Iterator<Item=(String, CoverageSummary)> {
let mut summaries: BTreeMap<String, CoverageSummary> = BTreeMap::new();

for (version, items) in self.items.iter() {
Expand All @@ -98,7 +101,7 @@ impl CoverageReport {
}

/// Get coverage items by source file path
pub fn items_by_source(&self) -> impl Iterator<Item = (String, Vec<CoverageItem>)> {
pub fn items_by_source(&self) -> impl Iterator<Item=(String, Vec<CoverageItem>)> {
let mut items_by_source: BTreeMap<String, Vec<CoverageItem>> = BTreeMap::new();

for (version, items) in self.items.iter() {
Expand Down Expand Up @@ -151,6 +154,30 @@ impl CoverageReport {
}
Ok(())
}

pub fn filter_out_ignored_sources(&mut self, filter: &impl CoverageFilter) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: docs

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

let mut new_items = HashMap::new();
for (version, items) in self.items.iter() {
let new_items_for_version = items
.iter()
.filter(|item| filter.matches_file_path(
Path::new(&self.get_source_path(version, item.loc.source_id))
))
.cloned()
.collect();
new_items.insert(version.clone(), new_items_for_version);
}
self.items = new_items;
}

fn get_source_path(&self, version: &Version, source_id: usize) -> String {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs here too

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

self.source_paths
.get(&(version.clone(), source_id))
.cloned()
.unwrap_or_else(|| {
format!("Unknown (ID: {}, solc: {version})", source_id)
})
}
}

/// A collection of [HitMap]s
Expand Down
34 changes: 20 additions & 14 deletions crates/forge/bin/cmd/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ use foundry_compilers::{
use foundry_config::{Config, SolcReq};
use semver::Version;
use std::{collections::HashMap, path::PathBuf, sync::mpsc::channel};
use std::ops::Deref;
use std::sync::Arc;
use yansi::Paint;

/// A map, keyed by contract ID, to a tuple of the deployment source map and the runtime source map.
Expand Down Expand Up @@ -56,10 +58,10 @@ pub struct CoverageArgs {
///
/// If not specified, the report will be stored in the root of the project.
#[arg(
long,
short,
value_hint = ValueHint::FilePath,
value_name = "PATH"
long,
short,
value_hint = ValueHint::FilePath,
value_name = "PATH"
)]
report_file: Option<PathBuf>,

Expand Down Expand Up @@ -117,11 +119,11 @@ impl CoverageArgs {

// print warning message
let msg = Paint::yellow(concat!(
"Warning! \"--ir-minimum\" flag enables viaIR with minimum optimization, \
"Warning! \"--ir-minimum\" flag enables viaIR with minimum optimization, \
which can result in inaccurate source mappings.\n",
"Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n",
"Note that \"viaIR\" is only available in Solidity 0.8.13 and above.\n",
"See more: https://github.com/foundry-rs/foundry/issues/3357",
"Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n",
"Note that \"viaIR\" is only available in Solidity 0.8.13 and above.\n",
"See more: https://github.com/foundry-rs/foundry/issues/3357",
));
p_println!(!self.opts.silent => "{msg}");

Expand Down Expand Up @@ -162,7 +164,7 @@ impl CoverageArgs {

// Filter out dependencies
if project_paths.has_library_ancestor(std::path::Path::new(&path)) {
continue
continue;
}

if let Some(ast) = source_file.ast.take() {
Expand Down Expand Up @@ -255,7 +257,7 @@ impl CoverageArgs {
)
})?,
)?
.analyze()?;
.analyze()?;
let anchors: HashMap<ContractId, Vec<ItemAnchor>> = source_analysis
.contract_items
.iter()
Expand Down Expand Up @@ -311,9 +313,10 @@ impl CoverageArgs {

// Run tests
let known_contracts = runner.known_contracts.clone();
let filter = self.filter;
let filter = Arc::new(self.filter.clone().merge_with_config(&config).args().to_owned());
let filter_ref_clone = filter.clone();
let (tx, rx) = channel::<(String, SuiteResult)>();
let handle = tokio::task::spawn_blocking(move || runner.test(&filter, tx));
let handle = tokio::task::spawn_blocking(move || runner.test(filter_ref_clone.deref(), tx));

// Add hit data to the coverage report
let data = rx
Expand Down Expand Up @@ -352,17 +355,20 @@ impl CoverageArgs {
}
}

// Filter out the ignored files from the report
report.filter_out_ignored_sources(filter.deref());

// Output final report
for report_kind in self.report {
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)
.report(&report);
} else {
return LcovReporter::new(&mut fs::create_file(root.join("lcov.info"))?)
.report(&report)
.report(&report);
}
}
CoverageReportKind::Bytecode => {
Expand Down
26 changes: 24 additions & 2 deletions crates/forge/bin/cmd/test/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use foundry_common::glob::GlobMatcher;
use foundry_compilers::{FileFilter, ProjectPathsConfig};
use foundry_config::Config;
use std::{fmt, path::Path};
use foundry_common::CoverageFilter;

/// The filter to use during testing.
///
Expand Down Expand Up @@ -40,6 +41,10 @@ pub struct FilterArgs {
value_name = "GLOB"
)]
pub path_pattern_inverse: Option<GlobMatcher>,

/// Only show coverage for files that do not match the specified glob pattern.
#[arg(long = "no-coverage-path", visible_alias = "ncp", value_name = "GLOB")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's name it ignore instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed

pub path_pattern_ignore_coverage: Option<GlobMatcher>,
}

impl FilterArgs {
Expand Down Expand Up @@ -73,6 +78,9 @@ impl FilterArgs {
if self.path_pattern_inverse.is_none() {
self.path_pattern_inverse = config.path_pattern_inverse.clone().map(Into::into);
}
if self.path_pattern_ignore_coverage.is_none() {
self.path_pattern_ignore_coverage = config.path_pattern_ignore_coverage.clone().map(Into::into);
}
ProjectPathsAwareFilter { args_filter: self, paths: config.project_paths() }
}
}
Expand All @@ -86,6 +94,7 @@ impl fmt::Debug for FilterArgs {
.field("no-match-contract", &self.contract_pattern_inverse.as_ref().map(|r| r.as_str()))
.field("match-path", &self.path_pattern.as_ref().map(|g| g.as_str()))
.field("no-match-path", &self.path_pattern_inverse.as_ref().map(|g| g.as_str()))
.field("no-coverage-path", &self.path_pattern_ignore_coverage.as_ref().map(|g| g.as_str()))
.finish_non_exhaustive()
}
}
Expand All @@ -97,10 +106,10 @@ impl FileFilter for FilterArgs {
/// [`FoundryPathExt::is_sol_test()`].
fn is_match(&self, file: &Path) -> bool {
if let Some(glob) = &self.path_pattern {
return glob.is_match(file)
return glob.is_match(file);
}
if let Some(glob) = &self.path_pattern_inverse {
return !glob.is_match(file)
return !glob.is_match(file);
}
file.is_sol_test()
}
Expand Down Expand Up @@ -141,6 +150,16 @@ impl TestFilter for FilterArgs {
}
}

impl CoverageFilter for FilterArgs {
/// Returns true if the file path does not match the ignore coverage pattern.
fn matches_file_path(&self, path: &Path) -> bool {
if let Some(glob) = &self.path_pattern_ignore_coverage {
return !glob.is_match(path);
}
true
}
}

impl fmt::Display for FilterArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(p) = &self.test_pattern {
Expand All @@ -161,6 +180,9 @@ impl fmt::Display for FilterArgs {
if let Some(p) = &self.path_pattern_inverse {
writeln!(f, "\tno-match-path: `{}`", p.as_str())?;
}
if let Some(p) = &self.path_pattern_ignore_coverage {
writeln!(f, "\tno-coverage-path: `{}`", p.as_str())?;
}
Ok(())
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/forge/tests/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ forgetest!(can_extract_config_values, |prj, cmd| {
contract_pattern_inverse: None,
path_pattern: None,
path_pattern_inverse: None,
path_pattern_ignore_coverage: None,
fuzz: FuzzConfig {
runs: 1000,
max_test_rejects: 100203,
Expand Down
2 changes: 2 additions & 0 deletions crates/forge/tests/cli/test_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ forgetest!(can_set_filter_values, |prj, cmd| {
contract_pattern_inverse: None,
path_pattern: Some(glob.clone()),
path_pattern_inverse: None,
path_pattern_ignore_coverage: None,
..Default::default()
};
prj.write_config(config);
Expand All @@ -29,6 +30,7 @@ forgetest!(can_set_filter_values, |prj, cmd| {
assert_eq!(config.contract_pattern_inverse, None);
assert_eq!(config.path_pattern.unwrap(), glob);
assert_eq!(config.path_pattern_inverse, None);
assert_eq!(config.path_pattern_ignore_coverage, None);
});

// tests that warning is displayed when there are no tests in project
Expand Down