diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b033fb26c0..4d3f4e86e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- Support custom environment in E2E tests - [#1645](https://github.com/paritytech/ink/pull/1645) + ### Changed - E2E: spawn a separate contracts node instance per test ‒ [#1642](https://github.com/paritytech/ink/pull/1642) diff --git a/crates/e2e/macro/src/codegen.rs b/crates/e2e/macro/src/codegen.rs index b8167c19f3a..5286739f0f6 100644 --- a/crates/e2e/macro/src/codegen.rs +++ b/crates/e2e/macro/src/codegen.rs @@ -72,6 +72,12 @@ impl InkE2ETest { syn::ReturnType::Type(rarrow, ret_type) => quote! { #rarrow #ret_type }, }; + let environment = self + .test + .config + .environment() + .unwrap_or_else(|| syn::parse_quote! { ::ink::env::DefaultEnvironment }); + let mut additional_contracts: Vec = self.test.config.additional_contracts(); let default_main_contract_manifest_path = String::from("Cargo.toml"); @@ -158,7 +164,7 @@ impl InkE2ETest { let mut client = ::ink_e2e::Client::< ::ink_e2e::PolkadotConfig, - ink::env::DefaultEnvironment + #environment >::new( node_proc.client(), [ #( #contracts ),* ] diff --git a/crates/e2e/macro/src/config.rs b/crates/e2e/macro/src/config.rs index 58b53ca2ef7..af1b918ddc4 100644 --- a/crates/e2e/macro/src/config.rs +++ b/crates/e2e/macro/src/config.rs @@ -28,6 +28,13 @@ pub struct E2EConfig { whitelisted_attributes: WhitelistedAttributes, /// Additional contracts that have to be built before executing the test. additional_contracts: Vec, + /// The [`Environment`](https://docs.rs/ink_env/4.0.0-rc/ink_env/trait.Environment.html) to use + /// during test execution. + /// + /// If no `Environment` is specified, the + /// [`DefaultEnvironment`](https://docs.rs/ink_env/4.0.0-rc/ink_env/enum.DefaultEnvironment.html) + /// will be used. + environment: Option, } impl TryFrom for E2EConfig { @@ -36,6 +43,7 @@ impl TryFrom for E2EConfig { fn try_from(args: ast::AttributeArgs) -> Result { let mut whitelisted_attributes = WhitelistedAttributes::default(); let mut additional_contracts: Option<(syn::LitStr, ast::MetaNameValue)> = None; + let mut environment: Option<(syn::Path, ast::MetaNameValue)> = None; for arg in args.into_iter() { if arg.name.is_ident("keep_attr") { @@ -46,7 +54,7 @@ impl TryFrom for E2EConfig { ast, arg, "additional_contracts", - "e2e test", + "E2E test", )) } if let ast::PathOrLit::Lit(syn::Lit::Str(lit_str)) = &arg.value { @@ -54,7 +62,19 @@ impl TryFrom for E2EConfig { } else { return Err(format_err_spanned!( arg, - "expected a bool literal for `additional_contracts` ink! e2e test configuration argument", + "expected a string literal for `additional_contracts` ink! E2E test configuration argument", + )) + } + } else if arg.name.is_ident("environment") { + if let Some((_, ast)) = environment { + return Err(duplicate_config_err(ast, arg, "environment", "E2E test")) + } + if let ast::PathOrLit::Path(path) = &arg.value { + environment = Some((path.clone(), arg)) + } else { + return Err(format_err_spanned!( + arg, + "expected a path for `environment` ink! E2E test configuration argument", )) } } else { @@ -67,9 +87,12 @@ impl TryFrom for E2EConfig { let additional_contracts = additional_contracts .map(|(value, _)| value.value().split(' ').map(String::from).collect()) .unwrap_or_else(Vec::new); + let environment = environment.map(|(path, _)| path); + Ok(E2EConfig { additional_contracts, whitelisted_attributes, + environment, }) } } @@ -80,20 +103,10 @@ impl E2EConfig { pub fn additional_contracts(&self) -> Vec { self.additional_contracts.clone() } -} - -/// The environmental types definition. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Environment { - /// The underlying Rust type. - pub path: syn::Path, -} -impl Default for Environment { - fn default() -> Self { - Self { - path: syn::parse_quote! { ::ink_env::DefaultEnvironment }, - } + /// Custom environment for the contracts, if specified. + pub fn environment(&self) -> Option { + self.environment.clone() } } @@ -128,18 +141,72 @@ mod tests { } #[test] - fn duplicate_args_fails() { + fn duplicate_additional_contracts_fails() { assert_try_from( syn::parse_quote! { additional_contracts = "adder/Cargo.toml", additional_contracts = "adder/Cargo.toml", }, Err( - "encountered duplicate ink! e2e test `additional_contracts` configuration argument", + "encountered duplicate ink! E2E test `additional_contracts` configuration argument", ), ); } + #[test] + fn duplicate_environment_fails() { + assert_try_from( + syn::parse_quote! { + environment = crate::CustomEnvironment, + environment = crate::CustomEnvironment, + }, + Err( + "encountered duplicate ink! E2E test `environment` configuration argument", + ), + ); + } + + #[test] + fn environment_as_literal_fails() { + assert_try_from( + syn::parse_quote! { + environment = "crate::CustomEnvironment", + }, + Err("expected a path for `environment` ink! E2E test configuration argument"), + ); + } + + #[test] + fn specifying_environment_works() { + assert_try_from( + syn::parse_quote! { + environment = crate::CustomEnvironment, + }, + Ok(E2EConfig { + environment: Some(syn::parse_quote! { crate::CustomEnvironment }), + ..Default::default() + }), + ); + } + + #[test] + fn full_config_works() { + assert_try_from( + syn::parse_quote! { + additional_contracts = "adder/Cargo.toml flipper/Cargo.toml", + environment = crate::CustomEnvironment, + }, + Ok(E2EConfig { + whitelisted_attributes: Default::default(), + additional_contracts: vec![ + "adder/Cargo.toml".into(), + "flipper/Cargo.toml".into(), + ], + environment: Some(syn::parse_quote! { crate::CustomEnvironment }), + }), + ); + } + #[test] fn keep_attr_works() { let mut attrs = WhitelistedAttributes::default(); @@ -152,6 +219,7 @@ mod tests { Ok(E2EConfig { whitelisted_attributes: attrs, additional_contracts: Vec::new(), + environment: None, }), ) } diff --git a/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr b/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr index 7e71ef2dea2..8464dbc60fd 100644 --- a/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr +++ b/crates/ink/tests/ui/contract/fail/message-returns-non-codec.stderr @@ -46,7 +46,7 @@ error[E0599]: the method `try_invoke` exists for struct `ink::ink_env::call::Cal = note: the following trait bounds were not satisfied: `NonCodecType: parity_scale_codec::Decode` note: the following trait must be implemented - --> $CARGO/parity-scale-codec-3.3.0/src/codec.rs + --> $CARGO/parity-scale-codec-3.4.0/src/codec.rs | | pub trait Decode: Sized { | ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ink/tests/ui/trait_def/fail/message_output_non_codec.stderr b/crates/ink/tests/ui/trait_def/fail/message_output_non_codec.stderr index d3221e593be..15f1692e6a3 100644 --- a/crates/ink/tests/ui/trait_def/fail/message_output_non_codec.stderr +++ b/crates/ink/tests/ui/trait_def/fail/message_output_non_codec.stderr @@ -33,7 +33,7 @@ error[E0599]: the method `try_invoke` exists for struct `CallBuilder $CARGO/parity-scale-codec-3.3.0/src/codec.rs + --> $CARGO/parity-scale-codec-3.4.0/src/codec.rs | | pub trait Decode: Sized { | ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/custom-environment/.gitignore b/examples/custom-environment/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/examples/custom-environment/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/examples/custom-environment/Cargo.toml b/examples/custom-environment/Cargo.toml new file mode 100644 index 00000000000..98810d18aea --- /dev/null +++ b/examples/custom-environment/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "custom-environment" +version = "4.0.0-rc" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] + +# Assumes that the node used in E2E testing allows for at least 6 event topics. +permissive-node = [] diff --git a/examples/custom-environment/README.md b/examples/custom-environment/README.md new file mode 100644 index 00000000000..8bd35b4614e --- /dev/null +++ b/examples/custom-environment/README.md @@ -0,0 +1,28 @@ +# `custom-environment` example + +## What is this example about? + +It demonstrates how to use custom environment, both in the contract and in the E2E tests. + +## Chain-side configuration + +To integrate this example into Substrate you need to adjust pallet contracts configuration in your runtime: + +```rust +// In your node's runtime configuration file (runtime.rs) +parameter_types! { + pub Schedule: pallet_contracts::Schedule = pallet_contracts::Schedule:: { + limits: pallet_contracts::Limits { + event_topics: 6, + ..Default::default() + }, + ..Default::default() + }; +} + +impl pallet_contracts::Config for Runtime { + … + type Schedule = Schedule; + … +} + ``` diff --git a/examples/custom-environment/lib.rs b/examples/custom-environment/lib.rs new file mode 100644 index 00000000000..139dc08d75d --- /dev/null +++ b/examples/custom-environment/lib.rs @@ -0,0 +1,171 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use ink::env::{ + DefaultEnvironment, + Environment, +}; + +/// Our custom environment diverges from the `DefaultEnvironment` in the event topics limit. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum EnvironmentWithManyTopics {} + +impl Environment for EnvironmentWithManyTopics { + // We allow for 5 topics in the event, therefore the contract pallet's schedule must allow for + // 6 of them (to allow the implicit topic for the event signature). + const MAX_EVENT_TOPICS: usize = + ::MAX_EVENT_TOPICS + 1; + + type AccountId = ::AccountId; + type Balance = ::Balance; + type Hash = ::Hash; + type BlockNumber = ::BlockNumber; + type Timestamp = ::Timestamp; + + type ChainExtension = ::ChainExtension; +} + +#[ink::contract(env = crate::EnvironmentWithManyTopics)] +mod runtime_call { + /// Trivial contract with a single message that emits an event with many topics. + #[ink(storage)] + #[derive(Default)] + pub struct Topics; + + /// An event that would be forbidden in the default environment, but is completely valid in + /// our custom one. + #[ink(event)] + #[derive(Default)] + pub struct EventWithTopics { + #[ink(topic)] + first_topic: Balance, + #[ink(topic)] + second_topic: Balance, + #[ink(topic)] + third_topic: Balance, + #[ink(topic)] + fourth_topic: Balance, + #[ink(topic)] + fifth_topic: Balance, + } + + impl Topics { + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Emit an event with many topics. + #[ink(message)] + pub fn trigger(&mut self) { + self.env().emit_event(EventWithTopics::default()); + } + } + + #[cfg(test)] + mod tests { + use super::*; + + type Event = ::Type; + + #[ink::test] + fn emits_event_with_many_topics() { + let mut contract = Topics::new(); + contract.trigger(); + + let emitted_events = ink::env::test::recorded_events().collect::>(); + assert_eq!(emitted_events.len(), 1); + + let emitted_event = + ::decode(&mut &emitted_events[0].data[..]) + .expect("encountered invalid contract event data buffer"); + + assert!(matches!( + emitted_event, + Event::EventWithTopics(EventWithTopics { .. }) + )); + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + + use ink_e2e::MessageBuilder; + + type E2EResult = Result>; + + #[cfg(feature = "permissive-node")] + #[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)] + async fn calling_custom_environment_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let constructor = TopicsRef::new(); + let contract_acc_id = client + .instantiate( + "custom-environment", + &ink_e2e::alice(), + constructor, + 0, + None, + ) + .await + .expect("instantiate failed") + .account_id; + + // when + let message = + MessageBuilder::::from_account_id( + contract_acc_id, + ) + .call(|caller| caller.trigger()); + + let call_res = client + .call(&ink_e2e::alice(), message, 0, None) + .await + .expect("call failed"); + + // then + call_res.contains_event("Contracts", "ContractEmitted"); + + Ok(()) + } + + #[cfg(not(feature = "permissive-node"))] + #[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)] + async fn calling_custom_environment_fails_if_incompatible_with_node( + mut client: Client, + ) -> E2EResult<()> { + // given + let constructor = TopicsRef::new(); + let contract_acc_id = client + .instantiate( + "custom-environment", + &ink_e2e::alice(), + constructor, + 0, + None, + ) + .await + .expect("instantiate failed") + .account_id; + + let message = + MessageBuilder::::from_account_id( + contract_acc_id, + ) + .call(|caller| caller.trigger()); + + // when + let call_res = client + .call_dry_run(&ink_e2e::alice(), &message, 0, None) + .await; + + // then + assert!(call_res.is_err()); + + Ok(()) + } + } +}