Skip to content

Commit

Permalink
feat(test): only compile files needed for tests (#7334)
Browse files Browse the repository at this point in the history
* feat(forge test): only compile files needed for tests

* remove comment

* clippy

* update fixtures

* getCode + getDeployedCode updates

* fixes

* fix path matching

* clippy

* add config flag

* fix

* docs

* fmt

* patch compilers

* fix Cargo.toml

* update patch

* update patch

* doc

* rm space

* cargo cheats

* new output selection fn

* log compiler errors on failure

* fixes
  • Loading branch information
klkvr committed Apr 6, 2024
1 parent ecfcca0 commit 5efa20e
Show file tree
Hide file tree
Showing 23 changed files with 241 additions and 75 deletions.
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.

1 change: 1 addition & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ k256.workspace = true
walkdir = "2"
p256 = "0.13.2"
thiserror = "1"
semver = "1"
rustc-hash.workspace = true
dialoguer = "0.11.0"
4 changes: 2 additions & 2 deletions crates/cheatcodes/assets/cheatcodes.json

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

6 changes: 4 additions & 2 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1397,11 +1397,13 @@ interface Vm {
#[cheatcode(group = Filesystem)]
function writeLine(string calldata path, string calldata data) external;

/// Gets the creation bytecode from an artifact file. Takes in the relative path to the json file.
/// Gets the creation bytecode from an artifact file. Takes in the relative path to the json file or the path to the
/// artifact in the form of <path>:<contract>:<version> where <contract> and <version> parts are optional.
#[cheatcode(group = Filesystem)]
function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode);

/// Gets the deployed bytecode from an artifact file. Takes in the relative path to the json file.
/// Gets the deployed bytecode from an artifact file. Takes in the relative path to the json file or the path to the
/// artifact in the form of <path>:<contract>:<version> where <contract> and <version> parts are optional.
#[cheatcode(group = Filesystem)]
function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode);

Expand Down
20 changes: 18 additions & 2 deletions crates/cheatcodes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::Result;
use crate::{script::ScriptWallets, Vm::Rpc};
use alloy_primitives::Address;
use foundry_common::fs::normalize_path;
use foundry_compilers::{utils::canonicalize, ProjectPathsConfig};
use foundry_compilers::{utils::canonicalize, ArtifactId, ProjectPathsConfig};
use foundry_config::{
cache::StorageCachingConfig, fs_permissions::FsAccessKind, Config, FsPermissions,
ResolvedRpcEndpoints,
Expand Down Expand Up @@ -43,18 +43,31 @@ pub struct CheatsConfig {
pub labels: HashMap<Address, String>,
/// Script wallets
pub script_wallets: Option<ScriptWallets>,
/// Artifacts which are guaranteed to be fresh (either recompiled or cached).
/// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list.
/// If None, no validation is performed.
pub available_artifacts: Option<Vec<ArtifactId>>,
}

impl CheatsConfig {
/// Extracts the necessary settings from the Config
pub fn new(config: &Config, evm_opts: EvmOpts, script_wallets: Option<ScriptWallets>) -> Self {
pub fn new(
config: &Config,
evm_opts: EvmOpts,
available_artifacts: Option<Vec<ArtifactId>>,
script_wallets: Option<ScriptWallets>,
) -> Self {
let mut allowed_paths = vec![config.__root.0.clone()];
allowed_paths.extend(config.libs.clone());
allowed_paths.extend(config.allow_paths.clone());

let rpc_endpoints = config.rpc_endpoints.clone().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 { None } else { available_artifacts };

Self {
ffi: evm_opts.ffi,
always_use_create_2_factory: evm_opts.always_use_create_2_factory,
Expand All @@ -68,6 +81,7 @@ impl CheatsConfig {
evm_opts,
labels: config.labels.clone(),
script_wallets,
available_artifacts,
}
}

Expand Down Expand Up @@ -185,6 +199,7 @@ impl Default for CheatsConfig {
evm_opts: Default::default(),
labels: Default::default(),
script_wallets: None,
available_artifacts: Default::default(),
}
}
}
Expand All @@ -199,6 +214,7 @@ mod tests {
&Config { __root: PathBuf::from(root).into(), fs_permissions, ..Default::default() },
Default::default(),
None,
None,
)
}

Expand Down
67 changes: 64 additions & 3 deletions crates/cheatcodes/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ use alloy_json_abi::ContractObject;
use alloy_primitives::U256;
use alloy_sol_types::SolValue;
use dialoguer::{Input, Password};
use foundry_common::{fs, get_artifact_path};
use foundry_common::fs;
use foundry_config::fs_permissions::FsAccessKind;
use semver::Version;
use std::{
collections::hash_map::Entry,
io::{BufRead, BufReader, Write},
path::Path,
path::{Path, PathBuf},
process::Command,
sync::mpsc,
thread,
Expand Down Expand Up @@ -269,9 +270,69 @@ impl Cheatcode for getDeployedCodeCall {
}
}

/// Returns the path to the json artifact depending on the input
fn get_artifact_path(state: &Cheatcodes, path: &str) -> Result<PathBuf> {
if path.ends_with(".json") {
Ok(PathBuf::from(path))
} else {
let mut parts = path.split(':');
let file = PathBuf::from(parts.next().unwrap());
let contract_name = parts.next();
let version = parts.next();

let version = if let Some(version) = version {
Some(Version::parse(version).map_err(|_| fmt_err!("Error parsing version"))?)
} else {
None
};

// Use available artifacts list if available
if let Some(available_ids) = &state.config.available_artifacts {
let mut artifact = None;

for id in available_ids.iter() {
// name might be in the form of "Counter.0.8.23"
let id_name = id.name.split('.').next().unwrap();

if !id.source.ends_with(&file) {
continue;
}
if let Some(name) = contract_name {
if id_name != name {
continue;
}
}
if let Some(ref version) = version {
if id.version.minor != version.minor ||
id.version.major != version.major ||
id.version.patch != version.patch
{
continue;
}
}
if artifact.is_some() {
return Err(fmt_err!("Multiple matching artifacts found"));
}
artifact = Some(id);
}

let artifact = artifact.ok_or_else(|| fmt_err!("No matching artifact found"))?;
Ok(artifact.path.clone())
} else {
let file = file.to_string_lossy();
let contract_name = if let Some(contract_name) = contract_name {
contract_name.to_owned()
} else {
file.replace(".sol", "")
};
Ok(state.config.paths.artifacts.join(format!("{file}/{contract_name}.json")))
}
}
}

/// Reads the bytecode object(s) from the matching artifact
fn read_bytecode(state: &Cheatcodes, path: &str) -> Result<ContractObject> {
let path = get_artifact_path(&state.config.paths, path);
let path = get_artifact_path(state, path)?;
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
let data = fs::read_to_string(path)?;
serde_json::from_str::<ContractObject>(&data).map_err(Into::into)
Expand Down
1 change: 1 addition & 0 deletions crates/chisel/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ impl SessionSource {
&self.config.foundry_config,
self.config.evm_opts.clone(),
None,
None,
)
.into(),
)
Expand Down
16 changes: 1 addition & 15 deletions crates/common/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ use alloy_primitives::{hex, Address, Selector, B256};
use eyre::Result;
use foundry_compilers::{
artifacts::{CompactContractBytecode, ContractBytecodeSome},
ArtifactId, ProjectPathsConfig,
ArtifactId,
};
use std::{
collections::BTreeMap,
fmt,
ops::{Deref, DerefMut},
path::PathBuf,
};

type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a (JsonAbi, Vec<u8>));
Expand Down Expand Up @@ -170,19 +169,6 @@ pub fn get_file_name(id: &str) -> &str {
id.split(':').next().unwrap_or(id)
}

