synor/crates/synor-contract-test/src/mock_storage.rs
2026-01-08 05:22:24 +05:30

537 lines
15 KiB
Rust

//! Mock storage implementation for contract testing.
//!
//! Provides isolated, in-memory storage for testing contracts without
//! affecting any persistent state.
use std::collections::HashMap;
use parking_lot::RwLock;
use synor_types::Hash256;
use synor_vm::{
storage::{ContractStorage, StorageKey, StorageValue},
ContractId, StorageChange,
};
/// Mock storage for isolated contract testing.
///
/// Provides a thread-safe, in-memory storage implementation with
/// additional features useful for testing:
/// - Snapshots for rollback
/// - Storage inspection and iteration
/// - Change tracking
///
/// # Example
///
/// ```rust,ignore
/// let mut storage = MockStorage::new();
///
/// // Set some values
/// storage.set(&contract_id, key, value);
///
/// // Take a snapshot
/// let snapshot = storage.snapshot();
///
/// // Make more changes
/// storage.set(&contract_id, key2, value2);
///
/// // Restore to snapshot
/// storage.restore(snapshot);
/// ```
#[derive(Debug, Default)]
pub struct MockStorage {
/// Storage data by contract.
data: RwLock<HashMap<ContractId, HashMap<StorageKey, StorageValue>>>,
/// Pending changes (for transaction-like behavior).
pending: RwLock<HashMap<ContractId, HashMap<StorageKey, Option<StorageValue>>>>,
/// Change history for inspection.
history: RwLock<Vec<StorageChange>>,
/// Whether to track history.
track_history: bool,
}
impl MockStorage {
/// Creates a new empty mock storage.
pub fn new() -> Self {
MockStorage {
data: RwLock::new(HashMap::new()),
pending: RwLock::new(HashMap::new()),
history: RwLock::new(Vec::new()),
track_history: true,
}
}
/// Creates mock storage with history tracking disabled.
pub fn without_history() -> Self {
MockStorage {
data: RwLock::new(HashMap::new()),
pending: RwLock::new(HashMap::new()),
history: RwLock::new(Vec::new()),
track_history: false,
}
}
/// Creates mock storage with pre-set values.
pub fn with_data(data: HashMap<ContractId, HashMap<StorageKey, StorageValue>>) -> Self {
MockStorage {
data: RwLock::new(data),
pending: RwLock::new(HashMap::new()),
history: RwLock::new(Vec::new()),
track_history: true,
}
}
/// Gets all keys for a contract.
pub fn keys(&self, contract: &ContractId) -> Vec<StorageKey> {
self.data
.read()
.get(contract)
.map(|m| m.keys().copied().collect())
.unwrap_or_default()
}
/// Gets all key-value pairs for a contract.
pub fn entries(&self, contract: &ContractId) -> Vec<(StorageKey, StorageValue)> {
self.data
.read()
.get(contract)
.map(|m| m.iter().map(|(k, v)| (*k, v.clone())).collect())
.unwrap_or_default()
}
/// Gets the number of storage entries for a contract.
pub fn len(&self, contract: &ContractId) -> usize {
self.data.read().get(contract).map(|m| m.len()).unwrap_or(0)
}
/// Checks if a contract has any storage.
pub fn is_empty(&self, contract: &ContractId) -> bool {
self.len(contract) == 0
}
/// Checks if any contract has storage.
pub fn is_completely_empty(&self) -> bool {
self.data.read().is_empty()
}
/// Clears all storage for a contract.
pub fn clear_contract(&self, contract: &ContractId) {
self.data.write().remove(contract);
self.pending.write().remove(contract);
}
/// Clears all storage.
pub fn clear_all(&self) {
self.data.write().clear();
self.pending.write().clear();
self.history.write().clear();
}
/// Takes a snapshot of the current storage state.
pub fn snapshot(&self) -> StorageSnapshot {
StorageSnapshot {
data: self.data.read().clone(),
}
}
/// Restores storage to a previous snapshot.
pub fn restore(&self, snapshot: StorageSnapshot) {
*self.data.write() = snapshot.data;
self.pending.write().clear();
}
/// Gets the change history.
pub fn get_history(&self) -> Vec<StorageChange> {
self.history.read().clone()
}
/// Clears the change history.
pub fn clear_history(&self) {
self.history.write().clear();
}
/// Gets pending changes without committing them.
pub fn pending_changes(&self) -> Vec<StorageChange> {
let pending = self.pending.read();
let data = self.data.read();
let mut changes = Vec::new();
for (contract, pending_changes) in pending.iter() {
for (key, new_value) in pending_changes {
let old_value = data.get(contract).and_then(|m| m.get(key)).cloned();
changes.push(StorageChange {
contract: *contract,
key: *key,
old_value,
new_value: new_value.clone(),
});
}
}
changes
}
/// Takes pending changes without committing.
pub fn take_pending_changes(&self) -> Vec<StorageChange> {
let changes = self.pending_changes();
self.pending.write().clear();
changes
}
/// Sets a value directly (bypasses pending/commit flow).
pub fn set_direct(&self, contract: &ContractId, key: StorageKey, value: StorageValue) {
self.data
.write()
.entry(*contract)
.or_default()
.insert(key, value);
}
/// Deletes a value directly.
pub fn delete_direct(&self, contract: &ContractId, key: &StorageKey) -> Option<StorageValue> {
self.data
.write()
.get_mut(contract)
.and_then(|m| m.remove(key))
}
/// Gets a value directly (ignores pending changes).
pub fn get_direct(&self, contract: &ContractId, key: &StorageKey) -> Option<StorageValue> {
self.data
.read()
.get(contract)
.and_then(|m| m.get(key))
.cloned()
}
/// Iterates over all contracts.
pub fn contracts(&self) -> Vec<ContractId> {
self.data.read().keys().copied().collect()
}
/// Gets statistics about storage usage.
pub fn stats(&self) -> StorageStats {
let data = self.data.read();
let mut total_entries = 0;
let mut total_bytes = 0;
for contract_storage in data.values() {
total_entries += contract_storage.len();
for value in contract_storage.values() {
total_bytes += value.len();
}
}
StorageStats {
contract_count: data.len(),
total_entries,
total_bytes,
history_len: self.history.read().len(),
}
}
}
impl ContractStorage for MockStorage {
fn get(&self, contract: &ContractId, key: &StorageKey) -> Option<StorageValue> {
// Check pending first
let pending = self.pending.read();
if let Some(contract_pending) = pending.get(contract) {
if let Some(pending_value) = contract_pending.get(key) {
return pending_value.clone();
}
}
drop(pending);
// Then check committed data
self.data
.read()
.get(contract)
.and_then(|m| m.get(key))
.cloned()
}
fn set(&mut self, contract: &ContractId, key: StorageKey, value: StorageValue) {
self.pending
.write()
.entry(*contract)
.or_default()
.insert(key, Some(value));
}
fn delete(&mut self, contract: &ContractId, key: &StorageKey) -> Option<StorageValue> {
let old_value = self.get(contract, key);
self.pending
.write()
.entry(*contract)
.or_default()
.insert(*key, None);
old_value
}
fn contains(&self, contract: &ContractId, key: &StorageKey) -> bool {
self.get(contract, key).is_some()
}
fn root(&self, contract: &ContractId) -> Hash256 {
let data = self.data.read();
let mut entries: Vec<_> = data
.get(contract)
.map(|m| m.iter().collect())
.unwrap_or_default();
entries.sort_by_key(|(k, _)| *k);
if entries.is_empty() {
return Hash256::default();
}
let mut hasher_input = Vec::new();
for (key, value) in entries {
hasher_input.extend_from_slice(&key.0);
hasher_input.extend_from_slice(&value.0);
}
Hash256::from_bytes(blake3::hash(&hasher_input).into())
}
fn commit(&mut self) {
let mut pending = self.pending.write();
let mut data = self.data.write();
let mut history = self.history.write();
for (contract, changes) in pending.drain() {
let contract_data = data.entry(contract).or_default();
for (key, new_value) in changes {
let old_value = contract_data.get(&key).cloned();
// Track history if enabled
if self.track_history {
history.push(StorageChange {
contract,
key,
old_value,
new_value: new_value.clone(),
});
}
// Apply change
match new_value {
Some(v) => {
contract_data.insert(key, v);
}
None => {
contract_data.remove(&key);
}
}
}
}
}
fn rollback(&mut self) {
self.pending.write().clear();
}
}
// Implement Clone for MockStorage (needed for some test patterns)
impl Clone for MockStorage {
fn clone(&self) -> Self {
MockStorage {
data: RwLock::new(self.data.read().clone()),
pending: RwLock::new(self.pending.read().clone()),
history: RwLock::new(self.history.read().clone()),
track_history: self.track_history,
}
}
}
/// Snapshot of storage state for rollback.
#[derive(Clone, Debug)]
pub struct StorageSnapshot {
data: HashMap<ContractId, HashMap<StorageKey, StorageValue>>,
}
impl StorageSnapshot {
/// Gets the number of contracts in the snapshot.
pub fn contract_count(&self) -> usize {
self.data.len()
}
/// Gets storage entries for a contract.
pub fn get_contract(
&self,
contract: &ContractId,
) -> Option<&HashMap<StorageKey, StorageValue>> {
self.data.get(contract)
}
}
/// Statistics about storage usage.
#[derive(Clone, Debug, Default)]
pub struct StorageStats {
/// Number of contracts with storage.
pub contract_count: usize,
/// Total number of storage entries.
pub total_entries: usize,
/// Total bytes of storage values.
pub total_bytes: usize,
/// Length of change history.
pub history_len: usize,
}
impl std::fmt::Display for StorageStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"StorageStats {{ contracts: {}, entries: {}, bytes: {}, history: {} }}",
self.contract_count, self.total_entries, self.total_bytes, self.history_len
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_contract_id() -> ContractId {
ContractId::from_bytes([0x42; 32])
}
fn test_key() -> StorageKey {
StorageKey::from_bytes(b"test_key")
}
fn test_value() -> StorageValue {
StorageValue::from_u64(12345)
}
#[test]
fn test_mock_storage_basic() {
let mut storage = MockStorage::new();
let contract = test_contract_id();
let key = test_key();
let value = test_value();
// Set value
storage.set(&contract, key, value.clone());
// Read pending
assert_eq!(storage.get(&contract, &key), Some(value.clone()));
// Commit
storage.commit();
// Read committed
assert_eq!(storage.get(&contract, &key), Some(value));
}
#[test]
fn test_mock_storage_rollback() {
let mut storage = MockStorage::new();
let contract = test_contract_id();
let key = test_key();
storage.set(&contract, key, StorageValue::from_u64(100));
storage.commit();
storage.set(&contract, key, StorageValue::from_u64(200));
assert_eq!(
storage.get(&contract, &key),
Some(StorageValue::from_u64(200))
);
storage.rollback();
assert_eq!(
storage.get(&contract, &key),
Some(StorageValue::from_u64(100))
);
}
#[test]
fn test_mock_storage_snapshot() {
let mut storage = MockStorage::new();
let contract = test_contract_id();
let key = test_key();
storage.set(&contract, key, StorageValue::from_u64(100));
storage.commit();
let snapshot = storage.snapshot();
storage.set(&contract, key, StorageValue::from_u64(200));
storage.commit();
assert_eq!(
storage.get(&contract, &key),
Some(StorageValue::from_u64(200))
);
storage.restore(snapshot);
assert_eq!(
storage.get(&contract, &key),
Some(StorageValue::from_u64(100))
);
}
#[test]
fn test_mock_storage_history() {
let mut storage = MockStorage::new();
let contract = test_contract_id();
let key = test_key();
storage.set(&contract, key, StorageValue::from_u64(100));
storage.commit();
storage.set(&contract, key, StorageValue::from_u64(200));
storage.commit();
let history = storage.get_history();
assert_eq!(history.len(), 2);
}
#[test]
fn test_mock_storage_stats() {
let mut storage = MockStorage::new();
let contract = test_contract_id();
storage.set(&contract, test_key(), test_value());
storage.commit();
let stats = storage.stats();
assert_eq!(stats.contract_count, 1);
assert_eq!(stats.total_entries, 1);
assert!(stats.total_bytes > 0);
}
#[test]
fn test_mock_storage_delete() {
let mut storage = MockStorage::new();
let contract = test_contract_id();
let key = test_key();
storage.set(&contract, key, test_value());
storage.commit();
assert!(storage.contains(&contract, &key));
storage.delete(&contract, &key);
storage.commit();
assert!(!storage.contains(&contract, &key));
}
#[test]
fn test_mock_storage_direct_access() {
let storage = MockStorage::new();
let contract = test_contract_id();
let key = test_key();
let value = test_value();
storage.set_direct(&contract, key, value.clone());
assert_eq!(storage.get_direct(&contract, &key), Some(value));
storage.delete_direct(&contract, &key);
assert_eq!(storage.get_direct(&contract, &key), None);
}
}