use super::*;
use crate::engine::log::LogWriter;
use crate::engine::reader::{CommittedState, LogReader};
use crate::kernel::bank::{BankApp, BankEvent, BankQuery, BankQueryResponse};
use std::fs;
use std::path::Path;
use std::sync::Arc;
fn serialize_event(event: &BankEvent) -> Vec<u8> {
bincode::serialize(event).unwrap()
}
#[test]
fn test_bank_execution_flow() {
let path = Path::new("/tmp/chr_bank_test.log");
let _ = fs::remove_file(path);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(path, 1).unwrap();
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(0);
let payload = serialize_event(&BankEvent::Withdraw {
user: "Alice".to_string(),
amount: 1000,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(1);
let payload = serialize_event(&BankEvent::Withdraw {
user: "Alice".to_string(),
amount: 50,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(2);
let payload = serialize_event(&BankEvent::PoisonPill);
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(3);
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 10,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(4);
drop(writer);
let reader = LogReader::open(path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::new(reader, app, 0);
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Applied { index: 0, .. }));
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(100)));
let result = executor.step().unwrap();
match result {
StepResult::Rejected { index: 1, error } => {
assert!(error.contains("Insufficient funds"));
}
other => panic!("Expected Rejected, got {:?}", other),
}
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(100)));
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Applied { index: 2, .. }));
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(50)));
let result = executor.step();
match result {
Err(FatalError::PoisonPill { index: 3, message }) => {
assert!(message.contains("POISON PILL"));
}
other => panic!("Expected PoisonPill error, got {:?}", other),
}
assert!(executor.is_halted());
assert_eq!(executor.status(), ExecutorStatus::Halted);
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(50)));
let result = executor.step();
assert!(matches!(result, Err(FatalError::Halted)));
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(50)));
assert_eq!(executor.next_index(), 3);
let _ = fs::remove_file(path);
}
#[test]
fn test_executor_idle_when_caught_up() {
let path = Path::new("/tmp/chr_executor_idle.log");
let _ = fs::remove_file(path);
let committed_state = Arc::new(CommittedState::new());
let writer = LogWriter::create(path, 1).unwrap();
drop(writer);
let reader = LogReader::open(path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::new(reader, app, 0);
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Idle));
let _ = fs::remove_file(path);
}
#[test]
fn test_executor_respects_committed_index() {
let path = Path::new("/tmp/chr_executor_visibility.log");
let _ = fs::remove_file(path);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(path, 1).unwrap();
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(0);
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 200,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
drop(writer);
let reader = LogReader::open(path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::new(reader, app, 0);
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Applied { index: 0, .. }));
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Idle));
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(100)));
committed_state.advance(1);
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Applied { index: 1, .. }));
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(300)));
let _ = fs::remove_file(path);
}
#[test]
fn test_snapshot_isolation() {
use crate::kernel::snapshot::SnapshotManifest;
let log_path = Path::new("/tmp/chr_snapshot_isolation.log");
let snapshot_dir = PathBuf::from("/tmp/chr_snapshot_isolation_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..5 {
let payload = serialize_event(&BankEvent::Deposit {
user: format!("User{}", i),
amount: (i + 1) * 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
for _ in 0..5 {
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Applied { .. }));
}
assert_eq!(executor.last_applied_index(), Some(4));
let mut reader_for_hash = LogReader::open(log_path, committed_state.clone()).unwrap();
let expected_chain_hash = reader_for_hash.get_chain_hash(4).unwrap();
let snapshot_index = executor.take_snapshot().unwrap();
assert_eq!(snapshot_index, 4);
let snapshot_filename = SnapshotManifest::filename_for_index(snapshot_index);
let snapshot_path = snapshot_dir.join(&snapshot_filename);
assert!(snapshot_path.exists(), "Snapshot file should exist");
let loaded_snapshot = SnapshotManifest::load_from_file(&snapshot_path).unwrap();
assert_eq!(loaded_snapshot.last_included_index, 4);
assert_eq!(
loaded_snapshot.chain_hash, expected_chain_hash,
"Snapshot chain_hash should match log entry chain_hash"
);
assert!(loaded_snapshot.side_effects_dropped);
use crate::engine::recovery::{LogRecovery, RecoveryOutcome};
let recovery = LogRecovery::open(log_path).unwrap().unwrap();
let outcome = recovery.scan().unwrap();
let (next_index, write_offset, tail_hash) = match outcome {
RecoveryOutcome::Clean { last_index, next_offset, tail_hash, .. } => {
(last_index + 1, next_offset, tail_hash)
}
_ => panic!("Expected clean recovery"),
};
let mut writer = LogWriter::open(
log_path,
next_index,
write_offset,
tail_hash,
1, ).unwrap();
for i in 5..8u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: format!("User{}", i),
amount: ((i + 1) * 100) as u64,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
for _ in 0..3 {
let result = executor.step().unwrap();
assert!(matches!(result, StepResult::Applied { .. }));
}
assert_eq!(executor.last_applied_index(), Some(7));
assert_eq!(executor.last_snapshot_index(), Some(4));
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_snapshot_no_entries_fails() {
let log_path = Path::new("/tmp/chr_snapshot_no_entries.log");
let snapshot_dir = PathBuf::from("/tmp/chr_snapshot_no_entries_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let writer = LogWriter::create(log_path, 1).unwrap();
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
let result = executor.take_snapshot();
assert!(matches!(result, Err(FatalError::SnapshotError(_))));
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_should_snapshot_threshold() {
let log_path = Path::new("/tmp/chr_snapshot_threshold.log");
let snapshot_dir = PathBuf::from("/tmp/chr_snapshot_threshold_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..10 {
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 10,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
executor.set_snapshot_threshold(5);
assert!(!executor.should_snapshot());
for _ in 0..4 {
executor.step().unwrap();
}
assert!(!executor.should_snapshot());
executor.step().unwrap();
assert!(executor.should_snapshot());
executor.take_snapshot().unwrap();
assert!(!executor.should_snapshot());
for _ in 0..4 {
executor.step().unwrap();
}
assert!(!executor.should_snapshot());
executor.step().unwrap();
assert!(executor.should_snapshot());
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_executor_restart_integrity() {
use crate::kernel::snapshot::SnapshotManifest;
let log_path = Path::new("/tmp/chr_restart_integrity.log");
let snapshot_dir = PathBuf::from("/tmp/chr_restart_integrity_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..10u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
for _ in 0..5 {
executor.step().unwrap();
}
let snapshot_index = executor.take_snapshot().unwrap();
assert_eq!(snapshot_index, 4);
for _ in 0..5 {
executor.step().unwrap();
}
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(1000)));
let final_next_index = executor.next_index();
assert_eq!(final_next_index, 10);
drop(executor);
let reader2 = LogReader::open(log_path, committed_state.clone()).unwrap();
let app2 = BankApp::new();
let recovered_executor = Executor::recover(reader2, app2, snapshot_dir.clone()).unwrap();
assert_eq!(recovered_executor.next_index(), 10);
let response = recovered_executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(1000)));
assert_eq!(recovered_executor.last_snapshot_index(), Some(4));
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_corrupt_bridge_fails() {
use crate::kernel::snapshot::SnapshotManifest;
let log_path = Path::new("/tmp/chr_corrupt_bridge.log");
let snapshot_dir = PathBuf::from("/tmp/chr_corrupt_bridge_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..5u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
for _ in 0..3 {
executor.step().unwrap();
}
executor.take_snapshot().unwrap();
drop(executor);
let snapshot_filename = SnapshotManifest::filename_for_index(2);
let snapshot_path = snapshot_dir.join(&snapshot_filename);
let mut snapshot = SnapshotManifest::load_from_file(&snapshot_path).unwrap();
snapshot.chain_hash[0] ^= 0xFF;
snapshot.chain_hash[1] ^= 0xFF;
let corrupted_manifest = SnapshotManifest::new(
snapshot.last_included_index,
snapshot.last_included_term,
snapshot.chain_hash, snapshot.state,
);
corrupted_manifest.save_to_file(&snapshot_path).unwrap();
let reader2 = LogReader::open(log_path, committed_state.clone()).unwrap();
let app2 = BankApp::new();
let result = Executor::recover(reader2, app2, snapshot_dir.clone());
match result {
Err(FatalError::ChainBridgeMismatch { .. }) => {},
Err(e) => panic!("Expected ChainBridgeMismatch, got {:?}", e),
Ok(_) => panic!("Expected ChainBridgeMismatch error, but recovery succeeded"),
}
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_log_gap_after_snapshot_fails() {
use crate::kernel::snapshot::SnapshotManifest;
let log_path = Path::new("/tmp/chr_log_gap.log");
let snapshot_dir = PathBuf::from("/tmp/chr_log_gap_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..5u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
for _ in 0..3 {
executor.step().unwrap();
}
executor.take_snapshot().unwrap();
drop(executor);
let fake_snapshot = SnapshotManifest::new(
10, 0,
[0u8; 16], bincode::serialize(&std::collections::HashMap::<String, u64>::new()).unwrap(),
);
let fake_path = snapshot_dir.join(SnapshotManifest::filename_for_index(10));
fake_snapshot.save_to_file(&fake_path).unwrap();
let reader2 = LogReader::open(log_path, committed_state.clone()).unwrap();
let app2 = BankApp::new();
let result = Executor::recover(reader2, app2, snapshot_dir.clone());
match result {
Err(FatalError::LogBehindSnapshot { .. }) => {},
Err(e) => panic!("Expected LogBehindSnapshot, got {:?}", e),
Ok(_) => panic!("Expected LogBehindSnapshot error, but recovery succeeded"),
}
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_recovery_no_snapshot() {
let log_path = Path::new("/tmp/chr_recovery_no_snapshot.log");
let snapshot_dir = PathBuf::from("/tmp/chr_recovery_no_snapshot_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..5u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let executor = Executor::recover(reader, app, snapshot_dir.clone()).unwrap();
assert_eq!(executor.next_index(), 5);
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(500)));
assert_eq!(executor.last_snapshot_index(), None);
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_recovery_fallback_to_older_snapshot() {
use crate::kernel::snapshot::SnapshotManifest;
let log_path = Path::new("/tmp/chr_fallback_snapshot.log");
let snapshot_dir = PathBuf::from("/tmp/chr_fallback_snapshot_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..10u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
});
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
for _ in 0..3 {
executor.step().unwrap();
}
executor.take_snapshot().unwrap();
for _ in 0..3 {
executor.step().unwrap();
}
executor.take_snapshot().unwrap();
for _ in 0..4 {
executor.step().unwrap();
}
drop(executor);
let latest_snapshot_path = snapshot_dir.join(SnapshotManifest::filename_for_index(5));
let mut data = fs::read(&latest_snapshot_path).unwrap();
data[10] ^= 0xFF; fs::write(&latest_snapshot_path, &data).unwrap();
let reader2 = LogReader::open(log_path, committed_state.clone()).unwrap();
let app2 = BankApp::new();
let executor = Executor::recover(reader2, app2, snapshot_dir.clone()).unwrap();
assert_eq!(executor.next_index(), 10);
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(1000)));
assert_eq!(executor.last_snapshot_index(), Some(2));
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}
#[test]
fn test_physical_compaction_integrity() {
use crate::kernel::snapshot::SnapshotManifest;
use crate::engine::recovery::{LogRecovery, RecoveryOutcome};
let log_path = Path::new("/tmp/chr_compaction_integrity.log");
let snapshot_dir = PathBuf::from("/tmp/chr_compaction_integrity_snapshots");
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
let committed_state = Arc::new(CommittedState::new());
let mut writer = LogWriter::create(log_path, 1).unwrap();
for i in 0..100u64 {
let payload = serialize_event(&BankEvent::Deposit {
user: format!("User{}", i),
amount: i + 1, });
writer.append(&payload, 0, 0, 1_000_000_000).unwrap();
committed_state.advance(i);
}
drop(writer);
let original_size = fs::metadata(log_path).unwrap().len();
let mut reader = LogReader::open(log_path, committed_state.clone()).unwrap();
let app = BankApp::new();
let mut executor = Executor::with_snapshot_dir(reader, app, 0, snapshot_dir.clone());
for _ in 0..51 {
executor.step().unwrap();
}
let snapshot_index = executor.take_snapshot().unwrap();
assert_eq!(snapshot_index, 50);
let mut reader_for_hash = LogReader::open(log_path, committed_state.clone()).unwrap();
let chain_hash = reader_for_hash.get_chain_hash(50).unwrap();
let cut_offset = reader_for_hash.get_authoritative_offset(51).unwrap();
drop(executor);
drop(reader_for_hash);
LogWriter::truncate_prefix(
log_path,
51, cut_offset, chain_hash, ).unwrap();
let new_size = fs::metadata(log_path).unwrap().len();
assert!(
new_size < original_size,
"File size should be reduced after truncation: {} < {}",
new_size, original_size
);
let mut reader2 = LogReader::open(log_path, committed_state.clone()).unwrap();
assert_eq!(reader2.base_index(), 51);
let entry51 = reader2.read(51).unwrap();
assert_eq!(entry51.index, 51);
match reader2.read(49) {
Err(crate::engine::reader::ReadError::IndexTruncated { requested: 49, base_index: 51 }) => {},
other => panic!("Expected IndexTruncated for index 49, got {:?}", other),
}
match reader2.read(50) {
Err(crate::engine::reader::ReadError::IndexTruncated { requested: 50, base_index: 51 }) => {},
other => panic!("Expected IndexTruncated for index 50, got {:?}", other),
}
drop(reader2);
let recovery = LogRecovery::open(log_path).unwrap().unwrap();
let outcome = recovery.scan().unwrap();
match outcome {
RecoveryOutcome::Clean { last_index, base_index, base_prev_hash, .. } => {
assert_eq!(last_index, 99, "Last index should be 99");
assert_eq!(base_index, 51, "Base index should be 51");
assert_eq!(base_prev_hash, chain_hash, "Base prev_hash should match snapshot chain_hash");
}
other => panic!("Expected Clean recovery, got {:?}", other),
}
let reader3 = LogReader::open(log_path, committed_state.clone()).unwrap();
let app3 = BankApp::new();
let recovered_executor = Executor::recover(reader3, app3, snapshot_dir.clone()).unwrap();
assert_eq!(recovered_executor.next_index(), 100);
assert_eq!(recovered_executor.last_snapshot_index(), Some(50));
let response = recovered_executor.query(BankQuery::Balance {
user: "User0".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(1)));
let response = recovered_executor.query(BankQuery::Balance {
user: "User50".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(51)));
let response = recovered_executor.query(BankQuery::Balance {
user: "User99".to_string(),
});
assert!(matches!(response, BankQueryResponse::Balance(100)));
let _ = fs::remove_file(log_path);
let _ = fs::remove_dir_all(&snapshot_dir);
}