Unverified Commit 64fbe40a authored by Michael Müller's avatar Michael Müller Committed by GitHub
Browse files

Add E2E testing framework MVP (#1395)

* Add missing words to spellcheck dictionary

* Add `contracts-node.scale` metadata

Has been exported via

	cargo install subxt-cli
	subxt metadata > contracts-node.scale

For `substrate-contracts-node` v0.20.0.

* Run `substrate-contracts-node` in CI

* Invoke `cargo doc` separately for each crate

* Add MVP for E2E testing framework

* Add E2E tests for `contract-transfer` example

* Add ToDo comment for migration to `state_call` RPC

* Update to new `ink` entrance crate

* Add ToDo for `node_log_contains`

* Update to `ink` entrance crate

* Migrate to `state_call` RPC

* Always initialize `env_logger`

* Use latest `subxt` release

* Remove superfluous TODO

* Apply `cargo fmt`

* Adapt test fixtures
parent 9fe9a422
Pipeline #217679 passed with stages
in 45 minutes and 29 seconds
......@@ -40,6 +40,7 @@ dereferencing
deserialize/S
deserialization
dispatchable/S
E2E
encodable
evaluable
fuzzer
......@@ -97,6 +98,7 @@ layout/JG
namespace/S
parameterize/SD
runtime/S
storable
struct/S
vec/S
vector/S
......@@ -107,4 +109,4 @@ natively
payability
unpayable
initializer
storable
WebSocket/S
......@@ -86,6 +86,9 @@ workflow:
tags:
- kubernetes-parity-build
.start-substrate-contracts-node: &start-substrate-contracts-node
- substrate-contracts-node -linfo,runtime::contracts=debug 2>&1 | tee /tmp/contracts-node.log &
#### stage: lint
#
# Note: For all of these lints we `allow_failure` so that the rest of the build can
......@@ -280,11 +283,21 @@ docs:
paths:
- ./crate-docs/
script:
- cargo doc --no-deps --all-features
-p scale-info -p ink_metadata -p ink_env
-p ink_storage -p ink_storage_traits
-p ink_primitives -p ink_prelude
-p ink -p ink_macro -p ink_ir -p ink_codegen
# All crate docs currently need to be built separately. The reason
# is that `smart-bench-macro` is a dependency now in a number of places.
# This crate uses e.g. `ink_metadata`, but in its published form. So if
# e.g. the `-p ink_metadata` is added to the `ink_lang` command this
# results in the cargo failure "multiple packages with same spec, ambiguous".
- cargo doc --no-deps --all-features -p ink_env
- cargo doc --no-deps --all-features -p ink_storage
- cargo doc --no-deps --all-features -p ink_storage_traits
- cargo doc --no-deps --all-features -p ink_primitives
- cargo doc --no-deps --all-features -p ink_prelude
- cargo doc --no-deps --all-features -p ink
- cargo doc --no-deps --all-features -p ink_macro
- cargo doc --no-deps --all-features -p ink_ir
- cargo doc --no-deps --all-features -p ink_codegen
- cargo doc --no-deps --all-features -p ink_metadata
- mv ${CARGO_TARGET_DIR}/doc ./crate-docs
# FIXME: remove me after CI image gets nonroot
- chown -R nonroot:nonroot ./crate-docs
......@@ -347,6 +360,7 @@ examples-test:
- job: clippy-std
artifacts: false
script:
- *start-substrate-contracts-node
- for example in examples/*/; do
if [ "$example" = "examples/upgradeable-contracts/" ]; then continue; fi;
cargo test --verbose --manifest-path ${example}/Cargo.toml;
......
......@@ -50,6 +50,26 @@ secp256k1 = { version = "0.24", features = ["recovery", "global-context"], optio
rand = { version = "0.8", default-features = false, features = ["alloc"], optional = true }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
contract-metadata = "2.0.0-alpha.2"
impl-serde = { version = "0.3.1", default-features = false }
jsonrpsee = { version = "0.14.0", features = ["ws-client"] }
pallet-contracts-primitives = "6.0.0"
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
serde_json = "1.0.81"
tokio = { version = "1.18.2", features = ["rt-multi-thread"] }
log = "0.4"
env_logger = "0.8"
subxt = "0.24.0"
# Substrate
sp-rpc = "6.0.0"
sp-core = "6.0.0"
sp-keyring = "6.0.0"
sp-runtime = "6.0.0"
# TODO(#xxx) `smart-bench_macro` needs to be forked.
smart-bench-macro = { git = "https://github.com/paritytech/smart-bench", branch = "cmichi-ink-e2e-test-mvp", package = "smart-bench-macro"}
[features]
default = ["std"]
std = [
......
// Copyright 2018-2022 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::{
client::api::runtime_types::{
frame_system::AccountInfo,
pallet_balances::AccountData,
},
log_error,
log_info,
sr25519,
xts::{
self,
api,
Call,
InstantiateWithCode,
},
ContractExecResult,
ContractInstantiateResult,
ContractsApi,
InkConstructor,
InkMessage,
Signer,
};
use crate::Environment;
use std::path::PathBuf;
use sp_runtime::traits::{
IdentifyAccount,
Verify,
};
use subxt::{
ext::bitvec::macros::internal::funty::Fundamental,
metadata::DecodeStaticType,
storage::address::{
StorageHasher,
StorageMapKey,
Yes,
},
tx::{
ExtrinsicParams,
TxEvents,
},
};
/// An encoded `#[ink(message)]`.
#[derive(Clone)]
pub struct EncodedMessage(Vec<u8>);
impl EncodedMessage {
fn new<M: InkMessage>(call: &M) -> Self {
let mut call_data = M::SELECTOR.to_vec();
<M as scale::Encode>::encode_to(call, &mut call_data);
Self(call_data)
}
}
impl<M> From<M> for EncodedMessage
where
M: InkMessage,
{
fn from(msg: M) -> Self {
EncodedMessage::new(&msg)
}
}
/// Result of a contract instantiation.
pub struct InstantiationResult<C: subxt::Config, E: Environment> {
/// The account id at which the contract was instantiated.
pub account_id: C::AccountId,
/// The result of the dry run, contains debug messages
/// if there were any.
pub dry_run: ContractInstantiateResult<C::AccountId, E::Balance>,
/// Events that happened with the contract instantiation.
pub events: TxEvents<C>,
}
/// We implement a custom `Debug` here, as to avoid requiring the trait
/// bound `Debug` for `E`.
// TODO(#xxx) Improve the `Debug` implementation.
impl<C, E> core::fmt::Debug for InstantiationResult<C, E>
where
C: subxt::Config,
E: Environment,
<E as Environment>::Balance: core::fmt::Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("CallResult")
.field("account_id", &self.account_id)
.field("dry_run", &self.dry_run)
.field("events", &self.events)
.finish()
}
}
/// Result of a contract call.
pub struct CallResult<C: subxt::Config, E: Environment> {
/// The result of the dry run, contains debug messages
/// if there were any.
pub dry_run: ContractExecResult<E::Balance>,
/// Events that happened with the contract instantiation.
pub events: TxEvents<C>,
}
/// We implement a custom `Debug` here, as to avoid requiring the trait
/// bound `Debug` for `E`.
// TODO(#xxx) Improve the `Debug` implementation.
impl<C, E> core::fmt::Debug for CallResult<C, E>
where
C: subxt::Config,
E: Environment,
<E as Environment>::Balance: core::fmt::Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("CallResult")
.field("dry_run", &self.dry_run)
.field("events", &self.events)
.finish()
}
}
/// An error occurred while interacting with the Substrate node.
///
/// We only convey errors here that are caused by the contract's
/// testing logic. For anything concerning the node (like inability
/// to communicate with it, fetch the nonce, account info, etc.) we
/// panic.
pub enum Error<C, E>
where
C: subxt::Config,
E: Environment,
<E as Environment>::Balance: core::fmt::Debug,
{
/// The `instantiate_with_code` dry run failed.
InstantiateDryRun(ContractInstantiateResult<C::AccountId, E::Balance>),
/// The `instantiate_with_code` extrinsic failed.
InstantiateExtrinsic(subxt::error::DispatchError),
/// The `call` dry run failed.
CallDryRun(ContractExecResult<E::Balance>),
/// The `call` extrinsic failed.
CallExtrinsic(subxt::error::DispatchError),
}
// We implement a custom `Debug` here, as to avoid requiring the trait
// bound `Debug` for `C`.
impl<C, E> core::fmt::Debug for Error<C, E>
where
C: subxt::Config,
E: Environment,
<E as Environment>::Balance: core::fmt::Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match &self {
Error::InstantiateDryRun(_) => f.write_str("InstantiateDryRun"),
Error::InstantiateExtrinsic(_) => f.write_str("InstantiateExtrinsic"),
Error::CallDryRun(_) => f.write_str("CallDryRun"),
Error::CallExtrinsic(_) => f.write_str("CallExtrinsic"),
}
}
}
/// A contract was successfully instantiated.
#[derive(Debug, scale::Decode, scale::Encode)]
struct ContractInstantiatedEvent<C: subxt::Config> {
/// Account id of the deployer.
pub deployer: C::AccountId,
/// Account id where the contract was instantiated to.
pub contract: C::AccountId,
}
impl<C> subxt::events::StaticEvent for ContractInstantiatedEvent<C>
where
C: subxt::Config,
{
const PALLET: &'static str = "Contracts";
const EVENT: &'static str = "Instantiated";
}
/// The `Client` takes care of communicating with the node.
///
/// This node's RPC interface will be used for instantiating the contract
/// and interacting with it .
pub struct Client<C, E>
where
C: subxt::Config,
E: Environment,
{
api: ContractsApi<C, E>,
node_log: String,
contract_path: PathBuf,
}
impl<C, E> Client<C, E>
where
C: subxt::Config,
C::AccountId: Into<C::Address> + serde::de::DeserializeOwned,
C::Address: From<C::AccountId>,
C::Signature: From<sr25519::Signature>,
<C::Signature as Verify>::Signer: From<sr25519::Public>,
<C::ExtrinsicParams as ExtrinsicParams<C::Index, C::Hash>>::OtherParams: Default,
<C::Signature as Verify>::Signer:
From<sr25519::Public> + IdentifyAccount<AccountId = C::AccountId>,
sr25519::Signature: Into<C::Signature>,
E: Environment,
E::Balance: core::fmt::Debug + scale::Encode,
Call<C, E::Balance>: scale::Encode,
InstantiateWithCode<E::Balance>: scale::Encode,
{
/// Creates a new [`Client`] instance.
pub async fn new(contract_path: &str, url: &str, node_log: &str) -> Self {
let client = subxt::OnlineClient::from_url(url)
.await
.unwrap_or_else(|err| {
log_error(
"Unable to create client! Please check that your node is running.",
);
panic!("Unable to create client: {:?}", err);
});
Self {
api: ContractsApi::new(client, url).await,
contract_path: PathBuf::from(contract_path),
node_log: node_log.to_string(),
}
}
/// This function extracts the metadata of the contract at the file path
/// `target/ink/$contract_name.contract`.
///
/// The function subsequently uploads and instantiates an instance of the contract.
///
/// Calling this function multiple times is idempotent, the contract is
/// newly instantiated each time using a unique salt. No existing contract
/// instance is reused!
pub async fn instantiate<CO>(
&mut self,
signer: &mut Signer<C>,
// TODO(#xxx) It has to be possible to supply a contact bundle path directly here.
// Otherwise cross-contract testing is not possible. Currently we instantiate just
// by default the contract for which the test is executed.
// contract_path: Option<PathBuf>,
constructor: CO,
value: E::Balance,
storage_deposit_limit: Option<E::Balance>,
) -> Result<InstantiationResult<C, E>, Error<C, E>>
where
CO: InkConstructor,
{
let reader = std::fs::File::open(&self.contract_path).unwrap_or_else(|err| {
panic!("contract path cannot be opened: {:?}", err);
});
let contract: contract_metadata::ContractMetadata =
serde_json::from_reader(reader).map_err(|err| {
panic!("error reading metadata: {:?}", err);
})?;
let code = contract
.source
.wasm
.expect("contract bundle is missing `source.wasm`");
log_info(&format!(
"{:?} has {} KiB",
self.contract_path,
code.0.len() / 1024
));
let nonce = self
.api
.client
.rpc()
.system_account_next_index(signer.account_id())
.await
.unwrap_or_else(|err| {
panic!(
"error getting next index for {:?}: {:?}",
signer.account_id(),
err
);
});
log_info(&format!("nonce: {:?}", nonce));
signer.set_nonce(nonce);
let ret = self
.exec_instantiate(signer, value, storage_deposit_limit, code.0, &constructor)
.await?;
log_info(&format!("instantiated contract at {:?}", ret.account_id));
Ok(ret)
}
/// Executes an `instantiate_with_code` call and captures the resulting events.
async fn exec_instantiate<CO: InkConstructor>(
&mut self,
signer: &mut Signer<C>,
value: E::Balance,
storage_deposit_limit: Option<E::Balance>,
code: Vec<u8>,
constructor: &CO,
) -> Result<InstantiationResult<C, E>, Error<C, E>> {
let mut data = CO::SELECTOR.to_vec();
log_info(&format!("instantiating with selector: {:?}", CO::SELECTOR));
<CO as scale::Encode>::encode_to(constructor, &mut data);
let salt = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("unable to get unix time")
.as_millis()
.as_u128()
.to_le_bytes()
.to_vec();
// dry run the instantiate to calculate the gas limit
let dry_run = self
.api
.instantiate_with_code_dry_run(
value,
storage_deposit_limit,
code.clone(),
data.clone(),
salt.clone(),
signer,
)
.await;
log_info(&format!(
"instantiate dry run debug message: {:?}",
String::from_utf8_lossy(&dry_run.debug_message)
));
log_info(&format!("instantiate dry run result: {:?}", dry_run.result));
if dry_run.result.is_err() {
return Err(Error::InstantiateDryRun(dry_run))
}
let tx_events = self
.api
.instantiate_with_code(
value,
dry_run.gas_required,
storage_deposit_limit,
code,
data.clone(),
salt,
signer,
)
.await;
signer.increment_nonce();
let mut account_id = None;
for evt in tx_events.iter() {
let evt = evt.unwrap_or_else(|err| {
panic!("unable to unwrap event: {:?}", err);
});
if let Some(instantiated) = evt
.as_event::<ContractInstantiatedEvent<C>>()
.unwrap_or_else(|err| {
panic!("event conversion to `Instantiated` failed: {:?}", err);
})
{
log_info(&format!(
"contract was instantiated at {:?}",
instantiated.contract
));
account_id = Some(instantiated.contract);
break
} else if evt
.as_event::<xts::api::system::events::ExtrinsicFailed>()
.unwrap_or_else(|err| {
panic!("event conversion to `ExtrinsicFailed` failed: {:?}", err)
})
.is_some()
{
let metadata = self.api.client.metadata();
let dispatch_error = subxt::error::DispatchError::decode_from(
evt.field_bytes(),
&metadata,
);
log_error(&format!(
"extrinsic for instantiate failed: {:?}",
dispatch_error
));
return Err(Error::InstantiateExtrinsic(dispatch_error))
}
}
Ok(InstantiationResult {
dry_run,
// The `account_id` must exist at this point. If the instantiation fails
// the dry-run must already return that.
account_id: account_id.expect("cannot extract account_id from events"),
events: tx_events,
})
}
/// Executes a `call` for the contract at `account_id`.
///
/// Returns when the transaction is included in a block. The return value
/// contains all events that are associated with this transaction.
pub async fn call(
&self,
signer: &mut Signer<C>,
account_id: C::AccountId,
contract_call: EncodedMessage,
value: E::Balance,
storage_deposit_limit: Option<E::Balance>,
) -> Result<CallResult<C, E>, Error<C, E>> {
let dry_run = self
.api
.call_dry_run(account_id.clone(), value, None, contract_call.0.clone())
.await;
log_info(&format!("call dry run: {:?}", &dry_run.result));
log_info(&format!(
"call dry run debug message: {}",
String::from_utf8_lossy(&dry_run.debug_message)
));
if dry_run.result.is_err() {
return Err(Error::CallDryRun(dry_run))
}
let tx_events = self
.api
.call(
sp_runtime::MultiAddress::Id(account_id),
value,
dry_run.gas_required,
storage_deposit_limit,
contract_call.0.clone(),
signer,
)
.await;
signer.increment_nonce();
for evt in tx_events.iter() {
let evt = evt.unwrap_or_else(|err| {
panic!("unable to unwrap event: {:?}", err);
});
if evt
.as_event::<xts::api::system::events::ExtrinsicFailed>()
.unwrap_or_else(|err| {
panic!("event conversion to `ExtrinsicFailed` failed: {:?}", err)
})
.is_some()
{
let metadata = self.api.client.metadata();
let dispatch_error = subxt::error::DispatchError::decode_from(
evt.field_bytes(),
&metadata,
);
log_error(&format!("extrinsic for call failed: {:?}", dispatch_error));
return Err(Error::InstantiateExtrinsic(dispatch_error))
}
}
Ok(CallResult {
dry_run,
events: tx_events,
})
}
/// Returns the balance of `account_id`.
pub async fn balance(
&self,
account_id: C::AccountId,
) -> Result<E::Balance, Error<C, E>> {
let account_addr = subxt::storage::StaticStorageAddress::<
DecodeStaticType<AccountInfo<C::Index, AccountData<E::Balance>>>,
Yes,
Yes,
(),
>::new(
"System",
"Account",
vec![StorageMapKey::new(
account_id.clone(),
StorageHasher::Blake2_128Concat,
)],
Default::default(),
)
.unvalidated();
let alice_pre: AccountInfo<C::Index, AccountData<E::Balance>> = self
.api
.client
.storage()
.fetch_or_default(&account_addr, None)
.await
.unwrap_or_else(|err| {
panic!("unable to fetch balance: {:?}", err);
});
log_info(&format!(
"balance of contract {:?} is {:?}",
account_id, alice_pre
));
Ok(alice_pre.data.free)
}
/// Returns true if the `substrate-contracts-node` log under
/// `/tmp/contracts-node.log` contains `msg`.