diff --git a/Cargo.lock b/Cargo.lock index 131c6351dfee..6cc3d5f254ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3402,9 +3402,9 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136f5e0af939b12cf072ac6577818a87cb7a408b269070f5d6f52ba23454660e" +checksum = "8fbc0b5eaf47ad7ec2faef29289e1a1a05ad1550fa78e13fe902764dbf0db1fe" dependencies = [ "alloy-json-abi", "alloy-primitives", diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 329e53213d90..c9124a4224f6 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -305,8 +305,8 @@ pub struct ContractSources { impl ContractSources { /// Collects the contract sources and artifacts from the project compile output. - pub fn from_project_output( - output: &ProjectCompileOutput, + pub fn from_project_output( + output: &ProjectCompileOutput, root: &Path, libraries: &Libraries, ) -> Result { diff --git a/crates/config/src/inline/natspec.rs b/crates/config/src/inline/natspec.rs index 27742eb56e4b..6f3001dafc3e 100644 --- a/crates/config/src/inline/natspec.rs +++ b/crates/config/src/inline/natspec.rs @@ -26,7 +26,7 @@ impl NatSpec { /// Factory function that extracts a vector of [`NatSpec`] instances from /// a solc compiler output. The root path is to express contract base dirs. /// That is essential to match per-test configs at runtime. - pub fn parse(output: &ProjectCompileOutput, root: &Path) -> Vec { + pub fn parse(output: &ProjectCompileOutput, root: &Path) -> Vec { let mut natspecs: Vec = vec![]; let solc = SolcParser::new(); diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index ec2118c16696..4c990e0fdac1 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -810,7 +810,7 @@ impl Config { compiler_config: CompilerConfig, settings: C::Settings, ) -> Result, SolcError> { - let project = ProjectBuilder::::new(Default::default()) + let project = ProjectBuilder::::new(Default::default()) .artifacts(self.configured_artifacts_handler()) .paths(self.project_paths()) .settings(settings) @@ -2799,6 +2799,24 @@ macro_rules! with_resolved_project { }; } +/// Helper trait to resolve project depending on [Compiler] generic. +pub trait ResolveProject { + /// Returns configured project. + fn resolve_project(&self) -> Result, SolcError>; +} + +impl ResolveProject for Config { + fn resolve_project(&self) -> Result, SolcError> { + self.project() + } +} + +impl ResolveProject for Config { + fn resolve_project(&self) -> Result, SolcError> { + self.vyper_project() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index c4bbbb8a21b3..40ad5e7811f3 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -90,7 +90,7 @@ impl BuildArgs { with_resolved_project!(config, |project| { let project = project?; - let filter = if let Some(ref skip) = self.skip { + let filter = if let Some(skip) = &self.skip { if !skip.is_empty() { let filter = SkipBuildFilters::new(skip.clone(), project.root().clone())?; Some(filter) diff --git a/crates/forge/bin/cmd/test/filter.rs b/crates/forge/bin/cmd/test/filter.rs index c30b3434ae94..4cde2d388529 100644 --- a/crates/forge/bin/cmd/test/filter.rs +++ b/crates/forge/bin/cmd/test/filter.rs @@ -1,9 +1,16 @@ use clap::Parser; use forge::TestFilter; use foundry_common::glob::GlobMatcher; -use foundry_compilers::{FileFilter, ProjectPathsConfig}; +use foundry_compilers::{ + compilers::vyper::parser::VyperParsedSource, + resolver::{parse::SolData, GraphEdges}, + FileFilter, ProjectPathsConfig, SolcSparseFileFilter, SparseOutputFileFilter, +}; use foundry_config::Config; -use std::{fmt, path::Path}; +use std::{ + fmt, + path::{Path, PathBuf}, +}; /// The filter to use during testing. /// @@ -214,3 +221,25 @@ impl fmt::Display for ProjectPathsAwareFilter { self.args_filter.fmt(f) } } + +impl FileFilter for &ProjectPathsAwareFilter { + fn is_match(&self, file: &Path) -> bool { + (*self).is_match(file) + } +} + +impl SparseOutputFileFilter for ProjectPathsAwareFilter { + fn sparse_sources(&self, file: &Path, graph: &GraphEdges) -> Vec { + SolcSparseFileFilter::new(self).sparse_sources(file, graph) + } +} + +impl SparseOutputFileFilter for ProjectPathsAwareFilter { + fn sparse_sources(&self, file: &Path, _graph: &GraphEdges) -> Vec { + if self.is_match(file) { + vec![file.to_path_buf()] + } else { + vec![] + } + } +} diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 1bb70aa7eb4e..76ed51f5190c 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -6,6 +6,7 @@ use forge::{ decode::decode_console_logs, gas_report::GasReport, multi_runner::matches_contract, + opts::EvmOpts, result::{SuiteResult, TestOutcome, TestStatus}, traces::{identifier::SignaturesIdentifier, CallTraceDecoderBuilder, TraceKind}, MultiContractRunner, MultiContractRunnerBuilder, TestFilter, TestOptions, TestOptionsBuilder, @@ -20,16 +21,18 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::output_selection::OutputSelection, utils::source_files_iter, SolcSparseFileFilter, - SOLC_EXTENSIONS, + artifacts::output_selection::OutputSelection, + compilers::{CompilationError, Compiler, CompilerSettings}, + utils::source_files_iter, + Project, SparseOutputFileFilter, }; use foundry_config::{ - figment, figment::{ + self, value::{Dict, Map}, Metadata, Profile, Provider, }, - get_available_profiles, Config, + get_available_profiles, with_resolved_project, Config, }; use foundry_debugger::Debugger; use foundry_evm::traces::identifier::TraceIdentifiers; @@ -146,16 +149,21 @@ impl TestArgs { /// Returns sources which include any tests to be executed. /// If no filters are provided, sources are filtered by existence of test/invariant methods in /// them, If filters are provided, sources are additionaly filtered by them. - pub fn get_sources_to_compile( + pub fn get_sources_to_compile( &self, - config: &Config, + project: &Project, filter: &ProjectPathsAwareFilter, - ) -> Result> { - let mut project = config.create_project(true, true)?; - project.settings.output_selection = + ) -> Result> + where + C: Compiler, + ProjectPathsAwareFilter: SparseOutputFileFilter, + { + let mut project = project.clone(); + *project.settings.output_selection_mut() = OutputSelection::common_output_selection(["abi".to_string()]); + project.no_artifacts = true; - let output = project.compile_sparse(Box::new(SolcSparseFileFilter::new(filter.clone())))?; + let output = project.compile_sparse(Box::new(filter.clone()))?; if output.has_compiler_errors() { println!("{}", output); @@ -205,7 +213,7 @@ impl TestArgs { } // Always recompile all sources to ensure that `getCode` cheatcode can use any artifact. - test_sources.extend(source_files_iter(project.paths.sources, SOLC_EXTENSIONS)); + test_sources.extend(source_files_iter(project.paths.sources, C::FILE_EXTENSIONS)); Ok(test_sources) } @@ -229,24 +237,36 @@ impl TestArgs { config.invariant.gas_report_samples = 0; } - // Set up the project. - let mut project = config.project()?; - // Install missing dependencies. if install::install_missing_dependencies(&mut config, self.build_args().silent) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); - project = config.project()?; } + with_resolved_project!(config, |project| { + self.run_with_project(config, evm_opts, project?).await + }) + } + + pub async fn run_with_project( + &self, + config: Config, + mut evm_opts: EvmOpts, + project: Project, + ) -> eyre::Result + where + C: Compiler, + C::CompilationError: Clone, + ProjectPathsAwareFilter: SparseOutputFileFilter, + { let mut filter = self.filter(&config); trace!(target: "forge::test", ?filter, "using filter"); - let sources_to_compile = self.get_sources_to_compile(&config, &filter)?; + let sources_to_compile = self.get_sources_to_compile(&project, &filter)?; - let compiler = ProjectCompiler::new() + let compiler = ProjectCompiler::::new() .quiet_if(self.json || self.opts.silent) .files(sources_to_compile); @@ -335,9 +355,9 @@ impl TestArgs { } /// Run all tests that matches the filter predicate from a test runner - pub async fn run_tests( + pub async fn run_tests( &self, - mut runner: MultiContractRunner, + mut runner: MultiContractRunner, config: Arc, verbosity: u8, filter: &ProjectPathsAwareFilter, @@ -603,8 +623,8 @@ impl Provider for TestArgs { } /// Lists all matching tests -fn list( - runner: MultiContractRunner, +fn list( + runner: MultiContractRunner, filter: &ProjectPathsAwareFilter, json: bool, ) -> Result { diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 6a9334b1679c..7d98b43d310a 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -45,8 +45,8 @@ pub struct TestOptions { impl TestOptions { /// Tries to create a new instance by detecting inline configurations from the project compile /// output. - pub fn new( - output: &ProjectCompileOutput, + pub fn new( + output: &ProjectCompileOutput, root: &Path, profiles: Vec, base_fuzz: FuzzConfig, @@ -201,9 +201,9 @@ impl TestOptionsBuilder { /// `root` is a reference to the user's project root dir. This is essential /// to determine the base path of generated contract identifiers. This is to provide correct /// matchers for inline test configs. - pub fn build( + pub fn build( self, - output: &ProjectCompileOutput, + output: &ProjectCompileOutput, root: &Path, ) -> Result { let profiles: Vec = diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 3653a0e3f064..ee571cfdf4dd 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -5,7 +5,10 @@ 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, Solc}; +use foundry_compilers::{ + artifacts::Libraries, compilers::CompilationError, Artifact, ArtifactId, ProjectCompileOutput, + Solc, +}; use foundry_config::Config; use foundry_evm::{ backend::Backend, decode::RevertDecoder, executors::ExecutorBuilder, fork::CreateFork, @@ -35,7 +38,7 @@ pub type DeployableContracts = BTreeMap; /// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds /// to run all test functions in these contracts. -pub struct MultiContractRunner { +pub struct MultiContractRunner { /// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which /// needs to be deployed & linked against pub contracts: DeployableContracts, @@ -62,10 +65,10 @@ pub struct MultiContractRunner { /// Whether to enable call isolation pub isolation: bool, /// Output of the project compilation - pub output: ProjectCompileOutput, + pub output: ProjectCompileOutput, } -impl MultiContractRunner { +impl MultiContractRunner { /// Returns an iterator over all contracts that match the filter. pub fn matching_contracts<'a>( &'a self, @@ -315,13 +318,13 @@ impl MultiContractRunnerBuilder { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm - pub fn build( + pub fn build( self, root: &Path, - output: ProjectCompileOutput, + output: ProjectCompileOutput, env: revm::primitives::Env, evm_opts: EvmOpts, - ) -> Result { + ) -> Result> { let output = output.with_stripped_file_prefixes(root); let linker = Linker::new(root, output.artifact_ids().collect()); diff --git a/crates/forge/tests/it/cheats.rs b/crates/forge/tests/it/cheats.rs index 47d6ebbb9ec5..4af883c7914f 100644 --- a/crates/forge/tests/it/cheats.rs +++ b/crates/forge/tests/it/cheats.rs @@ -4,14 +4,15 @@ use crate::{ config::*, test_helpers::{ ForgeTestData, RE_PATH_SEPARATOR, TEST_DATA_CANCUN, TEST_DATA_DEFAULT, - TEST_DATA_MULTI_VERSION, + TEST_DATA_MULTI_VERSION, TEST_DATA_VYPER, }, }; +use foundry_compilers::compilers::Compiler; use foundry_config::{fs_permissions::PathPermission, FsPermissions}; use foundry_test_utils::Filter; /// Executes all cheat code tests but not fork cheat codes or tests that require isolation mode -async fn test_cheats_local(test_data: &ForgeTestData) { +async fn test_cheats_local(test_data: &ForgeTestData) { let mut filter = Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}*")) .exclude_paths("Fork") .exclude_contracts("Isolated"); @@ -58,3 +59,8 @@ async fn test_cheats_local_multi_version() { async fn test_cheats_local_cancun() { test_cheats_local(&TEST_DATA_CANCUN).await } + +#[tokio::test(flavor = "multi_thread")] +async fn test_cheats_local_vyper() { + test_cheats_local(&TEST_DATA_VYPER).await +} diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index 1b2a1398d1f6..c8ae56b9f0d4 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -4,6 +4,7 @@ use forge::{ result::{SuiteResult, TestStatus}, MultiContractRunner, }; +use foundry_compilers::compilers::CompilationError; use foundry_evm::{ decode::decode_console_logs, revm::primitives::SpecId, @@ -15,18 +16,18 @@ use itertools::Itertools; use std::collections::BTreeMap; /// How to execute a test run. -pub struct TestConfig { - pub runner: MultiContractRunner, +pub struct TestConfig { + pub runner: MultiContractRunner, pub should_fail: bool, pub filter: Filter, } -impl TestConfig { - pub fn new(runner: MultiContractRunner) -> Self { +impl TestConfig { + pub fn new(runner: MultiContractRunner) -> Self { Self::with_filter(runner, Filter::matches_all()) } - pub fn with_filter(runner: MultiContractRunner, filter: Filter) -> Self { + pub fn with_filter(runner: MultiContractRunner, filter: Filter) -> Self { init_tracing(); Self { runner, should_fail: false, filter } } diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index 75856c5e47ce..e03df86b7059 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -59,7 +59,7 @@ async fn repro_config( should_fail: bool, sender: Option
, test_data: &ForgeTestData, -) -> TestConfig { +) -> TestConfig { foundry_test_utils::init_tracing(); let filter = Filter::path(&format!(".*repros/Issue{issue}.t.sol")); diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index b2993151d327..2b1a78b3690a 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -7,11 +7,12 @@ use forge::{ }; use foundry_compilers::{ artifacts::{Libraries, Settings}, - EvmVersion, Project, ProjectCompileOutput, SolcConfig, + compilers::{vyper::Vyper, Compiler}, + EvmVersion, Project, ProjectCompileOutput, Solc, SolcConfig, }; use foundry_config::{ fs_permissions::PathPermission, Config, FsPermissions, FuzzConfig, FuzzDictionaryConfig, - InvariantConfig, RpcEndpoint, RpcEndpoints, + InvariantConfig, Language, ResolveProject, RpcEndpoint, RpcEndpoints, }; use foundry_evm::{ constants::CALLER, @@ -34,6 +35,7 @@ pub enum ForgeTestProfile { Default, Cancun, MultiVersion, + Vyper, } impl fmt::Display for ForgeTestProfile { @@ -42,6 +44,7 @@ impl fmt::Display for ForgeTestProfile { ForgeTestProfile::Default => write!(f, "default"), ForgeTestProfile::Cancun => write!(f, "cancun"), ForgeTestProfile::MultiVersion => write!(f, "multi-version"), + ForgeTestProfile::Vyper => write!(f, "vyper"), } } } @@ -52,6 +55,11 @@ impl ForgeTestProfile { matches!(self, Self::Cancun) } + /// Returns true if the profile is for Vyper compilation. + pub fn is_vyper(&self) -> bool { + matches!(self, Self::Vyper) + } + pub fn root(&self) -> PathBuf { PathBuf::from(TESTDATA) } @@ -71,11 +79,7 @@ impl ForgeTestProfile { SolcConfig::builder().settings(settings).build() } - pub fn project(&self) -> Project { - self.config().project().expect("Failed to build project") - } - - pub fn test_opts(&self, output: &ProjectCompileOutput) -> TestOptions { + pub fn test_opts(&self, output: &ProjectCompileOutput, root: &Path) -> TestOptions { TestOptionsBuilder::default() .fuzz(FuzzConfig { runs: 256, @@ -109,7 +113,7 @@ impl ForgeTestProfile { gas_report_samples: 256, failure_persist_dir: Some(tempfile::tempdir().unwrap().into_path()), }) - .build(output, Path::new(self.project().root())) + .build(output, root) .expect("Config loaded") } @@ -154,30 +158,37 @@ impl ForgeTestProfile { config.evm_version = EvmVersion::Cancun; } + if self.is_vyper() { + config.lang = Language::Vyper; + } + config } } /// Container for test data for a specific test profile. -pub struct ForgeTestData { - pub project: Project, - pub output: ProjectCompileOutput, +pub struct ForgeTestData { + pub project: Project, + pub output: ProjectCompileOutput, pub test_opts: TestOptions, pub evm_opts: EvmOpts, pub config: Config, pub profile: ForgeTestProfile, } -impl ForgeTestData { +impl ForgeTestData { /// Builds [ForgeTestData] for the given [ForgeTestProfile]. /// /// Uses [get_compiled] to lazily compile the project. - pub fn new(profile: ForgeTestProfile) -> Self { - let project = profile.project(); - let output = get_compiled(&project); - let test_opts = profile.test_opts(&output); + pub fn new(profile: ForgeTestProfile) -> Self + where + Config: ResolveProject, + { let config = profile.config(); + let project = config.resolve_project().unwrap(); let evm_opts = profile.evm_opts(); + let output = get_compiled(&project); + let test_opts = profile.test_opts(&output, project.root()); Self { project, output, test_opts, evm_opts, config, profile } } @@ -196,7 +207,7 @@ impl ForgeTestData { } /// Builds a non-tracing runner - pub fn runner(&self) -> MultiContractRunner { + pub fn runner(&self) -> MultiContractRunner { let mut config = self.config.clone(); config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write(manifest_root())]); @@ -204,7 +215,10 @@ impl ForgeTestData { } /// Builds a non-tracing runner - pub fn runner_with_config(&self, mut config: Config) -> MultiContractRunner { + pub fn runner_with_config( + &self, + mut config: Config, + ) -> MultiContractRunner { config.rpc_endpoints = rpc_endpoints(); config.allow_paths.push(manifest_root().to_path_buf()); @@ -234,7 +248,7 @@ impl ForgeTestData { } /// Builds a tracing runner - pub fn tracing_runner(&self) -> MultiContractRunner { + pub fn tracing_runner(&self) -> MultiContractRunner { let mut opts = self.evm_opts.clone(); opts.verbosity = 5; self.base_runner() @@ -243,7 +257,7 @@ impl ForgeTestData { } /// Builds a runner that runs against forked state - pub async fn forked_runner(&self, rpc: &str) -> MultiContractRunner { + pub async fn forked_runner(&self, rpc: &str) -> MultiContractRunner { let mut opts = self.evm_opts.clone(); opts.env.chain_id = None; // clear chain id so the correct one gets fetched from the RPC @@ -259,7 +273,9 @@ impl ForgeTestData { } } -pub fn get_compiled(project: &Project) -> ProjectCompileOutput { +pub fn get_compiled( + project: &Project, +) -> ProjectCompileOutput { let lock_file_path = project.sources_path().join(".lock"); // Compile only once per test run. // We need to use a file lock because `cargo-nextest` runs tests in different processes. @@ -298,6 +314,10 @@ pub static TEST_DATA_CANCUN: Lazy = pub static TEST_DATA_MULTI_VERSION: Lazy = Lazy::new(|| ForgeTestData::new(ForgeTestProfile::MultiVersion)); +/// Data for tests requiring Cancun support on Solc and EVM level. +pub static TEST_DATA_VYPER: Lazy> = + Lazy::new(|| ForgeTestData::new(ForgeTestProfile::Vyper)); + pub fn manifest_root() -> &'static Path { let mut root = Path::new(env!("CARGO_MANIFEST_DIR")); // need to check here where we're executing the test from, if in `forge` we need to also allow diff --git a/testdata/vyper/cheats/PrankTest.vy b/testdata/vyper/cheats/PrankTest.vy new file mode 100644 index 000000000000..dbda076b0752 --- /dev/null +++ b/testdata/vyper/cheats/PrankTest.vy @@ -0,0 +1,23 @@ +import Vm as Vm +from . import PrankTest + +vm: constant(address) = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D + +@external +def test_prank_simple(sender: address): + Vm(vm).startPrank(sender) + PrankTest(self).assert_sender(sender) + +@external +def test_prank_with_origin(sender: address, origin: address): + Vm(vm).startPrank(sender, origin) + PrankTest(self).assert_sender(sender) + PrankTest(self).assert_origin(sender) + +@external +def assert_sender(expected_sender: address): + Vm(vm).assertEq(msg.sender, expected_sender) + +@external +def assert_origin(expected_sender: address): + Vm(vm).assertEq(msg.sender, expected_sender) \ No newline at end of file diff --git a/testdata/vyper/cheats/Vm.vy b/testdata/vyper/cheats/Vm.vy new file mode 100644 index 000000000000..c5388509ccd1 --- /dev/null +++ b/testdata/vyper/cheats/Vm.vy @@ -0,0 +1,7 @@ +@external +def startPrank(new_sender: address, new_origin: address = empty(address)): + pass + +@external +def assertEq(left: address, right: address): + pass \ No newline at end of file