/** * AriaEngine Page 格式单元测试 * 覆盖: PageHeader / Slot / Tuple 编解码 + PageFormat 整合 */ import { PAGE_SIZE, PageType, PAGE_HEADER_SIZE, SLOT_ENTRY_SIZE, } from '../../src/engine/aria/types'; import { encodePageHeader, decodePageHeader, initPageHeader, getPageType, getSlotCount, getFreeStart, setFreeStart, setFreeEnd, } from '../../src/engine/aria/page/header'; import { getSlotEntry, setSlotEntry, getSlotDirectorySize, getFreeSpace, hasEnoughSpace, allocateSlot, readSlotData, freeSlot, } from '../../src/engine/aria/page/slot'; import { encodeTuple, decodeTuple, getColumnEncodingMap, } from '../../src/engine/aria/page/tuple'; import { ColumnEncoding } from '../../src/engine/aria/types'; import { createPage, pageFromBuffer, pageInsertRow, pageReadRow, pageDeleteRow, pageUpdateRow, computeChecksum, verifyChecksum, updateChecksum, } from '../../src/engine/aria/page/format'; // =================================================================== // PageHeader // =================================================================== describe('AriaEngine Page — Header', () => { let buf: ArrayBuffer; beforeEach(() => { buf = new ArrayBuffer(PAGE_SIZE); }); it('initPageHeader 初始化头部字段', () => { initPageHeader(buf, 42, PageType.DATA); const h = decodePageHeader(buf); expect(h.pageId).toBe(42); expect(h.type).toBe(PageType.DATA); expect(h.slotCount).toBe(0); expect(h.freeStart).toBe(PAGE_HEADER_SIZE); expect(h.freeEnd).toBe(PAGE_SIZE); }); it('initPageHeader — INDEX 类型页面', () => { initPageHeader(buf, 99, PageType.INDEX); expect(getPageType(buf)).toBe(PageType.INDEX); }); it('encodePageHeader + decodePageHeader 往返一致', () => { const header = { pageId: 7, type: PageType.META, freeStart: 32, freeEnd: 4000, slotCount: 5, checksum: 0xdeadbeef }; encodePageHeader(header, buf); const decoded = decodePageHeader(buf); expect(decoded.pageId).toBe(7); expect(decoded.type).toBe(PageType.META); expect(decoded.freeStart).toBe(32); expect(decoded.freeEnd).toBe(4000); expect(decoded.slotCount).toBe(5); }); it('不同 pageId 正确编解码', () => { for (const id of [0, 1, 255, 65535, 0xffffffff]) { initPageHeader(buf, id, PageType.DATA); expect(decodePageHeader(buf).pageId).toBe(id >>> 0); } }); it('setFreeStart / setFreeEnd 修改字段', () => { initPageHeader(buf, 1, PageType.DATA); setFreeStart(buf, 100); setFreeEnd(buf, 3000); expect(getFreeStart(buf)).toBe(100); expect(decodePageHeader(buf).freeEnd).toBe(3000); }); }); // =================================================================== // Slot Directory // =================================================================== describe('AriaEngine Page — Slot', () => { let buf: ArrayBuffer; beforeEach(() => { buf = new ArrayBuffer(PAGE_SIZE); initPageHeader(buf, 1, PageType.DATA); }); it('getSlotEntry — 空页面 slotCount 为 0', () => { expect(getSlotCount(buf)).toBe(0); }); it('setSlotEntry + getSlotEntry 往返', () => { // 手动写一个 slot(不通过 allocateSlot) new DataView(buf).setUint16(9, 1, false); // slotCount = 1 setSlotEntry(buf, 0, { offset: 1000, length: 50 }); const entry = getSlotEntry(buf, 0); expect(entry.offset).toBe(1000); expect(entry.length).toBe(50); }); it('getSlotDirectorySize 计算正确', () => { expect(getSlotDirectorySize(0)).toBe(0); expect(getSlotDirectorySize(1)).toBe(SLOT_ENTRY_SIZE); expect(getSlotDirectorySize(10)).toBe(10 * SLOT_ENTRY_SIZE); }); it('getFreeSpace — 空页面有最大空闲空间', () => { const free = getFreeSpace(buf); expect(free).toBe(PAGE_SIZE - PAGE_HEADER_SIZE); }); it('hasEnoughSpace — 小数据返回 true', () => { expect(hasEnoughSpace(buf, 100)).toBe(true); }); it('hasEnoughSpace — 超大数据返回 false', () => { expect(hasEnoughSpace(buf, PAGE_SIZE * 2)).toBe(false); }); it('allocateSlot 分配并写入数据', () => { const data = new Uint8Array([1, 2, 3, 4, 5]); const idx = allocateSlot(buf, data); expect(idx).toBe(0); expect(getSlotCount(buf)).toBe(1); const readBack = readSlotData(buf, 0); expect(readBack).not.toBeNull(); expect(Array.from(readBack!)).toEqual([1, 2, 3, 4, 5]); }); it('allocateSlot 多次分配', () => { for (let i = 0; i < 10; i++) { const data = new Uint8Array([i, i + 1]); const idx = allocateSlot(buf, data); expect(idx).toBe(i); } expect(getSlotCount(buf)).toBe(10); // 验证每个 slot 数据正确 for (let i = 0; i < 10; i++) { const data = readSlotData(buf, i); expect(Array.from(data!)).toEqual([i, i + 1]); } }); it('freeSlot 标记为删除', () => { const data = new Uint8Array([10, 20, 30]); allocateSlot(buf, data); freeSlot(buf, 0); const entry = getSlotEntry(buf, 0); expect(entry.offset).toBe(0); expect(entry.length).toBe(0); }); it('readSlotData — 已删除 slot 返回 null', () => { allocateSlot(buf, new Uint8Array([1])); freeSlot(buf, 0); expect(readSlotData(buf, 0)).toBeNull(); }); it('allocateSlot — 空间不足返回 -1', () => { // 填满页面 const big = new Uint8Array(PAGE_SIZE - PAGE_HEADER_SIZE - SLOT_ENTRY_SIZE); const idx1 = allocateSlot(buf, big); expect(idx1).toBe(0); const idx2 = allocateSlot(buf, new Uint8Array([1])); expect(idx2).toBe(-1); }); }); // =================================================================== // Tuple Codec // =================================================================== describe('AriaEngine Page — Tuple', () => { const colOrder = ['id', 'name', 'age', 'active', 'data']; const colTypes: Record = { id: 'string', name: 'string', age: 'number', active: 'boolean', data: 'json', }; it('encodeTuple + decodeTuple 完整往返', () => { const row = { id: '1', name: 'Alice', age: 30, active: true, data: { x: 1 } }; const encoded = encodeTuple(row, colOrder, colTypes); expect(encoded.byteLength).toBeGreaterThan(0); const decoded = decodeTuple(encoded, colOrder, colTypes); expect(decoded).not.toBeNull(); expect(decoded!.id).toBe('1'); expect(decoded!.name).toBe('Alice'); expect(decoded!.age).toBe(30); expect(decoded!.active).toBe(true); expect(decoded!.data).toEqual({ x: 1 }); }); it('encodeTuple — null 值正确处理', () => { const row = { id: '2', name: null, age: 25, active: null, data: null }; const encoded = encodeTuple(row, colOrder, colTypes); const decoded = decodeTuple(encoded, colOrder, colTypes); expect(decoded!.name).toBeNull(); expect(decoded!.active).toBeNull(); expect(decoded!.data).toBeNull(); }); it('encodeTuple — undefined 值按 null 处理', () => { const row = { id: '3', age: 30 } as any; const encoded = encodeTuple(row, colOrder, colTypes); const decoded = decodeTuple(encoded, colOrder, colTypes); expect(decoded!.id).toBe('3'); expect(decoded!.name).toBeNull(); }); it('encodeTuple — date 类型', () => { const order = ['ts']; const types = { ts: 'date' }; const row = { ts: '2024-01-15T00:00:00.000Z' }; const encoded = encodeTuple(row, order, types); const decoded = decodeTuple(encoded, order, types); expect(decoded!.ts).toBe('2024-01-15T00:00:00.000Z'); }); it('encodeTuple — boolean false', () => { const order = ['flag']; const types = { flag: 'boolean' }; const encoded = encodeTuple({ flag: false }, order, types); const decoded = decodeTuple(encoded, order, types); expect(decoded!.flag).toBe(false); }); it('encodeTuple — 负数', () => { const order = ['val']; const types = { val: 'number' }; const encoded = encodeTuple({ val: -42.5 }, order, types); const decoded = decodeTuple(encoded, order, types); expect(decoded!.val).toBe(-42.5); }); it('encodeTuple — 空字符串', () => { const order = ['s']; const types = { s: 'string' }; const encoded = encodeTuple({ s: '' }, order, types); const decoded = decodeTuple(encoded, order, types); expect(decoded!.s).toBe(''); }); it('encodeTuple — 长字符串', () => { const order = ['s']; const types = { s: 'string' }; const long = 'x'.repeat(10000); const encoded = encodeTuple({ s: long }, order, types); const decoded = decodeTuple(encoded, order, types); expect(decoded!.s).toBe(long); }); it('getColumnEncodingMap 返回正确映射', () => { const map = getColumnEncodingMap(colOrder, colTypes); expect(map.get('id')).toBe(ColumnEncoding.STRING); expect(map.get('age')).toBe(ColumnEncoding.NUMBER); expect(map.get('active')).toBe(ColumnEncoding.BOOLEAN); expect(map.get('data')).toBe(ColumnEncoding.JSON); }); it('decodeTuple — 损坏数据返回 null', () => { const broken = new Uint8Array([0xff, 0xff, 0xff]); expect(decodeTuple(broken, colOrder, colTypes)).toBeNull(); }); }); // =================================================================== // Page Format 整合 // =================================================================== describe('AriaEngine Page — Format', () => { const colOrder = ['id', 'name']; const colTypes: Record = { id: 'string', name: 'string' }; it('createPage 创建合法页面', () => { const page = createPage(100, PageType.DATA); expect(page.pageId).toBe(100); expect(page.type).toBe(PageType.DATA); expect(page.pins).toBe(0); expect(page.dirty).toBe(true); }); it('pageInsertRow + pageReadRow 往返', () => { const page = createPage(1, PageType.DATA); const row = { id: 'u1', name: 'Test' }; const idx = pageInsertRow(page, row, colOrder, colTypes); expect(idx).toBe(0); const read = pageReadRow(page, 0, colOrder, colTypes); expect(read).not.toBeNull(); expect(read!.id).toBe('u1'); expect(read!.name).toBe('Test'); }); it('pageInsertRow — 多次插入', () => { const page = createPage(1, PageType.DATA); for (let i = 0; i < 50; i++) { const idx = pageInsertRow(page, { id: `${i}`, name: `User${i}` }, colOrder, colTypes); expect(idx).toBe(i); } for (let i = 0; i < 50; i++) { const row = pageReadRow(page, i, colOrder, colTypes); expect(row!.id).toBe(`${i}`); } }); it('pageDeleteRow 标记删除', () => { const page = createPage(1, PageType.DATA); pageInsertRow(page, { id: '1', name: 'A' }, colOrder, colTypes); pageInsertRow(page, { id: '2', name: 'B' }, colOrder, colTypes); pageDeleteRow(page, 0); expect(page.dirty).toBe(true); // 已删除的行读取失败 const row = pageReadRow(page, 0, colOrder, colTypes); expect(row).toBeNull(); // 未删除的行仍然可读 const row2 = pageReadRow(page, 1, colOrder, colTypes); expect(row2!.id).toBe('2'); }); it('pageFromBuffer 从 ArrayBuffer 恢复', () => { const page = createPage(5, PageType.INDEX); const restored = pageFromBuffer(5, page.data); expect(restored.pageId).toBe(5); expect(restored.type).toBe(PageType.INDEX); expect(restored.dirty).toBe(false); }); it('computeChecksum + verifyChecksum', () => { const page = createPage(1, PageType.DATA); updateChecksum(page); expect(verifyChecksum(page)).toBe(true); // 修改页面 → 校验和失效 new Uint8Array(page.data)[100] = 0xff; expect(verifyChecksum(page)).toBe(false); }); });