Skip to content

Commit

Permalink
[ink_e2e] build contracts at runtime instead of during codegen (#1881)
Browse files Browse the repository at this point in the history
* WIP moving building of contracts to runtime instead of codegen

* WIP

* Fix compiler errors

* Fix Client constructor

* Remove unused code

* Refactor contract building

* Only build each contract once per test run

* Commentl

* Comment

* Comment
  • Loading branch information
ascjones authored Aug 21, 2023
1 parent e52c31a commit f233bdb
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 195 deletions.
2 changes: 2 additions & 0 deletions crates/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ ink = { workspace = true, default-features = true }
ink_env = { workspace = true, default-features = true }
ink_primitives = { workspace = true, default-features = true }

cargo_metadata = { workspace = true }
contract-build = { workspace = true }
funty = { workspace = true }
impl-serde = { workspace = true }
jsonrpsee = { workspace = true, features = ["ws-client"] }
Expand Down
2 changes: 0 additions & 2 deletions crates/e2e/macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ proc-macro = true

[dependencies]
ink_ir = { workspace = true, default-features = true }
cargo_metadata = { workspace = true }
contract-build = { workspace = true }
derive_more = { workspace = true, default-features = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
serde_json = { workspace = true }
Expand Down
202 changes: 12 additions & 190 deletions crates/e2e/macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,9 @@
// limitations under the License.

use crate::ir;
use contract_build::{
ManifestPath,
Target,
};
use core::cell::RefCell;
use derive_more::From;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::{
collections::HashMap,
sync::Once,
};

/// We use this to only build the contracts once for all tests, at the
/// time of generating the Rust code for the tests, so at compile time.
static BUILD_ONCE: Once = Once::new();

thread_local! {
// We save a mapping of `contract_manifest_path` to the built `*.contract` files.
// This is necessary so that not each individual `#[ink_e2e::test]` starts
// rebuilding the main contract and possibly specified `additional_contracts` contracts.
pub static ALREADY_BUILT_CONTRACTS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
}

/// Returns the path to the `*.contract` file of the contract for which a test
/// is currently executed.
pub fn already_built_contracts() -> HashMap<String, String> {
ALREADY_BUILT_CONTRACTS.with(|already_built| already_built.borrow().clone())
}

/// Sets a new `HashMap` for the already built contracts.
pub fn set_already_built_contracts(hash_map: HashMap<String, String>) {
ALREADY_BUILT_CONTRACTS.with(|metadata_paths| {
*metadata_paths.borrow_mut() = hash_map;
});
}

/// Generates code for the `[ink::e2e_test]` macro.
#[derive(From)]
Expand Down Expand Up @@ -82,53 +49,17 @@ impl InkE2ETest {
.environment()
.unwrap_or_else(|| syn::parse_quote! { ::ink::env::DefaultEnvironment });

let contract_manifests = ContractManifests::from_cargo_metadata();
let additional_contracts = self.test.config.additional_contracts();

let contracts_to_build_and_import =
if self.test.config.additional_contracts().is_empty() {
contract_manifests.all_contracts_to_build()
} else {
// backwards compatibility if `additional_contracts` specified
let mut additional_contracts: Vec<String> =
self.test.config.additional_contracts();
let mut contracts_to_build_and_import: Vec<String> =
contract_manifests.root_package.iter().cloned().collect();
contracts_to_build_and_import.append(&mut additional_contracts);
contracts_to_build_and_import
};

let mut already_built_contracts = already_built_contracts();
if already_built_contracts.is_empty() {
// Build all of them for the first time and initialize everything
BUILD_ONCE.call_once(|| {
tracing_subscriber::fmt::init();
for manifest_path in contracts_to_build_and_import {
let dest_wasm = build_contract(&manifest_path);
let _ = already_built_contracts.insert(manifest_path, dest_wasm);
}
set_already_built_contracts(already_built_contracts.clone());
});
} else if !already_built_contracts.is_empty() {
// Some contracts have already been built and we check if the
// `additional_contracts` for this particular test contain ones
// that haven't been build before
for manifest_path in contracts_to_build_and_import {
if already_built_contracts.get(&manifest_path).is_none() {
let dest_wasm = build_contract(&manifest_path);
let _ = already_built_contracts.insert(manifest_path, dest_wasm);
}
let exec_build_contracts = if additional_contracts.is_empty() {
quote! {
::ink_e2e::build_root_and_contract_dependencies()
}
set_already_built_contracts(already_built_contracts.clone());
}

assert!(
!already_built_contracts.is_empty(),
"built contract artifacts must exist here"
);

let contracts = already_built_contracts.values().map(|wasm_path| {
quote! { #wasm_path }
});
} else {
quote! {
::ink_e2e::build_root_and_additional_contracts([ #( #additional_contracts ),* ])
}
};

const DEFAULT_CONTRACTS_NODE: &str = "substrate-contracts-node";

Expand Down Expand Up @@ -175,12 +106,14 @@ impl InkE2ETest {
::core::panic!("Error spawning substrate-contracts-node: {:?}", err)
);

let contracts = #exec_build_contracts;

let mut client = ::ink_e2e::Client::<
::ink_e2e::PolkadotConfig,
#environment
>::new(
node_proc.client(),
[ #( #contracts ),* ]
contracts,
).await;

let __ret = {
Expand All @@ -200,114 +133,3 @@ impl InkE2ETest {
}
}
}

#[derive(Debug)]
struct ContractManifests {
/// The manifest path of the root package where the E2E test is defined.
/// `None` if the root package is not an `ink!` contract definition.
root_package: Option<String>,
/// The manifest paths of any dependencies which are `ink!` contracts.
contract_dependencies: Vec<String>,
}

impl ContractManifests {
/// Load any manifests for packages which are detected to be `ink!` contracts. Any
/// package with the `ink-as-dependency` feature enabled is assumed to be an
/// `ink!` contract.
fn from_cargo_metadata() -> Self {
let cmd = cargo_metadata::MetadataCommand::new();
let metadata = cmd
.exec()
.unwrap_or_else(|err| panic!("Error invoking `cargo metadata`: {err}"));

fn maybe_contract_package(package: &cargo_metadata::Package) -> Option<String> {
package
.features
.iter()
.any(|(feat, _)| feat == "ink-as-dependency")
.then(|| package.manifest_path.to_string())
}

let root_package = metadata
.resolve
.as_ref()
.and_then(|resolve| resolve.root.as_ref())
.and_then(|root_package_id| {
metadata
.packages
.iter()
.find(|package| &package.id == root_package_id)
})
.and_then(maybe_contract_package);

let contract_dependencies = metadata
.packages
.iter()
.filter_map(maybe_contract_package)
.collect();

Self {
root_package,
contract_dependencies,
}
}

/// Returns all the contract manifests which are to be built, including the root
/// package if it is determined to be an `ink!` contract.
fn all_contracts_to_build(&self) -> Vec<String> {
let mut all_manifests: Vec<String> = self.root_package.iter().cloned().collect();
all_manifests.append(&mut self.contract_dependencies.clone());
all_manifests
}
}

/// Builds the contract at `manifest_path`, returns the path to the contract
/// Wasm build artifact.
fn build_contract(path_to_cargo_toml: &str) -> String {
use contract_build::{
BuildArtifacts,
BuildMode,
ExecuteArgs,
Features,
Network,
OptimizationPasses,
OutputType,
UnstableFlags,
Verbosity,
};

let manifest_path = ManifestPath::new(path_to_cargo_toml).unwrap_or_else(|err| {
panic!("Invalid manifest path {path_to_cargo_toml}: {err}")
});
let args = ExecuteArgs {
manifest_path,
verbosity: Verbosity::Default,
build_mode: BuildMode::Debug,
features: Features::default(),
network: Network::Online,
build_artifact: BuildArtifacts::CodeOnly,
unstable_flags: UnstableFlags::default(),
optimization_passes: Some(OptimizationPasses::default()),
keep_debug_symbols: false,
lint: false,
output_type: OutputType::HumanReadable,
skip_wasm_validation: false,
target: Target::Wasm,
..Default::default()
};

match contract_build::execute(args) {
Ok(build_result) => {
build_result
.dest_wasm
.expect("Wasm code artifact not generated")
.canonicalize()
.expect("Invalid dest bundle path")
.to_string_lossy()
.into()
}
Err(err) => {
panic!("contract build for {path_to_cargo_toml} failed: {err}")
}
}
}
Loading

0 comments on commit f233bdb

Please sign in to comment.