diff --git a/Cargo.lock b/Cargo.lock index d13ac688f..d810d49f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1961,7 +1961,7 @@ dependencies = [ "edr_provider", "edr_rpc_eth", "forge", - "foundry-compilers", + "foundry-common", "foundry-config", "itertools 0.12.1", "k256", @@ -2679,7 +2679,6 @@ dependencies = [ "alloy-primitives 0.7.7", "eyre", "foundry-common", - "foundry-compilers", "foundry-config", "foundry-evm-core", "foundry-evm-coverage", @@ -2707,7 +2706,6 @@ dependencies = [ "eyre", "foundry-block-explorers", "foundry-common", - "foundry-compilers", "foundry-config", "foundry-evm-core", "futures", diff --git a/crates/edr_napi/Cargo.toml b/crates/edr_napi/Cargo.toml index 44daf9c3c..30c2e02ea 100644 --- a/crates/edr_napi/Cargo.toml +++ b/crates/edr_napi/Cargo.toml @@ -35,7 +35,7 @@ mimalloc = { version = "0.1.39", default-features = false, features = ["local_dy # Solidity tests alloy-primitives = { workspace = true, features = ["serde"] } forge.workspace = true -foundry-compilers = { workspace = true, features = ["full"] } +foundry-common.workspace = true foundry-config.workspace = true tempfile = "3.10.1" diff --git a/crates/edr_napi/index.d.ts b/crates/edr_napi/index.d.ts index 5b9a8755f..9543453b4 100644 --- a/crates/edr_napi/index.d.ts +++ b/crates/edr_napi/index.d.ts @@ -341,10 +341,44 @@ export interface ExecutionResult { /** Optional contract address if the transaction created a new contract. */ contractAddress?: Buffer } +/** A compilation artifact. */ +export interface Artifact { + /** The identifier of the artifact. */ + id: ArtifactId + /** The test contract. */ + contract: ContractData +} +/** The identifier of a Solidity contract. */ +export interface ArtifactId { + /** The name of the contract. */ + name: string + /** Original source file path. */ + source: string + /** The solc semver string. */ + solcVersion: string +} +/** A test contract to execute. */ +export interface ContractData { + /** Contract ABI as json string. */ + abi: string + /** + * Contract creation code as hex string. It can be missing if the contract + * is ABI only. + */ + bytecode?: string + /** + * Contract runtime code as hex string. It can be missing if the contract + * is ABI only. + */ + deployedBytecode?: string +} /** See [forge::result::SuiteResult] */ export interface SuiteResult { - /** See [forge::result::SuiteResult::name] */ - readonly name: string + /** + * The artifact id can be used to match input to result in the progress + * callback + */ + readonly id: ArtifactId /** See [forge::result::SuiteResult::duration] */ readonly durationMs: bigint /** See [forge::result::SuiteResult::test_results] */ @@ -427,40 +461,6 @@ export interface BaseCounterExample { /** See [forge::fuzz::BaseCounterExample::args] */ readonly args?: string } -/** A test suite is a contract and its test methods. */ -export interface TestSuite { - /** The identifier of the test suite. */ - id: ArtifactId - /** The test contract. */ - contract: TestContract -} -/** The identifier of a Solidity test contract. */ -export interface ArtifactId { - /** The name of the contract. */ - name: string - /** Original source file path. */ - source: string - /** The solc semver string. */ - solcVersion: string - /** The artifact cache path. Currently unused. */ - artifactCachePath: string -} -/** A test contract to execute. */ -export interface TestContract { - /** The contract ABI as a JSON string. */ - abi: string - /** The contract bytecode including all libraries as a hex string. */ - bytecode: string - /** Vector of library bytecodes to deploy as hex string. */ - libsToDeploy: Array - /** - * Vector of library specifications of the form corresponding to libs to - * deploy, example item: - * `"src/DssSpell.sol:DssExecLib: - * 0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4"` - */ - libraries: Array -} /** * Executes Solidity tests. * @@ -469,7 +469,7 @@ export interface TestContract { * It is up to the caller to track how many times the callback is called to * know when all tests are done. */ -export function runSolidityTests(testSuites: Array, gasReport: boolean, progressCallback: (result: SuiteResult) => void): void +export function runSolidityTests(artifacts: Array, testSuites: Array, gasReport: boolean, progressCallback: (result: SuiteResult) => void): void export interface SubscriptionEvent { filterId: bigint result: any diff --git a/crates/edr_napi/src/solidity_tests.rs b/crates/edr_napi/src/solidity_tests.rs index 85e01600e..0f395fd97 100644 --- a/crates/edr_napi/src/solidity_tests.rs +++ b/crates/edr_napi/src/solidity_tests.rs @@ -1,11 +1,13 @@ +mod artifact; mod config; mod runner; mod test_results; -mod test_suite; -use std::{path::Path, sync::Arc}; +use std::{collections::BTreeMap, path::Path, sync::Arc}; +use artifact::Artifact; use forge::TestFilter; +use foundry_common::{ContractData, ContractsByArtifact}; use napi::{ threadsafe_function::{ ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, @@ -17,7 +19,7 @@ use napi::{ use napi_derive::napi; use crate::solidity_tests::{ - runner::build_runner, test_results::SuiteResult, test_suite::TestSuite, + artifact::ArtifactId, runner::build_runner, test_results::SuiteResult, }; /// Executes Solidity tests. @@ -30,7 +32,8 @@ use crate::solidity_tests::{ #[allow(dead_code)] #[napi] pub fn run_solidity_tests( - test_suites: Vec, + artifacts: Vec, + test_suites: Vec, gas_report: bool, #[napi(ts_arg_type = "(result: SuiteResult) => void")] progress_callback: JsFunction, ) -> napi::Result<()> { @@ -41,14 +44,23 @@ pub fn run_solidity_tests( |ctx: ThreadSafeCallContext| Ok(vec![ctx.value]), )?; - let test_suites = test_suites + let known_contracts: ContractsByArtifact = artifacts .into_iter() .map(|item| Ok((item.id.try_into()?, item.contract.try_into()?))) - .collect::, napi::Error>>()?; - let runner = build_runner(test_suites, gas_report)?; + .collect::, napi::Error>>()? + .into(); + + let test_suites = test_suites + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?; + + let runner = build_runner(&known_contracts, test_suites, gas_report)?; - let (tx_results, mut rx_results) = - tokio::sync::mpsc::unbounded_channel::<(String, forge::result::SuiteResult)>(); + let (tx_results, mut rx_results) = tokio::sync::mpsc::unbounded_channel::<( + foundry_common::ArtifactId, + forge::result::SuiteResult, + )>(); let runtime = runtime::Handle::current(); runtime.spawn(async move { @@ -68,7 +80,11 @@ pub fn run_solidity_tests( }); // Returns immediately after test suite execution is started - runner.test_hardhat(Arc::new(EverythingFilter), tx_results); + runner.test_hardhat( + Arc::new(known_contracts), + Arc::new(EverythingFilter), + tx_results, + ); Ok(()) } diff --git a/crates/edr_napi/src/solidity_tests/artifact.rs b/crates/edr_napi/src/solidity_tests/artifact.rs new file mode 100644 index 000000000..43abb8d7c --- /dev/null +++ b/crates/edr_napi/src/solidity_tests/artifact.rs @@ -0,0 +1,97 @@ +use napi_derive::napi; + +/// A compilation artifact. +#[derive(Clone, Debug)] +#[napi(object)] +pub struct Artifact { + /// The identifier of the artifact. + pub id: ArtifactId, + /// The test contract. + pub contract: ContractData, +} + +/// The identifier of a Solidity contract. +#[derive(Clone, Debug)] +#[napi(object)] +pub struct ArtifactId { + /// The name of the contract. + pub name: String, + /// Original source file path. + pub source: String, + /// The solc semver string. + pub solc_version: String, +} + +impl From for ArtifactId { + fn from(value: foundry_common::ArtifactId) -> Self { + Self { + name: value.name, + source: value.source.to_string_lossy().to_string(), + solc_version: value.version.to_string(), + } + } +} + +impl TryFrom for foundry_common::contracts::ArtifactId { + type Error = napi::Error; + + fn try_from(value: ArtifactId) -> napi::Result { + Ok(foundry_common::contracts::ArtifactId { + name: value.name, + source: value.source.parse().map_err(|_err| { + napi::Error::new(napi::Status::GenericFailure, "Invalid source path") + })?, + version: value.solc_version.parse().map_err(|_err| { + napi::Error::new(napi::Status::GenericFailure, "Invalid solc semver string") + })?, + }) + } +} + +/// A test contract to execute. +#[derive(Clone, Debug)] +#[napi(object)] +pub struct ContractData { + /// Contract ABI as json string. + pub abi: String, + /// Contract creation code as hex string. It can be missing if the contract + /// is ABI only. + pub bytecode: Option, + /// Contract runtime code as hex string. It can be missing if the contract + /// is ABI only. + pub deployed_bytecode: Option, +} + +impl TryFrom for foundry_common::contracts::ContractData { + type Error = napi::Error; + + fn try_from(contract: ContractData) -> napi::Result { + Ok(foundry_common::contracts::ContractData { + abi: serde_json::from_str(&contract.abi).map_err(|_err| { + napi::Error::new(napi::Status::GenericFailure, "Invalid JSON ABI") + })?, + bytecode: contract + .bytecode + .map(|b| { + b.parse().map_err(|_err| { + napi::Error::new( + napi::Status::GenericFailure, + "Invalid hex bytecode for contract", + ) + }) + }) + .transpose()?, + deployed_bytecode: contract + .deployed_bytecode + .map(|b| { + b.parse().map_err(|_err| { + napi::Error::new( + napi::Status::GenericFailure, + "Invalid hex deployed bytecode for contract", + ) + }) + }) + .transpose()?, + }) + } +} diff --git a/crates/edr_napi/src/solidity_tests/config.rs b/crates/edr_napi/src/solidity_tests/config.rs index 654de5f8c..d9d8ca8fd 100644 --- a/crates/edr_napi/src/solidity_tests/config.rs +++ b/crates/edr_napi/src/solidity_tests/config.rs @@ -6,10 +6,9 @@ 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, + cache::StorageCachingConfig, FsPermissions, FuzzConfig, GasLimit, InvariantConfig, RpcEndpoint, + RpcEndpoints, }; /// Solidity tests configuration @@ -66,16 +65,6 @@ impl SolidityTestsConfig { "/../../crates/foundry/testdata" )); - // TODO https://github.com/NomicFoundation/edr/issues/487 - let project_paths_config: ProjectPathsConfig = - ProjectPathsConfig::builder().build_with_root(project_root.clone()); - - 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([( @@ -85,7 +74,8 @@ impl SolidityTestsConfig { unchecked_cheatcode_artifacts: false, prompt_timeout: 0, rpc_storage_caching: StorageCachingConfig::default(), - fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]), + // Hardhat doesn't support loading artifacts from disk + fs_permissions: FsPermissions::new(vec![]), labels: HashMap::default(), }; diff --git a/crates/edr_napi/src/solidity_tests/runner.rs b/crates/edr_napi/src/solidity_tests/runner.rs index d12fc2f96..5ad50daa3 100644 --- a/crates/edr_napi/src/solidity_tests/runner.rs +++ b/crates/edr_napi/src/solidity_tests/runner.rs @@ -1,16 +1,18 @@ use std::sync::Arc; -/// Based on `crates/foundry/forge/tests/it/test_helpers.rs`. use forge::{ - decode::RevertDecoder, multi_runner::TestContract, revm::primitives::SpecId, + decode::RevertDecoder, + multi_runner::{DeployableContracts, TestContract}, + revm::primitives::SpecId, MultiContractRunner, TestOptionsBuilder, }; -use foundry_compilers::ArtifactId; +use foundry_common::ContractsByArtifact; use crate::solidity_tests::config::SolidityTestsConfig; pub(super) fn build_runner( - test_suites: Vec<(ArtifactId, TestContract)>, + known_contracts: &ContractsByArtifact, + test_suites: Vec, gas_report: bool, ) -> napi::Result { let config = SolidityTestsConfig::new(gas_report); @@ -29,16 +31,43 @@ pub(super) fn build_runner( .build_hardhat() .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{e:?}")))?; - let abis = test_suites.iter().map(|(_, contract)| &contract.abi); + // Build revert decoder from ABIs of all artifacts. + let abis = known_contracts.iter().map(|(_, contract)| &contract.abi); let revert_decoder = RevertDecoder::new().with_abis(abis); + let contracts = test_suites + .iter() + .map(|artifact_id| { + let contract_data = known_contracts.get(artifact_id).ok_or_else(|| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Unknown contract: {}", artifact_id.identifier()), + ) + })?; + + let bytecode = contract_data.bytecode.clone().ok_or_else(|| { + napi::Error::new( + napi::Status::GenericFailure, + format!( + "No bytecode for test suite contract: {}", + artifact_id.identifier() + ), + ) + })?; + + let test_contract = TestContract::new_hardhat(contract_data.abi.clone(), bytecode); + + Ok((artifact_id.clone(), test_contract)) + }) + .collect::>()?; + let sender = Some(evm_opts.sender); let evm_env = evm_opts.local_evm_env(); Ok(MultiContractRunner { project_root, cheats_config_opts: Arc::new(cheats_config_options), - contracts: test_suites.into_iter().collect(), + contracts, evm_opts, env: evm_env, evm_spec: SpecId::CANCUN, diff --git a/crates/edr_napi/src/solidity_tests/test_results.rs b/crates/edr_napi/src/solidity_tests/test_results.rs index c689c4ae5..e3517e089 100644 --- a/crates/edr_napi/src/solidity_tests/test_results.rs +++ b/crates/edr_napi/src/solidity_tests/test_results.rs @@ -6,13 +6,16 @@ use napi::{ }; use napi_derive::napi; +use crate::solidity_tests::artifact::ArtifactId; + /// See [forge::result::SuiteResult] #[napi(object)] #[derive(Debug, Clone)] pub struct SuiteResult { - /// See [forge::result::SuiteResult::name] + /// The artifact id can be used to match input to result in the progress + /// callback #[napi(readonly)] - pub name: String, + pub id: ArtifactId, /// See [forge::result::SuiteResult::duration] #[napi(readonly)] pub duration_ms: BigInt, @@ -24,10 +27,10 @@ pub struct SuiteResult { pub warnings: Vec, } -impl From<(String, forge::result::SuiteResult)> for SuiteResult { - fn from((name, suite_result): (String, forge::result::SuiteResult)) -> Self { +impl From<(foundry_common::ArtifactId, forge::result::SuiteResult)> for SuiteResult { + fn from((id, suite_result): (foundry_common::ArtifactId, forge::result::SuiteResult)) -> Self { Self { - name, + id: id.into(), duration_ms: BigInt::from(suite_result.duration.as_millis()), test_results: suite_result .test_results diff --git a/crates/edr_napi/src/solidity_tests/test_suite.rs b/crates/edr_napi/src/solidity_tests/test_suite.rs deleted file mode 100644 index 1d7706c5d..000000000 --- a/crates/edr_napi/src/solidity_tests/test_suite.rs +++ /dev/null @@ -1,100 +0,0 @@ -use foundry_compilers::artifacts::Libraries; -use napi_derive::napi; - -/// A test suite is a contract and its test methods. -#[derive(Clone)] -#[napi(object)] -pub struct TestSuite { - /// The identifier of the test suite. - pub id: ArtifactId, - /// The test contract. - pub contract: TestContract, -} - -/// The identifier of a Solidity test contract. -#[derive(Clone)] -#[napi(object)] -pub struct ArtifactId { - /// The name of the contract. - pub name: String, - /// Original source file path. - pub source: String, - /// The solc semver string. - pub solc_version: String, - /// The artifact cache path. Currently unused. - pub artifact_cache_path: String, -} - -impl TryFrom for foundry_compilers::ArtifactId { - type Error = napi::Error; - - fn try_from(value: ArtifactId) -> napi::Result { - Ok(foundry_compilers::ArtifactId { - path: value.artifact_cache_path.parse().map_err(|_err| { - napi::Error::new(napi::Status::GenericFailure, "Invalid artifact cache path") - })?, - name: value.name, - source: value.source.parse().map_err(|_err| { - napi::Error::new(napi::Status::GenericFailure, "Invalid source path") - })?, - version: value.solc_version.parse().map_err(|_err| { - napi::Error::new(napi::Status::GenericFailure, "Invalid solc semver string") - })?, - }) - } -} - -/// A test contract to execute. -#[derive(Clone)] -#[napi(object)] -pub struct TestContract { - /// The contract ABI as a JSON string. - pub abi: String, - /// The contract bytecode including all libraries as a hex string. - pub bytecode: String, - /// Vector of library bytecodes to deploy as hex string. - pub libs_to_deploy: Vec, - /// Vector of library specifications of the form corresponding to libs to - /// deploy, example item: - /// `"src/DssSpell.sol:DssExecLib: - /// 0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4"` - pub libraries: Vec, -} - -impl TryFrom for forge::multi_runner::TestContract { - type Error = napi::Error; - - fn try_from(contract: TestContract) -> napi::Result { - Ok(forge::multi_runner::TestContract { - abi: serde_json::from_str(&contract.abi).map_err(|_err| { - napi::Error::new(napi::Status::GenericFailure, "Invalid JSON ABI") - })?, - bytecode: contract.bytecode.parse().map_err(|_err| { - napi::Error::new( - napi::Status::GenericFailure, - "Invalid hex bytecode for test contract", - ) - })?, - // Hardhat builds all libraries into the contract bytecode, so we don't need to link any - // other libraries. - libs_to_deploy: contract - .libs_to_deploy - .into_iter() - .map(|lib| { - lib.parse().map_err(|_err| { - napi::Error::new( - napi::Status::GenericFailure, - "Invalid hex bytecode for library", - ) - }) - }) - .collect::, _>>()?, - libraries: Libraries::parse(&contract.libraries).map_err(|_err| { - napi::Error::new( - napi::Status::GenericFailure, - "Invalid library specifications", - ) - })?, - }) - } -} diff --git a/crates/edr_napi/test/solidity-tests.ts b/crates/edr_napi/test/solidity-tests.ts index 6aaf0b43d..9c7d92ee7 100644 --- a/crates/edr_napi/test/solidity-tests.ts +++ b/crates/edr_napi/test/solidity-tests.ts @@ -1,70 +1,74 @@ import { assert } from "chai"; import { - TestSuite, - TestContract, ArtifactId, + ContractData, SuiteResult, runSolidityTests, + Artifact, } from ".."; describe("Solidity Tests", () => { it("executes basic tests", async function () { - const testSuites = [ + const artifacts = [ loadContract("./artifacts/SetupConsistencyCheck.json"), loadContract("./artifacts/PaymentFailureTest.json"), ]; + // All artifacts are test suites. + const testSuites = artifacts.map((artifact) => artifact.id); + const results: Array = await new Promise((resolve) => { const gasReport = false; const resultsFromCallback: Array = []; - runSolidityTests(testSuites, gasReport, (result: SuiteResult) => { - resultsFromCallback.push(result); - if (resultsFromCallback.length === testSuites.length) { - resolve(resultsFromCallback); - } - }); + runSolidityTests( + artifacts, + testSuites, + gasReport, + (result: SuiteResult) => { + resultsFromCallback.push(result); + if (resultsFromCallback.length === artifacts.length) { + resolve(resultsFromCallback); + } + }, + ); }); - assert.equal(results.length, testSuites.length); + assert.equal(results.length, artifacts.length); for (let res of results) { - if (res.name.includes("SetupConsistencyCheck")) { + if (res.id.name.includes("SetupConsistencyCheck")) { assert.equal(res.testResults.length, 2); assert.equal(res.testResults[0].status, "Success"); assert.equal(res.testResults[1].status, "Success"); - } else if (res.name.includes("PaymentFailureTest")) { + } else if (res.id.name.includes("PaymentFailureTest")) { assert.equal(res.testResults.length, 1); assert.equal(res.testResults[0].status, "Failure"); } else { - assert.fail("Unexpected test suite name: " + res.name); + assert.fail("Unexpected test suite name: " + res.id.name); } } }); }); // Load a contract built with Hardhat into a test suite -function loadContract(artifactPath: string): TestSuite { +function loadContract(artifactPath: string): Artifact { const compiledContract = require(artifactPath); - const artifactId: ArtifactId = { - // Artifact cache path is ignored in this test - artifactCachePath: "./none", + const id: ArtifactId = { name: compiledContract.contractName, solcVersion: "0.8.18", source: compiledContract.sourceName, }; - const testContract: TestContract = { + const contract: ContractData = { abi: JSON.stringify(compiledContract.abi), bytecode: compiledContract.bytecode, - libsToDeploy: [], - libraries: [], }; return { - id: artifactId, - contract: testContract, + id, + contract, }; } diff --git a/crates/foundry/common/src/contracts.rs b/crates/foundry/common/src/contracts.rs index d01395d3b..fa82da23d 100644 --- a/crates/foundry/common/src/contracts.rs +++ b/crates/foundry/common/src/contracts.rs @@ -1,23 +1,55 @@ //! Commonly used contract types and functions. -use std::{ - collections::BTreeMap, - ops::{Deref, DerefMut}, -}; +use std::{collections::BTreeMap, path::PathBuf}; -use alloy_json_abi::{Event, Function, JsonAbi}; -use alloy_primitives::{Address, Bytes, Selector, B256}; +use alloy_json_abi::JsonAbi; +use alloy_primitives::{Address, Bytes}; use eyre::Result; -use foundry_compilers::{ - artifacts::{CompactContractBytecode, ContractBytecodeSome}, - ArtifactId, -}; +use foundry_compilers::artifacts::{CompactContractBytecode, ContractBytecodeSome}; +use semver::Version; +use serde::{Deserialize, Serialize}; + +// Adapted from +/// Represents compilation artifact output +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ArtifactId { + /// The name of the contract + pub name: String, + /// Original source file path + pub source: PathBuf, + /// `solc` version that produced this artifact + pub version: Version, +} + +impl ArtifactId { + // Copied from + /// Returns a `:` slug that uniquely identifies an + /// artifact + pub fn identifier(&self) -> String { + format!("{}:{}", self.source.to_string_lossy(), self.name) + } +} + +impl From for ArtifactId { + fn from(value: foundry_compilers::ArtifactId) -> Self { + let foundry_compilers::ArtifactId { + path: _, + name, + source, + version, + } = value; + + Self { + name, + source, + version, + } + } +} /// Container for commonly used contract data. #[derive(Debug, Clone)] pub struct ContractData { - /// Contract name. - pub name: String, /// Contract ABI. pub abi: JsonAbi, /// Contract creation code. @@ -30,7 +62,7 @@ type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a ContractData); /// Wrapper type that maps an artifact to a contract ABI and bytecode. #[derive(Clone, Default, Debug)] -pub struct ContractsByArtifact(pub BTreeMap); +pub struct ContractsByArtifact(BTreeMap); impl ContractsByArtifact { /// Creates a new instance by collecting all artifacts with present bytecode @@ -38,12 +70,13 @@ impl ContractsByArtifact { /// /// It is recommended to use this method with an output of /// [`foundry_linking::Linker::get_linked_artifacts`]. - pub fn new(artifacts: impl IntoIterator) -> Self { + pub fn new_from_foundry_linker( + artifacts: impl IntoIterator, + ) -> Self { Self( artifacts .into_iter() .filter_map(|(id, artifact)| { - let name = id.name.clone(); let bytecode = artifact .bytecode .and_then(foundry_compilers::artifacts::CompactBytecode::into_bytes)?; @@ -59,9 +92,8 @@ impl ContractsByArtifact { let abi = artifact.abi?; Some(( - id, + id.into(), ContractData { - name, abi, bytecode, deployed_bytecode, @@ -72,15 +104,29 @@ impl ContractsByArtifact { ) } - /// Finds a contract which has a similar bytecode as `code`. - pub fn find_by_creation_code(&self, code: &[u8]) -> Option> { - self.iter().find(|(_, contract)| { - if let Some(bytecode) = &contract.bytecode { - bytecode_diff_score(bytecode.as_ref(), code) <= 0.1 - } else { - false - } - }) + /// Returns an iterator over all ids and contracts. + pub fn iter(&self) -> impl Iterator> { + self.0.iter() + } + + /// Get a contract by its id. + pub fn get(&self, id: &ArtifactId) -> Option<&ContractData> { + self.0.get(id) + } + + /// Returns the number of contracts. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if there are no contracts. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Iterate over the contracts. + pub fn values(&self) -> impl Iterator { + self.0.values() } /// Finds a contract which has a similar deployed bytecode as `code`. @@ -111,42 +157,11 @@ impl ContractsByArtifact { Ok(contracts.first().cloned()) } - - /// Flattens the contracts into functions, events and errors. - pub fn flatten(&self) -> (BTreeMap, BTreeMap, JsonAbi) { - let mut funcs = BTreeMap::new(); - let mut events = BTreeMap::new(); - let mut errors_abi = JsonAbi::new(); - for (_name, contract) in self.iter() { - for func in contract.abi.functions() { - funcs.insert(func.selector(), func.clone()); - } - for event in contract.abi.events() { - events.insert(event.selector(), event.clone()); - } - for error in contract.abi.errors() { - errors_abi - .errors - .entry(error.name.clone()) - .or_default() - .push(error.clone()); - } - } - (funcs, events, errors_abi) - } -} - -impl Deref for ContractsByArtifact { - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } } -impl DerefMut for ContractsByArtifact { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 +impl From> for ContractsByArtifact { + fn from(value: BTreeMap) -> Self { + Self(value) } } diff --git a/crates/foundry/evm/fuzz/Cargo.toml b/crates/foundry/evm/fuzz/Cargo.toml index 6cdf248b4..acf796386 100644 --- a/crates/foundry/evm/fuzz/Cargo.toml +++ b/crates/foundry/evm/fuzz/Cargo.toml @@ -7,7 +7,6 @@ edition.workspace = true [dependencies] foundry-common.workspace = true -foundry-compilers.workspace = true foundry-config.workspace = true foundry-evm-core.workspace = true foundry-evm-coverage.workspace = true diff --git a/crates/foundry/evm/fuzz/src/invariant/filters.rs b/crates/foundry/evm/fuzz/src/invariant/filters.rs index 61829f6a4..e4008d624 100644 --- a/crates/foundry/evm/fuzz/src/invariant/filters.rs +++ b/crates/foundry/evm/fuzz/src/invariant/filters.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Selector}; -use foundry_compilers::ArtifactId; +use foundry_common::ArtifactId; use foundry_evm_core::utils::get_function; /// Contains which contracts are to be targeted or excluded on an invariant test diff --git a/crates/foundry/evm/traces/Cargo.toml b/crates/foundry/evm/traces/Cargo.toml index 73babf08d..ebc8f87ab 100644 --- a/crates/foundry/evm/traces/Cargo.toml +++ b/crates/foundry/evm/traces/Cargo.toml @@ -8,7 +8,6 @@ edition.workspace = true [dependencies] foundry-block-explorers.workspace = true foundry-common.workspace = true -foundry-compilers.workspace = true foundry-config.workspace = true foundry-evm-core.workspace = true diff --git a/crates/foundry/evm/traces/src/identifier/local.rs b/crates/foundry/evm/traces/src/identifier/local.rs index b4f8ea066..d7238e854 100644 --- a/crates/foundry/evm/traces/src/identifier/local.rs +++ b/crates/foundry/evm/traces/src/identifier/local.rs @@ -2,8 +2,10 @@ use std::borrow::Cow; use alloy_json_abi::JsonAbi; use alloy_primitives::Address; -use foundry_common::contracts::{bytecode_diff_score, ContractsByArtifact}; -use foundry_compilers::ArtifactId; +use foundry_common::{ + contracts::{bytecode_diff_score, ContractsByArtifact}, + ArtifactId, +}; use super::{AddressIdentity, TraceIdentifier}; diff --git a/crates/foundry/evm/traces/src/identifier/mod.rs b/crates/foundry/evm/traces/src/identifier/mod.rs index fe69dc662..1b1a35a72 100644 --- a/crates/foundry/evm/traces/src/identifier/mod.rs +++ b/crates/foundry/evm/traces/src/identifier/mod.rs @@ -2,8 +2,7 @@ use std::borrow::Cow; use alloy_json_abi::JsonAbi; use alloy_primitives::Address; -use foundry_common::ContractsByArtifact; -use foundry_compilers::ArtifactId; +use foundry_common::{ArtifactId, ContractsByArtifact}; use foundry_config::{Chain, Config}; mod local; diff --git a/crates/foundry/forge/src/lib.rs b/crates/foundry/forge/src/lib.rs index 08b2e5e19..b910f2d21 100644 --- a/crates/foundry/forge/src/lib.rs +++ b/crates/foundry/forge/src/lib.rs @@ -96,56 +96,17 @@ impl TestOptions { }) } - /// Tries to create a new instance by detecting inline configurations from - /// the Hardhat project compile output. + /// Creates a new test options configuration for Hardhat. + /// As opposed to Foundry, Hardhat doesn't support inline configuration. pub fn new_hardhat( - profiles: Vec, base_fuzz: FuzzConfig, base_invariant: InvariantConfig, ) -> Result { - // TODO: add this back - // https://github.com/NomicFoundation/edr/issues/487 - // let natspecs: Vec = NatSpec::parse(output, root); - let natspecs: Vec = Vec::default(); - let mut inline_invariant = InlineConfig::::default(); - let mut inline_fuzz = InlineConfig::::default(); - - for natspec in natspecs { - // Perform general validation - validate_profiles(&natspec, &profiles)?; - FuzzConfig::validate_configs(&natspec)?; - InvariantConfig::validate_configs(&natspec)?; - - // Apply in-line configurations for the current profile - let configs: Vec = natspec.current_profile_configs().collect(); - let c: &str = &natspec.contract; - let f: &str = &natspec.function; - let line: String = natspec.debug_context(); - - match base_fuzz.try_merge(&configs) { - Ok(Some(conf)) => inline_fuzz.insert(c, f, conf), - Ok(None) => { /* No inline config found, do nothing */ } - Err(e) => Err(InlineConfigError { - line: line.clone(), - source: e, - })?, - } - - match base_invariant.try_merge(&configs) { - Ok(Some(conf)) => inline_invariant.insert(c, f, conf), - Ok(None) => { /* No inline config found, do nothing */ } - Err(e) => Err(InlineConfigError { - line: line.clone(), - source: e, - })?, - } - } - Ok(Self { fuzz: base_fuzz, invariant: base_invariant, - inline_fuzz, - inline_invariant, + inline_fuzz: InlineConfig::::default(), + inline_invariant: InlineConfig::::default(), }) } @@ -296,11 +257,8 @@ impl TestOptionsBuilder { /// "fuzz" and "invariant" fallbacks, and extracting all inline test /// configs, if available. pub fn build_hardhat(self) -> Result { - let profiles: Vec = self - .profiles - .unwrap_or_else(|| vec![Config::selected_profile().into()]); let base_fuzz = self.fuzz.unwrap_or_default(); let base_invariant = self.invariant.unwrap_or_default(); - TestOptions::new_hardhat(profiles, base_fuzz, base_invariant) + TestOptions::new_hardhat(base_fuzz, base_invariant) } } diff --git a/crates/foundry/forge/src/multi_runner.rs b/crates/foundry/forge/src/multi_runner.rs index 8a3da5e37..79ab98e31 100644 --- a/crates/foundry/forge/src/multi_runner.rs +++ b/crates/foundry/forge/src/multi_runner.rs @@ -11,8 +11,8 @@ use std::{ use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; -use foundry_common::{get_contract_name, ContractsByArtifact, TestFunctionExt}; -use foundry_compilers::{artifacts::Libraries, Artifact, ArtifactId, ProjectCompileOutput}; +use foundry_common::{get_contract_name, ArtifactId, ContractsByArtifact, TestFunctionExt}; +use foundry_compilers::{artifacts::Libraries, Artifact, ProjectCompileOutput}; use foundry_config::Config; use foundry_evm::{ backend::Backend, @@ -38,6 +38,19 @@ pub struct TestContract { pub libraries: Libraries, } +impl TestContract { + /// Creates a new test contract with the given ABI and bytecode. + /// Library linking isn't supported for Hardhat test suites + pub fn new_hardhat(abi: JsonAbi, bytecode: Bytes) -> Self { + Self { + abi, + bytecode, + libs_to_deploy: vec![], + libraries: Libraries::default(), + } + } +} + pub type DeployableContracts = BTreeMap; /// A multi contract runner receives a set of contracts deployed in an EVM @@ -202,8 +215,9 @@ impl MultiContractRunner { /// Each Executor gets its own instance of the `Backend`. pub fn test_hardhat( mut self, + known_contracts: Arc, filter: Arc, - tx: tokio::sync::mpsc::UnboundedSender<(String, SuiteResult)>, + tx: tokio::sync::mpsc::UnboundedSender<(ArtifactId, SuiteResult)>, ) { trace!("running all tests"); @@ -224,26 +238,28 @@ impl MultiContractRunner { ); let this = Arc::new(self); - let args = contracts - .into_iter() - .zip(std::iter::repeat((this, db, filter, tx))); + let args = + contracts + .into_iter() + .zip(std::iter::repeat((this, db, filter, known_contracts, tx))); let handle = tokio::runtime::Handle::current(); handle.spawn(async { futures::stream::iter(args) .for_each_concurrent( Some(num_cpus::get()), - |((id, contract), (this, db, filter, tx))| async move { + |((id, contract), (this, db, filter, known_contracts, tx))| async move { tokio::task::spawn_blocking(move || { let handle = tokio::runtime::Handle::current(); let result = this.run_tests_hardhat( &id, &contract, + known_contracts, db.clone(), filter.as_ref(), &handle, ); - let _ = tx.send((id.identifier(), result)); + let _ = tx.send((id, result)); }) .await .expect("failed to join task"); @@ -271,7 +287,9 @@ impl MultiContractRunner { let linked_contracts = linker .get_linked_artifacts(&contract.libraries) .unwrap_or_default(); - let known_contracts = Arc::new(ContractsByArtifact::new(linked_contracts)); + let known_contracts = Arc::new(ContractsByArtifact::new_from_foundry_linker( + linked_contracts, + )); let cheats_config = CheatsConfig::new( self.project_root.clone(), @@ -321,6 +339,7 @@ impl MultiContractRunner { &self, artifact_id: &ArtifactId, contract: &TestContract, + known_contracts: Arc, db: Backend, filter: &dyn TestFilter, handle: &tokio::runtime::Handle, @@ -328,18 +347,6 @@ impl MultiContractRunner { let identifier = artifact_id.identifier(); let mut span_name = identifier.as_str(); - // TODO make sure these are passed in - // https://github.com/NomicFoundation/edr/issues/487 - // let linker = Linker::new( - // self.config.project_paths().root, - // self.output.artifact_ids().collect(), - // ); - // let linked_contracts = linker - // .get_linked_artifacts(&contract.libraries) - // .unwrap_or_default(); - // let known_contracts = Arc::new(ContractsByArtifact::new(linked_contracts)); - let known_contracts = Arc::new(ContractsByArtifact::new(Vec::default())); - let cheats_config = CheatsConfig::new( self.project_root.clone(), (*self.cheats_config_opts).clone(), @@ -523,7 +530,7 @@ impl MultiContractRunnerBuilder { }; deployable_contracts.insert( - id.clone(), + id.clone().into(), TestContract { abi: abi.clone().into_owned(), bytecode, diff --git a/crates/tools/js/benchmark/solidity-tests.js b/crates/tools/js/benchmark/solidity-tests.js index 0c0f5a7b7..5ddbec0d1 100644 --- a/crates/tools/js/benchmark/solidity-tests.js +++ b/crates/tools/js/benchmark/solidity-tests.js @@ -56,19 +56,25 @@ async function runForgeStdTests(forgeStdRepoPath) { path.join(forgeStdRepoPath, "hardhat.config.js"), ); - const testSuites = listFilesRecursively(artifactsDir) - .filter((p) => !p.endsWith(".dbg.json") && p.includes(".t.sol")) - .map(loadContract.bind(null, hardhatConfig)) - .filter((ts) => !EXCLUDED_TEST_SUITES.has(ts.id.name)); + const artifacts = listFilesRecursively(artifactsDir) + .filter((p) => !p.endsWith(".dbg.json") && !p.includes("build-info")) + .map(loadArtifact.bind(null, hardhatConfig)); + + const testSuiteIds = artifacts + .filter( + (a) => + a.id.source.includes(".t.sol") && !EXCLUDED_TEST_SUITES.has(a.id.name), + ) + .map((a) => a.id); const results = await new Promise((resolve) => { const resultsFromCallback = []; - runSolidityTests(testSuites, gasReport, (result) => { - console.error(`${result.name} took ${elapsedSec(start)} seconds`); + runSolidityTests(artifacts, testSuiteIds, gasReport, (result) => { + console.error(`${result.id.name} took ${elapsedSec(start)} seconds`); resultsFromCallback.push(result); - if (resultsFromCallback.length === testSuites.length) { + if (resultsFromCallback.length === artifacts.length) { resolve(resultsFromCallback); } }); @@ -116,13 +122,11 @@ function listFilesRecursively(dir, fileList = []) { return fileList; } -// Load a contract built with Hardhat into a test suite -function loadContract(hardhatConfig, artifactPath) { +// Load a contract built with Hardhat +function loadArtifact(hardhatConfig, artifactPath) { const compiledContract = require(artifactPath); const artifactId = { - // Artifact cache path is ignored - artifactCachePath: "./none", name: compiledContract.contractName, solcVersion: hardhatConfig.solidity.version, source: compiledContract.sourceName, @@ -131,8 +135,7 @@ function loadContract(hardhatConfig, artifactPath) { const testContract = { abi: JSON.stringify(compiledContract.abi), bytecode: compiledContract.bytecode, - libsToDeploy: [], - libraries: [], + deployedBytecode: compiledContract.deployedBytecode, }; return { diff --git a/hardhat-solidity-tests/src/index.ts b/hardhat-solidity-tests/src/index.ts index cad9d2009..e640a5fb0 100644 --- a/hardhat-solidity-tests/src/index.ts +++ b/hardhat-solidity-tests/src/index.ts @@ -1,4 +1,9 @@ -import { SuiteResult, TestSuite } from "@nomicfoundation/edr"; +import { + SuiteResult, + Artifact, + ArtifactId, + ContractData, +} from "@nomicfoundation/edr"; const { task } = require("hardhat/config"); task("test:solidity").setAction(async (_: any, hre: any) => { @@ -13,70 +18,73 @@ task("test:solidity").setAction(async (_: any, hre: any) => { let totalTests = 0; let failedTests = 0; - const tests: TestSuite[] = []; + const artifacts: Artifact[] = []; + const testSuiteIds: ArtifactId[] = []; const fqns = await hre.artifacts.getAllFullyQualifiedNames(); for (const fqn of fqns) { - const sourceName = fqn.split(":")[0]; + const artifact = hre.artifacts.readArtifactSync(fqn); + const buildInfo = hre.artifacts.getBuildInfoSync(fqn); + + const id = { + name: artifact.contractName, + solcVersion: buildInfo.solcVersion, + source: artifact.sourceName, + }; + + const contract: ContractData = { + abi: JSON.stringify(artifact.abi), + bytecode: artifact.bytecode, + deployedBytecode: artifact.deployedBytecode, + }; + + artifacts.push({ id, contract }); + + const sourceName = artifact.sourceName; const isTestFile = sourceName.endsWith(".t.sol") && sourceName.startsWith("contracts/") && !sourceName.startsWith("contracts/forge-std/") && !sourceName.startsWith("contracts/ds-test/"); - if (!isTestFile) { - continue; - } - - const artifact = hre.artifacts.readArtifactSync(fqn); - - const buildInfo = hre.artifacts.getBuildInfoSync(fqn); - - const test = { - id: { - artifactCachePath: hre.config.paths.cache, - name: artifact.contractName, - solcVersion: buildInfo.solcVersion, - source: artifact.sourceName, - }, - contract: { - abi: JSON.stringify(artifact.abi), - bytecode: artifact.bytecode, - libsToDeploy: [], - libraries: [], - }, - }; - tests.push(test); + if (isTestFile) { + testSuiteIds.push(id); + } } await new Promise((resolve) => { const gasReport = false; - runSolidityTests(tests, gasReport, (suiteResult: SuiteResult) => { - for (const testResult of suiteResult.testResults) { - let name = suiteResult.name + " | " + testResult.name; - if ("runs" in testResult?.kind) { - name += ` (${testResult.kind.runs} runs)`; + runSolidityTests( + artifacts, + testSuiteIds, + gasReport, + (suiteResult: SuiteResult) => { + for (const testResult of suiteResult.testResults) { + let name = suiteResult.id.name + " | " + testResult.name; + if ("runs" in testResult?.kind) { + name += ` (${testResult.kind.runs} runs)`; + } + + let failed = testResult.status === "Failure"; + totalTests++; + if (failed) { + failedTests++; + } + + specReporter.write({ + type: failed ? "test:fail" : "test:pass", + data: { + name, + }, + }); } - let failed = testResult.status === "Failure"; - totalTests++; - if (failed) { - failedTests++; + if (totalTests === artifacts.length) { + resolve(); } - - specReporter.write({ - type: failed ? "test:fail" : "test:pass", - data: { - name, - }, - }); - } - - if (totalTests === tests.length) { - resolve(); - } - }); + }, + ); }); console.log(`\n${totalTests} tests found, ${failedTests} failed`);