Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 7 additions & 1 deletion crates/e2e/macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> =
self.test.config.additional_contracts();
let default_main_contract_manifest_path = String::from("Cargo.toml");
Expand Down Expand Up @@ -158,7 +164,7 @@ impl InkE2ETest {

let mut client = ::ink_e2e::Client::<
::ink_e2e::PolkadotConfig,
ink::env::DefaultEnvironment
#environment
>::new(
node_proc.client(),
[ #( #contracts ),* ]
Expand Down
84 changes: 67 additions & 17 deletions crates/e2e/macro/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub struct E2EConfig {
whitelisted_attributes: WhitelistedAttributes,
/// Additional contracts that have to be built before executing the test.
additional_contracts: Vec<String>,
/// Custom environment for the contracts, if specified. Otherwise `DefaultEnvironment` is used.
environment: Option<syn::Path>,
}

impl TryFrom<ast::AttributeArgs> for E2EConfig {
Expand All @@ -36,6 +38,7 @@ impl TryFrom<ast::AttributeArgs> for E2EConfig {
fn try_from(args: ast::AttributeArgs) -> Result<Self, Self::Error> {
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") {
Expand All @@ -46,15 +49,27 @@ impl TryFrom<ast::AttributeArgs> for E2EConfig {
ast,
arg,
"additional_contracts",
"e2e test",
"E2E test",
))
}
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 `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 {
Expand All @@ -67,9 +82,12 @@ impl TryFrom<ast::AttributeArgs> 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,
})
}
}
Expand All @@ -80,20 +98,10 @@ impl E2EConfig {
pub fn additional_contracts(&self) -> Vec<String> {
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<syn::Path> {
self.environment.clone()
}
}

Expand Down Expand Up @@ -128,18 +136,59 @@ 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() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would also be cool to see some UI tests for the E2E macros, but that would be for a future PR

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 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();
Expand All @@ -152,6 +201,7 @@ mod tests {
Ok(E2EConfig {
whitelisted_attributes: attrs,
additional_contracts: Vec::new(),
environment: None,
}),
)
}
Expand Down
9 changes: 9 additions & 0 deletions examples/custom-environment/.gitignore
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions examples/custom-environment/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "custom-environment"
version = "4.0.0-rc"
authors = ["Parity Technologies <[email protected]>"]
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 = []
28 changes: 28 additions & 0 deletions examples/custom-environment/README.md
Original file line number Diff line number Diff line change
@@ -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<Runtime> = pallet_contracts::Schedule::<Runtime> {
limits: pallet_contracts::Limits {
event_topics: 6,
..Default::default()
},
..Default::default()
};
}

impl pallet_contracts::Config for Runtime {
type Schedule = Schedule;
}
```
132 changes: 132 additions & 0 deletions examples/custom-environment/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#![cfg_attr(not(feature = "std"), no_std)]

use ink::env::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 =
<ink::env::DefaultEnvironment as Environment>::MAX_EVENT_TOPICS + 1;

type AccountId = <ink::env::DefaultEnvironment as Environment>::AccountId;
type Balance = <ink::env::DefaultEnvironment as Environment>::Balance;
type Hash = <ink::env::DefaultEnvironment as Environment>::Hash;
type BlockNumber = <ink::env::DefaultEnvironment as Environment>::BlockNumber;
type Timestamp = <ink::env::DefaultEnvironment as Environment>::Timestamp;

type ChainExtension = <ink::env::DefaultEnvironment as Environment>::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 Topicer;

/// An event that would be forbidden in the default environment, but is completely valid in
/// our custom one.
#[ink(event)]
#[derive(Default)]
pub struct TopicedEvent {
#[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 Topicer {
#[ink(constructor)]
pub fn new() -> Self {
Self {}
}

/// Emit an event with many topics.
#[ink(message)]
pub fn trigger(&mut self) {
self.env().emit_event(TopicedEvent::default());
}
}

#[cfg(test)]
mod tests {
use super::*;

#[ink::test]
fn emits_event_with_many_topics() {
let mut contract = Topicer::new();
contract.trigger();

let emitted_events = ink::env::test::recorded_events().collect::<Vec<_>>();
assert_eq!(1, emitted_events.len());

let _ = <<Topicer as ink::reflect::ContractEventBase>::Type as scale::Decode>::decode(&mut &emitted_events[0].data[..])
.expect("encountered invalid contract event data buffer");
}
}

#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {
use super::*;
use crate::EnvironmentWithManyTopics;
use ink_e2e::MessageBuilder;

type E2EResult<T> = Result<T, Box<dyn std::error::Error>>;

#[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)]
#[ignore = "Requires that the pallet contract is configured with a schedule allowing for \
more event topics. For example:\
```rust
pub Schedule: pallet_contracts::Schedule<Runtime> = pallet_contracts::Schedule::<Runtime> {
limits: pallet_contracts::Limits {
event_topics: 6,
..Default::default()
},
..Default::default()
};
```"]
async fn it_works(mut client: Client<C, E>) -> E2EResult<()> {
// given
let constructor = TopicerRef::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::<EnvironmentWithManyTopics, TopicerRef>::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(())
}
}
}