Skip to content
Merged
7 changes: 4 additions & 3 deletions crates/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ include = ["/Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"]
ink_e2e_macro = { version = "4.0.0-alpha.3", path = "./macro" }
ink_env = { version = "4.0.0-alpha.3", path = "../env" }

contract-metadata = { version = "2.0.0-alpha.2" }
contract-metadata = { version = "2.0.0-alpha.4" }
impl-serde = { version = "0.3.1", default-features = false }
jsonrpsee = { version = "0.15.1", features = ["ws-client"] }
pallet-contracts-primitives = { version = "6.0.0" }
Expand All @@ -28,7 +28,8 @@ tokio = { version = "1.18.2", features = ["rt-multi-thread"] }
log = { version = "0.4" }
env_logger = { version = "0.9" }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "full"] }
subxt = { version = "0.24.0" }
# TODO we need to use `subxt` `master` until the next release 0.25 is published.
subxt = { git = "https://github.com/paritytech/subxt" }

# Substrate
sp-rpc = { version = "6.0.0" }
Expand All @@ -37,7 +38,7 @@ sp-keyring = { version = "6.0.0" }
sp-runtime = { version = "6.0.0" }

# TODO(#1421) `smart-bench_macro` needs to be forked.
smart-bench-macro = { git = "https://github.com/paritytech/smart-bench", branch = "aj-ink-e2e-test-mvp", package = "smart-bench-macro" }
smart-bench-macro = { git = "https://github.com/paritytech/smart-bench", branch = "cmichi-ink-e2e-test-mvp-cross-contract", package = "smart-bench-macro" }

[features]
default = ["std"]
Expand Down
185 changes: 102 additions & 83 deletions crates/e2e/macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,32 @@ use derive_more::From;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::{
path::PathBuf,
collections::HashMap,
sync::Once,
};

/// We use this to only build the contract once for all tests.
/// 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();

