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(test): only compile files needed for tests #7334

Merged
merged 24 commits into from
Apr 4, 2024
Merged
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
5 changes: 3 additions & 2 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 @@ -42,5 +42,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
69 changes: 65 additions & 4 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 Expand Up @@ -424,7 +485,7 @@ fn prompt(
mod tests {
use super::*;
use crate::CheatsConfig;
use std::{path::PathBuf, sync::Arc};
use std::sync::Arc;

fn cheats() -> Cheatcodes {
let config = CheatsConfig {
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
Loading