diff --git a/integration-tests/test_events_decode/Cargo.toml b/integration-tests/test_events_decode/Cargo.toml new file mode 100644 index 00000000000..217edb3cd7f --- /dev/null +++ b/integration-tests/test_events_decode/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "testing_event_decode" +version = "4.1.0" +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.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 = [] diff --git a/integration-tests/test_events_decode/README.md b/integration-tests/test_events_decode/README.md new file mode 100644 index 00000000000..08653a42570 --- /dev/null +++ b/integration-tests/test_events_decode/README.md @@ -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. + +The end-to-end event test/check is awkward and could certainly be improved on. + +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). diff --git a/integration-tests/test_events_decode/lib.rs b/integration-tests/test_events_decode/lib.rs new file mode 100644 index 00000000000..885e5725b4b --- /dev/null +++ b/integration-tests/test_events_decode/lib.rs @@ -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 = ::Type; + + fn decode_events(emitted_events: Vec) -> Vec { + emitted_events + .into_iter() + .map(|event| { + ::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::test::set_caller::(accounts.bob); + + let mut contract = TestingEventDecode::new(); + contract.flipflop(); + + // Decode event. + let emitted_events = ink::env::test::recorded_events().collect::>(); + 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 = std::result::Result>; + + /// 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, + ) -> 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::(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 = ::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::(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 = ::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(()) + } + } +}