Skip to content

Commit

Permalink
feat: inject contract id into the namespace for tests automatically (#…
Browse files Browse the repository at this point in the history
…3729)

closes #3673.

## About this PR

This PR adds `CONTRACT_ID` injection while building contracts with `forc
build --test`. `CONTRACT_ID` is the id of the contract without the tests
added. To find out that id, we first compile the contract without tests
enabled.

The rough outline of stuff happening here:

1. We iterate over `BuildPlan` members to find out contracts and collect
their contract id into an injection map. In this step only the contracts
are built. To determine contract id we need to compile the contract
without tests. Since we also need the bytecode of the contract without
tests, we are collecting them as we come across them while iterating
over members.
2. With the injection map build and execute all the tests, so basically
after first step we are just doing the old `forc-test` behaviour.

So basically I added a pre-processing step for contract id collection
for those members that require it (contracts).

This enables the following test structure:

```rust
let caller = abi(MyContract, CONTRACT_ID);
let result = caller.test_function {}();
assert(result == true)
```
  • Loading branch information
kayagokalp authored and sdankel committed Jan 25, 2023
1 parent 5f415ab commit b8aa28b
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 59 deletions.
8 changes: 3 additions & 5 deletions docs/book/src/testing/unit-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ To test the `test_function()`, a unit test like the following can be written.
```sway
#[test]
fn test_success() {
let contract_id = 0xa8f18533afc18453323bdf17c83750c556916ab183daacf46d7a8d3c633a40ee;
let caller = abi(MyContract, contract_id);
let caller = abi(MyContract, CONTRACT_ID);
let result = caller.test_function {}();
assert(result == true)
}
Expand All @@ -86,11 +85,10 @@ It is also possible to test failure with contract calls as well.
```sway
#[test(should_revert)]
fn test_fail() {
let contract_id = 0xa8f18533afc18453323bdf17c83750c556916ab183daacf46d7a8d3c633a40ee;
let caller = abi(MyContract, contract_id);
let caller = abi(MyContract, CONTRACT_ID);
let result = caller.test_function {}();
assert(result == false)
}
```

> **Note:** `contract_id` is needed for the `abi` cast used in the test. Running `forc test` will output deployed contract's id and that can be used for the cast. This means, before writing the `test_success()` test, `forc test` needs to be executed to retrieve the `contract_id`. This will not be necessary in the future and you can track the progress at [here](https://github.com/FuelLabs/sway/issues/3673).
> **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.
52 changes: 43 additions & 9 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,17 @@ pub struct BuiltPackage {
source_map: SourceMap,
pub pkg_name: String,
pub decl_engine: DeclEngine,
pub built_pkg_descriptor: BuiltPackageDescriptor,
}

/// The package descriptors that a `BuiltPackage` holds so that the source used for building the
/// package can be retrieved later on.
#[derive(Debug, Clone)]
pub struct BuiltPackageDescriptor {
/// The manifest file of the package.
pub manifest_file: PackageManifestFile,
/// The pinned version of the package.
pub pinned: Pinned,
}

/// The result of successfully compiling a workspace.
Expand Down Expand Up @@ -333,7 +343,25 @@ pub struct BuildOpts {
/// Include all test functions within the build.
pub tests: bool,
/// List of constants to inject for each package.
pub inject_map: ConstInjectionMap,
pub const_inject_map: ConstInjectionMap,
}

impl BuildOpts {
/// Return a `BuildOpts` with modified `tests` field.
pub fn include_tests(self, include_tests: bool) -> Self {
Self {
tests: include_tests,
..self
}
}

/// Return a `BuildOpts` with modified `injection_map` field.
pub fn const_injection_map(self, const_inject_map: ConstInjectionMap) -> Self {
Self {
const_inject_map,
..self
}
}
}

impl GitSourceIndex {
Expand Down Expand Up @@ -2228,6 +2256,9 @@ pub fn sway_build_config(
Ok(build_config)
}

/// The name of the constant holding the contract's id.
pub const CONTRACT_ID_CONSTANT_NAME: &str = "CONTRACT_ID";

/// Builds the dependency namespace for the package at the given node index within the graph.
///
/// This function is designed to be called for each node in order of compilation.
Expand Down Expand Up @@ -2273,14 +2304,13 @@ pub fn dependency_namespace(
};

// Construct namespace with contract id
let contract_dep_constant_name = "CONTRACT_ID";
let contract_id_value = format!("0x{dep_contract_id}");
let contract_id_constant = ConfigTimeConstant {
r#type: "b256".to_string(),
value: contract_id_value,
public: true,
};
constants.insert(contract_dep_constant_name.to_string(), contract_id_constant);
constants.insert(CONTRACT_ID_CONSTANT_NAME.to_string(), contract_id_constant);
namespace::Module::default_with_constants(engines, constants)?
}
};
Expand Down Expand Up @@ -2504,6 +2534,10 @@ pub fn compile(
}
}

let built_pkg_descriptor = BuiltPackageDescriptor {
manifest_file: manifest.clone(),
pinned: pkg.clone(),
};
let bytecode = bytes;
let built_package = BuiltPackage {
build_target,
Expand All @@ -2515,7 +2549,7 @@ pub fn compile(
source_map: source_map.to_owned(),
pkg_name: pkg.name.clone(),
decl_engine: engines.de().clone(),
manifest_file: manifest.clone(),
built_pkg_descriptor,
};
Ok((built_package, namespace))
}
Expand Down Expand Up @@ -2597,7 +2631,7 @@ pub fn build_with_options(build_options: BuildOpts) -> Result<Built> {
binary_outfile,
debug_outfile,
pkg,
inject_map,
const_inject_map,
build_target,
..
} = &build_options;
Expand Down Expand Up @@ -2640,7 +2674,7 @@ pub fn build_with_options(build_options: BuildOpts) -> Result<Built> {
*build_target,
&build_profile,
&outputs,
inject_map,
const_inject_map,
)?;
let output_dir = pkg.output_directory.as_ref().map(PathBuf::from);
for (node_ix, built_package) in built_packages.into_iter() {
Expand Down Expand Up @@ -2674,7 +2708,7 @@ pub fn build_with_options(build_options: BuildOpts) -> Result<Built> {
}

/// Returns the ContractId of a built_package contract with specified `salt`.
fn contract_id(built_package: &BuiltPackage, salt: &fuel_tx::Salt) -> ContractId {
pub fn contract_id(built_package: &BuiltPackage, salt: &fuel_tx::Salt) -> ContractId {
// Construct the contract ID
let contract = Contract::from(built_package.bytecode.clone());
let mut storage_slots = built_package.storage_slots.clone();
Expand Down Expand Up @@ -2718,7 +2752,7 @@ pub fn build(
target: BuildTarget,
profile: &BuildProfile,
outputs: &HashSet<NodeIx>,
inject_map: &ConstInjectionMap,
const_inject_map: &ConstInjectionMap,
) -> anyhow::Result<Vec<(NodeIx, BuiltPackage)>> {
let mut built_packages = Vec::new();

Expand All @@ -2741,7 +2775,7 @@ pub fn build(
let mut source_map = SourceMap::new();
let pkg = &plan.graph()[node];
let manifest = &plan.manifest_map()[&pkg.id()];
let constants = if let Some(injected_ctc) = inject_map.get(pkg) {
let constants = if let Some(injected_ctc) = const_inject_map.get(pkg) {
let mut constants = manifest.config_time_constants();
constants.extend(
injected_ctc
Expand Down
4 changes: 2 additions & 2 deletions forc-plugins/forc-client/src/ops/deploy/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub async fn deploy_pkg(
}

fn build_opts_from_cmd(cmd: &DeployCommand) -> pkg::BuildOpts {
let inject_map = std::collections::HashMap::new();
let const_inject_map = std::collections::HashMap::new();
pkg::BuildOpts {
pkg: pkg::PkgOpts {
path: cmd.path.clone(),
Expand All @@ -147,6 +147,6 @@ fn build_opts_from_cmd(cmd: &DeployCommand) -> pkg::BuildOpts {
binary_outfile: cmd.binary_outfile.clone(),
debug_outfile: cmd.debug_outfile.clone(),
tests: false,
inject_map,
const_inject_map,
}
}
4 changes: 2 additions & 2 deletions forc-plugins/forc-client/src/ops/run/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ async fn send_tx(
}

fn build_opts_from_cmd(cmd: &RunCommand) -> pkg::BuildOpts {
let inject_map = std::collections::HashMap::new();
let const_inject_map = std::collections::HashMap::new();
pkg::BuildOpts {
pkg: pkg::PkgOpts {
path: cmd.path.clone(),
Expand All @@ -168,6 +168,6 @@ fn build_opts_from_cmd(cmd: &RunCommand) -> pkg::BuildOpts {
binary_outfile: cmd.binary_outfile.clone(),
debug_outfile: cmd.debug_outfile.clone(),
tests: false,
inject_map,
const_inject_map,
}
}
130 changes: 95 additions & 35 deletions forc-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
use std::{collections::HashSet, fs, path::PathBuf, sync::Arc};
use std::{
collections::{HashMap, HashSet},
fs,
path::PathBuf,
sync::Arc,
};

use forc_pkg as pkg;
use fuel_tx as tx;
use fuel_vm::{self as vm, prelude::Opcode};
use pkg::{Built, BuiltPackage};
use pkg::{Built, BuiltPackage, CONTRACT_ID_CONSTANT_NAME};
use rand::{distributions::Standard, prelude::Distribution, Rng, SeedableRng};
use sway_core::{language::ty::TyFunctionDeclaration, transform::AttributeKind, BuildTarget};
use sway_types::{Span, Spanned};
use sway_core::{
language::{parsed::TreeType, ty::TyFunctionDeclaration},
transform::AttributeKind,
BuildTarget,
};
use sway_types::{ConfigTimeConstant, Span, Spanned};
use tx::{AssetId, TxPointer, UtxoId};
use vm::prelude::SecretKey;

Expand Down Expand Up @@ -135,23 +144,21 @@ struct TestSetup {
impl BuiltTests {
/// Constructs a `PackageTests` from `Built`.
///
/// If the `built` is a workspace, `opts` is patched for the members of the workspace that
/// is a `Contract` so that only that contract is re-built.
///
/// If the `built` is a package, `PackageTests::from_built_pkg` is used.
pub(crate) fn from_built(built: Built, opts: Opts) -> anyhow::Result<BuiltTests> {
/// 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,
built_contracts: HashMap<pkg::Pinned, BuiltPackage>,
) -> anyhow::Result<BuiltTests> {
let built = match built {
Built::Package(pkg) => BuiltTests::Package(PackageTests::from_built_pkg(*pkg, opts)?),
Built::Workspace(workspace) => {
let packages = workspace
Built::Package(built_pkg) => {
BuiltTests::Package(PackageTests::from_built_pkg(*built_pkg, &built_contracts)?)
}
Built::Workspace(built_workspace) => {
let pkg_tests = built_workspace
.into_values()
.map(|built_pkg| {
let path = built_pkg.manifest_file.dir();
let patched_opts = opts.clone().patch_opts(path);
PackageTests::from_built_pkg(built_pkg, patched_opts)
})
.map(|built_pkg| PackageTests::from_built_pkg(built_pkg, &built_contracts))
.collect::<anyhow::Result<_>>()?;
BuiltTests::Workspace(packages)
BuiltTests::Workspace(pkg_tests)
}
};
Ok(built)
Expand All @@ -172,18 +179,21 @@ impl<'a> PackageTests {

/// Construct a `PackageTests` from `BuiltPackage`.
///
/// If the built package is a `Contract`, this will re-compile the package with tests disabled.
fn from_built_pkg(built_pkg: BuiltPackage, opts: Opts) -> anyhow::Result<PackageTests> {
/// If the `BuiltPackage` is a contract, match the contract with the contract's
fn from_built_pkg(
built_pkg: BuiltPackage,
built_contracts: &HashMap<pkg::Pinned, BuiltPackage>,
) -> anyhow::Result<PackageTests> {
let tree_type = &built_pkg.tree_type;
let package_test = match tree_type {
sway_core::language::parsed::TreeType::Contract => {
let mut build_opts_without_tests = opts.into_build_opts();
build_opts_without_tests.tests = false;
let pkg_without_tests =
pkg::build_with_options(build_opts_without_tests)?.expect_pkg()?;
let built_pkg_descriptor = &built_pkg.built_pkg_descriptor;
let built_contract_without_tests = built_contracts
.get(&built_pkg_descriptor.pinned)
.ok_or_else(|| anyhow::anyhow!("missing built contract without tests"))?;
let contract_to_test = ContractToTest {
tests_included: built_pkg,
tests_excluded: pkg_without_tests,
tests_excluded: built_contract_without_tests.clone(),
};
PackageTests::Contract(contract_to_test)
}
Expand Down Expand Up @@ -283,7 +293,7 @@ impl Distribution<TxMetadata> for Standard {
impl Opts {
/// Convert this set of test options into a set of build options.
pub fn into_build_opts(self) -> pkg::BuildOpts {
let inject_map = std::collections::HashMap::new();
let const_inject_map = std::collections::HashMap::new();
pkg::BuildOpts {
pkg: self.pkg,
print: self.print,
Expand All @@ -295,7 +305,7 @@ impl Opts {
release: self.release,
time_phases: self.time_phases,
tests: true,
inject_map,
const_inject_map,
}
}

Expand Down Expand Up @@ -367,12 +377,67 @@ impl BuiltTests {
}
}

/// Build all contracts in the given buld plan without tests.
fn build_contracts_without_tests(
opts: &Opts,
build_plan: &pkg::BuildPlan,
) -> Vec<(pkg::Pinned, anyhow::Result<BuiltPackage>)> {
let manifest_map = build_plan.manifest_map();
build_plan
.member_pinned_pkgs()
.map(|pinned_pkg| {
let pkg_manifest = manifest_map
.get(&pinned_pkg.id())
.expect("missing manifest for member to test");
(pinned_pkg, pkg_manifest)
})
.filter(|(_, pkg_manifest)| matches!(pkg_manifest.program_type(), Ok(TreeType::Contract)))
.map(|(pinned_pkg, pkg_manifest)| {
let pkg_path = pkg_manifest.dir();
let build_opts_without_tests = opts
.clone()
.patch_opts(pkg_path)
.into_build_opts()
.include_tests(false);
let built_pkg =
pkg::build_with_options(build_opts_without_tests).and_then(|pkg| pkg.expect_pkg());
(pinned_pkg, built_pkg)
})
.collect()
}

/// First builds the package or workspace, ready for execution.
///
/// If the workspace contains contracts, those contracts will be built first without tests
/// in order to determine their `CONTRACT_ID`s and enable contract calling.
pub fn build(opts: Opts) -> anyhow::Result<BuiltTests> {
let build_opts = opts.clone().into_build_opts();
let built = pkg::build_with_options(build_opts)?;
let built_tests = BuiltTests::from_built(built, opts)?;
Ok(built_tests)

let build_plan = pkg::BuildPlan::from_build_opts(&build_opts)?;
let mut const_inject_map = HashMap::new();
let mut built_contracts = HashMap::new();
let built_contracts_without_tests = build_contracts_without_tests(&opts, &build_plan);
for (pinned_contract, built_contract) in built_contracts_without_tests {
let built_contract = built_contract?;
let contract_id = pkg::contract_id(&built_contract, &fuel_tx::Salt::zeroed());
built_contracts.insert(pinned_contract.clone(), built_contract);

// Construct namespace with contract id
let contract_id_constant_name = CONTRACT_ID_CONSTANT_NAME.to_string();
let contract_id_value = format!("0x{contract_id}");
let contract_id_constant = ConfigTimeConstant {
r#type: "b256".to_string(),
value: contract_id_value.clone(),
public: true,
};
let constant_declarations = vec![(contract_id_constant_name, contract_id_constant)];
const_inject_map.insert(pinned_contract, constant_declarations);
}

// Injection map is collected in the previous pass, we should build the workspace/package with injection map.
let build_opts_with_injection = build_opts.const_injection_map(const_inject_map);
let built = pkg::build_with_options(build_opts_with_injection)?;
BuiltTests::from_built(built, built_contracts)
}

/// Deploys the provided contract and returns an interpreter instance ready to be used in test
Expand All @@ -387,11 +452,6 @@ fn deploy_test_contract(built_pkg: BuiltPackage) -> anyhow::Result<TestSetup> {
let state_root = tx::Contract::initial_state_root(storage_slots.iter());
let salt = tx::Salt::zeroed();
let contract_id = contract.id(&salt, &root, &state_root);
// TODO: Remove this prompt once https://github.com/FuelLabs/sway/issues/3673 is addressed.
println!(
" Deploying contract with id {:?} for testing",
contract_id
);

// Setup the interpreter for deployment.
let params = tx::ConsensusParameters::default();
Expand Down

0 comments on commit b8aa28b

Please sign in to comment.