// We save the name of the currently executing test here.
thread_local! {
pub static CONTRACT_PATH: RefCell<Option<PathBuf>> = RefCell::new(None);
// We save a mapping of `contract_manifest_path` to the built `*.contract` files.
// This is necessary so that not each individual `#[ink_e2e::e2e_tests]` 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 bundle of the contract for which a test
/// Returns the path to the `*.contract` file of the contract for which a test
/// is currently executed.
pub fn contract_path() -> Option<PathBuf> {
CONTRACT_PATH.with(|metadata_path| metadata_path.borrow().clone())
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.
Expand Down Expand Up @@ -64,86 +74,51 @@ impl InkE2ETest {

let ws_url = &self.test.config.ws_url();
let node_log = &self.test.config.node_log();
let skip_build = &self.test.config.skip_build();

// This path will only be used in case `skip_build` is activated
// and no path was specified for it.
// TODO(#xxx) we should require specifying a path for `skip_build`.
let mut path = PathBuf::from("./target/ink/metadata.json".to_string());
let mut additional_contracts: Vec<String> =
self.test.config.additional_contracts();
let default_main_contract_manifest_path = String::from("Cargo.toml");
let mut contracts_to_build_and_import = vec![default_main_contract_manifest_path];
contracts_to_build_and_import.append(&mut additional_contracts);

// If a prior test did already build the contract and set the path
// to the metadata file.
if let Some(metadata_path) = contract_path() {
path = metadata_path;
}

if !skip_build.value && contract_path().is_none() {
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(|| {
env_logger::init();
use std::process::{
Command,
Stdio,
};
let output = Command::new("cargo")
// TODO(#xxx) Add possibility of configuring `skip_linting` in attributes.
.args(["+stable", "contract", "build", "--skip-linting", "--output-json"])
.env("RUST_LOG", "")
.stderr(Stdio::inherit())
.output()
.expect("failed to execute `cargo-contract` build process");

log::info!("`cargo-contract` returned status: {}", output.status);
eprintln!("`cargo-contract` returned status: {}", output.status);
log::info!(
"`cargo-contract` stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
eprintln!(
"`cargo-contract` stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
if !output.status.success() {
log::info!(
"`cargo-contract` stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
eprintln!(
"`cargo-contract` stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
for manifest_path in contracts_to_build_and_import {
let dest_metadata = build_contract(&manifest_path);
let _ = already_built_contracts.insert(manifest_path, dest_metadata);
}

assert!(output.status.success());

let json = String::from_utf8_lossy(&output.stdout);
let metadata: serde_json::Value =
serde_json::from_str(&json).expect("cannot convert json to utf8");
let mut dest_metadata =
metadata["metadata_result"]["dest_bundle"].to_string();
dest_metadata = dest_metadata.trim_matches('"').to_string();
path = PathBuf::from(dest_metadata);
log::info!("extracted metadata path: {}", path.display());

CONTRACT_PATH.with(|metadata_path| {
*metadata_path.borrow_mut() = Some(path.clone());
});
});
} else {
BUILD_ONCE.call_once(|| {
env_logger::init();
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("Cargo.toml").is_none() {
let dest_metadata = build_contract(&manifest_path);
let _ = already_built_contracts.insert(manifest_path, dest_metadata);
}
}
set_already_built_contracts(already_built_contracts.clone());
}

log::info!("using metadata path: {:?}", path);

path.try_exists().unwrap_or_else(|err| {
panic!("path {:?} does not exist: {:?}", path, err);
});
let os_path = path
.as_os_str()
.to_str()
.expect("converting path to str failed");
let path = syn::LitStr::new(os_path, proc_macro2::Span::call_site());
assert!(
!already_built_contracts.is_empty(),
"built contract artifacts must exist here"
);
let meta: Vec<TokenStream2> = already_built_contracts
.iter()
.map(|(_manifest_path, bundle_path)| {
let path = syn::LitStr::new(bundle_path, proc_macro2::Span::call_site());
quote! {
// TODO(#1421) `smart-bench_macro` needs to be forked.
::ink_e2e::smart_bench_macro::contract!(#path);
}
})
.collect();

quote! {
#( #attrs )*
Expand All @@ -160,9 +135,7 @@ impl InkE2ETest {
::ink_e2e::env_logger::init();
});

log_info("extracting metadata");
// TODO(#1421) `smart-bench_macro` needs to be forked.
::ink_e2e::smart_bench_macro::contract!(#path);
#( #meta )*

log_info("creating new client");

Expand All @@ -171,7 +144,7 @@ impl InkE2ETest {
let mut client = ::ink_e2e::Client::<
::ink_e2e::PolkadotConfig,
ink::env::DefaultEnvironment
>::new(&#path, &#ws_url, &#node_log).await;
>::new(&#ws_url, &#node_log).await;

let __ret = {
#block
Expand All @@ -190,3 +163,49 @@ impl InkE2ETest {
}
}
}

/// Builds the contract at `manifest_path`, returns the path to the contract
/// bundle build artifact.
fn build_contract(manifest_path: &str) -> String {
use std::process::{
Command,
Stdio,
};
let output = Command::new("cargo")
.args([
"+stable",
"contract",
"build",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we control cargo-contract it might be really nice to have a lib to allow invocation of contract builds...would certainly simplify the code here.

Made a quick start on it here use-ink/cargo-contract#787.

If you agree it is a good idea I can complete that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it's a good idea!

"--skip-linting",
"--output-json",
&format!("--manifest-path={}", manifest_path),
])
.env("RUST_LOG", "")
.stderr(Stdio::inherit())
.output()
.expect("failed to execute `cargo-contract` build process");

log::info!("`cargo-contract` returned status: {}", output.status);
log::info!(
"`cargo-contract` stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
if !output.status.success() {
log::error!(
"`cargo-contract` stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}

assert!(
output.status.success(),
"contract build for {} failed",
manifest_path
);

let json = String::from_utf8_lossy(&output.stdout);
let metadata: serde_json::Value =
serde_json::from_str(&json).expect("cannot convert json to utf8");
let dest_metadata = metadata["metadata_result"]["dest_bundle"].to_string();
dest_metadata.trim_matches('"').to_string()
}
49 changes: 28 additions & 21 deletions crates/e2e/macro/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ pub struct E2EConfig {
node_log: Option<syn::LitStr>,
/// The WebSocket URL where to connect with the node.
ws_url: Option<syn::LitStr>,
/// Denotes if the contract should be build before executing the test.
skip_build: Option<syn::LitBool>,
/// The set of attributes that can be passed to call builder in the codegen.
whitelisted_attributes: WhitelistedAttributes,
/// Additional contracts that have to be built before executing the test.
additional_contracts: Vec<String>,
}

impl TryFrom<ast::AttributeArgs> for E2EConfig {
Expand All @@ -40,8 +40,8 @@ impl TryFrom<ast::AttributeArgs> for E2EConfig {
fn try_from(args: ast::AttributeArgs) -> Result<Self, Self::Error> {
let mut node_log: Option<(syn::LitStr, ast::MetaNameValue)> = None;
let mut ws_url: Option<(syn::LitStr, ast::MetaNameValue)> = None;
let mut skip_build: Option<(syn::LitBool, ast::MetaNameValue)> = None;
let mut whitelisted_attributes = WhitelistedAttributes::default();
let mut additional_contracts: Option<(syn::LitStr, ast::MetaNameValue)> = None;

for arg in args.into_iter() {
if arg.name.is_ident("node_log") {
Expand All @@ -68,31 +68,39 @@ impl TryFrom<ast::AttributeArgs> for E2EConfig {
"expected a string literal for `ws_url` ink! e2e test configuration argument",
))
}
} else if arg.name.is_ident("skip_build") {
if let Some((_, ast)) = skip_build {
return Err(duplicate_config_err(ast, arg, "skip_build", "e2e test"))
} else if arg.name.is_ident("keep_attr") {
whitelisted_attributes.parse_arg_value(&arg)?;
} else if arg.name.is_ident("additional_contracts") {
if let Some((_, ast)) = additional_contracts {
return Err(duplicate_config_err(
ast,
arg,
"additional_contracts",
"e2e test",
))
}
if let ast::PathOrLit::Lit(syn::Lit::Bool(lit_bool)) = &arg.value {
skip_build = Some((lit_bool.clone(), arg))
if let ast::PathOrLit::Lit(syn::Lit::Str(lit_str)) = &arg.value {
additional_contracts = Some((lit_str.clone(), arg))
} else {
return Err(format_err_spanned!(
arg,
"expected a bool literal for `skip_build` ink! e2e test configuration argument",
"expected a bool literal for `additional_contracts` ink! e2e test configuration argument",
))
}
} else if arg.name.is_ident("keep_attr") {
whitelisted_attributes.parse_arg_value(&arg)?;
} else {
return Err(format_err_spanned!(
arg,
"encountered unknown or unsupported ink! configuration argument",
))
}
}
let additional_contracts = additional_contracts
.map(|(value, _)| value.value().split(" ").map(String::from).collect())
.unwrap_or_else(|| Vec::new());
Ok(E2EConfig {
node_log: node_log.map(|(value, _)| value),
ws_url: ws_url.map(|(value, _)| value),
skip_build: skip_build.map(|(value, _)| value),
additional_contracts,
whitelisted_attributes,
})
}
Expand All @@ -116,11 +124,10 @@ impl E2EConfig {
self.ws_url.clone().unwrap_or(default_ws_url)
}

/// Returns `true` if `skip_build = true` was configured.
/// Otherwise returns `false`.
pub fn skip_build(&self) -> syn::LitBool {
let default_skip_build = syn::LitBool::new(false, proc_macro2::Span::call_site());
self.skip_build.clone().unwrap_or(default_skip_build)
/// Returns a vector of additional contracts that have to be built
/// and imported before executing the test.
pub fn additional_contracts(&self) -> Vec<String> {
self.additional_contracts.clone()
}
}

Expand Down Expand Up @@ -173,11 +180,11 @@ mod tests {
fn duplicate_args_fails() {
assert_try_from(
syn::parse_quote! {
skip_build = true,
skip_build = true,
additional_contracts = "adder/Cargo.toml",
additional_contracts = "adder/Cargo.toml",
},
Err(
"encountered duplicate ink! e2e test `skip_build` configuration argument",
"encountered duplicate ink! e2e test `additional_contracts` configuration argument",
),
);
}
Expand All @@ -195,7 +202,7 @@ mod tests {
node_log: None,
ws_url: None,
whitelisted_attributes: attrs,
skip_build: None,
additional_contracts: Vec::new(),
}),
)
}
Expand Down
Loading