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: forge coverage #1576

Merged
merged 36 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
62b50c3
feat(coverage): fn and statement detection
onbjerg Apr 28, 2022
d0f6389
fix: account for nested branches
onbjerg May 8, 2022
d56baa9
refactor: move stuff to new modules
onbjerg May 12, 2022
6862258
feat: support some yul nodes
onbjerg May 12, 2022
f27f3de
feat: add branch path id
onbjerg May 12, 2022
fe48e2e
feat: more detailed source locations for items
onbjerg May 12, 2022
aa23360
feat: map addresses to artifact ids
onbjerg Jun 3, 2022
6235a0c
feat: sort of hook up hit map
onbjerg Jun 3, 2022
4791b2e
fix: hit map collection
onbjerg Jun 3, 2022
caa4d2c
fix: cap summary reporter percentage decimals
onbjerg Jun 3, 2022
52641e4
refactor: use new source map getter
onbjerg Jun 5, 2022
b62ba81
refactor: clean up a bit
onbjerg Jun 5, 2022
ccf5b02
feat: coverage for both runtime and creation
onbjerg Jun 5, 2022
39f6aed
feat: debug reporter
onbjerg Jun 5, 2022
a17b673
feat: naively filter out tests and deps
onbjerg Jun 5, 2022
6e32d36
chore: note to self
onbjerg Jun 5, 2022
d20e050
feat: detect executable lines
onbjerg Jun 14, 2022
70bf2c4
feat: improve reporters a bit
onbjerg Jun 14, 2022
a6faf22
fix: don't add exec lines for function defs
onbjerg Jun 14, 2022
6cacea9
fix: better coverage detection using anchors
onbjerg Jun 14, 2022
c654db6
fix: more robust anchor selection
onbjerg Jun 14, 2022
6c1182e
chore: clippy
onbjerg Jun 14, 2022
b807c96
chore: revert Cargo.toml
onbjerg Jun 14, 2022
5b4e10b
chore: remove unnecessary fn
onbjerg Jun 20, 2022
bfcf841
feat: support `--silent`
onbjerg Jun 22, 2022
0875d88
refactor: use `FoundryPathExt::is_sol_test`
onbjerg Jun 22, 2022
f559f10
refactor: unnecessary casting
onbjerg Jun 22, 2022
5e98405
refactor: use `impl Into<PathBuf>`
onbjerg Jun 22, 2022
137bcc5
refactor: move assumptions to fn docs
onbjerg Jun 22, 2022
782ce82
refactor: more ergonomic `ExecutorBuilder`
onbjerg Jun 22, 2022
ba42e72
refactor: `with_coverage` -> `set_coverage`
onbjerg Jun 22, 2022
3cc28d1
refactor: simplify coverage reporters
onbjerg Jun 22, 2022
6dc7d7a
refactor: impl `AddAssign` for `CoverageSummary`
onbjerg Jun 22, 2022
49757dc
refactor: lcov reporter dest
onbjerg Jun 22, 2022
ea41b29
refactor: use wrapped fs
onbjerg Jun 22, 2022
5453969
chore: nits
onbjerg Jun 22, 2022
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ debug = 0
#ethers-providers = { path = "../ethers-rs/ethers-providers" }
#ethers-signers = { path = "../ethers-rs/ethers-signers" }
#ethers-etherscan = { path = "../ethers-rs/ethers-etherscan" }
#ethers-solc = { path = "../ethers-rs/ethers-solc" }
#ethers-solc = { path = "../ethers-rs/ethers-solc" }
268 changes: 268 additions & 0 deletions cli/src/cmd/forge/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
//! Coverage command
use crate::{
cmd::{
forge::{build::CoreBuildArgs, test::Filter},
Cmd,
},
compile::ProjectCompiler,
utils::{self, p_println, FoundryPathExt},
};
use cast::trace::identifier::TraceIdentifier;
use clap::{AppSettings, ArgEnum, Parser};
use ethers::{
prelude::{Artifact, Project, ProjectCompileOutput},
solc::{artifacts::contract::CompactContractBytecode, sourcemap::SourceMap, ArtifactId},
};
use forge::{
coverage::{
CoverageMap, CoverageReporter, DebugReporter, LcovReporter, SummaryReporter, Visitor,
},
executor::opts::EvmOpts,
result::SuiteResult,
trace::identifier::LocalTraceIdentifier,
MultiContractRunnerBuilder,
};
use foundry_common::{evm::EvmArgs, fs};
use foundry_config::{figment::Figment, Config};
use std::{collections::HashMap, path::PathBuf, sync::mpsc::channel, thread};

// Loads project's figment and merges the build cli arguments into it
foundry_config::impl_figment_convert!(CoverageArgs, opts, evm_opts);

/// Generate coverage reports for your tests.
#[derive(Debug, Clone, Parser)]
#[clap(global_setting = AppSettings::DeriveDisplayOrder)]
pub struct CoverageArgs {
#[clap(
long,
arg_enum,
default_value = "summary",
help = "The report type to use for coverage."
)]
report: CoverageReportKind,

