Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ink_e2e] build contracts at runtime instead of during codegen #1881

Merged
merged 11 commits into from
Aug 21, 2023
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