Skip to content
Closed
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
28 changes: 28 additions & 0 deletions integration-tests/test_events_decode/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "testing_event_decode"
version = "4.1.0"
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.5", 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 = []
11 changes: 11 additions & 0 deletions integration-tests/test_events_decode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Testing Events Reference Implementation

Capturing events for ink! end-to-end tests in particular is not clearly documented, nor are there clear reference implementations. This contract contains a minimalist flipflop contract that emits two different events when called. It contains both unit and e2e test modules illustrating one way to check for multiple events in both testing environments.
Copy link
Contributor

Choose a reason for hiding this comment

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

This contract contains a minimalist flipper contract that emits two different events when called. It contains both unit and e2e test modules illustrating one way to check for multiple events in both testing environments.


The end-to-end event test/check is awkward and could certainly be improved on.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please create a follow-up issue for this?


Notes:

- e2e: depending on the field number and content of an event struct, the byte range in the `&event` that is decoded may be either `&event[34..]` or `&event[35..]`. This is a point to improve on, dealing with the prepending bytes that don't pertain to the field values.

- There is probably a way to filter for event types simultaneously, instead of one at a time (that is, keeping track of which events were caught by using a boolean and assert).
241 changes: 241 additions & 0 deletions integration-tests/test_events_decode/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//! This is a reference implementation with one approach to decoding
//! (capturing emitted) events within unit and E2E tests.

#![cfg_attr(not(feature = "std"), no_std)]

#[ink::contract]
mod testing_event_decode {

#[ink(storage)]
pub struct TestingEventDecode {}

#[ink(event)]
pub struct FlipEvent {
#[ink(topic)]
flipper: AccountId,
#[ink(topic)]
value: bool,
}

#[ink(event)]
pub struct FlopEvent {
#[ink(topic)]
flipper: AccountId,
#[ink(topic)]
flopper: AccountId,
#[ink(topic)]
value: bool,
}

impl Default for TestingEventDecode {
fn default() -> Self {
Self::new()
}
}

impl TestingEventDecode {

#[ink(constructor)]
pub fn new() -> Self {
Self {}
}

/// This message emits two events for test verification.
#[ink(message)]
pub fn flipflop(&mut self) {
let caller = self.env().caller();
let contract = self.env().account_id();

// Emit FlipEvent.
self.env().emit_event(FlipEvent {
flipper: caller,
value: true,
});

// Emit FlopEvent.
self.env().emit_event(FlopEvent {
flipper: caller,
flopper: contract,
value: false,
});
}
}

/// This is one way to capture and check event emission in ink unit tests.
#[cfg(test)]
mod tests {

use super::*;
use ink::env::test::EmittedEvent;

type Event = <TestingEventDecode as ::ink::reflect::ContractEventBase>::Type;

fn decode_events(emitted_events: Vec<EmittedEvent>) -> Vec<Event> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is actually a very useful function to be added to e2e create directly. Can you please look into this?

Copy link
Author

Choose a reason for hiding this comment

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

To clarify, do you mean reimplement this decode_events function to work within the ink_e2e test scope? Or do you mean include this particular decode_events function as ink::env::test::decode_events? Or both?

If the former, this would be part of the followup issue regarding improving event decode to not be so awkward.

In any case, this might be something @ganesh1233456 or @alessandro-baldassarre may wish to address individually. I will see to it that the proper issues are created.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think for unit tests, we want to have something like ink::env::test::recorded_events().decode() or ink::env::test::decoded_events(). To check whether the events have been emitted, you can use something like .contains() or similar list functions.

With e2e tests it's a little bit trickier, we probably want to have something like let flip_event = flipflop_result.events.find_decoded::<FlipEvent>() or similar to automatically check whether the event has been emitted, decode it and then assert on its values.

emitted_events
.into_iter()
.map(|event| {
<Event as scale::Decode>::decode(&mut &event.data[..])
.expect("Invalid event data")
})
.collect()
}

#[ink::test]
fn unittest_event_emission_capture_decode() {
let accounts =
ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);

let mut contract = TestingEventDecode::new();
contract.flipflop();

// Decode event.
let emitted_events = ink::env::test::recorded_events().collect::<Vec<_>>();
let decoded_events = decode_events(emitted_events);

let mut gotflip = false;
let mut gotflop = false;

// Check events were emitted by the `flipflop` function.
for event in &decoded_events {
match event {
Event::FlipEvent(FlipEvent { flipper, value }) => {
assert!(*value, "unexpected FlipEvent.value");
assert_eq!(
*flipper, accounts.bob,
"unexpected FlipEvent.flipper"
);
gotflip = true;
}
Event::FlopEvent(FlopEvent {
flipper,
flopper,
value,
}) => {
assert!(!*value, "unexpected FlopEvent.value");
assert_eq!(
*flipper, accounts.bob,
"unexpected FlopEvent.flipper"
);
assert_eq!(
*flopper, accounts.alice,
"unexpected FlopEvent.flopper"
);
gotflop = true;
}
};
}
assert!(gotflip, "expected flip event not captured");
assert!(gotflop, "expected flop event not captured");
}
}