#[clap(flatten, next_help_heading = "TEST FILTERING")]
filter: Filter,

#[clap(flatten, next_help_heading = "EVM OPTIONS")]
evm_opts: EvmArgs,

#[clap(flatten, next_help_heading = "BUILD OPTIONS")]
opts: CoreBuildArgs,
}

impl CoverageArgs {
/// Returns the flattened [`CoreBuildArgs`]
pub fn build_args(&self) -> &CoreBuildArgs {
&self.opts
}

/// Returns the currently configured [Config] and the extracted [EvmOpts] from that config
pub fn config_and_evm_opts(&self) -> eyre::Result<(Config, EvmOpts)> {
// Merge all configs
let figment: Figment = self.into();
let evm_opts = figment.extract()?;
let config = Config::from_provider(figment).sanitized();

Ok((config, evm_opts))
}
}

impl Cmd for CoverageArgs {
type Output = ();

fn run(self) -> eyre::Result<Self::Output> {
let (config, evm_opts) = self.configure()?;
let (project, output) = self.build(&config)?;
p_println!(!self.opts.silent => "Analysing contracts...");
let (map, source_maps) = self.prepare(output.clone())?;

p_println!(!self.opts.silent => "Running tests...");
self.collect(project, output, source_maps, map, config, evm_opts)
}
}

/// A map, keyed by artifact ID, to a tuple of the deployment source map and the runtime source map.
type SourceMaps = HashMap<ArtifactId, (SourceMap, SourceMap)>;

// The main flow of the command itself
impl CoverageArgs {
/// Collects and adjusts configuration.
fn configure(&self) -> eyre::Result<(Config, EvmOpts)> {
// Merge all configs
let (config, mut evm_opts) = self.config_and_evm_opts()?;

// We always want traces
evm_opts.verbosity = 3;

Ok((config, evm_opts))
}

/// Builds the project.
fn build(&self, config: &Config) -> eyre::Result<(Project, ProjectCompileOutput)> {
// Set up the project
let project = {
let mut project = config.ephemeral_no_artifacts_project()?;

// Disable the optimizer for more accurate source maps
project.solc_config.settings.optimizer.disable();

project
};

let output = ProjectCompiler::default()
.compile(&project)?
.with_stripped_file_prefixes(project.root());

Ok((project, output))
}

/// Builds the coverage map.
fn prepare(&self, output: ProjectCompileOutput) -> eyre::Result<(CoverageMap, SourceMaps)> {
// Get sources and source maps
let (artifacts, sources) = output.into_artifacts_with_sources();

let source_maps: SourceMaps = artifacts
.into_iter()
.map(|(id, artifact)| (id, CompactContractBytecode::from(artifact)))
.filter_map(|(id, artifact): (ArtifactId, CompactContractBytecode)| {
Some((
id,
(
artifact.get_source_map()?.ok()?,
artifact
.get_deployed_bytecode()
.as_ref()?
.bytecode
.as_ref()?
.source_map()?
.ok()?,
),
))
})
.collect();

let mut map = CoverageMap::default();
for (path, versioned_sources) in sources.0.into_iter() {
// TODO: Make these checks robust
// NOTE: We should actually filter out test contracts in the AST
// instead of on a source file level. Repositories like Solmate
// have a lot of abstract contracts that are being tested, and these
// are usually defined in the test files themselves.
let is_test = path.is_sol_test();
let is_dependency = path.starts_with("lib");
if is_test || is_dependency {
continue
}

for mut versioned_source in versioned_sources {
let source = &mut versioned_source.source_file;
if let Some(ast) = source.ast.take() {
let source_maps: HashMap<String, SourceMap> = source_maps
.iter()
.filter(|(id, _)| {
id.version == versioned_source.version &&
id.source == PathBuf::from(&path)
})
.map(|(id, (_, source_map))| {
// TODO: Deploy source map too?
(id.name.clone(), source_map.clone())
})
.collect();

let items = Visitor::new(source.id, fs::read_to_string(&path)?, source_maps)
.visit_ast(ast)?;

if items.is_empty() {
continue
}

map.add_source(path.clone(), versioned_source, items);
}
}
}

Ok((map, source_maps))
}

/// Runs tests, collects coverage data and generates the final report.
fn collect(
self,
project: Project,
output: ProjectCompileOutput,
source_maps: SourceMaps,
mut map: CoverageMap,
config: Config,
evm_opts: EvmOpts,
) -> eyre::Result<()> {
// Setup the fuzzer
// TODO: Add CLI Options to modify the persistence
let cfg = proptest::test_runner::Config {
failure_persistence: None,
cases: config.fuzz_runs,
max_local_rejects: config.fuzz_max_local_rejects,
max_global_rejects: config.fuzz_max_global_rejects,
..Default::default()
};
let fuzzer = proptest::test_runner::TestRunner::new(cfg);
let root = project.paths.root;

// Build the contract runner
let evm_spec = utils::evm_spec(&config.evm_version);
let mut runner = MultiContractRunnerBuilder::default()
.fuzzer(fuzzer)
.initial_balance(evm_opts.initial_balance)
.evm_spec(evm_spec)
.sender(evm_opts.sender)
.with_fork(utils::get_fork(&evm_opts, &config.rpc_storage_caching))
.set_coverage(true)
.build(root.clone(), output, evm_opts)?;
let (tx, rx) = channel::<(String, SuiteResult)>();

// Set up identifier
let local_identifier = LocalTraceIdentifier::new(&runner.known_contracts);

// TODO: Coverage for fuzz tests
let handle = thread::spawn(move || runner.test(&self.filter, Some(tx), false).unwrap());
for mut result in rx.into_iter().flat_map(|(_, suite)| suite.test_results.into_values()) {
if let Some(hit_map) = result.coverage.take() {
for (_, trace) in &mut result.traces {
local_identifier
.identify_addresses(trace.addresses().into_iter().collect())
.into_iter()
.filter_map(|identity| {
let artifact_id = identity.artifact_id?;
let source_map = source_maps.get(&artifact_id)?;

Some((artifact_id, source_map, hit_map.get(&identity.address)?))
})
.for_each(|(id, source_map, hits)| {
// TODO: Distinguish between creation/runtime in a smart way
map.add_hit_map(id.version.clone(), &source_map.0, hits.clone());
map.add_hit_map(id.version, &source_map.1, hits.clone())
});
}
}
}

// Reattach the thread
let _ = handle.join();
mattsse marked this conversation as resolved.
Show resolved Hide resolved

match self.report {
CoverageReportKind::Summary => SummaryReporter::default().report(map),
// TODO: Sensible place to put the LCOV file
CoverageReportKind::Lcov => {
LcovReporter::new(&mut fs::create_file(root.join("lcov.info"))?).report(map)
}
CoverageReportKind::Debug => DebugReporter::default().report(map),
}
}
}

