Skip to content

Commit

Permalink
refactor: remove foundry_config::Config dep from `MultiContractRunn…
Browse files Browse the repository at this point in the history
…er` (#530)
  • Loading branch information
agostbiro authored Jul 10, 2024
1 parent 736a018 commit e1eeb28
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 134 deletions.
1 change: 1 addition & 0 deletions crates/edr_napi/src/solidity_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod config;
mod runner;
mod test_results;
mod test_suite;
Expand Down
99 changes: 99 additions & 0 deletions crates/edr_napi/src/solidity_tests/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::{collections::HashMap, path::PathBuf};

use alloy_primitives::{address, Address};
use edr_eth::U256;
use forge::{
inspectors::cheatcodes::CheatsConfigOptions,
opts::{Env as EvmEnv, EvmOpts},
};
use foundry_compilers::ProjectPathsConfig;
use foundry_config::{
cache::StorageCachingConfig, fs_permissions::PathPermission, FsPermissions, FuzzConfig,
GasLimit, InvariantConfig, RpcEndpoint, RpcEndpoints,
};

/// Solidity tests configuration
#[derive(Clone, Debug)]
pub(super) struct SolidityTestsConfig {
/// Project paths configuration
pub project_paths_config: ProjectPathsConfig,
/// Cheats configuration options
pub cheats_config_options: CheatsConfigOptions,
/// EVM options
pub evm_opts: EvmOpts,
/// Configuration for fuzz testing
pub fuzz: FuzzConfig,
/// Configuration for invariant testing
pub invariant: InvariantConfig,
}

/// Default address for tx.origin for Foundry
///
/// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
const DEFAULT_SENDER: Address = address!("1804c8AB1F12E6bbf3894d4083f33e07309d1f38");

impl SolidityTestsConfig {
/// Create a new `SolidityTestsConfig` instance
pub fn new(gas_report: bool) -> Self {
// Matches Foundry config defaults
let gas_limit: GasLimit = i64::MAX.into();
let evm_opts = EvmOpts {
env: EvmEnv {
gas_limit: gas_limit.into(),
chain_id: None,
tx_origin: DEFAULT_SENDER,
block_number: 1,
block_timestamp: 1,
..Default::default()
},
sender: DEFAULT_SENDER,
initial_balance: U256::from(0xffffffffffffffffffffffffu128),
ffi: false,
verbosity: 0,
memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
..EvmOpts::default()
};

// Matches Foundry config defaults
let mut fuzz = FuzzConfig::new("cache/fuzz".into());
let mut invariant = InvariantConfig::new("cache/invariant".into());
if !gas_report {
fuzz.gas_report_samples = 0;
invariant.gas_report_samples = 0;
}
let project_root = PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../crates/foundry/testdata"
));

// TODO https://github.com/NomicFoundation/edr/issues/487
let project_paths_config = ProjectPathsConfig::builder().build_with_root(project_root);

let artifacts: PathBuf = project_paths_config
.artifacts
.file_name()
.expect("artifacts are not relative")
.into();

// Matches Foundry config defaults
let cheats_config_options = CheatsConfigOptions {
rpc_endpoints: RpcEndpoints::new([(
"alchemy",
RpcEndpoint::Url("${ALCHEMY_URL}".to_string()),
)]),
unchecked_cheatcode_artifacts: false,
prompt_timeout: 0,
rpc_storage_caching: StorageCachingConfig::default(),
fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
labels: HashMap::default(),
};

Self {
project_paths_config,
cheats_config_options,
evm_opts,
fuzz,
invariant,
}
}
}
119 changes: 27 additions & 92 deletions crates/edr_napi/src/solidity_tests/runner.rs
Original file line number Diff line number Diff line change
@@ -1,119 +1,54 @@
/// Based on `crates/foundry/forge/tests/it/test_helpers.rs`.
use std::{path::PathBuf, sync::Arc};
use std::sync::Arc;

use alloy_primitives::U256;
/// Based on `crates/foundry/forge/tests/it/test_helpers.rs`.
use forge::{
constants::CALLER,
decode::RevertDecoder,
multi_runner::TestContract,
opts::{Env as EvmEnv, EvmOpts},
revm::primitives::SpecId,
MultiContractRunner, MultiContractRunnerBuilder, TestOptionsBuilder,
decode::RevertDecoder, multi_runner::TestContract, revm::primitives::SpecId,
MultiContractRunner, TestOptionsBuilder,
};
use foundry_compilers::ArtifactId;
use foundry_config::{Config, RpcEndpoint, RpcEndpoints};

