//! Assertion macros and utilities for contract testing. //! //! Provides convenient macros for testing contract execution results, //! storage state, and emitted events. use synor_vm::ExecutionResult; /// Result type returned from assertion checks. pub type AssertionResult = Result<(), AssertionError>; /// Errors from assertion checks. #[derive(Debug, thiserror::Error)] pub enum AssertionError { /// Expected success but got error. #[error("Expected success but got error: {0}")] ExpectedSuccess(String), /// Expected revert but got success. #[error("Expected revert but got success")] ExpectedRevert, /// Expected specific revert message. #[error("Expected revert message '{expected}' but got '{actual}'")] RevertMessageMismatch { expected: String, actual: String }, /// Storage value mismatch. #[error("Storage mismatch for key {key}: expected {expected:?}, got {actual:?}")] StorageMismatch { key: String, expected: String, actual: String, }, /// Storage key not found. #[error("Storage key not found: {0}")] StorageKeyNotFound(String), /// Event not found. #[error("Expected event '{0}' was not emitted")] EventNotFound(String), /// Event topic mismatch. #[error("Event topic mismatch: expected {expected}, got {actual}")] EventTopicMismatch { expected: String, actual: String }, /// Return data mismatch. #[error("Return data mismatch: expected {expected:?}, got {actual:?}")] ReturnDataMismatch { expected: Vec, actual: Vec }, /// Gas assertion failed. #[error("Gas assertion failed: {0}")] GasAssertion(String), } /// Asserts that a contract execution succeeded. /// /// # Examples /// /// ```rust,ignore /// let result = env.call_contract(contract, "transfer", &args)?; /// assert_success!(result); /// ``` #[macro_export] macro_rules! assert_success { ($result:expr) => { match &$result { Ok(_) => {} Err(e) => { panic!("Expected success but got error: {:?}", e); } } }; ($result:expr, $msg:expr) => { match &$result { Ok(_) => {} Err(e) => { panic!("{}: Expected success but got error: {:?}", $msg, e); } } }; } /// Asserts that a contract execution reverted. /// /// # Examples /// /// ```rust,ignore /// let result = env.call_contract(contract, "transfer", &args); /// assert_revert!(result); /// assert_revert!(result, "insufficient balance"); /// ``` #[macro_export] macro_rules! assert_revert { ($result:expr) => { match &$result { Ok(_) => { panic!("Expected revert but got success"); } Err($crate::synor_vm::VmError::Revert(_)) => {} Err(e) => { // Also accept other errors as "revert" in some sense let _ = e; } } }; ($result:expr, $message:expr) => { match &$result { Ok(_) => { panic!("Expected revert but got success"); } Err($crate::synor_vm::VmError::Revert(msg)) => { if !msg.contains($message) { panic!( "Expected revert message containing '{}' but got '{}'", $message, msg ); } } Err(e) => { panic!( "Expected Revert error with message '{}' but got: {:?}", $message, e ); } } }; } /// Asserts that a storage value equals the expected value. /// /// # Examples /// /// ```rust,ignore /// assert_storage_eq!(env, contract, "balance", 1000u64); /// assert_storage_eq!(env, contract, key_bytes, expected_value); /// ``` #[macro_export] macro_rules! assert_storage_eq { ($env:expr, $contract:expr, $key:expr, $expected:expr) => {{ let storage_key = $crate::synor_vm::StorageKey::from_bytes($key.as_ref()); match $env.get_storage(&$contract, &storage_key) { Some(value) => { let expected_bytes = $crate::to_storage_bytes(&$expected); if value.as_bytes() != expected_bytes.as_slice() { panic!( "Storage mismatch for key {:?}: expected {:?}, got {:?}", $key, expected_bytes, value.as_bytes() ); } } None => { panic!("Storage key not found: {:?}", $key); } } }}; } /// Asserts that an event was emitted during contract execution. /// /// # Examples /// /// ```rust,ignore /// let result = env.call_contract(contract, "transfer", &args)?; /// assert_event_emitted!(result, "Transfer"); /// assert_event_emitted!(result, "Transfer", topic1, topic2); /// ``` #[macro_export] macro_rules! assert_event_emitted { ($result:expr, $event_type:expr) => {{ let event_selector = $crate::event_selector($event_type); let found = $result.logs.iter().any(|log| { log.topics .first() .map(|t| t.as_bytes()[..4] == event_selector) .unwrap_or(false) }); if !found { panic!("Expected event '{}' was not emitted", $event_type); } }}; ($result:expr, $event_type:expr, $($topic:expr),+) => {{ let event_selector = $crate::event_selector($event_type); let expected_topics: Vec<[u8; 32]> = vec![$($crate::to_topic_bytes(&$topic)),+]; let found = $result.logs.iter().any(|log| { if log.topics.first() .map(|t| t.as_bytes()[..4] == event_selector) .unwrap_or(false) { // Check additional topics if log.topics.len() < expected_topics.len() + 1 { return false; } expected_topics.iter().enumerate().all(|(i, expected)| { log.topics.get(i + 1) .map(|t| t.as_bytes() == expected) .unwrap_or(false) }) } else { false } }); if !found { panic!( "Expected event '{}' with specified topics was not emitted", $event_type ); } }}; } /// Asserts that no events were emitted. #[macro_export] macro_rules! assert_no_events { ($result:expr) => { if !$result.logs.is_empty() { panic!( "Expected no events but {} events were emitted", $result.logs.len() ); } }; } /// Asserts that gas used is within a range. #[macro_export] macro_rules! assert_gas_used { ($result:expr, $min:expr, $max:expr) => { let gas_used = $result.gas_used; if gas_used < $min || gas_used > $max { panic!( "Gas used {} is outside expected range [{}, {}]", gas_used, $min, $max ); } }; } /// Asserts that the return data matches expected value. #[macro_export] macro_rules! assert_return_data { ($result:expr, $expected:expr) => { let expected_bytes: Vec = $crate::to_return_bytes(&$expected); if $result.return_data != expected_bytes { panic!( "Return data mismatch: expected {:?}, got {:?}", expected_bytes, $result.return_data ); } }; } /// Asserts that storage is empty for a contract. #[macro_export] macro_rules! assert_storage_empty { ($env:expr, $contract:expr) => { if $env.has_storage(&$contract) { panic!("Expected empty storage for contract {:?}", $contract); } }; } /// Asserts that a storage key does not exist. #[macro_export] macro_rules! assert_storage_none { ($env:expr, $contract:expr, $key:expr) => {{ let storage_key = $crate::synor_vm::StorageKey::from_bytes($key.as_ref()); if $env.get_storage(&$contract, &storage_key).is_some() { panic!("Expected no storage at key {:?}", $key); } }}; } // Helper functions for the macros /// Computes an event selector (first 4 bytes of blake3 hash). pub fn event_selector(event_name: &str) -> [u8; 4] { let hash: [u8; 32] = blake3::hash(event_name.as_bytes()).into(); [hash[0], hash[1], hash[2], hash[3]] } /// Converts a value to storage bytes. pub fn to_storage_bytes(value: &T) -> Vec { value.to_storage_bytes() } /// Converts a value to topic bytes (32 bytes). pub fn to_topic_bytes(value: &T) -> [u8; 32] { value.to_topic_bytes() } /// Converts a value to return bytes. pub fn to_return_bytes(value: &T) -> Vec { value.to_return_bytes() } /// Trait for types that can be encoded as storage values. pub trait StorageEncodable { fn to_storage_bytes(&self) -> Vec; } impl StorageEncodable for u64 { fn to_storage_bytes(&self) -> Vec { self.to_le_bytes().to_vec() } } impl StorageEncodable for u128 { fn to_storage_bytes(&self) -> Vec { self.to_le_bytes().to_vec() } } impl StorageEncodable for bool { fn to_storage_bytes(&self) -> Vec { vec![if *self { 1 } else { 0 }] } } impl StorageEncodable for [u8; 32] { fn to_storage_bytes(&self) -> Vec { self.to_vec() } } impl StorageEncodable for Vec { fn to_storage_bytes(&self) -> Vec { self.clone() } } impl StorageEncodable for &[u8] { fn to_storage_bytes(&self) -> Vec { self.to_vec() } } /// Trait for types that can be encoded as event topics. pub trait TopicEncodable { fn to_topic_bytes(&self) -> [u8; 32]; } impl TopicEncodable for [u8; 32] { fn to_topic_bytes(&self) -> [u8; 32] { *self } } impl TopicEncodable for u64 { fn to_topic_bytes(&self) -> [u8; 32] { let mut bytes = [0u8; 32]; bytes[24..32].copy_from_slice(&self.to_be_bytes()); bytes } } impl TopicEncodable for synor_types::Address { fn to_topic_bytes(&self) -> [u8; 32] { *self.payload() } } /// Trait for types that can be encoded as return data. pub trait ReturnEncodable { fn to_return_bytes(&self) -> Vec; } impl ReturnEncodable for () { fn to_return_bytes(&self) -> Vec { Vec::new() } } impl ReturnEncodable for u64 { fn to_return_bytes(&self) -> Vec { self.to_le_bytes().to_vec() } } impl ReturnEncodable for u128 { fn to_return_bytes(&self) -> Vec { self.to_le_bytes().to_vec() } } impl ReturnEncodable for bool { fn to_return_bytes(&self) -> Vec { vec![if *self { 1 } else { 0 }] } } impl ReturnEncodable for Vec { fn to_return_bytes(&self) -> Vec { self.clone() } } impl ReturnEncodable for &[u8] { fn to_return_bytes(&self) -> Vec { self.to_vec() } } /// Assertion helper for checking multiple conditions. pub struct AssertionChain<'a> { result: &'a ExecutionResult, errors: Vec, } impl<'a> AssertionChain<'a> { /// Creates a new assertion chain for a result. pub fn new(result: &'a ExecutionResult) -> Self { AssertionChain { result, errors: Vec::new(), } } /// Asserts gas used is within range. pub fn gas_in_range(mut self, min: u64, max: u64) -> Self { if self.result.gas_used < min || self.result.gas_used > max { self.errors.push(format!( "Gas {} not in range [{}, {}]", self.result.gas_used, min, max )); } self } /// Asserts event count. pub fn event_count(mut self, expected: usize) -> Self { if self.result.logs.len() != expected { self.errors.push(format!( "Expected {} events, got {}", expected, self.result.logs.len() )); } self } /// Asserts return data length. pub fn return_data_len(mut self, expected: usize) -> Self { if self.result.return_data.len() != expected { self.errors.push(format!( "Expected return data length {}, got {}", expected, self.result.return_data.len() )); } self } /// Asserts storage change count. pub fn storage_changes(mut self, expected: usize) -> Self { if self.result.storage_changes.len() != expected { self.errors.push(format!( "Expected {} storage changes, got {}", expected, self.result.storage_changes.len() )); } self } /// Checks if all assertions passed. pub fn is_ok(&self) -> bool { self.errors.is_empty() } /// Finishes the chain and returns result. pub fn finish(self) -> Result<(), String> { if self.errors.is_empty() { Ok(()) } else { Err(self.errors.join("\n")) } } /// Panics if any assertion failed. pub fn unwrap(self) { if !self.errors.is_empty() { panic!("Assertion failures:\n{}", self.errors.join("\n")); } } } #[cfg(test)] mod tests { use super::*; use synor_types::Hash256; use synor_vm::ContractLog; fn mock_result() -> ExecutionResult { ExecutionResult { return_data: vec![1, 2, 3, 4], gas_used: 1000, logs: vec![ContractLog { contract: synor_vm::ContractId::from_bytes([0x42; 32]), topics: vec![Hash256::from_bytes({ let mut bytes = [0u8; 32]; let selector = event_selector("Transfer"); bytes[..4].copy_from_slice(&selector); bytes })], data: vec![], }], storage_changes: vec![], internal_calls: vec![], } } #[test] fn test_event_selector() { let sel1 = event_selector("Transfer"); let sel2 = event_selector("Transfer"); assert_eq!(sel1, sel2); let sel3 = event_selector("Approval"); assert_ne!(sel1, sel3); } #[test] fn test_storage_encodable() { assert_eq!(to_storage_bytes(&42u64), 42u64.to_le_bytes().to_vec()); assert_eq!(to_storage_bytes(&true), vec![1]); assert_eq!(to_storage_bytes(&false), vec![0]); } #[test] fn test_assertion_chain() { let result = mock_result(); AssertionChain::new(&result) .gas_in_range(500, 1500) .event_count(1) .return_data_len(4) .unwrap(); } #[test] #[should_panic(expected = "Gas")] fn test_assertion_chain_failure() { let result = mock_result(); AssertionChain::new(&result) .gas_in_range(2000, 3000) // Will fail .unwrap(); } }