synor/crates/synor-contract-test/src/assertions.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

553 lines
15 KiB
Rust

//! 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<u8>, actual: Vec<u8> },
/// 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<u8> = $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<T: StorageEncodable>(value: &T) -> Vec<u8> {
value.to_storage_bytes()
}
/// Converts a value to topic bytes (32 bytes).
pub fn to_topic_bytes<T: TopicEncodable>(value: &T) -> [u8; 32] {
value.to_topic_bytes()
}
/// Converts a value to return bytes.
pub fn to_return_bytes<T: ReturnEncodable>(value: &T) -> Vec<u8> {
value.to_return_bytes()
}
/// Trait for types that can be encoded as storage values.
pub trait StorageEncodable {
fn to_storage_bytes(&self) -> Vec<u8>;
}
impl StorageEncodable for u64 {
fn to_storage_bytes(&self) -> Vec<u8> {
self.to_le_bytes().to_vec()
}
}
impl StorageEncodable for u128 {
fn to_storage_bytes(&self) -> Vec<u8> {
self.to_le_bytes().to_vec()
}
}
impl StorageEncodable for bool {
fn to_storage_bytes(&self) -> Vec<u8> {
vec![if *self { 1 } else { 0 }]
}
}
impl StorageEncodable for [u8; 32] {
fn to_storage_bytes(&self) -> Vec<u8> {
self.to_vec()
}
}
impl StorageEncodable for Vec<u8> {
fn to_storage_bytes(&self) -> Vec<u8> {
self.clone()
}
}
impl StorageEncodable for &[u8] {
fn to_storage_bytes(&self) -> Vec<u8> {
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<u8>;
}
impl ReturnEncodable for () {
fn to_return_bytes(&self) -> Vec<u8> {
Vec::new()
}
}
impl ReturnEncodable for u64 {
fn to_return_bytes(&self) -> Vec<u8> {
self.to_le_bytes().to_vec()
}
}
impl ReturnEncodable for u128 {
fn to_return_bytes(&self) -> Vec<u8> {
self.to_le_bytes().to_vec()
}
}
impl ReturnEncodable for bool {
fn to_return_bytes(&self) -> Vec<u8> {
vec![if *self { 1 } else { 0 }]
}
}
impl ReturnEncodable for Vec<u8> {
fn to_return_bytes(&self) -> Vec<u8> {
self.clone()
}
}
impl ReturnEncodable for &[u8] {
fn to_return_bytes(&self) -> Vec<u8> {
self.to_vec()
}
}
/// Assertion helper for checking multiple conditions.
pub struct AssertionChain<'a> {
result: &'a ExecutionResult,
errors: Vec<String>,
}
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();
}
}