// TODO: HTML
#[derive(Debug, Clone, ArgEnum)]
pub enum CoverageReportKind {
Summary,
Lcov,
Debug,
}
1 change: 1 addition & 0 deletions cli/src/cmd/forge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub mod bind;
pub mod build;
pub mod cache;
pub mod config;
pub mod coverage;
pub mod create;
pub mod debug;
pub mod flatten;
Expand Down
17 changes: 6 additions & 11 deletions cli/src/cmd/forge/script/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,15 @@ impl ScriptArgs {
)
.await;

let mut builder = ExecutorBuilder::default()
let executor = ExecutorBuilder::default()
.with_cheatcodes(CheatsConfig::new(&script_config.config, &script_config.evm_opts))
.with_config(env)
.with_spec(crate::utils::evm_spec(&script_config.config.evm_version))
.with_gas_limit(script_config.evm_opts.gas_limit());
.with_gas_limit(script_config.evm_opts.gas_limit())
.set_tracing(script_config.evm_opts.verbosity >= 3 || self.debug)
.set_debugger(self.debug)
.build(db);

if script_config.evm_opts.verbosity >= 3 {
builder = builder.with_tracing();
}

if self.debug {
builder = builder.with_tracing().with_debugger();
}

ScriptRunner::new(builder.build(db), script_config.evm_opts.initial_balance, sender)
ScriptRunner::new(executor, script_config.evm_opts.initial_balance, sender)
}
}
3 changes: 3 additions & 0 deletions cli/src/forge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ fn main() -> eyre::Result<()> {
Subcommands::Script(cmd) => {
utils::block_on(cmd.run_script())?;
}
Subcommands::Coverage(cmd) => {
cmd.run()?;
}
Subcommands::Bind(cmd) => {
cmd.run()?;
}
Expand Down
7 changes: 5 additions & 2 deletions cli/src/opts/forge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::cmd::forge::{
bind::BindArgs,
build::BuildArgs,
cache::CacheArgs,
config,
config, coverage,
create::CreateArgs,
debug::DebugArgs,
flatten,
Expand Down Expand Up @@ -59,7 +59,10 @@ pub enum Subcommands {
#[clap(visible_alias = "s")]
Script(ScriptArgs),

#[clap(visible_alias = "bi")]
#[clap(about = "Generate coverage reports.")]
Coverage(coverage::CoverageArgs),

#[clap(alias = "bi")]
#[clap(about = "Generate Rust bindings for smart contracts.")]
Bind(BindArgs),

Expand Down
3 changes: 3 additions & 0 deletions evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@ proptest = "1.0.0"
yansi = "0.5.1"
url = "2.2.2"

# Coverage
semver = "1.0.5"

[dev-dependencies]
tempfile = "3.3.0"
Loading