using System.Reflection; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; namespace ZB.MOM.WW.CBDD.Tests; public class CheckpointModeTests { /// /// Verifies default checkpoint mode truncates WAL. /// [Fact] public void Checkpoint_Default_ShouldUseTruncate() { string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); db.Users.Insert(new User { Name = "checkpoint-default", Age = 42 }); db.SaveChanges(); db.Storage.GetWalSize().ShouldBeGreaterThan(0); var result = db.Checkpoint(); result.Mode.ShouldBe(CheckpointMode.Truncate); result.Executed.ShouldBeTrue(); result.Truncated.ShouldBeTrue(); db.Storage.GetWalSize().ShouldBe(0); } finally { CleanupFiles(dbPath); } } /// /// Verifies passive mode skips when checkpoint lock is contended. /// [Fact] public void Checkpoint_Passive_ShouldSkip_WhenLockIsContended() { string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); var gate = GetCommitGate(storage); gate.Wait(TestContext.Current.CancellationToken); try { var result = storage.Checkpoint(CheckpointMode.Passive); result.Mode.ShouldBe(CheckpointMode.Passive); result.Executed.ShouldBeFalse(); result.Truncated.ShouldBeFalse(); result.Restarted.ShouldBeFalse(); } finally { gate.Release(); } } finally { CleanupFiles(dbPath); } } /// /// Verifies full checkpoint applies data and appends a checkpoint marker without truncating WAL. /// [Fact] public void Checkpoint_Full_ShouldAppendMarker_AndPreserveWal() { string dbPath = NewDbPath(); string walPath = Path.ChangeExtension(dbPath, ".wal"); try { using (var db = new TestDbContext(dbPath)) { db.Users.Insert(new User { Name = "checkpoint-full", Age = 50 }); db.SaveChanges(); long walBefore = db.Storage.GetWalSize(); walBefore.ShouldBeGreaterThan(0); var result = db.Checkpoint(CheckpointMode.Full); result.Mode.ShouldBe(CheckpointMode.Full); result.Executed.ShouldBeTrue(); result.Truncated.ShouldBeFalse(); result.WalBytesAfter.ShouldBeGreaterThan(0); db.Storage.GetWalSize().ShouldBeGreaterThan(0); } using var wal = new WriteAheadLog(walPath); wal.ReadAll().Any(r => r.Type == WalRecordType.Checkpoint).ShouldBeTrue(); } finally { CleanupFiles(dbPath); } } /// /// Verifies restart checkpoint clears WAL and allows subsequent writes. /// [Fact] public void Checkpoint_Restart_ShouldResetWal_AndAcceptNewWrites() { string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); db.Users.Insert(new User { Name = "restart-before", Age = 30 }); db.SaveChanges(); db.Storage.GetWalSize().ShouldBeGreaterThan(0); var result = db.Checkpoint(CheckpointMode.Restart); result.Mode.ShouldBe(CheckpointMode.Restart); result.Executed.ShouldBeTrue(); result.Truncated.ShouldBeTrue(); result.Restarted.ShouldBeTrue(); db.Storage.GetWalSize().ShouldBe(0); db.Users.Insert(new User { Name = "restart-after", Age = 31 }); db.SaveChanges(); db.Storage.GetWalSize().ShouldBeGreaterThan(0); } finally { CleanupFiles(dbPath); } } /// /// Verifies recovery remains deterministic after a full checkpoint boundary. /// [Fact] public void Recover_AfterFullCheckpoint_ShouldApplyLatestCommitDeterministically() { string dbPath = NewDbPath(); try { uint pageId; using (var storage = new StorageEngine(dbPath, PageFileConfig.Default)) { pageId = storage.AllocatePage(); using (var tx1 = storage.BeginTransaction()) { var first = new byte[storage.PageSize]; first[0] = 1; storage.WritePage(pageId, tx1.TransactionId, first); tx1.Commit(); } storage.Checkpoint(CheckpointMode.Full); using (var tx2 = storage.BeginTransaction()) { var second = new byte[storage.PageSize]; second[0] = 2; storage.WritePage(pageId, tx2.TransactionId, second); tx2.Commit(); } } using (var recovered = new StorageEngine(dbPath, PageFileConfig.Default)) { var buffer = new byte[recovered.PageSize]; recovered.ReadPage(pageId, 0, buffer); buffer[0].ShouldBe((byte)2); recovered.GetWalSize().ShouldBe(0); } } finally { CleanupFiles(dbPath); } } /// /// Verifies asynchronous mode-based checkpoints return expected result metadata. /// [Fact] public async Task CheckpointAsync_Full_ShouldReturnResult() { string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); db.Users.Insert(new User { Name = "checkpoint-async", Age = 38 }); db.SaveChanges(); var result = await db.CheckpointAsync(CheckpointMode.Full, TestContext.Current.CancellationToken); result.Mode.ShouldBe(CheckpointMode.Full); result.Executed.ShouldBeTrue(); result.Truncated.ShouldBeFalse(); } finally { CleanupFiles(dbPath); } } private static SemaphoreSlim GetCommitGate(StorageEngine storage) { var field = typeof(StorageEngine).GetField("_commitLock", BindingFlags.Instance | BindingFlags.NonPublic); field.ShouldNotBeNull(); return (SemaphoreSlim)field!.GetValue(storage)!; } private static string NewDbPath() { return Path.Combine(Path.GetTempPath(), $"checkpoint_mode_{Guid.NewGuid():N}.db"); } private static void CleanupFiles(string dbPath) { if (File.Exists(dbPath)) File.Delete(dbPath); string walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(markerPath)) File.Delete(markerPath); } }