/// Returns the path to the json artifact depending on the input
pub fn get_artifact_path(paths: &ProjectPathsConfig, path: &str) -> PathBuf {
if path.ends_with(".json") {
PathBuf::from(path)
} else {
let parts: Vec<&str> = path.split(':').collect();
let file = parts[0];
let contract_name =
if parts.len() == 1 { parts[0].replace(".sol", "") } else { parts[1].to_string() };
paths.artifacts.join(format!("{file}/{contract_name}.json"))
}
}

/// Helper function to convert CompactContractBytecode ~> ContractBytecodeSome
pub fn compact_to_contract(contract: CompactContractBytecode) -> Result<ContractBytecodeSome> {
Ok(ContractBytecodeSome {
Expand Down
8 changes: 7 additions & 1 deletion crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ pub struct Config {
/// Address labels
pub labels: HashMap<Address, String>,

/// 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,

/// The root path where the config detection started from, `Config::with_root`
#[doc(hidden)]
// We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
Expand Down Expand Up @@ -662,7 +666,8 @@ impl Config {
self.create_project(false, true)
}

fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
/// Creates a [Project] with the given `cached` and `no_artifacts` flags
pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
let mut project = Project::builder()
.artifacts(self.configured_artifacts_handler())
.paths(self.project_paths())
Expand Down Expand Up @@ -1925,6 +1930,7 @@ impl Default for Config {
fmt: Default::default(),
doc: Default::default(),
labels: Default::default(),
unchecked_cheatcode_artifacts: false,
__non_exhaustive: (),
__warnings: vec![],
}
Expand Down
9 changes: 8 additions & 1 deletion crates/forge/bin/cmd/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,14 +303,21 @@ impl CoverageArgs {
) -> Result<()> {
let root = project.paths.root;

let artifact_ids = output.artifact_ids().map(|(id, _)| id).collect();

// Build the contract runner
let env = evm_opts.evm_env().await?;
let mut runner = MultiContractRunnerBuilder::default()
.initial_balance(evm_opts.initial_balance)
.evm_spec(config.evm_spec_id())
.sender(evm_opts.sender)
.with_fork(evm_opts.get_fork(&config, env.clone()))
.with_cheats_config(CheatsConfig::new(&config, evm_opts.clone(), None))
.with_cheats_config(CheatsConfig::new(
&config,
evm_opts.clone(),
Some(artifact_ids),
None,
))
.with_test_options(TestOptions {
fuzz: config.fuzz,
invariant: config.invariant,
Expand Down
Loading

0 comments on commit 5efa20e

Please sign in to comment.