using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; namespace ZB.MOM.WW.CBDD.Tests; public class StorageEngineTransactionProtocolTests { /// /// Verifies preparing an unknown transaction returns false. /// [Fact] public void PrepareTransaction_Should_ReturnFalse_For_Unknown_Transaction() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); storage.PrepareTransaction(999_999).ShouldBeFalse(); } finally { CleanupFiles(dbPath); } } /// /// Verifies committing a detached transaction object throws. /// [Fact] public void CommitTransaction_With_TransactionObject_Should_Throw_When_Not_Active() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); var detached = new Transaction(123, storage); Should.Throw(() => storage.CommitTransaction(detached)); } finally { CleanupFiles(dbPath); } } /// /// Verifies committing a transaction object persists writes and clears active state. /// [Fact] public void CommitTransaction_With_TransactionObject_Should_Commit_Writes() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); var pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[0] = 0xAB; storage.WritePage(pageId, txn.TransactionId, data); storage.CommitTransaction(txn); storage.ActiveTransactionCount.ShouldBe(0); var readBuffer = new byte[storage.PageSize]; storage.ReadPage(pageId, 0, readBuffer); readBuffer[0].ShouldBe((byte)0xAB); } finally { CleanupFiles(dbPath); } } /// /// Verifies committing by identifier with no writes does not throw. /// [Fact] public void CommitTransaction_ById_With_NoWrites_Should_Not_Throw() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); storage.CommitTransaction(424242); } finally { CleanupFiles(dbPath); } } /// /// Verifies committed transaction cache moves into readable state and active count is cleared. /// [Fact] public void MarkTransactionCommitted_Should_Move_Cache_And_Clear_ActiveCount() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); var pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[5] = 0x5A; storage.WritePage(pageId, txn.TransactionId, data); storage.ActiveTransactionCount.ShouldBe(1); storage.MarkTransactionCommitted(txn.TransactionId); storage.ActiveTransactionCount.ShouldBe(0); var readBuffer = new byte[storage.PageSize]; storage.ReadPage(pageId, 0, readBuffer); readBuffer[5].ShouldBe((byte)0x5A); } finally { CleanupFiles(dbPath); } } /// /// Verifies rollback discards uncommitted page writes. /// [Fact] public void RollbackTransaction_Should_Discard_Uncommitted_Write() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); var pageId = storage.AllocatePage(); var baseline = new byte[storage.PageSize]; baseline[0] = 0x11; storage.WritePageImmediate(pageId, baseline); using var txn = storage.BeginTransaction(); var changed = new byte[storage.PageSize]; changed[0] = 0x99; storage.WritePage(pageId, txn.TransactionId, changed); storage.ActiveTransactionCount.ShouldBe(1); storage.RollbackTransaction(txn.TransactionId); storage.ActiveTransactionCount.ShouldBe(0); var readBuffer = new byte[storage.PageSize]; storage.ReadPage(pageId, 0, readBuffer); readBuffer[0].ShouldBe((byte)0x11); } finally { CleanupFiles(dbPath); } } /// /// Verifies marking a transaction committed transitions state correctly. /// [Fact] public void Transaction_MarkCommitted_Should_Transition_State() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); var pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[3] = 0x33; storage.WritePage(pageId, txn.TransactionId, data); txn.MarkCommitted(); txn.State.ShouldBe(TransactionState.Committed); storage.ActiveTransactionCount.ShouldBe(0); var readBuffer = new byte[storage.PageSize]; storage.ReadPage(pageId, 0, readBuffer); readBuffer[3].ShouldBe((byte)0x33); } finally { CleanupFiles(dbPath); } } /// /// Verifies preparing then committing writes WAL data and updates transaction state. /// [Fact] public void Transaction_Prepare_Should_Write_Wal_And_Transition_State() { var dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); var pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[11] = 0x7B; storage.WritePage(pageId, txn.TransactionId, data); txn.Prepare().ShouldBeTrue(); txn.State.ShouldBe(TransactionState.Preparing); txn.Commit(); txn.State.ShouldBe(TransactionState.Committed); } finally { CleanupFiles(dbPath); } } private static string NewDbPath() => Path.Combine(Path.GetTempPath(), $"storage_txn_{Guid.NewGuid():N}.db"); private static void CleanupFiles(string dbPath) { if (File.Exists(dbPath)) File.Delete(dbPath); var walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); var altWalPath = dbPath + "-wal"; if (File.Exists(altWalPath)) File.Delete(altWalPath); } }