use crate::solidity_tests::config::SolidityTestsConfig;

pub(super) fn build_runner(
test_suites: Vec<(ArtifactId, TestContract)>,
gas_report: bool,
) -> napi::Result<MultiContractRunner> {
let config = foundry_config(gas_report);
let config = SolidityTestsConfig::new(gas_report);

let mut evm_opts = evm_opts();
evm_opts.isolate = config.isolate;
evm_opts.verbosity = config.verbosity;
evm_opts.memory_limit = config.memory_limit;
evm_opts.env.gas_limit = config.gas_limit.into();
let SolidityTestsConfig {
evm_opts,
project_paths_config,
cheats_config_options,
fuzz,
invariant,
} = config;

let test_options = TestOptionsBuilder::default()
.fuzz(config.fuzz.clone())
.invariant(config.invariant.clone())
.fuzz(fuzz)
.invariant(invariant)
.build_hardhat()
.expect("Config loaded");

let builder = MultiContractRunnerBuilder::new(Arc::new(config))
.sender(evm_opts.sender)
.with_test_options(test_options);
.map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{e:?}")))?;

let abis = test_suites.iter().map(|(_, contract)| &contract.abi);
let revert_decoder = RevertDecoder::new().with_abis(abis);

let sender = Some(evm_opts.sender);
let evm_env = evm_opts.local_evm_env();

Ok(MultiContractRunner {
project_paths_config: Arc::new(project_paths_config),
cheats_config_opts: Arc::new(cheats_config_options),
contracts: test_suites.into_iter().collect(),
evm_opts,
env: evm_env,
evm_spec: builder.evm_spec.unwrap_or(SpecId::MERGE),
sender: builder.sender,
evm_spec: SpecId::MERGE,
sender,
revert_decoder,
fork: builder.fork,
config: builder.config,
coverage: builder.coverage,
debug: builder.debug,
test_options: builder.test_options.unwrap_or_default(),
isolation: builder.isolation,
fork: None,
coverage: false,
debug: false,
test_options,
isolation: false,
output: None,
})
}

fn project_root() -> PathBuf {
PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../crates/foundry/testdata"
))
}

