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

Implement cross-contract testing in E2E framework #1432

Merged
merged 13 commits into from
Oct 19, 2022
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