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(coverage): separate dir + caching for coverage artifacts #9366

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions crates/evm/coverage/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use alloy_primitives::map::HashMap;
use foundry_common::TestFunctionExt;
use foundry_compilers::artifacts::ast::{self, Ast, Node, NodeType};
use rayon::prelude::*;
use std::sync::Arc;
use std::{borrow::Cow, sync::Arc};

/// A visitor that walks the AST of a single contract and finds coverage items.
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -592,5 +592,5 @@ pub struct SourceFile<'a> {
/// The source code.
pub source: String,
/// The AST of the source code.
pub ast: &'a Ast,
pub ast: Cow<'a, Ast>,
}
101 changes: 82 additions & 19 deletions crates/forge/bin/cmd/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use foundry_config::{Config, SolcReq};
use rayon::prelude::*;
use semver::Version;
use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
};
Expand Down Expand Up @@ -92,7 +93,28 @@ impl CoverageArgs {
/// Builds the project.
fn build(&self, config: &Config) -> Result<(Project, ProjectCompileOutput)> {
// Set up the project
let mut project = config.create_project(false, false)?;
let mut project = config.create_project(config.cache, false)?;

// Set a different artifacts path for coverage. `out/coverage`.
// This is done to avoid overwriting the artifacts of the main build that maybe built with
// different optimizer settings or --via-ir. Optimizer settings are disabled for
// coverage builds.
let coverage_artifacts_path = project.artifacts_path().join("coverage");
project.paths.artifacts = coverage_artifacts_path.clone();
project.paths.build_infos = coverage_artifacts_path.join("build-info");
yash-atreya marked this conversation as resolved.
Show resolved Hide resolved

// Set a different compiler cache path for coverage. `cache/coverage`.
let cache_file = project
.paths
.cache
.components()
.last()
.ok_or_else(|| eyre::eyre!("Cache path is empty"))?;

let cache_dir =
project.paths.cache.parent().ok_or_else(|| eyre::eyre!("Cache path is empty"))?;
project.paths.cache = cache_dir.join("coverage").join(cache_file);

if self.ir_minimum {
// print warning message
sh_warn!("{}", concat!(
Expand Down Expand Up @@ -124,6 +146,8 @@ impl CoverageArgs {
project.settings.solc.via_ir = None;
}

sh_warn!("optimizer settings have been disabled for accurate coverage reports")?;

let output = ProjectCompiler::default()
.compile(&project)?
.with_stripped_file_prefixes(project.root());
Expand All @@ -139,28 +163,67 @@ impl CoverageArgs {
// Collect source files.
let project_paths = &project.paths;
let mut versioned_sources = HashMap::<Version, SourceFiles<'_>>::default();
for (path, source_file, version) in output.output().sources.sources_with_version() {
report.add_source(version.clone(), source_file.id as usize, path.clone());

// Filter out dependencies
if !self.include_libs && project_paths.has_library_ancestor(path) {
continue;
}
if !output.output().sources.is_empty() {
// Freshly compiled sources
for (path, source_file, version) in output.output().sources.sources_with_version() {
report.add_source(version.clone(), source_file.id as usize, path.clone());

if let Some(ast) = &source_file.ast {
let file = project_paths.root.join(path);
trace!(root=?project_paths.root, ?file, "reading source file");
// Filter out dependencies
if !self.include_libs && project_paths.has_library_ancestor(path) {
continue;
}

if let Some(ast) = &source_file.ast {
let file = project_paths.root.join(path);
trace!(root=?project_paths.root, ?file, "reading source file");

let source = SourceFile {
ast: Cow::Borrowed(ast),
source: fs::read_to_string(&file)
.wrap_err("Could not read source code for analysis")?,
};
versioned_sources
.entry(version.clone())
.or_default()
.sources
.insert(source_file.id as usize, source);
}
}
} else {
// Cached sources
for (id, artifact) in output.artifact_ids() {
// Filter out dependencies
if !self.include_libs && project_paths.has_library_ancestor(&id.source) {
continue;
}

let source = SourceFile {
ast,
source: fs::read_to_string(&file)
.wrap_err("Could not read source code for analysis")?,
let version = id.version;
let source_file = if let Some(source_file) = artifact.source_file() {
source_file
} else {
sh_warn!("ast source file not found for {}", id.source.display())?;
continue;
};
versioned_sources
.entry(version.clone())
.or_default()
.sources
.insert(source_file.id as usize, source);

report.add_source(version.clone(), source_file.id as usize, id.source.clone());

if let Some(ast) = source_file.ast {
let file = project_paths.root.join(id.source);
trace!(root=?project_paths.root, ?file, "reading source file");

let source = SourceFile {
ast: Cow::Owned(ast),
source: fs::read_to_string(&file)
.wrap_err("Could not read source code for analysis")?,
};

versioned_sources
.entry(version.clone())
.or_default()
.sources
.insert(source_file.id as usize, source);
}
}
}

Expand Down
77 changes: 77 additions & 0 deletions crates/forge/tests/cli/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1447,3 +1447,80 @@ contract AContract {

"#]]);
});

forgetest!(test_diff_coverage_dir, |prj, cmd| {
prj.insert_ds_test();
prj.add_source(
"AContract.sol",
r#"
contract AContract {
int public i;

function init() public {
i = 0;
}

function foo() public {
i = 1;
}
}
"#,
)
.unwrap();

prj.add_source(
"AContractTest.sol",
r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
AContract a;

function setUp() public {
a = new AContract();
a.init();
}

function testFoo() public {
a.foo();
}
}
"#,
)
.unwrap();

// forge build
cmd.arg("build").assert_success();

// forge coverage
// Assert 100% coverage (init function coverage called in setUp is accounted).
cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(
str![[r#"
...
| 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) |

"#]],
);

// forge build - Should not compile the contracts again.
cmd.forge_fuse().arg("build").assert_success().stdout_eq(
r#"No files changed, compilation skipped
"#,
);

// forge coverage - Should not compile the contracts again.
cmd.forge_fuse().arg("coverage").args(["--summary".to_string()]).assert_success().stdout_eq(
str![[r#"No files changed, compilation skipped
...
| 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) |

"#]],
);
});
Loading