Skip to content

Commit

Permalink
feat: multi contract calls in unit tests for contracts (#4156)
Browse files Browse the repository at this point in the history
## 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.

<img width="787" alt="image"
src="https://user-images.githubusercontent.com/20915464/224345002-92dc2bcb-823d-4971-9041-31111cf85e77.png">

### 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 <kayagokalp@fuel.sh>
  • Loading branch information
2 people authored and anton-trunov committed Mar 27, 2023
1 parent a02b125 commit 2c54fcf
Show file tree
Hide file tree
Showing 27 changed files with 442 additions and 82 deletions.
12 changes: 12 additions & 0 deletions docs/book/src/testing/unit-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
```
19 changes: 19 additions & 0 deletions examples/multi_contract_calls/Forc.lock
Original file line number Diff line number Diff line change
@@ -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']
3 changes: 3 additions & 0 deletions examples/multi_contract_calls/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[workspace]

members = ["caller", "callee"]
2 changes: 2 additions & 0 deletions examples/multi_contract_calls/callee/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
8 changes: 8 additions & 0 deletions examples/multi_contract_calls/callee/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "main.sw"
license = "Apache-2.0"
name = "callee"

[dependencies]
std = { path = "../../../sway-lib-std/" }
11 changes: 11 additions & 0 deletions examples/multi_contract_calls/callee/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
contract;

abi CalleeContract {
fn test_true() -> bool;
}

impl CalleeContract for Contract {
fn test_true() -> bool {
true
}
}
2 changes: 2 additions & 0 deletions examples/multi_contract_calls/caller/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
13 changes: 13 additions & 0 deletions examples/multi_contract_calls/caller/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ANCHOR: multi_contract_call_toml
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
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
28 changes: 28 additions & 0 deletions examples/multi_contract_calls/caller/src/main.sw
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion examples/storage_variables/src/main.sw
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 8 additions & 8 deletions forc-pkg/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ impl ManifestFile {
type PatchMap = BTreeMap<String, Dependency>;

/// 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,
Expand All @@ -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,
Expand All @@ -148,7 +148,7 @@ pub struct PackageManifest {
pub contract_dependencies: Option<BTreeMap<String, ContractDependency>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct Project {
pub authors: Option<Vec<String>>,
Expand All @@ -161,14 +161,14 @@ pub struct Project {
pub forc_version: Option<semver::Version>,
}

#[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)]
Expand All @@ -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.
Expand All @@ -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<String>,
Expand All @@ -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,
Expand Down
60 changes: 43 additions & 17 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use std::{
hash::{Hash, Hasher},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use sway_core::{
abi_generation::{
Expand Down Expand Up @@ -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<String, BuiltPackage>;
pub type BuiltWorkspace = Vec<Arc<BuiltPackage>>;

#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Built {
/// Represents a standalone package build.
Package(Box<BuiltPackage>),
Package(Arc<BuiltPackage>),
/// Represents a workspace build.
Workspace(BuiltWorkspace),
}
Expand Down Expand Up @@ -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<HashMap<String, BuiltPackage>> {
/// Returns an iterator yielding all member built packages.
pub fn into_members<'a>(
&'a self,
) -> Box<dyn Iterator<Item = (&Pinned, Arc<BuiltPackage>)> + '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<BuiltPackage> {
pub fn expect_pkg(self) -> Result<Arc<BuiltPackage>> {
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`"),
}
}
Expand Down Expand Up @@ -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<Item = NodeIx> + '_ {
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,
Expand Down Expand Up @@ -2039,7 +2065,7 @@ pub fn build_with_options(build_options: BuildOpts) -> Result<Built> {
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,
Expand Down Expand Up @@ -2069,15 +2095,16 @@ pub fn build_with_options(build_options: BuildOpts) -> Result<Built> {
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)),
}
Expand Down Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions forc-plugins/forc-client/src/util/pkg.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -7,7 +7,7 @@ use pkg::{build_with_options, BuiltPackage, PackageManifestFile};
pub(crate) fn built_pkgs_with_manifest(
path: &Path,
build_opts: BuildOpts,
) -> Result<Vec<(PackageManifestFile, BuiltPackage)>> {
) -> Result<Vec<(PackageManifestFile, Arc<BuiltPackage>)>> {
let manifest_file = ManifestFile::from_dir(path)?;
let mut member_manifests = manifest_file.member_manifests()?;
let lock_path = manifest_file.lock_path()?;
Expand All @@ -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");
Expand Down
Loading

0 comments on commit 2c54fcf

Please sign in to comment.