using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Core.Storage; using System.Text; using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class DictionaryPageTests { private const int PageSize = 16384; [Fact] public void Initialize_ShouldSetupEmptyPage() { var page = new byte[PageSize]; DictionaryPage.Initialize(page, 1); var header = PageHeader.ReadFrom(page); header.PageType.ShouldBe(PageType.Dictionary); header.PageId.ShouldBe(1u); var count = BitConverter.ToUInt16(page, 32); // CountOffset count.ShouldBe((ushort)0); var freeSpaceEnd = BitConverter.ToUInt16(page, 34); // FreeSpaceEndOffset freeSpaceEnd.ShouldBe((ushort)PageSize); } [Fact] public void Insert_ShouldAddEntryAndSort() { var page = new byte[PageSize]; DictionaryPage.Initialize(page, 1); // Insert "B" bool inserted = DictionaryPage.Insert(page, "B", 20); inserted.ShouldBeTrue(); // Insert "A" (should go before B) inserted = DictionaryPage.Insert(page, "A", 10); inserted.ShouldBeTrue(); // Insert "C" (should go after B) inserted = DictionaryPage.Insert(page, "C", 30); inserted.ShouldBeTrue(); // Verify Order var entries = DictionaryPage.GetAll(page).ToList(); entries.Count.ShouldBe(3); entries[0].Key.ShouldBe("A"); entries[0].Value.ShouldBe((ushort)10); entries[1].Key.ShouldBe("B"); entries[1].Value.ShouldBe((ushort)20); entries[2].Key.ShouldBe("C"); entries[2].Value.ShouldBe((ushort)30); } [Fact] public void TryFind_ShouldReturnCorrectValue() { var page = new byte[PageSize]; DictionaryPage.Initialize(page, 1); DictionaryPage.Insert(page, "Key1", 100); DictionaryPage.Insert(page, "Key2", 200); DictionaryPage.Insert(page, "Key3", 300); bool found = DictionaryPage.TryFind(page, Encoding.UTF8.GetBytes("Key2"), out ushort value); found.ShouldBeTrue(); value.ShouldBe((ushort)200); found = DictionaryPage.TryFind(page, Encoding.UTF8.GetBytes("Key999"), out value); found.ShouldBeFalse(); } [Fact] public void Overflow_ShouldReturnFalse_WhenFull() { var page = new byte[PageSize]; DictionaryPage.Initialize(page, 1); string bigKey = new string('X', 250); int count = 0; while (true) { // Use unique keys var key = bigKey + count; if (!DictionaryPage.Insert(page, key, (ushort)count)) { // Should fail here break; } count++; if (count > 1000) throw new ShouldAssertException("Should have filled the page much earlier"); } // Now page is full enough that `bigKey` (250 bytes) shouldn't fit. // We can't guarantee a small key won't fit (fragmentation/remaining space), // but a key of the SAME size that triggered the break should definitely fail. bool inserted = DictionaryPage.Insert(page, bigKey + "X", 9999); inserted.ShouldBeFalse(); } [Fact] public void Chaining_ShouldFindKeysInLinkedPages() { var dbPath = Path.Combine(Path.GetTempPath(), $"test_dict_chain_{Guid.NewGuid()}.db"); using var storage = new StorageEngine(dbPath, PageFileConfig.Default); // 1. Create First Page var page1Id = storage.AllocatePage(); var pageBuffer = new byte[storage.PageSize]; DictionaryPage.Initialize(pageBuffer, page1Id); // Fill Page 1 DictionaryPage.Insert(pageBuffer, "Key1", 100); DictionaryPage.Insert(pageBuffer, "KeyA", 200); // 2. Create Second Page var page2Id = storage.AllocatePage(); var page2Buffer = new byte[storage.PageSize]; DictionaryPage.Initialize(page2Buffer, page2Id); // Fill Page 2 DictionaryPage.Insert(page2Buffer, "Key2", 300); DictionaryPage.Insert(page2Buffer, "KeyB", 400); // 400 // 3. Link Page 1 -> Page 2 var header1 = PageHeader.ReadFrom(pageBuffer); header1.NextPageId = page2Id; header1.WriteTo(pageBuffer); // 4. Write pages to storage storage.WritePageImmediate(page1Id, pageBuffer); storage.WritePageImmediate(page2Id, page2Buffer); // 5. Test Global Find // Find in Page 1 bool found = DictionaryPage.TryFindGlobal(storage, page1Id, "Key1", out ushort val); found.ShouldBeTrue(); val.ShouldBe((ushort)100); // Find in Page 2 found = DictionaryPage.TryFindGlobal(storage, page1Id, "KeyB", out val); found.ShouldBeTrue(); val.ShouldBe((ushort)400); // Not Found found = DictionaryPage.TryFindGlobal(storage, page1Id, "KeyMissing", out val); found.ShouldBeFalse(); storage.Dispose(); if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(Path.ChangeExtension(dbPath, ".wal"))) File.Delete(Path.ChangeExtension(dbPath, ".wal")); } [Fact] public void FindAllGlobal_ShouldRetrieveAllKeys() { var dbPath = Path.Combine(Path.GetTempPath(), $"test_dict_findall_{Guid.NewGuid()}.db"); using var storage = new StorageEngine(dbPath, PageFileConfig.Default); // 1. Create Chain of 3 Pages var page1Id = storage.AllocatePage(); var page2Id = storage.AllocatePage(); var page3Id = storage.AllocatePage(); var buf = new byte[storage.PageSize]; // Page 1 DictionaryPage.Initialize(buf, page1Id); DictionaryPage.Insert(buf, "P1_A", 10); DictionaryPage.Insert(buf, "P1_B", 11); var h1 = PageHeader.ReadFrom(buf); h1.NextPageId = page2Id; h1.WriteTo(buf); storage.WritePageImmediate(page1Id, buf); // Page 2 DictionaryPage.Initialize(buf, page2Id); DictionaryPage.Insert(buf, "P2_A", 20); var h2 = PageHeader.ReadFrom(buf); h2.NextPageId = page3Id; h2.WriteTo(buf); storage.WritePageImmediate(page2Id, buf); // Page 3 DictionaryPage.Initialize(buf, page3Id); DictionaryPage.Insert(buf, "P3_A", 30); DictionaryPage.Insert(buf, "P3_B", 31); DictionaryPage.Insert(buf, "P3_C", 32); storage.WritePageImmediate(page3Id, buf); // 2. Execute FindAllGlobal var allEntries = DictionaryPage.FindAllGlobal(storage, page1Id).ToList(); // 3. Verify allEntries.Count.ShouldBe(6); allEntries.ShouldContain(e => e.Key == "P1_A" && e.Value == 10); allEntries.ShouldContain(e => e.Key == "P2_A" && e.Value == 20); allEntries.ShouldContain(e => e.Key == "P3_C" && e.Value == 32); storage.Dispose(); if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(Path.ChangeExtension(dbPath, ".wal"))) File.Delete(Path.ChangeExtension(dbPath, ".wal")); } }