/// This is one way to capture and check event emission in ink E2E tests.
#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {

use super::*;
use ink_e2e::build_message;

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

/// Verify that we can capture emitted events and decode to check their fields.
#[ink_e2e::test]
async fn e2etest_event_emission_capture_decode(
mut client: ink_e2e::Client<C, E>,
) -> E2EResult<()> {
let bob_account = ink_e2e::account_id(ink_e2e::AccountKeyring::Bob);
let constructor = TestingEventDecodeRef::new();
let contract_account_id = client
.instantiate(
"testing_event_decode",
&ink_e2e::bob(),
constructor,
0,
None,
)
.await
.expect("instantiate failed")
.account_id;

// Check that FlipEvent was successfully emitted.
let flipflop_msg =
build_message::<TestingEventDecodeRef>(contract_account_id)
.call(|contract| contract.flipflop());
let flipflop_result = client
.call(&ink_e2e::bob(), flipflop_msg, 0, None)
.await
.expect("flipflop failed");

// Filter the events.
let contract_emitted_event = flipflop_result
.events
.iter()
.find(|event| {
event.as_ref().expect("bad event").event_metadata().event()
== "ContractEmitted"
&& String::from_utf8_lossy(
event.as_ref().expect("bad event").bytes(),
)
.to_string()
.contains("TestingEventDecode::FlipEvent")
})
.expect("Expected flip event")
.unwrap();

// Decode the expected event type.
let event = contract_emitted_event.field_bytes();
let decode_event = <FlipEvent as scale::Decode>::decode(&mut &event[34..])
.expect("invalid data");

let FlipEvent { flipper, value } = decode_event;

// Check that FlopEvent was successully emitted.
assert!(value, "unexpected FlipEvent.value");
assert_eq!(flipper, bob_account, "unexpected FlipEvent.flipper");

// Build flipflop message.
let flipflop_msg =
build_message::<TestingEventDecodeRef>(contract_account_id)
.call(|contract| contract.flipflop());
let flipflop_result = client
.call(&ink_e2e::bob(), flipflop_msg, 0, None)
.await
.expect("flipflop failed");

// Filter the events.
let contract_emitted_event = flipflop_result
.events
.iter()
.find(|event| {
event.as_ref().expect("bad event").event_metadata().event()
== "ContractEmitted"
&& String::from_utf8_lossy(
event.as_ref().expect("bad event").bytes(),
)
.to_string()
.contains("TestingEventDecode::FlopEvent")
})
.expect("Expected flop event")
.unwrap();

// Decode the expected event type.
let event = contract_emitted_event.field_bytes();
let decode_event = <FlopEvent as scale::Decode>::decode(&mut &event[35..])
.expect("invalid data");

let FlopEvent {
flipper,
flopper,
value,
} = decode_event;

// Check event emitted by the `flip` function.
assert!(!value, "unexpected FlopEvent.value");
assert_eq!(flipper, bob_account, "unexpected FlopEvent.flipper");
assert_eq!(flopper, contract_account_id, "unexpected FlopEvent.flopper");

Ok(())
}
}
}