Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `ink_revive_types` (and remove `pallet-revive` dependency from `ink_e2e`) - [#2657](https://github.com/use-ink/ink/pull/2657)
- non-allocating Solidity ABI encoder - [#2655](https://github.com/use-ink/ink/pull/2655)
- Implement XCM precompile, stabilize XCM API - [#2687](https://github.com/use-ink/ink/pull/2687)
- Add `ink_precompiles` crate with ERC-20 assets precompile interface - [#2686](https://github.com/use-ink/ink/pull/2686)

### Changed
- Marks the `pallet-revive` host function `account_id` stable - [#2578](https://github.com/use-ink/ink/pull/2578)
Expand Down
213 changes: 213 additions & 0 deletions crates/e2e/src/assertions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright (C) Use Ink (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.

//! Assertion helpers for ink! E2E tests.
//!
//! These macros provide convenient assertions similar to FRAME's testing macros,
//! adapted for contract call results.

/// Assert that a contract call succeeded without reverting.
///
/// This macro follows FRAME's `assert_ok!` convention for consistency across
/// the Polkadot ecosystem. It verifies that a contract call completed successfully
/// and did not revert.
///
/// # Variants
///
/// - `assert_ok!(result)` - Assert the call didn't revert
/// - `assert_ok!(result, expected)` - Assert the call didn't revert AND the return
/// value equals `expected`
///
/// # Examples
///
/// ```ignore
/// // Just assert success
/// let result = client.call(&alice, &contract_call.transfer(bob, amount))
/// .submit()
/// .await?;
/// assert_ok!(result);
///
/// // Assert success and check return value
/// let result = client.call(&alice, &contract_call.balance_of(bob))
/// .dry_run()
/// .await?;
/// assert_ok!(result, expected_balance);
/// ```
#[macro_export]
macro_rules! assert_ok {
($result:expr $(,)?) => {{
let result = $result;
if result.dry_run.did_revert() {
panic!(
"Expected call to succeed but it reverted.\nError: {:?}",
result.extract_error()
);
}
result
}};
($result:expr, $expected:expr $(,)?) => {{
let result = $result;
if result.dry_run.did_revert() {
panic!(
"Expected call to succeed but it reverted.\nError: {:?}",
result.extract_error()
);
}
assert_eq!(
result.return_value(),
$expected,
"Return value mismatch"
);
result
}};
}

/// Assert that a contract call reverted with a specific error.
///
/// This macro follows FRAME's `assert_noop!` convention, which stands for
/// "assert no operation" - meaning the call should fail without changing state.
/// Since reverted contract calls don't mutate state, this verifies the call
/// reverted with the expected error message.
///
/// # Variants
///
/// - `assert_noop!(result, expected_error)` - Assert the call reverted with an error
/// containing `expected_error`
///
/// # Examples
///
/// ```ignore
/// let result = client.call(&alice, &contract_call.transfer(bob, huge_amount))
/// .submit()
/// .await?;
/// assert_noop!(result, "BalanceLow");
/// ```
#[macro_export]
macro_rules! assert_noop {
($result:expr, $expected_error:expr $(,)?) => {{
let result = $result;
if !result.dry_run.did_revert() {
panic!(
"Expected call to revert with '{}' but it succeeded.\nReturn value: {:?}",
$expected_error,
result.return_data()
);
}

let actual_error = result.extract_error();
if actual_error != Some($expected_error.to_string()) {
panic!(
"Expected error '{}' but got {:?}",
$expected_error,
actual_error
);
}
result
}};
}

/// Assert that the last event from a contract call matches the expected event.
///
/// This macro extracts events from the contract result and compares the last
/// emitted event with the expected event structure by comparing encoded bytes.
///
/// # Examples
///
/// ```ignore
/// let result = client.call(&alice, &contract_call.transfer(bob_address, amount))
/// .submit()
/// .await?;
///
/// assert_last_event!(
/// &result,
/// Transfer {
/// from: contract.addr,
/// to: bob_address,
/// value: amount
/// }
/// );
/// ```
#[macro_export]
macro_rules! assert_last_event {
($result:expr, $expected_event:expr) => {{ $crate::assert_last_event_internal($result, $expected_event) }};
}

use crate::CallResult;
use ink_env::Environment;
use scale::{
Decode,
Encode,
};
use subxt::{
blocks::ExtrinsicEvents,
config::HashFor,
};

/// A trait for types that can expose the last contract-emitted event for assertions.
#[allow(dead_code)]
pub trait ContractEventReader {
fn fetch_last_contract_event(self) -> Result<Vec<u8>, String>;
}

impl<'a, E, V, C, Abi> ContractEventReader
for &'a CallResult<E, V, ExtrinsicEvents<C>, Abi>
where
E: Environment,
C: subxt::Config,
HashFor<C>: Into<sp_core::H256>,
{
fn fetch_last_contract_event(self) -> Result<Vec<u8>, String> {
let events = self
.contract_emitted_events()
.map_err(|err| format!("failed to get contract events: {err:?}"))?;

let last_event = events
.last()
.ok_or_else(|| "no contract events were emitted".to_string())?;

Ok(last_event.event.data.clone())
}
}

/// Shared implementation that decodes the last contract event and compares it against the
/// expected value.
#[allow(dead_code)]
pub fn assert_last_event_internal<R, E>(reader: R, expected_event: E)
where
R: ContractEventReader,
E: Decode + Encode + core::fmt::Debug,
{
let last_event_data = reader
.fetch_last_contract_event()
.unwrap_or_else(|err| panic!("Contract event assertion failed: {err}"));

let expected_bytes = expected_event.encode();

if expected_bytes != last_event_data {
let decoded_event =
E::decode(&mut &last_event_data[..]).unwrap_or_else(|error| {
panic!(
"failed to decode last contract event as {}: bytes={:?}, error={:?}",
core::any::type_name::<E>(),
last_event_data,
error
);
});

panic!(
"event mismatch!\nExpected: {:?}\nActual: {:?}",
expected_event, decoded_event
);
}
}
37 changes: 37 additions & 0 deletions crates/e2e/src/conversions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::sr25519::Keypair;
use sp_core::crypto::AccountId32;

/// Trait for types that can be converted into an `AccountId`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe there are Into implementations that you added in #2686 which could be used here.

pub trait IntoAccountId<TargetAccountId> {
fn into_account_id(self) -> TargetAccountId;
}

impl IntoAccountId<AccountId32> for AccountId32 {
fn into_account_id(self) -> AccountId32 {
self
}
}

impl IntoAccountId<AccountId32> for &AccountId32 {
fn into_account_id(self) -> AccountId32 {
self.clone()
}
}

impl<AccountId> IntoAccountId<AccountId> for &ink_primitives::AccountId
where
AccountId: From<[u8; 32]>,
{
fn into_account_id(self) -> AccountId {
AccountId::from(*AsRef::<[u8; 32]>::as_ref(self))
}
}

impl<AccountId> IntoAccountId<AccountId> for &Keypair
where
AccountId: From<[u8; 32]>,
{
fn into_account_id(self) -> AccountId {
AccountId::from(self.public_key().0)
}
}
7 changes: 7 additions & 0 deletions crates/e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@
html_favicon_url = "https://use.ink/crate-docs/favicon.png"
)]

mod assertions;
mod backend;
mod backend_calls;
mod builders;
mod client_utils;
mod contract_build;
mod contract_results;
mod conversions;
mod error;
pub mod events;
mod node_proc;
mod subxt_client;
mod xts;

pub use crate::contract_build::build_root_and_contract_dependencies;
pub use assertions::{
ContractEventReader,
assert_last_event_internal,
};
pub use backend::{
BuilderClient,
ChainBackend,
Expand Down Expand Up @@ -61,6 +67,7 @@ pub use contract_results::{
InstantiationResult,
UploadResult,
};
pub use conversions::IntoAccountId;
pub use ink_e2e_macro::test;
pub use ink_revive_types::evm::CallTrace;
pub use node_proc::{
Expand Down
Loading
Loading