From 18f32ecd81577de4361fd2ee61eaecf36ab6207c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Mi=C5=9Bkowicz?= Date: Mon, 24 Jun 2024 15:07:30 +0200 Subject: [PATCH] Refactor event logic (#2251) Closes #1889 ## Introduced changes - revamp event logic Original stack here: - #2193 - #2194 - #2195 - #2224 ## Checklist - [ ] Linked relevant issue - [ ] Updated relevant documentation - [ ] Added relevant tests - [ ] Performed self-review of the code - [ ] Added changes to `CHANGELOG.md` --- CHANGELOG.md | 2 + .../execution/syscall_hooks.rs | 13 +- .../cheatcodes/spy_events.rs | 28 +-- .../forge_runtime_extension/mod.rs | 15 +- crates/cheatnet/src/state.rs | 6 +- .../tests/cheatcodes/cheat_caller_address.rs | 7 +- .../cheatnet/tests/cheatcodes/spy_events.rs | 204 +++++------------- crates/forge/tests/integration/spy_events.rs | 155 ++++++++++--- crates/forge/tests/integration/test_state.rs | 23 +- docs/src/appendix/cheatcodes/spy_events.md | 50 ++--- docs/src/testing/testing-events.md | 125 +++++------ .../src/testing/testing_contract_internals.md | 13 +- snforge_std/src/cheatcodes/events.cairo | 128 ++++++----- snforge_std/src/lib.cairo | 6 +- 14 files changed, 360 insertions(+), 415 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ebd86459b..58bac3d9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed - `SyscallResultStringErrorTrait::map_error_to_string` removed in favor of utility function (`snforge_std::byte_array::try_deserialize_bytearray_error`) +- Updated event testing - read more [here](./docs/src/testing/testing-events.md) on how it now works and [here](./docs/src/appendix/cheatcodes/spy_events.md) +about updated `spy_events` cheatcode ### Cast diff --git a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/syscall_hooks.rs b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/syscall_hooks.rs index 7fafaa22a4..ceaac76104 100644 --- a/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/syscall_hooks.rs +++ b/crates/cheatnet/src/runtime_extensions/call_to_blockifier_runtime_extension/execution/syscall_hooks.rs @@ -37,14 +37,7 @@ pub fn emit_event_hook( ) { let contract_address = syscall_handler.contract_address(); let last_event = syscall_handler.last_event(); - let is_spied_on = cheatnet_state - .spies - .iter() - .any(|spy_on| spy_on.does_spy(contract_address)); - - if is_spied_on { - cheatnet_state - .detected_events - .push(Event::from_ordered_event(last_event, contract_address)); - } + cheatnet_state + .detected_events + .push(Event::from_ordered_event(last_event, contract_address)); } diff --git a/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/cheatcodes/spy_events.rs b/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/cheatcodes/spy_events.rs index ca725c3a1e..a7a7af2659 100644 --- a/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/cheatcodes/spy_events.rs +++ b/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/cheatcodes/spy_events.rs @@ -1,13 +1,11 @@ use crate::CheatnetState; use blockifier::execution::call_info::OrderedEvent; use cairo_felt::Felt252; -use cairo_vm::hint_processor::hint_processor_utils::felt_to_usize; use conversions::{ serde::{deserialize::CairoDeserialize, serialize::CairoSerialize}, FromConv, }; use starknet_api::core::ContractAddress; -use std::mem::take; /// Represents an emitted event. It is used in the `CheatnetState` to keep track of events /// emitted in the `cheatnet::src::rpc::call_contract` @@ -63,29 +61,7 @@ impl SpyTarget { } impl CheatnetState { - pub fn spy_events(&mut self, spy_on: SpyTarget) -> usize { - self.spies.push(spy_on); - self.spies.len() - 1 - } - - pub fn fetch_events(&mut self, id: &Felt252) -> Vec { - let spy_on = &self.spies[felt_to_usize(id).unwrap()]; - - // replace with empty to get ownership - let emitted_events = take(&mut self.detected_events); - - emitted_events - .into_iter() - .filter_map(|event| { - if spy_on.does_spy(event.from) { - Some(event) - } else { - // push back unconsumed ones - self.detected_events.push(event); - - None - } - }) - .collect() + pub fn get_events(&mut self, event_offset: usize) -> Vec { + self.detected_events[event_offset..].to_vec() } } diff --git a/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/mod.rs b/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/mod.rs index f281dfecd7..99a16e65e6 100644 --- a/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/mod.rs +++ b/crates/cheatnet/src/runtime_extensions/forge_runtime_extension/mod.rs @@ -273,24 +273,23 @@ impl<'a> ExtensionLogic for ForgeExtension<'a> { Ok(CheatcodeHandlingResult::Handled(parsed_content)) } "spy_events" => { - let spy_on = input_reader.read()?; - - let id = extended_runtime + let events_offset = extended_runtime .extended_runtime .extension .cheatnet_state - .spy_events(spy_on); + .detected_events + .len(); - Ok(CheatcodeHandlingResult::from_serializable(id)) + Ok(CheatcodeHandlingResult::from_serializable(events_offset)) } - "fetch_events" => { - let id = input_reader.read()?; + "get_events" => { + let events_offset = input_reader.read()?; let events = extended_runtime .extended_runtime .extension .cheatnet_state - .fetch_events(&id); + .get_events(events_offset); Ok(CheatcodeHandlingResult::from_serializable(events)) } diff --git a/crates/cheatnet/src/state.rs b/crates/cheatnet/src/state.rs index 954df821aa..b2be46115d 100644 --- a/crates/cheatnet/src/state.rs +++ b/crates/cheatnet/src/state.rs @@ -4,9 +4,7 @@ use crate::runtime_extensions::call_to_blockifier_runtime_extension::rpc::CallRe use crate::runtime_extensions::forge_runtime_extension::cheatcodes::cheat_execution_info::{ ExecutionInfoMock, ResourceBounds, }; -use crate::runtime_extensions::forge_runtime_extension::cheatcodes::spy_events::{ - Event, SpyTarget, -}; +use crate::runtime_extensions::forge_runtime_extension::cheatcodes::spy_events::Event; use blockifier::blockifier::block::BlockInfo; use blockifier::execution::call_info::OrderedL2ToL1Message; use blockifier::execution::entry_point::CallEntryPoint; @@ -320,7 +318,6 @@ pub struct CheatnetState { pub mocked_functions: HashMap>>>, pub replaced_bytecode_contracts: HashMap, - pub spies: Vec, pub detected_events: Vec, pub deploy_salt_base: u32, pub block_info: BlockInfo, @@ -341,7 +338,6 @@ impl Default for CheatnetState { global_cheated_execution_info: Default::default(), mocked_functions: Default::default(), replaced_bytecode_contracts: Default::default(), - spies: vec![], detected_events: vec![], deploy_salt_base: 0, block_info: SerializableBlockInfo::default().into(), diff --git a/crates/cheatnet/tests/cheatcodes/cheat_caller_address.rs b/crates/cheatnet/tests/cheatcodes/cheat_caller_address.rs index ec03b8efaf..2125d166d1 100644 --- a/crates/cheatnet/tests/cheatcodes/cheat_caller_address.rs +++ b/crates/cheatnet/tests/cheatcodes/cheat_caller_address.rs @@ -3,9 +3,7 @@ use crate::common::get_contracts; use cairo_felt::Felt252; use cairo_lang_starknet_classes::keccak::starknet_keccak; use cheatnet::constants::TEST_ADDRESS; -use cheatnet::runtime_extensions::forge_runtime_extension::cheatcodes::spy_events::{ - Event, SpyTarget, -}; +use cheatnet::runtime_extensions::forge_runtime_extension::cheatcodes::spy_events::Event; use cheatnet::state::CheatSpan; use conversions::IntoConv; use starknet_api::core::{ContractAddress, PatriciaKey}; @@ -306,7 +304,6 @@ fn cheat_caller_address_cairo0_callback() { let contract_address = test_env.deploy("Cairo1Contract_v1", &[]); test_env.start_cheat_caller_address(contract_address, 123); - let id = test_env.cheatnet_state.spy_events(SpyTarget::All); let expected_caller_address = Felt252::from(123); @@ -326,7 +323,7 @@ fn cheat_caller_address_cairo0_callback() { &[], ); - let events = test_env.cheatnet_state.fetch_events(&Felt252::from(id)); + let events = test_env.cheatnet_state.get_events(0); // make sure end() was called by cairo0 contract assert_eq!( diff --git a/crates/cheatnet/tests/cheatcodes/spy_events.rs b/crates/cheatnet/tests/cheatcodes/spy_events.rs index 338d1b56d8..d87ffa420e 100644 --- a/crates/cheatnet/tests/cheatcodes/spy_events.rs +++ b/crates/cheatnet/tests/cheatcodes/spy_events.rs @@ -4,9 +4,7 @@ use crate::common::state::create_fork_cached_state_at; use crate::common::{call_contract, deploy_contract, felt_selector_from_name}; use cairo_felt::Felt252; use cairo_lang_starknet_classes::keccak::starknet_keccak; -use cheatnet::runtime_extensions::forge_runtime_extension::cheatcodes::spy_events::{ - Event, SpyTarget, -}; +use cheatnet::runtime_extensions::forge_runtime_extension::cheatcodes::spy_events::Event; use cheatnet::state::CheatnetState; use conversions::string::TryFromHexStr; use conversions::IntoConv; @@ -14,31 +12,24 @@ use std::vec; use tempfile::TempDir; trait SpyTrait { - fn spy_events(&mut self, spy_on: SpyTarget) -> usize; - fn fetch_events(&mut self, id: usize) -> Vec; + fn get_events(&mut self, id: usize) -> Vec; } impl SpyTrait for TestEnvironment { - fn spy_events(&mut self, spy_on: SpyTarget) -> usize { - self.cheatnet_state.spy_events(spy_on) - } - - fn fetch_events(&mut self, id: usize) -> Vec { - self.cheatnet_state.fetch_events(&Felt252::from(id)) + fn get_events(&mut self, events_offset: usize) -> Vec { + self.cheatnet_state.get_events(events_offset) } } #[test] -fn spy_events_complex() { +fn spy_events_zero_offset() { let mut test_env = TestEnvironment::new(); let contract_address = test_env.deploy("SpyEventsChecker", &[]); - let id = test_env.spy_events(SpyTarget::All); - test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); - let events = test_env.fetch_events(id); + let events = test_env.get_events(0); assert_eq!(events.len(), 1, "There should be one event"); assert_eq!( @@ -53,11 +44,41 @@ fn spy_events_complex() { test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); - let length = test_env.fetch_events(id).len(); - assert_eq!(length, 1, "There should be one new event"); + let length = test_env.get_events(0).len(); + assert_eq!(length, 2, "There should be one more new event"); +} + +#[test] +fn spy_events_some_offset() { + let mut test_env = TestEnvironment::new(); - let length = test_env.fetch_events(id).len(); - assert_eq!(length, 0, "There should be no new events"); + let contract_address = test_env.deploy("SpyEventsChecker", &[]); + + test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); + test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); + test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); + + let events = test_env.get_events(2); + + assert_eq!( + events.len(), + 1, + "There should be only one event fetched after accounting for an offset of 2" + ); + assert_eq!( + events[0], + Event { + from: contract_address, + keys: vec![starknet_keccak("FirstEvent".as_ref()).into()], + data: vec![Felt252::from(123)] + }, + "Wrong event" + ); + + test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); + + let length = test_env.get_events(2).len(); + assert_eq!(length, 2, "There should be one more new event"); } #[test] @@ -67,8 +88,6 @@ fn check_events_order() { let spy_events_checker_address = test_env.deploy("SpyEventsChecker", &[]); let spy_events_order_checker_address = test_env.deploy("SpyEventsOrderChecker", &[]); - let id = test_env.spy_events(SpyTarget::All); - test_env.call_contract( &spy_events_order_checker_address, "emit_and_call_another", @@ -80,7 +99,7 @@ fn check_events_order() { ], ); - let events = test_env.fetch_events(id); + let events = test_env.get_events(0); assert_eq!(events.len(), 3, "There should be three events"); assert_eq!( @@ -112,66 +131,6 @@ fn check_events_order() { ); } -#[test] -fn check_events_captured_only_for_spied_contracts() { - let mut test_env = TestEnvironment::new(); - - let spy_events_checker_address = test_env.deploy("SpyEventsChecker", &[]); - - test_env.call_contract( - &spy_events_checker_address, - "emit_one_event", - &[Felt252::from(123)], - ); - - let id = test_env.spy_events(SpyTarget::One(spy_events_checker_address)); - test_env.call_contract( - &spy_events_checker_address, - "emit_one_event", - &[Felt252::from(123)], - ); - - let events = test_env.fetch_events(id); - - assert_eq!(events.len(), 1, "There should be one event"); - assert_eq!( - events[0], - Event { - from: spy_events_checker_address, - keys: vec![starknet_keccak("FirstEvent".as_ref()).into()], - data: vec![Felt252::from(123)] - }, - "Wrong event" - ); -} - -#[test] -fn duplicate_spies_on_one_address() { - let mut test_env = TestEnvironment::new(); - - let contract_address = test_env.deploy("SpyEventsChecker", &[]); - - let id1 = test_env.spy_events(SpyTarget::One(contract_address)); - let id2 = test_env.spy_events(SpyTarget::One(contract_address)); - - test_env.call_contract(&contract_address, "emit_one_event", &[Felt252::from(123)]); - - let events1 = test_env.fetch_events(id1); - let length2 = test_env.fetch_events(id2).len(); - - assert_eq!(events1.len(), 1, "There should be one event"); - assert_eq!(length2, 0, "There should be no events"); - assert_eq!( - events1[0], - Event { - from: contract_address, - keys: vec![starknet_keccak("FirstEvent".as_ref()).into()], - data: vec![Felt252::from(123)] - }, - "Wrong event" - ); -} - #[test] fn library_call_emits_event() { let mut test_env = TestEnvironment::new(); @@ -180,15 +139,13 @@ fn library_call_emits_event() { let class_hash = test_env.declare("SpyEventsChecker", &contracts_data); let contract_address = test_env.deploy("SpyEventsLibCall", &[]); - let id = test_env.spy_events(SpyTarget::All); - test_env.call_contract( &contract_address, "call_lib_call", &[Felt252::from(123), class_hash.into_()], ); - let events = test_env.fetch_events(id); + let events = test_env.get_events(0); assert_eq!(events.len(), 1, "There should be one event"); assert_eq!( @@ -206,11 +163,9 @@ fn library_call_emits_event() { fn event_emitted_in_constructor() { let mut test_env = TestEnvironment::new(); - let id = test_env.spy_events(SpyTarget::All); - let contract_address = test_env.deploy("ConstructorSpyEventsChecker", &[Felt252::from(123)]); - let events = test_env.fetch_events(id); + let events = test_env.get_events(0); assert_eq!(events.len(), 1, "There should be one event"); assert_eq!( @@ -224,41 +179,6 @@ fn event_emitted_in_constructor() { ); } -#[test] -fn check_if_there_is_no_interference() { - let mut test_env = TestEnvironment::new(); - - let contracts_data = get_contracts(); - let class_hash = test_env.declare("SpyEventsChecker", &contracts_data); - - let spy_events_checker_address = test_env.deploy_wrapper(&class_hash, &[]); - let other_spy_events_checker_address = test_env.deploy_wrapper(&class_hash, &[]); - - let id1 = test_env.spy_events(SpyTarget::One(spy_events_checker_address)); - let id2 = test_env.spy_events(SpyTarget::One(other_spy_events_checker_address)); - - test_env.call_contract( - &spy_events_checker_address, - "emit_one_event", - &[Felt252::from(123)], - ); - - let events1 = test_env.fetch_events(id1); - let length2 = test_env.fetch_events(id2).len(); - - assert_eq!(events1.len(), 1, "There should be one event"); - assert_eq!(length2, 0, "There should be no events"); - assert_eq!( - events1[0], - Event { - from: spy_events_checker_address, - keys: vec![starknet_keccak("FirstEvent".as_ref()).into()], - data: vec![Felt252::from(123)] - }, - "Wrong event" - ); -} - #[test] fn test_nested_calls() { let mut test_env = TestEnvironment::new(); @@ -273,15 +193,13 @@ fn test_nested_calls() { let spy_events_checker_top_proxy_address = test_env.deploy_wrapper(&class_hash, &[spy_events_checker_proxy_address.into_()]); - let id = test_env.spy_events(SpyTarget::All); - test_env.call_contract( &spy_events_checker_top_proxy_address, "emit_one_event", &[Felt252::from(123)], ); - let events = test_env.fetch_events(id); + let events = test_env.get_events(0); assert_eq!(events.len(), 3, "There should be three events"); assert_eq!( @@ -327,32 +245,32 @@ fn use_multiple_spies() { let spy_events_checker_top_proxy_address = test_env.deploy_wrapper(&class_hash, &[spy_events_checker_proxy_address.into_()]); - let id1 = test_env.spy_events(SpyTarget::One(spy_events_checker_address)); - let id2 = test_env.spy_events(SpyTarget::One(spy_events_checker_proxy_address)); - let id3 = test_env.spy_events(SpyTarget::One(spy_events_checker_top_proxy_address)); - + // Events emitted in the order of: + // - spy_events_checker_top_proxy_address, + // - spy_events_checker_proxy_address, + // - spy_events_checker_address test_env.call_contract( &spy_events_checker_top_proxy_address, "emit_one_event", &[Felt252::from(123)], ); - let events1 = test_env.fetch_events(id1); - let events2 = test_env.fetch_events(id2); - let events3 = test_env.fetch_events(id3); + let events1 = test_env.get_events(0); + let events2 = test_env.get_events(1); + let events3 = test_env.get_events(2); - assert_eq!(events1.len(), 1, "There should be one event"); - assert_eq!(events2.len(), 1, "There should be one event"); + assert_eq!(events1.len(), 3, "There should be one event"); + assert_eq!(events2.len(), 2, "There should be one event"); assert_eq!(events3.len(), 1, "There should be one event"); assert_eq!( events1[0], Event { - from: spy_events_checker_address, + from: spy_events_checker_top_proxy_address, keys: vec![starknet_keccak("FirstEvent".as_ref()).into()], data: vec![Felt252::from(123)] }, - "Wrong spy_events_checker event" + "Wrong spy_events_checker_top_proxy event" ); assert_eq!( events2[0], @@ -366,11 +284,11 @@ fn use_multiple_spies() { assert_eq!( events3[0], Event { - from: spy_events_checker_top_proxy_address, + from: spy_events_checker_address, keys: vec![starknet_keccak("FirstEvent".as_ref()).into()], data: vec![Felt252::from(123)] }, - "Wrong spy_events_checker_top_proxy event" + "Wrong spy_events_checker event" ); } @@ -380,15 +298,13 @@ fn test_emitted_by_emit_events_syscall() { let contract_address = test_env.deploy("SpyEventsChecker", &[]); - let id = test_env.spy_events(SpyTarget::All); - test_env.call_contract( &contract_address, "emit_event_syscall", &[Felt252::from(123), Felt252::from(456)], ); - let events = test_env.fetch_events(id); + let events = test_env.get_events(0); assert_eq!(events.len(), 1, "There should be one event"); assert_eq!( @@ -415,8 +331,6 @@ fn capture_cairo0_event() { &[], ); - let id = cheatnet_state.spy_events(SpyTarget::All); - let selector = felt_selector_from_name("test_cairo0_event_collection"); let cairo0_contract_address = Felt252::try_from_hex_str( @@ -432,7 +346,7 @@ fn capture_cairo0_event() { &[cairo0_contract_address.clone()], ); - let events = cheatnet_state.fetch_events(&Felt252::from(id)); + let events = cheatnet_state.get_events(0); assert_eq!(events.len(), 1, "There should be one event"); diff --git a/crates/forge/tests/integration/spy_events.rs b/crates/forge/tests/integration/spy_events.rs index 83d48398d5..482339f243 100644 --- a/crates/forge/tests/integration/spy_events.rs +++ b/crates/forge/tests/integration/spy_events.rs @@ -13,7 +13,10 @@ fn spy_events_simple() { use array::ArrayTrait; use result::ResultTrait; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, EventAssertions, SpyOn }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; #[starknet::interface] trait ISpyEventsChecker { @@ -45,8 +48,8 @@ fn spy_events_simple() { let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); - assert(spy._id == 0, 'Id should be 0'); + let mut spy = spy_events(); + assert(spy._event_offset == 0, 'Event offset should be 0'); dispatcher.emit_one_event(123); @@ -58,7 +61,7 @@ fn spy_events_simple() { ) ) ]); - assert(spy.events.len() == 0, 'There should be no events'); + assert(spy.get_events().events.len() == 1, 'There should be one event'); } "# ), @@ -82,8 +85,10 @@ fn assert_emitted_fails() { use array::ArrayTrait; use result::ResultTrait; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, - EventAssertions, SpyOn }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; #[starknet::interface] trait ISpyEventsChecker { @@ -115,7 +120,7 @@ fn assert_emitted_fails() { let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.do_not_emit(); spy.assert_emitted(@array![ @@ -157,8 +162,10 @@ fn expect_three_events_while_two_emitted() { use traits::Into; use starknet::contract_address_const; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, - EventAssertions, SpyOn }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; #[starknet::interface] trait ISpyEventsChecker { @@ -210,7 +217,7 @@ fn expect_three_events_while_two_emitted() { let some_more_data = contract_address_const::<789>(); let even_more_data = 0; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.emit_two_events(some_data, some_more_data); spy.assert_emitted(@array![ @@ -268,8 +275,10 @@ fn expect_two_events_while_three_emitted() { use traits::Into; use starknet::contract_address_const; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, - EventAssertions, SpyOn }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; #[starknet::interface] trait ISpyEventsChecker { @@ -318,7 +327,7 @@ fn expect_two_events_while_three_emitted() { let some_more_data = contract_address_const::<789>(); let even_more_data = u256 { low: 1, high: 0 }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.emit_three_events(some_data, some_more_data, even_more_data); spy.assert_emitted(@array![ @@ -358,8 +367,10 @@ fn event_emitted_wrong_data_asserted() { use array::ArrayTrait; use result::ResultTrait; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, - EventAssertions, SpyOn }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; #[starknet::interface] trait ISpyEventsChecker { @@ -391,7 +402,7 @@ fn event_emitted_wrong_data_asserted() { let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.emit_one_event(123); spy.assert_emitted(@array![ @@ -437,10 +448,9 @@ fn emit_unnamed_event() { use traits::Into; use starknet::contract_address_const; use starknet::ContractAddress; - use snforge_std::{ - declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, - EventAssertions, Event, SpyOn + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait }; #[starknet::interface] @@ -454,7 +464,7 @@ fn emit_unnamed_event() { let (contract_address, _) = contract.deploy(@array![]).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.emit_event_syscall(123, 456); spy.assert_emitted(@array![ @@ -486,8 +496,10 @@ fn assert_not_emitted_pass() { use array::ArrayTrait; use result::ResultTrait; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, - EventAssertions, SpyOn }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; #[starknet::interface] trait ISpyEventsChecker { @@ -519,7 +531,7 @@ fn assert_not_emitted_pass() { let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.do_not_emit(); spy.assert_not_emitted(@array![ @@ -554,8 +566,8 @@ fn assert_not_emitted_fails() { use result::ResultTrait; use starknet::ContractAddress; use snforge_std::{ - declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, - EventAssertions, SpyOn + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait }; #[starknet::interface] @@ -588,9 +600,7 @@ fn assert_not_emitted_fails() { let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); - assert(spy._id == 0, 'Id should be 0'); - + let mut spy = spy_events(); dispatcher.emit_one_event(123); spy.assert_not_emitted(@array![ @@ -630,8 +640,10 @@ fn capture_cairo0_event() { use array::ArrayTrait; use result::ResultTrait; use starknet::{{ContractAddress, contract_address_const}}; - use snforge_std::{{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, - EventAssertions, SpyOn }}; + use snforge_std::{{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }}; #[starknet::interface] trait ISpyEventsChecker {{ @@ -672,7 +684,7 @@ fn capture_cairo0_event() { let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher {{ contract_address }}; - let mut spy = spy_events(SpyOn::All); + let mut spy = spy_events(); dispatcher.test_cairo0_event_collection(cairo0_contract_address.into()); dispatcher.emit_one_event(420); @@ -719,3 +731,84 @@ fn capture_cairo0_event() { assert_passed(&result); } + +#[test] +fn test_filtering() { + let test = test_case!( + indoc!( + r#" + use array::ArrayTrait; + use result::ResultTrait; + use starknet::ContractAddress; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait + }; + + #[starknet::interface] + trait ISpyEventsChecker { + fn emit_one_event(ref self: TContractState, some_data: felt252); + } + + #[starknet::contract] + mod SpyEventsChecker { + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + FirstEvent: FirstEvent + } + + #[derive(Drop, starknet::Event)] + struct FirstEvent { + some_data: felt252 + } + } + + #[test] + fn filter_events() { + let contract = declare("SpyEventsChecker").unwrap(); + let (first_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); + let (second_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); + + let first_dispatcher = ISpyEventsCheckerDispatcher { contract_address: first_address }; + let second_dispatcher = ISpyEventsCheckerDispatcher { contract_address: second_address }; + + let mut spy = spy_events(); + assert(spy._event_offset == 0, 'Event offset should be 0'); + + first_dispatcher.emit_one_event(123); + second_dispatcher.emit_one_event(234); + + let events_from_first_address = spy.get_events().emitted_by(first_address); + let events_from_second_address = spy.get_events().emitted_by(second_address); + + let (from, event) = events_from_first_address.events.at(0); + assert(from == @first_address, 'Emitted from wrong address'); + assert(event.keys.len() == 1, 'There should be one key'); + assert(event.keys.at(0) == @selector!("FirstEvent"), 'Wrong event name'); + assert(event.data.len() == 1, 'There should be one data'); + + let (from, event) = events_from_second_address.events.at(0); + assert(from == @second_address, 'Emitted from wrong address'); + assert(event.keys.len() == 1, 'There should be one key'); + assert(event.keys.at(0) == @selector!("FirstEvent"), 'Wrong event name'); + assert(event.data.len() == 1, 'There should be one data'); + } + "#, + ), + Contract::from_code_path( + "SpyEventsChecker".to_string(), + Path::new("tests/data/contracts/spy_events_checker.cairo"), + ) + .unwrap() + ); + + let result = run_test_case(&test); + + assert_passed(&result); +} diff --git a/crates/forge/tests/integration/test_state.rs b/crates/forge/tests/integration/test_state.rs index 546d362f4d..d802a0ca96 100644 --- a/crates/forge/tests/integration/test_state.rs +++ b/crates/forge/tests/integration/test_state.rs @@ -577,14 +577,16 @@ fn spy_events_simple() { use result::ResultTrait; use starknet::SyscallResultTrait; use starknet::ContractAddress; - use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, - EventAssertions, Event, SpyOn, test_address }; + use snforge_std::{ + declare, ContractClassTrait, spy_events, Event, EventSpy, + EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait, test_address + }; #[test] fn spy_events_simple() { let contract_address = test_address(); - let mut spy = spy_events(SpyOn::One(contract_address)); - assert(spy._id == 0, 'Id should be 0'); + let mut spy = spy_events(); + assert(spy._event_offset == 0, 'Events offset should be 0'); starknet::emit_event_syscall(array![1234].span(), array![2345].span()).unwrap_syscall(); @@ -594,8 +596,6 @@ fn spy_events_simple() { Event { keys: array![1234], data: array![2345] } ) ]); - - assert(spy.events.len() == 0, 'There should be no events left'); } " ),); @@ -610,10 +610,9 @@ fn spy_struct_events() { let test = test_case!(indoc!( r" use array::ArrayTrait; - use snforge_std::{ - declare, ContractClassTrait, spy_events, - EventSpy, EventFetcher, - EventAssertions, Event, SpyOn, test_address + use snforge_std::{ + declare, ContractClassTrait, spy_events, EventSpy, + EventSpyTrait, EventSpyAssertionsTrait, EventsFilterTrait, test_address }; #[starknet::interface] @@ -653,11 +652,11 @@ fn spy_struct_events() { #[test] fn spy_struct_events() { let contract_address = test_address(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); let mut testing_state = Emitter::contract_state_for_testing(); Emitter::EmitterImpl::emit_event(ref testing_state); - + spy.assert_emitted( @array![ ( diff --git a/docs/src/appendix/cheatcodes/spy_events.md b/docs/src/appendix/cheatcodes/spy_events.md index 77b334c598..ff20c76650 100644 --- a/docs/src/appendix/cheatcodes/spy_events.md +++ b/docs/src/appendix/cheatcodes/spy_events.md @@ -1,55 +1,57 @@ # `spy_events` -> `fn spy_events(spy_on: SpyOn) -> EventSpy` +> `fn spy_events() -> EventSpy` -Creates `EventSpy` instance which spies on events emitted by contracts defined under the `spy_on` argument. +Creates `EventSpy` instance which spies on events emitted after its creation. ```rust struct EventSpy { - events: Array<(ContractAddress, Event)>, + ... } ``` -An event spy structure, along with the events collected so far in the test. -`events` are mutable and can be updated with `fetch_events`. +An event spy structure. ```rust -struct Event { - keys: Array, - data: Array +struct Events { + events: Array<(ContractAddress, Event)> } ``` - -Raw event format (as seen via the RPC-API), can be used for asserting the emitted events. +A wrapper structure on an array of events to handle event filtering. ```rust -enum SpyOn { - All: (), - One: ContractAddress, - Multiple: Array +struct Event { + keys: Array, + data: Array } ``` - -Allows specifying which contracts you want to capture events from. +Raw event format (as seen via the RPC-API), can be used for asserting the emitted events. ## Implemented traits -### EventFetcher +### EventSpyTrait ```rust -trait EventFetcher { - fn fetch_events(ref self: EventSpy); +trait EventSpyTrait { + fn get_events(ref self: EventSpy) -> Events; } ``` +Gets all events since the creation of the given `EventSpy`. -Allows to update the structs' events field, from the spied contracts. - -### EventAssertions +### EventSpyAssertionsTrait ```rust -trait EventAssertions, impl TDrop: Drop> { +trait EventSpyAssertionsTrait, impl TDrop: Drop> { fn assert_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>); fn assert_not_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>); } ``` +Allows to assert the expected events emission (or lack thereof), in the scope of the `EventSpy` structure. + +### EventsFilterTrait -Allows to assert the expected events emission (or lack thereof), in the scope of the spy. \ No newline at end of file +```rust +trait EventsFilterTrait { + fn emitted_by(self: @Events, contract_address: ContractAddress) -> Events; +} +``` +Filters events emitted by a given `ContractAddress`. diff --git a/docs/src/testing/testing-events.md b/docs/src/testing/testing-events.md index c9317f1d48..dcc4de9193 100644 --- a/docs/src/testing/testing-events.md +++ b/docs/src/testing/testing-events.md @@ -27,8 +27,8 @@ This is the simpler way, in which you don't have to fetch the events explicitly. See the below code for reference: ```rust -use snforge_std::{declare, ContractClassTrait, spy_events, SpyOn, EventSpy, - EventAssertions}; +use snforge_std::{declare, ContractClassTrait, spy_events, EventSpy, EventSpyTrait, + EventSpyAssertionsTrait}; use SpyEventsChecker; @@ -43,11 +43,11 @@ fn test_simple_assertions() { let (contract_address, _) = contract.deploy(array![]).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); // Ad. 1 dispatcher.emit_one_event(123); - spy.assert_emitted(@array![ + spy.assert_emitted(@array![ // Ad. 2 ( contract_address, SpyEventsChecker::Event::FirstEvent( @@ -55,16 +55,15 @@ fn test_simple_assertions() { ) ) ]); - assert(spy.events.len() == 0, 'There should be no events'); } ``` Let's go through the code: -1. After the contract is called, we don't have to call `fetch_events` on the spy (it is done inside - the `assert_emitted` method). -2. `assert_emitted` takes the array snapshot of tuples `(ContractAddress, event)` we expect were emitted. -3. After the assertion, found events are removed from the spy. It stays clean and ready for the next events. +1. After contract deployment, we created the spy using `spy_events` cheatcode. From this moment all emitted events +will be spied. +2. Asserting is done using the `assert_emitted` method. It takes an array snapshot of `(ContractAddress, event)` +tuples we expect that were emitted. > 📝 **Note** > We can pass events defined in the contract and construct them like in the `self.emit` method! @@ -92,11 +91,13 @@ Note that both the event name and event data are checked. If a function emitted an event with the same name but a different payload, the `assert_not_emitted` function will pass. ## Asserting the events manually -You can also use the `event` field directly and assert data selectively, if you don't want to assert the whole thing. -This however, requires you to fetch the events **manually**. - +If you wish to assert the data manually, you can do that on the `Events` structure. +Simply call `get_events()` on your `EventSpy` and access `events` field on the returned `Events` value. +Then, you can access the events and assert data by yourself. + ```rust -use snforge_std::{declare, ContractClassTrait, spy_events, SpyOn, EventSpy, EventFetcher, Event}; +use snforge_std::{declare, ContractClassTrait, spy_events, EventSpyTrait, EventsAssertionsTrait, + Event}; #[starknet::interface] trait ISpyEventsChecker { @@ -109,50 +110,44 @@ fn test_complex_assertions() { let (contract_address, _) = contract.deploy(array![]).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); // Ad 1. + let mut spy = spy_events(); // Ad 1. dispatcher.emit_one_event(123); - spy.fetch_events(); // Ad 2. + let events = spy.get_events(); // Ad 2. - assert(spy.events.len() == 1, 'There should be one event'); + assert(events.events.len() == 1, 'There should be one event'); - let (from, event) = spy.events.at(0); // Ad 3. + let (from, event) = events.events.at(0); // Ad 3. assert(from == @contract_address, 'Emitted from wrong address'); assert(event.keys.len() == 1, 'There should be one key'); assert(event.keys.at(0) == @selector!("FirstEvent"), 'Wrong event name'); // Ad 4. assert(event.data.len() == 1, 'There should be one data'); - - dispatcher.emit_one_event(123); - assert(spy.events.len() == 1, 'There should be one event'); // Ad 5. - Still one event - - spy.fetch_events(); - assert(spy.events.len() == 2, 'There should be two events'); } ``` Let's go through important parts of the provided code: 1. After contract deployment we created the spy with `spy_events` cheatcode. - From this moment all events emitted by the `SpyEventsChecker` contract will be spied. -2. We have to call `fetch_events` method on the created spy to load emitted events into it. -3. When events are fetched they are loaded into the `events` property of our spy, and we can assert them. +From this moment all events emitted by the `SpyEventsChecker` contract will be spied. +2. We have to call `get_events` method on the created spy to fetch our events and get the `Events` structure. +3. To get our particular event, we need to access the `events` property and get the event under an index. +Since `events` is an array holding a tuple of `ContractAddress` and `Event`, we unpack it using `let (from, event)`. 4. If the event is emitted by calling `self.emit` method, its hashed name is saved under the `keys.at(0)` (this way Starknet handles events) -5. It is worth noting that when we call the method which emits an event, `spy` is not updated immediately. > 📝 **Note** > To assert the `name` property we have to hash a string with the `selector!` macro. -## Splitting Events Between Multiple Spies +## Filtering Events -Sometimes it is easier to split events between multiple spies. -For example - one spy for ERC20 contract, and one for your own contracts. -Let's do it. +Sometimes, when you assert the events manually, you might not want to get all the events, but only ones from +a particular address. You can address that by using the method `emitted_by` on the `Events` structure. ```rust -use snforge_std::{declare, ContractClassTrait, spy_events, SpyOn, EventSpy, EventAssertions}; +use snforge_std::{declare, ContractClassTrait, spy_events, EventSpyTrait, + EventsAssertionsTrait, EventsFilterTrait}; use SpyEventsChecker; @@ -162,49 +157,39 @@ trait ISpyEventsChecker { } #[test] -fn test_simple_assertions() { +fn test_assertions_with_filtering() { let contract = declare("SpyEventsChecker").unwrap(); let (first_address, _) = contract.deploy(array![]).unwrap(); let (second_address, _) = contract.deploy(array![]).unwrap(); - let (third_address, _) = contract.deploy(array![]).unwrap(); - let first_dispatcher = ISpyEventsCheckerDispatcher { first_address }; - let second_dispatcher = ISpyEventsCheckerDispatcher { second_address }; - let third_dispatcher = ISpyEventsCheckerDispatcher { third_address }; + let first_dispatcher = ISpyEventsCheckerDispatcher { contract_address: first_address }; + let second_dispatcher = ISpyEventsCheckerDispatcher { contract_address: second_address }; - let mut spy_one = spy_events(SpyOn::One(first_address)); - let mut spy_two = spy_events(SpyOn::Multiple(array![second_address, third_address])); + let mut spy = spy_events(); first_dispatcher.emit_one_event(123); second_dispatcher.emit_one_event(234); - third_dispatcher.emit_one_event(345); + second_dispatcher.emit_one_event(345); - spy_one.assert_emitted(@array![ - ( - first_address, - SpyEventsChecker::Event::FirstEvent( - SpyEventsChecker::FirstEvent { some_data: 123 } - ) - ) - ]); - spy_two.assert_emitted(@array![ - ( - second_address, - SpyEventsChecker::Event::FirstEvent( - SpyEventsChecker::FirstEvent { some_data: 234 } - ) - ), - ( - third_address, - SpyEventsChecker::Event::FirstEvent( - SpyEventsChecker::FirstEvent { some_data: 345 } - ) - ) - ]); + let events_from_first_address = spy.get_events().emitted_by(first_address); + let events_from_second_address = spy.get_events().emitted_by(second_address); + + let (from_first, event_from_first) = events_from_first_address.events.at(0); + assert(from_first == @first_address, 'Emitted from wrong address'); + assert(event_from_first.data.at(0) == @123.into(), 'Data should be 123'); + + let (from_second_one, event_from_second_one) = events_from_second_address.events.at(0); + assert(from_second_one == @second_address, 'Emitted from wrong address'); + assert(event_from_second_one.data.at(0) == @234.into(), 'Data should be 234'); + + let (from_second_two, event_from_second_two) = events_from_second_address.events.at(1); + assert(from_second_two == @second_address, 'Emitted from wrong address'); + assert(event_from_second_two.data.at(0) == @345.into(), 'Data should be 345'); } ``` -The first spy gets events emitted by the first contract only. Second one gets events emitted by the rest. +`events_from_first_address` has events emitted by the first contract only. +Similarly, `events_from_second_address` has events emitted by the second contract. ## Asserting Events Emitted With `emit_event_syscall` @@ -222,8 +207,8 @@ fn emit_event_syscall(ref self: ContractState, some_key: felt252, some_data: fel And the test. ```rust -use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, - EventAssertions, Event, SpyOn }; +use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpyTrait, + EventsAssertionsTrait, Event }; #[starknet::interface] trait ISpyEventsChecker { @@ -231,12 +216,12 @@ trait ISpyEventsChecker { } #[test] -fn test_simple_assertions() { +fn test_nonstandard_events() { let contract = declare("SpyEventsChecker").unwrap(); let (contract_address, _) = contract.deploy(array![]).unwrap(); let dispatcher = ISpyEventsCheckerDispatcher { contract_address }; - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); dispatcher.emit_event_syscall(123, 456); spy.assert_emitted(@array![ @@ -250,9 +235,3 @@ fn test_simple_assertions() { Using `Event` struct from the `snforge_std` library we can easily assert nonstandard events. This also allows for testing the events you don't have the code of, or you don't want to import those. - - -> ⚠️ **Warning** -> -> Spying on the same contract with multiple spies can result in unexpected behavior — avoid it if possible. - diff --git a/docs/src/testing/testing_contract_internals.md b/docs/src/testing/testing_contract_internals.md index 41bd41365e..65b987eed3 100644 --- a/docs/src/testing/testing_contract_internals.md +++ b/docs/src/testing/testing_contract_internals.md @@ -132,13 +132,13 @@ You can implement this test: use array::ArrayTrait; use snforge_std::{ declare, ContractClassTrait, spy_events, - EventSpy, EventFetcher, - EventAssertions, Event, SpyOn, test_address + EventSpy, EventSpyTrait, EventSpyAssertionsTrait, + Event, test_address }; #[test] fn test_expect_event() { let contract_address = test_address(); - let mut spy = spy_events(SpyOn::One(contract_address)); + let mut spy = spy_events(); let mut testing_state = Emitter::contract_state_for_testing(); Emitter::emit_event(ref testing_state); @@ -160,14 +160,13 @@ use array::ArrayTrait; use result::ResultTrait; use starknet::SyscallResultTrait; use starknet::ContractAddress; -use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher, - EventAssertions, Event, SpyOn, test_address }; +use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventSpyTrait, + EventSpyAssertionsTrait, Event, test_address }; #[test] fn test_expect_events_simple() { let test_address = test_address(); - let mut spy = spy_events(SpyOn::One(test_address)); - assert(spy._id == 0, 'Id should be 0'); + let mut spy = spy_events(); starknet::emit_event_syscall(array![1234].span(), array![2345].span()).unwrap_syscall(); diff --git a/snforge_std/src/cheatcodes/events.cairo b/snforge_std/src/cheatcodes/events.cairo index 778e4403b3..7c5cdddc08 100644 --- a/snforge_std/src/cheatcodes/events.cairo +++ b/snforge_std/src/cheatcodes/events.cairo @@ -4,22 +4,12 @@ use starknet::testing::cheatcode; use starknet::ContractAddress; use super::super::_cheatcode::handle_cheatcode; -/// Allows specifying which contracts you want to capture events from. -#[derive(Drop, Serde)] -enum SpyOn { - All: (), - One: ContractAddress, - Multiple: Array -} +/// Creates `EventSpy` instance that spies on all events emitted after its creation. +fn spy_events() -> EventSpy { + let mut event_offset = handle_cheatcode(cheatcode::<'spy_events'>(array![].span())); + let parsed_event_offset: usize = Serde::::deserialize(ref event_offset).unwrap(); -/// Creates `EventSpy` instance which spies on events emitted by contracts defined under the -/// `spy_on` argument. -fn spy_events(spy_on: SpyOn) -> EventSpy { - let mut inputs = array![]; - spy_on.serialize(ref inputs); - let output = handle_cheatcode(cheatcode::<'spy_events'>(inputs.span())); - - EventSpy { _id: *output[0], events: array![] } + EventSpy { _event_offset: parsed_event_offset } } /// Raw event format (as seen via the RPC-API), can be used for asserting the emitted events. @@ -29,50 +19,72 @@ struct Event { data: Array } - -/// An event spy structure, along with the events collected so far in the test. -/// `events` are mutable and can be updated with `fetch_events`. +/// An event spy structure allowing to get events emitted only after its creation. #[derive(Drop, Serde)] struct EventSpy { - _id: felt252, - events: Array<(ContractAddress, Event)>, + _event_offset: usize +} + +/// A wrapper structure on an array of events to handle filtering smoothly. +#[derive(Drop, Serde)] +struct Events { + events: Array<(ContractAddress, Event)> } -trait EventFetcher { - /// Allows to update the structs' events field, from the spied contracts - fn fetch_events(ref self: EventSpy); +trait EventSpyTrait { + /// Gets all events given [`EventSpy`] spies for. + fn get_events(ref self: EventSpy) -> Events; } -impl EventFetcherImpl of EventFetcher { - fn fetch_events(ref self: EventSpy) { - let mut output = handle_cheatcode(cheatcode::<'fetch_events'>(array![self._id].span())); +impl EventSpyTraitImpl of EventSpyTrait { + fn get_events(ref self: EventSpy) -> Events { + let mut output = handle_cheatcode( + cheatcode::<'get_events'>(array![self._event_offset.into()].span()) + ); let events = Serde::>::deserialize(ref output).unwrap(); - let mut i = 0; - while i < events.len() { - let (from, event) = events.at(i); - self.events.append((*from, event.clone())); - i += 1; - } + Events { events } + } +} + +trait EventsFilterTrait { + /// Filter events emitted by a given [`ContractAddress`]. + fn emitted_by(self: @Events, contract_address: ContractAddress) -> Events; +} + +impl EventsFilterTraitImpl of EventsFilterTrait { + fn emitted_by(self: @Events, contract_address: ContractAddress) -> Events { + let mut counter = 0; + let mut new_events = array![]; + + while counter < self.events.len() { + let (from, event) = self.events.at(counter); + if *from == contract_address { + new_events.append((*from, event.clone())); + }; + counter += 1; + }; + Events { events: new_events } } } -/// Allows to assert the expected events emission (or lack thereof), in the scope of the spy -trait EventAssertions, impl TDrop: Drop> { +/// Allows to assert the expected events emission (or lack thereof), +/// in the scope of [`EventSpy`] structure. +trait EventSpyAssertionsTrait, impl TDrop: Drop> { fn assert_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>); fn assert_not_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>); } -impl EventAssertionsImpl< +impl EventSpyAssertionsTraitImpl< T, impl TEvent: starknet::Event, impl TDrop: Drop -> of EventAssertions { +> of EventSpyAssertionsTrait { fn assert_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>) { - self.fetch_events(); - let mut i = 0; + let received_events = self.get_events(); + while i < events.len() { let (from, event) = events.at(i); - let emitted = is_emitted(ref self, from, event); + let emitted = is_emitted(@received_events, from, event); if !emitted { let from: felt252 = (*from).into(); @@ -84,12 +96,12 @@ impl EventAssertionsImpl< } fn assert_not_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>) { - self.fetch_events(); - let mut i = 0; + let received_events = self.get_events(); + while i < events.len() { let (from, event) = events.at(i); - let emitted = is_emitted(ref self, from, event); + let emitted = is_emitted(@received_events, from, event); if emitted { let from: felt252 = (*from).into(); @@ -102,50 +114,34 @@ impl EventAssertionsImpl< } fn is_emitted, impl TDrop: Drop>( - ref self: EventSpy, expected_from: @ContractAddress, expected_event: @T + self: @Events, expected_emitted_by: @ContractAddress, expected_event: @T ) -> bool { - let emitted_events = @self.events; - let mut expected_keys = array![]; let mut expected_data = array![]; expected_event.append_keys_and_data(ref expected_keys, ref expected_data); - let mut j = 0; + let mut i = 0; let mut is_emitted = false; - while j < emitted_events.len() { - let (from, event) = emitted_events.at(j); + while i < self.events.len() { + let (from, event) = self.events.at(i); - if from == expected_from && event.keys == @expected_keys && event.data == @expected_data { - remove_event(ref self, j); + if from == expected_emitted_by + && event.keys == @expected_keys + && event.data == @expected_data { is_emitted = true; break; }; - j += 1; + i += 1; }; return is_emitted; } -fn remove_event(ref self: EventSpy, index: usize) { - let emitted_events = @self.events; - let mut emitted_events_deleted_event = array![]; - let mut k = 0; - while k < emitted_events.len() { - if k != index { - let (from, event) = emitted_events.at(k); - emitted_events_deleted_event.append((*from, event.clone())); - } - k += 1; - }; - self.events = emitted_events_deleted_event; -} - impl EventTraitImpl of starknet::Event { fn append_keys_and_data(self: @Event, ref keys: Array, ref data: Array) { keys.append_span(self.keys.span()); data.append_span(self.data.span()); } - fn deserialize(ref keys: Span, ref data: Span) -> Option { Option::None } diff --git a/snforge_std/src/lib.cairo b/snforge_std/src/lib.cairo index cafcbfc1bb..5bf6b9e165 100644 --- a/snforge_std/src/lib.cairo +++ b/snforge_std/src/lib.cairo @@ -11,11 +11,11 @@ use cheatcodes::l1_handler::L1HandlerTrait; use cheatcodes::fork::BlockTag; use cheatcodes::fork::BlockId; -use cheatcodes::events::SpyOn; use cheatcodes::events::Event; use cheatcodes::events::EventSpy; -use cheatcodes::events::EventFetcher; -use cheatcodes::events::EventAssertions; +use cheatcodes::events::EventSpyTrait; +use cheatcodes::events::EventSpyAssertionsTrait; +use cheatcodes::events::EventsFilterTrait; use cheatcodes::events::spy_events; use cheatcodes::storage::store;