pub mod builder;
pub mod executor;
pub mod log;
pub mod types;
pub use builder::{
BatchBuilder, BatchResult, TxnBuilder, quick_create, quick_delete, quick_mkdir, quick_rename,
};
pub use executor::{MemoryFs, Transaction, TxnExecutor, TxnFilesystem};
pub use log::{TxnLog, TxnLogHeader};
pub use types::{
DEFAULT_TXN_TIMEOUT, MAX_OPS_PER_TXN, MAX_TXN_SIZE, RecoveryStats, TXN_LOG_MAGIC,
TXN_LOG_VERSION, TxnError, TxnId, TxnLogEntry, TxnLogType, TxnOperation, TxnResult,
TxnResultType, TxnState,
};
#[cfg(test)]
mod tests {
use super::*;
use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn test_exports_accessible() {
let _ = TxnId::new(1);
let _ = TxnState::Active;
let _ = TxnLogType::Begin;
let _ = TxnBuilder::new("test");
let _ = BatchBuilder::new();
}
#[test]
fn test_full_transaction_flow() {
let fs = Box::new(MemoryFs::new());
let mut executor = TxnExecutor::new("test/pool", fs, 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Create {
path: "/file1.txt".into(),
content: b"content1".to_vec(),
mode: 0o644,
},
1001,
)
.unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Create {
path: "/file2.txt".into(),
content: b"content2".to_vec(),
mode: 0o644,
},
1002,
)
.unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Mkdir {
path: "/subdir".into(),
mode: 0o755,
},
1003,
)
.unwrap();
let result = executor.commit(txn_id, 1004).unwrap();
assert!(result.is_success());
assert_eq!(result.ops_executed, 3);
}
#[test]
fn test_builder_with_executor() {
let fs = Box::new(MemoryFs::new());
let mut executor = TxnExecutor::new("test/pool", fs, 1000);
let result = TxnBuilder::new("test/pool")
.create("/a.txt", b"hello")
.create("/b.txt", b"world")
.commit_with_executor(&mut executor, 1000)
.unwrap();
assert!(result.is_success());
assert_eq!(result.ops_executed, 2);
}
#[test]
fn test_transaction_abort() {
let fs = Box::new(MemoryFs::new());
let mut executor = TxnExecutor::new("test/pool", fs, 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Create {
path: "/will_be_aborted.txt".into(),
content: b"never saved".to_vec(),
mode: 0o644,
},
1001,
)
.unwrap();
let result = executor.abort(txn_id, 1002).unwrap();
assert!(!result.is_success());
assert_eq!(result.state, TxnState::Aborted);
}
#[test]
fn test_rename_transaction() {
let fs = MemoryFs::new();
fs.create("/original.txt", b"content", 0o644).unwrap();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Rename {
old_path: "/original.txt".into(),
new_path: "/renamed.txt".into(),
},
1001,
)
.unwrap();
let result = executor.commit(txn_id, 1002).unwrap();
assert!(result.is_success());
}
#[test]
fn test_chained_operations() {
let ops = TxnBuilder::new("test/pool")
.mkdir("/project", 0o755)
.create("/project/README.md", b"# Project")
.create("/project/main.rs", b"fn main() {}")
.chmod("/project/main.rs", 0o755)
.build();
assert_eq!(ops.len(), 4);
}
#[test]
fn test_operation_descriptions() {
let ops = vec![
TxnOperation::Create {
path: "/test.txt".into(),
content: vec![1, 2, 3],
mode: 0o644,
},
TxnOperation::Rename {
old_path: "/old".into(),
new_path: "/new".into(),
},
TxnOperation::Delete {
path: "/remove.txt".into(),
content: None,
mode: None,
},
];
for op in ops {
let desc = op.description();
assert!(!desc.is_empty());
}
}
#[test]
fn test_log_entry_lifecycle() {
let mut log = TxnLog::new("test/pool", 1, 1000);
let txn_id = TxnId::new(1);
log.begin(txn_id, 1000).unwrap();
log.log_operation(
txn_id,
TxnOperation::Create {
path: "/test.txt".into(),
content: b"content".to_vec(),
mode: 0o644,
},
1001,
)
.unwrap();
log.prepare(txn_id, 1002).unwrap();
assert_eq!(log.txn_state(txn_id), Some(TxnState::Prepared));
let ops = log.commit(txn_id, 1003).unwrap();
assert_eq!(ops.len(), 1);
}
#[test]
fn test_recovery_stats() {
let mut stats = RecoveryStats::new();
stats.txns_recovered = 5;
stats.txns_rolled_forward = 2;
stats.txns_rolled_back = 3;
stats.ops_replayed = 10;
let mut stats2 = RecoveryStats::new();
stats2.txns_recovered = 3;
stats2.ops_replayed = 5;
stats.merge(&stats2);
assert_eq!(stats.txns_recovered, 8);
assert_eq!(stats.ops_replayed, 15);
}
#[test]
fn test_error_types() {
let errors = vec![
TxnError::NotFound(TxnId::new(1)),
TxnError::AlreadyExists(TxnId::new(2)),
TxnError::PathNotFound("/missing".into()),
TxnError::PathExists("/exists".into()),
TxnError::PermissionDenied("/forbidden".into()),
];
for err in errors {
let msg = err.to_string();
assert!(!msg.is_empty());
}
}
#[test]
fn test_txn_id_display() {
let id = TxnId::new(0x123456);
let display = id.to_string();
assert!(display.contains("123456"));
}
#[test]
fn test_batch_with_failures() {
let fs1 = MemoryFs::new();
let fs2 = MemoryFs::new();
fs2.create("/exists.txt", b"already here", 0o644).unwrap();
let result = BatchBuilder::new()
.stop_on_error(false)
.add(
TxnBuilder::new("pool1")
.with_fs(Box::new(fs1))
.create("/new.txt", b"new"),
)
.add(
TxnBuilder::new("pool2")
.with_fs(Box::new(fs2))
.create("/exists.txt", b"conflict"), )
.execute();
assert_eq!(result.success_count(), 1);
assert_eq!(result.failure_count(), 1);
}
#[test]
fn test_constants() {
const { assert!(TXN_LOG_MAGIC > 0) };
const { assert!(TXN_LOG_VERSION > 0) };
const { assert!(MAX_OPS_PER_TXN > 0) };
const { assert!(MAX_TXN_SIZE > 0) };
const { assert!(DEFAULT_TXN_TIMEOUT > 0) };
}
#[test]
fn test_quick_functions() {
assert!(quick_create("pool", "/file.txt", b"content").is_ok());
assert!(quick_mkdir("pool", "/dir", 0o755).is_ok());
}
#[test]
fn test_write_operation() {
let fs = MemoryFs::new();
fs.create("/file.txt", b"hello world", 0o644).unwrap();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Write {
path: "/file.txt".into(),
offset: 0,
content: b"HELLO".to_vec(),
original: None,
},
1001,
)
.unwrap();
let result = executor.commit(txn_id, 1002).unwrap();
assert!(result.is_success());
}
#[test]
fn test_truncate_operation() {
let fs = MemoryFs::new();
fs.create("/file.txt", b"hello world", 0o644).unwrap();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Truncate {
path: "/file.txt".into(),
new_size: 5,
original_size: None,
},
1001,
)
.unwrap();
let result = executor.commit(txn_id, 1002).unwrap();
assert!(result.is_success());
}
#[test]
fn test_link_operation() {
let fs = MemoryFs::new();
fs.create("/source.txt", b"content", 0o644).unwrap();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Link {
source: "/source.txt".into(),
target: "/link.txt".into(),
},
1001,
)
.unwrap();
let result = executor.commit(txn_id, 1002).unwrap();
assert!(result.is_success());
}
#[test]
fn test_symlink_operation() {
let fs = MemoryFs::new();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Symlink {
path: "/link".into(),
target: "/target".into(),
},
1001,
)
.unwrap();
let result = executor.commit(txn_id, 1002).unwrap();
assert!(result.is_success());
}
#[test]
fn test_attr_operations() {
let fs = MemoryFs::new();
fs.create("/file.txt", b"content", 0o644).unwrap();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::SetAttr {
path: "/file.txt".into(),
attr: "user.test".into(),
value: b"value".to_vec(),
original: None,
},
1001,
)
.unwrap();
executor
.add_operation(
txn_id,
TxnOperation::RemoveAttr {
path: "/file.txt".into(),
attr: "user.test".into(),
original: None,
},
1002,
)
.unwrap();
let result = executor.commit(txn_id, 1003).unwrap();
assert!(result.is_success());
assert_eq!(result.ops_executed, 2);
}
#[test]
fn test_chown_operation() {
let fs = MemoryFs::new();
fs.create("/file.txt", b"content", 0o644).unwrap();
let mut executor = TxnExecutor::new("test/pool", Box::new(fs), 1000);
let txn_id = executor.begin(1000).unwrap();
executor
.add_operation(
txn_id,
TxnOperation::Chown {
path: "/file.txt".into(),
uid: 1000,
gid: 1000,
original_uid: None,
original_gid: None,
},
1001,
)
.unwrap();
let result = executor.commit(txn_id, 1002).unwrap();
assert!(result.is_success());
}
}