fn foundry_config(gas_report: bool) -> Config {
const TEST_PROFILE: &str = "default";

// Forge project root.
let root = project_root();

let mut config = Config::with_root(&root);

config.ast = true;
config.src = root.join(TEST_PROFILE);
config.out = root.join("out").join(TEST_PROFILE);
config.cache_path = root.join("cache").join(TEST_PROFILE);
config.libraries =
vec!["fork/Fork.t.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string()];

config.rpc_endpoints = rpc_endpoints();
// TODO https://github.com/NomicFoundation/edr/issues/487
// config.allow_paths.push(manifest_root().to_path_buf());

// no prompt testing
config.prompt_timeout = 0;

if !gas_report {
config.fuzz.gas_report_samples = 0;
config.invariant.gas_report_samples = 0;
}

config
}

fn evm_opts() -> EvmOpts {
EvmOpts {
env: EvmEnv {
gas_limit: u64::MAX,
chain_id: None,
tx_origin: CALLER,
block_number: 1,
block_timestamp: 1,
..Default::default()
},
sender: CALLER,
initial_balance: U256::MAX,
ffi: true,
verbosity: 3,
memory_limit: 1 << 26,
..Default::default()
}
}

/// The RPC endpoints used during tests.
fn rpc_endpoints() -> RpcEndpoints {
RpcEndpoints::new([("alchemy", RpcEndpoint::Url("${ALCHEMY_URL}".to_string()))])
}
87 changes: 69 additions & 18 deletions crates/foundry/cheatcodes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use foundry_common::{fs::normalize_path, ContractsByArtifact};
use foundry_compilers::{utils::canonicalize, ProjectPathsConfig};
use foundry_config::{
cache::StorageCachingConfig, fs_permissions::FsAccessKind, Config, FsPermissions,
ResolvedRpcEndpoints,
ResolvedRpcEndpoints, RpcEndpoints,
};
use foundry_evm_core::opts::EvmOpts;
use semver::Version;
Expand Down Expand Up @@ -56,40 +56,85 @@ pub struct CheatsConfig {
pub running_version: Option<Version>,
}

/// Configuration options specific to cheat codes.
#[derive(Clone, Debug)]
pub struct CheatsConfigOptions {
/// Multiple rpc endpoints and their aliases
pub rpc_endpoints: RpcEndpoints,
/// Whether to enable safety checks for `vm.getCode` and
/// `vm.getDeployedCode` invocations. If disabled, it is possible to
/// access artifacts which were not recompiled or cached.
pub unchecked_cheatcode_artifacts: bool,
/// Sets a timeout in seconds for vm.prompt cheatcodes
pub prompt_timeout: u64,
/// RPC storage caching settings determines what chains and endpoints to
/// cache
pub rpc_storage_caching: StorageCachingConfig,
/// Configures the permissions of cheat codes that touch the file system.
///
/// This includes what operations can be executed (read, write)
pub fs_permissions: FsPermissions,
/// Address labels
pub labels: HashMap<Address, String>,
}

impl From<Config> for CheatsConfigOptions {
fn from(value: Config) -> Self {
Self {
rpc_endpoints: value.rpc_endpoints,
unchecked_cheatcode_artifacts: value.unchecked_cheatcode_artifacts,
prompt_timeout: value.prompt_timeout,
rpc_storage_caching: value.rpc_storage_caching,
fs_permissions: value.fs_permissions,
labels: value.labels,
}
}
}

impl CheatsConfig {
/// Extracts the necessary settings from the Config
pub fn new(
config: &Config,
paths: ProjectPathsConfig,
config: CheatsConfigOptions,
evm_opts: EvmOpts,
available_artifacts: Option<Arc<ContractsByArtifact>>,
running_version: Option<Version>,
) -> Self {
let mut allowed_paths = vec![config.__root.0.clone()];
allowed_paths.extend(config.libs.clone());
allowed_paths.extend(config.allow_paths.clone());
let CheatsConfigOptions {
rpc_endpoints,
unchecked_cheatcode_artifacts,
prompt_timeout,
rpc_storage_caching,
fs_permissions,
labels,
} = config;

let rpc_endpoints = config.rpc_endpoints.clone().resolved();
let rpc_endpoints = rpc_endpoints.resolved();
trace!(?rpc_endpoints, "using resolved rpc endpoints");

// If user explicitly disabled safety checks, do not set available_artifacts
let available_artifacts = if config.unchecked_cheatcode_artifacts {
let available_artifacts = if unchecked_cheatcode_artifacts {
None
} else {
available_artifacts
};

let fs_permissions = fs_permissions.joined(&paths.root);
let root = paths.root.clone();
let allowed_paths = paths.allowed_paths.clone().into_iter().collect();

Self {
ffi: evm_opts.ffi,
always_use_create_2_factory: evm_opts.always_use_create_2_factory,
prompt_timeout: Duration::from_secs(config.prompt_timeout),
rpc_storage_caching: config.rpc_storage_caching.clone(),
prompt_timeout: Duration::from_secs(prompt_timeout),
rpc_storage_caching,
rpc_endpoints,
paths: config.project_paths(),
fs_permissions: config.fs_permissions.clone().joined(&config.__root),
root: config.__root.0.clone(),
paths,
fs_permissions,
root,
allowed_paths,
evm_opts,
labels: config.labels.clone(),
labels,
available_artifacts,
running_version,
}
Expand Down Expand Up @@ -244,12 +289,18 @@ mod tests {
use super::*;

fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
let config = Config {
__root: PathBuf::from(root).into(),
fs_permissions,
..Default::default()
};

let project_paths_config = config.project_paths();
let cheats_config_options = CheatsConfigOptions::from(config);

CheatsConfig::new(
&Config {
__root: PathBuf::from(root).into(),
fs_permissions,
..Default::default()
},
project_paths_config,
cheats_config_options,
EvmOpts::default(),
None,
None,
Expand Down
2 changes: 1 addition & 1 deletion crates/foundry/cheatcodes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub extern crate foundry_cheatcodes_spec as spec;
extern crate tracing;

use alloy_primitives::Address;
pub use config::CheatsConfig;
pub use config::{CheatsConfig, CheatsConfigOptions};
pub use error::{Error, ErrorKind, Result};
use foundry_evm_core::backend::DatabaseExt;
pub use inspector::{BroadcastableTransaction, BroadcastableTransactions, Cheatcodes, Context};
Expand Down
Loading

0 comments on commit e1eeb28

Please sign in to comment.