From 2c54fcf803eeef506401c53bff222463b9b302a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Thu, 16 Mar 2023 13:01:16 +0300 Subject: [PATCH] feat: multi contract calls in unit tests for contracts (#4156) ## Description closes #3571. closes #4162. This PR adds the ability of calling multiple contracts from sway unit tests if they are added as `[contract-dependencies]`. This is limited with contracts currently but I will be having a follow-up which builds upon this to introduce this support to scripts as well. As these contracts are already declared under `[contract-dependencies]` their contract ids are injected into their namespace by `forc-pkg`. A bug related to this step is fixed in #4159. image ### Follow-ups - #4161 - ~#4162~ ## Checklist - [x] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [x] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [x] I have added tests that prove my fix is effective or that my feature works. - [x] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [x] I have requested a review from the relevant team or maintainers. --------- Co-authored-by: Kaya Gokalp --- docs/book/src/testing/unit-testing.md | 12 ++ examples/multi_contract_calls/Forc.lock | 19 ++ examples/multi_contract_calls/Forc.toml | 3 + .../multi_contract_calls/callee/.gitignore | 2 + .../multi_contract_calls/callee/Forc.toml | 8 + .../multi_contract_calls/callee/src/main.sw | 11 + .../multi_contract_calls/caller/.gitignore | 2 + .../multi_contract_calls/caller/Forc.toml | 13 ++ .../multi_contract_calls/caller/src/main.sw | 28 +++ examples/storage_variables/src/main.sw | 1 - forc-pkg/src/manifest.rs | 16 +- forc-pkg/src/pkg.rs | 60 ++++-- forc-plugins/forc-client/src/util/pkg.rs | 12 +- forc-test/src/lib.rs | 199 ++++++++++++++---- forc-util/src/lib.rs | 2 +- sway-types/src/lib.rs | 2 +- test/src/e2e_vm_tests/mod.rs | 11 +- .../contract_multi_test/src/main.sw | 4 +- .../unit_tests/multi-contract-calls/Forc.lock | 19 ++ .../unit_tests/multi-contract-calls/Forc.toml | 2 + .../multi-contract-calls/contract2/Forc.lock | 13 ++ .../multi-contract-calls/contract2/Forc.toml | 8 + .../contract2/src/main.sw | 11 + .../contract_multi_test/Forc.lock | 13 ++ .../contract_multi_test/Forc.toml | 11 + .../contract_multi_test/src/main.sw | 41 ++++ .../unit_tests/multi-contract-calls/test.toml | 1 + 27 files changed, 442 insertions(+), 82 deletions(-) create mode 100644 examples/multi_contract_calls/Forc.lock create mode 100644 examples/multi_contract_calls/Forc.toml create mode 100644 examples/multi_contract_calls/callee/.gitignore create mode 100644 examples/multi_contract_calls/callee/Forc.toml create mode 100644 examples/multi_contract_calls/callee/src/main.sw create mode 100644 examples/multi_contract_calls/caller/.gitignore create mode 100644 examples/multi_contract_calls/caller/Forc.toml create mode 100644 examples/multi_contract_calls/caller/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/test.toml diff --git a/docs/book/src/testing/unit-testing.md b/docs/book/src/testing/unit-testing.md index 30ab60a1b04..b91ba8e06f5 100644 --- a/docs/book/src/testing/unit-testing.md +++ b/docs/book/src/testing/unit-testing.md @@ -92,3 +92,15 @@ fn test_fail() { ``` > **Note:** When running `forc test`, your contract will be built twice: first *without* unit tests in order to determine the contract's ID, then a second time *with* unit tests with the `CONTRACT_ID` provided to their namespace. This `CONTRACT_ID` can be used with the `abi` cast to enable contract calls within unit tests. + +Unit tests can call methods of external contracts if those contracts are added as contract dependencies, i.e. in the the [`contract-dependencies`](../forc/manifest_reference.md#the-contract-dependencies-section) section of the manifest file. An example of such calls is shown below: + +```sway +{{#include ../../../../examples/multi_contract_calls/caller/src/main.sw:multi_contract_calls}} +``` + +Example `Forc.toml` for contract above: + +```toml +{{#include ../../../../examples/multi_contract_calls/caller/Forc.toml:multi_contract_call_toml}} +``` diff --git a/examples/multi_contract_calls/Forc.lock b/examples/multi_contract_calls/Forc.lock new file mode 100644 index 00000000000..9b1b6bec564 --- /dev/null +++ b/examples/multi_contract_calls/Forc.lock @@ -0,0 +1,19 @@ +[[package]] +name = 'callee' +source = 'member' +dependencies = ['std'] + +[[package]] +name = 'caller' +source = 'member' +dependencies = ['std'] +contract-dependencies = ['callee'] + +[[package]] +name = 'core' +source = 'path+from-root-9B44250BDFED688D' + +[[package]] +name = 'std' +source = 'path+from-root-9B44250BDFED688D' +dependencies = ['core'] diff --git a/examples/multi_contract_calls/Forc.toml b/examples/multi_contract_calls/Forc.toml new file mode 100644 index 00000000000..84f0b629dae --- /dev/null +++ b/examples/multi_contract_calls/Forc.toml @@ -0,0 +1,3 @@ +[workspace] + +members = ["caller", "callee"] diff --git a/examples/multi_contract_calls/callee/.gitignore b/examples/multi_contract_calls/callee/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/examples/multi_contract_calls/callee/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/examples/multi_contract_calls/callee/Forc.toml b/examples/multi_contract_calls/callee/Forc.toml new file mode 100644 index 00000000000..9959c9fcb97 --- /dev/null +++ b/examples/multi_contract_calls/callee/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "callee" + +[dependencies] +std = { path = "../../../sway-lib-std/" } diff --git a/examples/multi_contract_calls/callee/src/main.sw b/examples/multi_contract_calls/callee/src/main.sw new file mode 100644 index 00000000000..8cdc91e8fce --- /dev/null +++ b/examples/multi_contract_calls/callee/src/main.sw @@ -0,0 +1,11 @@ +contract; + +abi CalleeContract { + fn test_true() -> bool; +} + +impl CalleeContract for Contract { + fn test_true() -> bool { + true + } +} diff --git a/examples/multi_contract_calls/caller/.gitignore b/examples/multi_contract_calls/caller/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/examples/multi_contract_calls/caller/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/examples/multi_contract_calls/caller/Forc.toml b/examples/multi_contract_calls/caller/Forc.toml new file mode 100644 index 00000000000..ec9ddce7d28 --- /dev/null +++ b/examples/multi_contract_calls/caller/Forc.toml @@ -0,0 +1,13 @@ +# ANCHOR: multi_contract_call_toml +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "caller" + +[dependencies] +std = { path = "../../../sway-lib-std/" } + +[contract-dependencies] +callee = { path = "../callee" } +# ANCHOR: multi_contract_call_toml diff --git a/examples/multi_contract_calls/caller/src/main.sw b/examples/multi_contract_calls/caller/src/main.sw new file mode 100644 index 00000000000..fdb02cbcbfa --- /dev/null +++ b/examples/multi_contract_calls/caller/src/main.sw @@ -0,0 +1,28 @@ +// ANCHOR: multi_contract_calls +contract; + +abi CallerContract { + fn test_false() -> bool; +} + +impl CallerContract for Contract { + fn test_false() -> bool { + false + } +} + +abi CalleeContract { + fn test_true() -> bool; +} + +#[test] +fn test_multi_contract_calls() { + let caller = abi(CallerContract, CONTRACT_ID); + let callee = abi(CalleeContract, callee::CONTRACT_ID); + + let should_be_false = caller.test_false(); + let should_be_true = callee.test_true(); + assert(!should_be_false); + assert(should_be_true); +} +// ANCHOR: multi_contract_calls diff --git a/examples/storage_variables/src/main.sw b/examples/storage_variables/src/main.sw index 3b2ec658223..0fc1ff970ea 100644 --- a/examples/storage_variables/src/main.sw +++ b/examples/storage_variables/src/main.sw @@ -38,7 +38,6 @@ impl StorageExample for Contract { storage.var2.z = true; } // ANCHOR_END: storage_write - // ANCHOR: storage_read #[storage(read)] fn get_something() -> (u64, u64, b256, bool) { diff --git a/forc-pkg/src/manifest.rs b/forc-pkg/src/manifest.rs index be29c71f928..e11f48b1684 100644 --- a/forc-pkg/src/manifest.rs +++ b/forc-pkg/src/manifest.rs @@ -125,7 +125,7 @@ impl ManifestFile { type PatchMap = BTreeMap; /// A [PackageManifest] that was deserialized from a file at a particular path. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct PackageManifestFile { /// The deserialized `Forc.toml`. manifest: PackageManifest, @@ -134,7 +134,7 @@ pub struct PackageManifestFile { } /// A direct mapping to a `Forc.toml`. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct PackageManifest { pub project: Project, @@ -148,7 +148,7 @@ pub struct PackageManifest { pub contract_dependencies: Option>, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Project { pub authors: Option>, @@ -161,14 +161,14 @@ pub struct Project { pub forc_version: Option, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Network { #[serde(default = "default_url")] pub url: String, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct ContractDependency { #[serde(flatten)] @@ -177,7 +177,7 @@ pub struct ContractDependency { pub salt: fuel_tx::Salt, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(untagged)] pub enum Dependency { /// In the simple format, only a version is specified, eg. @@ -189,7 +189,7 @@ pub enum Dependency { Detailed(DependencyDetails), } -#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct DependencyDetails { pub(crate) version: Option, @@ -202,7 +202,7 @@ pub struct DependencyDetails { } /// Parameters to pass through to the `sway_core::BuildConfig` during compilation. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct BuildProfile { pub print_ast: bool, diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 27e816cf6da..936bc795999 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -26,6 +26,7 @@ use std::{ hash::{Hash, Hasher}, path::{Path, PathBuf}, str::FromStr, + sync::Arc, }; use sway_core::{ abi_generation::{ @@ -162,14 +163,12 @@ pub struct PkgTestEntry { } /// The result of successfully compiling a workspace. -/// -/// This is a map from each member package name to its associated built package. -pub type BuiltWorkspace = HashMap; +pub type BuiltWorkspace = Vec>; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Built { /// Represents a standalone package build. - Package(Box), + Package(Arc), /// Represents a workspace build. Workspace(BuiltWorkspace), } @@ -513,20 +512,30 @@ impl BuiltPackage { } impl Built { - /// Returns a map between package names and their corresponding built package. - pub fn into_members(self) -> Result> { + /// Returns an iterator yielding all member built packages. + pub fn into_members<'a>( + &'a self, + ) -> Box)> + 'a> { + // NOTE: Since pkg is a `Arc<_>`, pkg clones in this function are only reference + // increments. `BuiltPackage` struct does not get copied.` match self { - Built::Package(built_pkg) => { - Ok(std::iter::once((built_pkg.descriptor.name.clone(), *built_pkg)).collect()) + Built::Package(pkg) => { + let pinned = &pkg.as_ref().descriptor.pinned; + let pkg = pkg.clone(); + Box::new(std::iter::once((pinned, pkg))) } - Built::Workspace(built_workspace) => Ok(built_workspace), + Built::Workspace(workspace) => Box::new( + workspace + .iter() + .map(|pkg| (&pkg.descriptor.pinned, pkg.clone())), + ), } } /// Tries to retrieve the `Built` as a `BuiltPackage`. - pub fn expect_pkg(self) -> Result { + pub fn expect_pkg(self) -> Result> { match self { - Built::Package(built_pkg) => Ok(*built_pkg), + Built::Package(built_pkg) => Ok(built_pkg), Built::Workspace(_) => bail!("expected `Built` to be `Built::Package`"), } } @@ -685,6 +694,23 @@ impl BuildPlan { Ok(plan) } + /// Produce an iterator yielding all contract dependencies of given node in the order of + /// compilation. + pub fn contract_dependencies(&self, node: NodeIx) -> impl Iterator + '_ { + let graph = self.graph(); + let connected: HashSet<_> = Dfs::new(graph, node).iter(graph).collect(); + self.compilation_order() + .iter() + .cloned() + .filter(move |&n| n != node) + .filter(|&n| { + graph + .edges_directed(n, Direction::Incoming) + .any(|edge| matches!(edge.weight().kind, DepKind::Contract { .. })) + }) + .filter(move |&n| connected.contains(&n)) + } + /// Produce an iterator yielding all workspace member nodes in order of compilation. /// /// In the case that this `BuildPlan` was constructed for a single package, @@ -2039,7 +2065,7 @@ pub fn build_with_options(build_options: BuildOpts) -> Result { let outputs = member_filter.filter_outputs(&build_plan, outputs); // Build it! - let mut built_workspace = HashMap::new(); + let mut built_workspace = Vec::new(); let build_start = std::time::Instant::now(); let built_packages = build( &build_plan, @@ -2069,15 +2095,16 @@ pub fn build_with_options(build_options: BuildOpts) -> Result { built_package.write_debug_info(outfile.as_ref())?; } built_package.write_output(minify.clone(), &pkg_manifest.project.name, &output_dir)?; - built_workspace.insert(pinned.name.clone(), built_package); + built_workspace.push(Arc::new(built_package)); } match curr_manifest { Some(pkg_manifest) => { let built_pkg = built_workspace - .remove(&pkg_manifest.project.name.to_string()) + .into_iter() + .find(|pkg| pkg.descriptor.manifest_file == *pkg_manifest) .expect("package didn't exist in workspace"); - Ok(Built::Package(Box::new(built_pkg))) + Ok(Built::Package(built_pkg)) } None => Ok(Built::Workspace(built_workspace)), } @@ -2252,7 +2279,6 @@ pub fn build( let constant_declarations = vec![(contract_id_constant_name, contract_id_constant)]; const_inject_map.insert(pkg.clone(), constant_declarations); } - Some(compiled_without_tests.bytecode) } else { None diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 401ae5dee77..ce026fae755 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path, sync::Arc}; use anyhow::Result; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; @@ -7,7 +7,7 @@ use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; pub(crate) fn built_pkgs_with_manifest( path: &Path, build_opts: BuildOpts, -) -> Result> { +) -> Result)>> { let manifest_file = ManifestFile::from_dir(path)?; let mut member_manifests = manifest_file.member_manifests()?; let lock_path = manifest_file.lock_path()?; @@ -18,15 +18,17 @@ pub(crate) fn built_pkgs_with_manifest( build_opts.pkg.offline, )?; let graph = build_plan.graph(); - let mut built_pkgs = build_with_options(build_opts)?.into_members()?; + let built = build_with_options(build_opts)?; + let mut built_pkgs: HashMap<&pkg::Pinned, Arc<_>> = built.into_members().collect(); let mut pkgs_with_manifest = Vec::new(); for member_index in build_plan.member_nodes() { - let pkg_name = &graph[member_index].name; + let pkg = &graph[member_index]; + let pkg_name = &pkg.name; // Check if the currrent member is built. // // For indivual members of the workspace, member nodes would be iterating // over all the members but only the relevant member would be built. - if let Some(built_pkg) = built_pkgs.remove(pkg_name) { + if let Some(built_pkg) = built_pkgs.remove(pkg) { let member_manifest = member_manifests .remove(pkg_name) .expect("Member manifest file is missing"); diff --git a/forc-test/src/lib.rs b/forc-test/src/lib.rs index 376ae68929b..273faff5f51 100644 --- a/forc-test/src/lib.rs +++ b/forc-test/src/lib.rs @@ -1,5 +1,3 @@ -use std::{fs, path::PathBuf, sync::Arc}; - use forc_pkg as pkg; use fuel_abi_types::error_codes::ErrorSignal; use fuel_tx as tx; @@ -9,6 +7,7 @@ use fuel_vm::{self as vm, fuel_asm, prelude::Instruction}; use pkg::TestPassCondition; use pkg::{Built, BuiltPackage}; use rand::{Rng, SeedableRng}; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; use sway_core::BuildTarget; use sway_types::Span; @@ -55,6 +54,8 @@ pub struct TestResult { } const TEST_METADATA_SEED: u64 = 0x7E57u64; +/// A mapping from each member package of a build plan to its compiled contract dependencies. +type ContractDependencyMap = HashMap>>; /// A package or a workspace that has been built, ready for test execution. pub enum BuiltTests { @@ -69,16 +70,17 @@ pub enum BuiltTests { #[derive(Debug)] pub enum PackageTests { Contract(ContractToTest), - NonContract(pkg::BuiltPackage), + NonContract(Arc), } /// A built contract ready for test execution. #[derive(Debug)] pub struct ContractToTest { /// Tests included contract. - pub pkg: pkg::BuiltPackage, + pub pkg: Arc, /// Bytecode of the contract without tests. pub without_tests_bytecode: pkg::BuiltPackageBytecode, + pub contract_dependencies: Vec>, } /// The set of options provided to the `test` function. @@ -115,24 +117,117 @@ pub struct TestPrintOpts { /// The storage and the contract id (if a contract is being tested) for a test. #[derive(Debug)] -struct TestSetup { +enum TestSetup { + ContractSetup(ContractTestSetup), + NonContractSetup(vm::storage::MemoryStorage), +} + +impl TestSetup { + /// Returns the storage for this test setup + fn storage(&self) -> &vm::storage::MemoryStorage { + match self { + TestSetup::ContractSetup(contract_setup) => &contract_setup.storage, + TestSetup::NonContractSetup(storage) => storage, + } + } + + /// Produces an iterator yielding contract ids of contract dependencies for this test setup. + fn contract_dependency_ids(&self) -> impl Iterator + '_ { + match self { + TestSetup::ContractSetup(contract_setup) => { + contract_setup.contract_dependency_ids.iter() + } + TestSetup::NonContractSetup(_) => [].iter(), + } + } + + /// Return the root contract id if this is a contract setup. + fn root_contract_id(&self) -> Option { + if let TestSetup::ContractSetup(contract_setup) = self { + Some(contract_setup.root_contract_id) + } else { + None + } + } + + /// Produces an iterator yielding all contract ids required to be included in the transaction + /// for this test setup. + fn contract_ids(&self) -> impl Iterator + '_ { + self.contract_dependency_ids() + .cloned() + .chain(self.root_contract_id()) + } +} + +/// The data collected to test a contract. +#[derive(Debug)] +struct ContractTestSetup { storage: vm::storage::MemoryStorage, - contract_id: Option, + contract_dependency_ids: Vec, + root_contract_id: tx::ContractId, +} + +impl ContractToTest { + /// Deploy the contract dependencies and tests excluded version for this package. + fn deploy(&self) -> anyhow::Result { + // Setup the interpreter for deployment. + let params = tx::ConsensusParameters::default(); + let storage = vm::storage::MemoryStorage::default(); + let mut interpreter = + vm::interpreter::Interpreter::with_storage(storage, params, GasCosts::default()); + + // Iterate and create deployment transactions for contract dependencies of the root + // contract. + let contract_dependency_setups = self + .contract_dependencies + .iter() + .map(|built_pkg| deployment_transaction(built_pkg, &built_pkg.bytecode, params)); + + // Deploy contract dependencies of the root contract and collect their ids. + let contract_dependency_ids = contract_dependency_setups + .map(|(contract_id, tx)| { + // Transact the deployment transaction constructed for this contract dependency. + interpreter.transact(tx)?; + Ok(contract_id) + }) + .collect::>>()?; + + // Root contract is the contract that we are going to be running the tests of, after this + // deployment. + let (root_contract_id, root_contract_tx) = + deployment_transaction(&self.pkg, &self.without_tests_bytecode, params); + + // Deploy the root contract. + interpreter.transact(root_contract_tx)?; + let storage = interpreter.as_ref().clone(); + + let contract_test_setup = ContractTestSetup { + storage, + contract_dependency_ids, + root_contract_id, + }; + + Ok(TestSetup::ContractSetup(contract_test_setup)) + } } impl BuiltTests { /// Constructs a `PackageTests` from `Built`. /// - /// Contracts are already compiled once without tests included to do `CONTRACT_ID` injection. `built_contracts` map holds already compiled contracts so that they can be matched with their "tests included" version. - pub(crate) fn from_built(built: Built) -> anyhow::Result { + /// `contract_dependencies` represents ordered (by deployment order) packages that needs to be deployed for each package, before executing the test. + pub(crate) fn from_built( + built: Built, + contract_dependencies: &ContractDependencyMap, + ) -> anyhow::Result { let built = match built { - Built::Package(built_pkg) => { - BuiltTests::Package(PackageTests::from_built_pkg(*built_pkg)) - } + Built::Package(built_pkg) => BuiltTests::Package(PackageTests::from_built_pkg( + built_pkg, + contract_dependencies, + )), Built::Workspace(built_workspace) => { let pkg_tests = built_workspace - .into_values() - .map(PackageTests::from_built_pkg) + .into_iter() + .map(|built_pkg| PackageTests::from_built_pkg(built_pkg, contract_dependencies)) .collect(); BuiltTests::Workspace(pkg_tests) } @@ -154,15 +249,21 @@ impl<'a> PackageTests { } /// Construct a `PackageTests` from `BuiltPackage`. - /// - /// If the `BuiltPackage` is a contract, match the contract with the contract's - fn from_built_pkg(built_pkg: BuiltPackage) -> PackageTests { + fn from_built_pkg( + built_pkg: Arc, + contract_dependencies: &ContractDependencyMap, + ) -> PackageTests { let built_without_tests_bytecode = built_pkg.bytecode_without_tests.clone(); + let contract_dependencies: Vec> = contract_dependencies + .get(&built_pkg.descriptor.pinned) + .cloned() + .unwrap_or_default(); match built_without_tests_bytecode { Some(contract_without_tests) => { let contract_to_test = ContractToTest { pkg: built_pkg, without_tests_bytecode: contract_without_tests, + contract_dependencies, }; PackageTests::Contract(contract_to_test) } @@ -233,15 +334,12 @@ impl<'a> PackageTests { fn setup(&self) -> anyhow::Result { match self { PackageTests::Contract(contract_to_test) => { - let contract_pkg = &contract_to_test.pkg; - let contract_pkg_without_tests = &contract_to_test.without_tests_bytecode; - let test_setup = deploy_test_contract(contract_pkg, contract_pkg_without_tests)?; + let test_setup = contract_to_test.deploy()?; Ok(test_setup) } - PackageTests::NonContract(_) => Ok(TestSetup { - storage: vm::storage::MemoryStorage::default(), - contract_id: None, - }), + PackageTests::NonContract(_) => Ok(TestSetup::NonContractSetup( + vm::storage::MemoryStorage::default(), + )), } } } @@ -345,16 +443,40 @@ impl BuiltTests { /// First builds the package or workspace, ready for execution. pub fn build(opts: Opts) -> anyhow::Result { let build_opts = opts.into_build_opts(); + let build_plan = pkg::BuildPlan::from_build_opts(&build_opts)?; let built = pkg::build_with_options(build_opts)?; - BuiltTests::from_built(built) + let built_members: HashMap<&pkg::Pinned, Arc> = built.into_members().collect(); + + // For each member node collect their contract dependencies. + let member_contract_dependencies: HashMap>> = + build_plan + .member_nodes() + .map(|member_node| { + let graph = build_plan.graph(); + let pinned_member = graph[member_node].clone(); + let contract_dependencies = build_plan + .contract_dependencies(member_node) + .map(|contract_depency_node_ix| graph[contract_depency_node_ix].clone()) + .filter_map(|pinned| built_members.get(&pinned)) + .cloned() + .collect(); + + (pinned_member, contract_dependencies) + }) + .collect(); + BuiltTests::from_built(built, &member_contract_dependencies) } +/// Result of preparing a deployment transaction setup for a contract. +type ContractDeploymentSetup = (tx::ContractId, vm::checked_transaction::Checked); + /// Deploys the provided contract and returns an interpreter instance ready to be used in test /// executions with deployed contract. -fn deploy_test_contract( +fn deployment_transaction( built_pkg: &pkg::BuiltPackage, without_tests_bytecode: &pkg::BuiltPackageBytecode, -) -> anyhow::Result { + params: tx::ConsensusParameters, +) -> ContractDeploymentSetup { // Obtain the contract id for deployment. let mut storage_slots = built_pkg.storage_slots.clone(); storage_slots.sort(); @@ -365,12 +487,6 @@ fn deploy_test_contract( let salt = tx::Salt::zeroed(); let contract_id = contract.id(&salt, &root, &state_root); - // Setup the interpreter for deployment. - let params = tx::ConsensusParameters::default(); - let storage = vm::storage::MemoryStorage::default(); - let mut interpreter = - vm::interpreter::Interpreter::with_storage(storage, params, GasCosts::default()); - // Create the deployment transaction. let mut rng = rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED); @@ -388,14 +504,7 @@ fn deploy_test_contract( .add_output(tx::Output::contract_created(contract_id, state_root)) .maturity(maturity) .finalize_checked(block_height, ¶ms, &GasCosts::default()); - - // Deploy the contract. - interpreter.transact(tx)?; - let storage_after_deploy = interpreter.as_ref(); - Ok(TestSetup { - storage: storage_after_deploy.clone(), - contract_id: Some(contract_id), - }) + (contract_id, tx) } /// Build the given package and run its tests, returning the results. @@ -462,8 +571,7 @@ fn exec_test( std::time::Duration, Vec, ) { - let storage = test_setup.storage; - let contract_id = test_setup.contract_id; + let storage = test_setup.storage().clone(); // Patch the bytecode to jump to the relevant test. let bytecode = patch_test_bytecode(bytecode, test_offset).into_owned(); @@ -487,7 +595,9 @@ fn exec_test( .gas_limit(tx::ConsensusParameters::DEFAULT.max_gas_per_tx) .maturity(maturity) .clone(); - if let Some(contract_id) = contract_id { + let mut output_index = 1; + // Insert contract ids into tx input + for contract_id in test_setup.contract_ids() { tx.add_input(tx::Input::Contract { utxo_id: tx::UtxoId::new(tx::Bytes32::zeroed(), 0), balance_root: tx::Bytes32::zeroed(), @@ -496,10 +606,11 @@ fn exec_test( contract_id, }) .add_output(tx::Output::Contract { - input_index: 1, + input_index: output_index, balance_root: fuel_tx::Bytes32::zeroed(), state_root: tx::Bytes32::zeroed(), }); + output_index += 1; } let tx = tx.finalize_checked(block_height, ¶ms, &GasCosts::default()); diff --git a/forc-util/src/lib.rs b/forc-util/src/lib.rs index 1eefe5873e6..e4001b36205 100644 --- a/forc-util/src/lib.rs +++ b/forc-util/src/lib.rs @@ -192,7 +192,7 @@ pub fn print_compiling(ty: Option<&TreeType>, name: &str, src: &dyn std::fmt::Di Some(ty) => format!("{} ", program_type_str(ty)), None => "".to_string(), }; - tracing::error!( + tracing::info!( " {} {ty}{} ({src})", Colour::Green.bold().paint("Compiling"), ansi_term::Style::new().bold().paint(name) diff --git a/sway-types/src/lib.rs b/sway-types/src/lib.rs index 9ea58efb2ab..264a71e45c7 100644 --- a/sway-types/src/lib.rs +++ b/sway-types/src/lib.rs @@ -98,7 +98,7 @@ where } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct ConfigTimeConstant { pub r#type: String, pub value: String, diff --git a/test/src/e2e_vm_tests/mod.rs b/test/src/e2e_vm_tests/mod.rs index cb20a912b74..a32c64e2a4a 100644 --- a/test/src/e2e_vm_tests/mod.rs +++ b/test/src/e2e_vm_tests/mod.rs @@ -127,7 +127,7 @@ impl TestContext { let compiled = result?; let compiled = match compiled { - forc_pkg::Built::Package(built_pkg) => *built_pkg, + forc_pkg::Built::Package(built_pkg) => built_pkg.as_ref().clone(), forc_pkg::Built::Workspace(_) => { panic!("workspaces are not supported in the test suite yet") } @@ -209,11 +209,16 @@ impl TestContext { built_pkg.warnings.len() ))); } - vec![(name.clone(), *built_pkg)] + vec![(name.clone(), built_pkg.as_ref().clone())] } forc_pkg::Built::Workspace(built_workspace) => built_workspace .iter() - .map(|(n, b)| (n.clone(), b.clone())) + .map(|built_pkg| { + ( + built_pkg.descriptor.pinned.name.clone(), + built_pkg.as_ref().clone(), + ) + }) .collect(), }; diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/contract_multi_test/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/contract_multi_test/src/main.sw index 9b586da2bff..df83b693358 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/contract_multi_test/src/main.sw +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/contract_multi_test/src/main.sw @@ -19,14 +19,14 @@ fn test_foo() { fn test_fail() { let caller = abi(MyContract, CONTRACT_ID); let result = caller.test_function {}(); - assert(result == false) + assert(!result) } #[test] fn test_success() { let caller = abi(MyContract, CONTRACT_ID); let result = caller.test_function {}(); - assert(result == true) + assert(result) } #[test] diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.lock new file mode 100644 index 00000000000..abdb0654b29 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.lock @@ -0,0 +1,19 @@ +[[package]] +name = 'contract2' +source = 'member' +dependencies = ['std'] + +[[package]] +name = 'contract_multi_test' +source = 'member' +dependencies = ['std'] +contract-dependencies = ['contract2'] + +[[package]] +name = 'core' +source = 'path+from-root-C59B26C9F5F20739' + +[[package]] +name = 'std' +source = 'path+from-root-C59B26C9F5F20739' +dependencies = ['core'] diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.toml new file mode 100644 index 00000000000..a2e7b78d22c --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/Forc.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["contract_multi_test", "contract2"] diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.lock new file mode 100644 index 00000000000..cc15549119c --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.lock @@ -0,0 +1,13 @@ +[[package]] +name = 'contract_multi_test' +source = 'member' +dependencies = ['std'] + +[[package]] +name = 'core' +source = 'path+from-root-28E4A5A6A7E567F7' + +[[package]] +name = 'std' +source = 'path+from-root-28E4A5A6A7E567F7' +dependencies = ['core'] diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.toml new file mode 100644 index 00000000000..0f3d4106ed2 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract2" + +[dependencies] +std = { path = "../../../../../../../../sway-lib-std" } diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/src/main.sw new file mode 100644 index 00000000000..d3804544fbb --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract2/src/main.sw @@ -0,0 +1,11 @@ +contract; + +abi MyContract2 { + fn test_false() -> bool; +} + +impl MyContract2 for Contract { + fn test_false() -> bool { + false + } +} diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.lock new file mode 100644 index 00000000000..cc15549119c --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.lock @@ -0,0 +1,13 @@ +[[package]] +name = 'contract_multi_test' +source = 'member' +dependencies = ['std'] + +[[package]] +name = 'core' +source = 'path+from-root-28E4A5A6A7E567F7' + +[[package]] +name = 'std' +source = 'path+from-root-28E4A5A6A7E567F7' +dependencies = ['core'] diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.toml new file mode 100644 index 00000000000..5d8e7a99c00 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/Forc.toml @@ -0,0 +1,11 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "contract_multi_test" + +[dependencies] +std = { path = "../../../../../../../../sway-lib-std" } + +[contract-dependencies] +contract2 = { path = "../contract2/" } diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/src/main.sw new file mode 100644 index 00000000000..4a46963be16 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/contract_multi_test/src/main.sw @@ -0,0 +1,41 @@ +contract; + +abi MyContract { + fn test_true() -> bool; +} + +impl MyContract for Contract { + fn test_true() -> bool { + true + } +} + +abi MyContract2 { + fn test_false() -> bool; +} + +#[test] +fn test_contract_call() { + let caller = abi(MyContract, CONTRACT_ID); + let result = caller.test_true {}(); + assert(result == true) +} + +#[test] +fn test_contract_2_call() { + let caller = abi(MyContract2, contract2::CONTRACT_ID); + let result = caller.test_false {}(); + assert(result == false) +} + +#[test] +fn test_contract_multi_call() { + let caller = abi(MyContract, CONTRACT_ID); + let caller2 = abi(MyContract2, contract2::CONTRACT_ID); + + let should_be_true = caller.test_true {}(); + let should_be_false = caller2.test_false {}(); + + assert(should_be_true == true); + assert(should_be_false == false); +} diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/test.toml b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/test.toml new file mode 100644 index 00000000000..0f3f6d7e866 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/unit_tests/multi-contract-calls/test.toml @@ -0,0 +1 @@ +category = "unit_tests_pass"