Files
MetonaSqlark/tests/engine/aria-page-store.test.ts
T
thzxx 0dba1abf2a test(P0): v0.8.0 验证基座与工程门禁根治
工作流 C-1 / C-3 前半 + 测试代码类型检查。

【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts
- TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入
  (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃)
- 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现
  导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness)
- 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码
  与 entry.content.subarray 恒等分支)
- FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close()
  (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相)
- 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组)

【覆盖率口径】jest.config.cjs
- 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体
  排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由
- 新增 coverageThreshold 门禁(此前完全不存在)
- 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43%
- 修正 testMatch 使 tests/helpers 下的测试可被发现

【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests
- 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏)
- 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode
  以断言收窄替代 as any
- 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码)
- parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例,
  当前红灯,对应总账第 11 项,将在工作流 A 修复)

【版本契约】新增 tests/version-contract.test.ts
- 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量

【CI 门禁】.gitea/workflows/ci.yml
- lint 去掉 continue-on-error(此前永远不让 CI 变红)
- 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验
- 版本 0.7.4 升至 0.8.0
2026-09-14 21:03:06 +08:00

320 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AriaEngine — 页面化 SSTable 存储测试
*
* 覆盖:
* 1. PageSSTableStore 单元:save/load 往返(页边界、非整数页)、delete 释放页面
* 2. AriaEngine + OPFS(页面模式):写入 → flush → close → reopen 数据完整
* 3. 大 SSTable 多页 + pageIds 注入 meta 验证
* 4. 页面损坏(CRC 校验失败)→ 打开自愈清理
* 5. 页面模式与非页面模式混合(pageIds 兼容:新页面写 + 旧整 value 读)
* 6. 页面化 + 加密 + 压缩组合
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { PageSSTableStore } from '../../src/engine/aria/store/page_sstable_store';
import { FileManager } from '../../src/engine/aria/store/file_manager';
import { BufferPool } from '../../src/engine/aria/buffer/pool';
import { MemoryBackend } from '../../src/engine/aria/store/backend';
import { createSchema } from '../../src/table/schema';
import { PAGE_SIZE } from '../../src/engine/aria/types';
// ===================================================================
// OPFS mock(共享工具)
// ===================================================================
import { resetOPFSMock } from '../helpers/storage-harness';
import { decode as decodeBytes } from '../helpers/assertions';
const SCHEMA = () => createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
note: { type: 'string' },
});
// ===================================================================
// PageSSTableStore 单元
// ===================================================================
describe('AriaEngine — PageSSTableStore 单元', () => {
it('save/load 往返:单页数据', async () => {
const backend = new MemoryBackend();
await backend.open('pg-unit-1');
const fm = new FileManager(backend);
await fm.init('pg-unit-1');
const bp = new BufferPool(fm, 64);
const store = new PageSSTableStore(fm, bp);
const data = new Uint8Array(new TextEncoder().encode('hello page store'));
await store.save(1, data);
const pageIds = store.getPageIds(1)!;
expect(pageIds.length).toBe(1);
const back = await store.load(1, pageIds, data.byteLength);
expect(back).not.toBeNull();
expect(decodeBytes(back)).toBe('hello page store');
// 页面已落盘(backend 有 pg_ 键)
expect(await backend.exists('pg_1')).toBe(true);
await store.delete(1, pageIds);
expect(await backend.exists('pg_1')).toBe(false);
await backend.close();
});
it('save/load 往返:多页数据(跨 4KB 边界)', async () => {
const backend = new MemoryBackend();
await backend.open('pg-unit-2');
const fm = new FileManager(backend);
await fm.init('pg-unit-2');
const bp = new BufferPool(fm, 64);
const store = new PageSSTableStore(fm, bp);
// 3 页:4KB * 2 + 1KB
const size = PAGE_SIZE * 2 + 1024;
const data = new Uint8Array(size);
let x = 42;
for (let i = 0; i < size; i++) {
x = (x * 31 + 7) & 0xffff;
data[i] = x & 0xff;
}
await store.save(7, data);
const pageIds = store.getPageIds(7)!;
expect(pageIds.length).toBe(3);
const back = await store.load(7, pageIds, size);
expect(back).not.toBeNull();
expect(back!.byteLength).toBe(size);
expect(back).toEqual(data);
await backend.close();
});
it('load 使用 totalSize 截断最后一页的 0 填充', async () => {
const backend = new MemoryBackend();
await backend.open('pg-unit-3');
const fm = new FileManager(backend);
await fm.init('pg-unit-3');
const bp = new BufferPool(fm, 64);
const store = new PageSSTableStore(fm, bp);
// 非整数页:4096 + 100 字节
const real = new Uint8Array(PAGE_SIZE + 100);
for (let i = 0; i < real.length; i++) real[i] = i & 0xff;
await store.save(9, real);
const pageIds = store.getPageIds(9)!;
expect(pageIds.length).toBe(2);
const back = await store.load(9, pageIds, real.length);
expect(back!.byteLength).toBe(real.length);
expect(back).toEqual(real);
await backend.close();
});
it('页面缺失(磁盘文件丢失 + 缓存清空)→ load 返回 null', async () => {
const backend = new MemoryBackend();
await backend.open('pg-unit-4');
const fm = new FileManager(backend);
await fm.init('pg-unit-4');
const bp = new BufferPool(fm, 64);
const store = new PageSSTableStore(fm, bp);
await store.save(5, new Uint8Array(100));
const pageIds = store.getPageIds(5)!;
// 模拟崩溃重开:磁盘文件丢失 + 页面缓存清空
await backend.delete('pg_' + pageIds[0]);
bp.removePage(pageIds[0]);
const back = await store.load(5, pageIds, 100);
expect(back).toBeNull();
await backend.close();
});
});
// ===================================================================
// AriaEngine + OPFS 页面模式集成
// ===================================================================
describe('AriaEngine — 页面化 SSTable 集成(OPFS', () => {
it('写入 → flush → close → reopen 数据完整(页面模式默认启用)', async () => {
resetOPFSMock();
const engine = new AriaEngine({
storageBackend: 'opfs',
memtableSizeThreshold: 64 * 1024 * 1024,
checkpointInterval: 100000,
});
await engine.open('page-db-1', 1);
await engine.createTable(SCHEMA());
// 多行 + 大 note → 超过 4KB → 多页
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 30; i++) {
rows.push({ id: `u-${i}`, name: `User${i}`, note: 'page-store-note-'.repeat(40) });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
// 页面化落盘:backend 有 pg_ 键,meta 含 pageIds
const backend = (engine as any).backend;
const keys = await backend.listKeys();
const pgKeys = keys.filter((k: string) => k.startsWith('pg_'));
const sstKeys = keys.filter((k: string) => k.startsWith('sst_'));
expect(pgKeys.length).toBeGreaterThan(0);
expect(sstKeys.length).toBe(0); // 页面模式下不写整 value SSTable
const metas = await (engine as any).lsm.sstableStore.listMeta();
expect(metas.length).toBeGreaterThan(0);
expect(metas[0].pageIds).toBeDefined();
expect(metas[0].pageIds.length).toBeGreaterThan(0);
await engine.close();
// reopen:数据完整
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open('page-db-1', 1);
const found = await engine2.find('users', { table: 'users' });
expect(found).toHaveLength(30);
expect(found[0].note).toBe('page-store-note-'.repeat(40));
expect(await engine2.count('users')).toBe(30);
await engine2.close();
});
it('多级 compaction 后页面化数据仍完整', async () => {
resetOPFSMock();
const engine = new AriaEngine({
storageBackend: 'opfs',
memtableSizeThreshold: 16 * 1024,
checkpointInterval: 100000,
});
await engine.open('page-db-2', 1);
await engine.createTable(SCHEMA());
// 大量小批量写入 → 多次 flush → 触发 compaction
for (let batch = 0; batch < 8; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 20; i++) {
rows.push({ id: `b${batch}-u${i}`, name: `U${i}`, note: 'x'.repeat(200) });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
// 强制压缩
await (engine as any).lsm.compactLevel(0);
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open('page-db-2', 1);
expect(await engine2.count('users')).toBe(160);
const one = await engine2.find('users', { table: 'users', where: { id: 'b3-u10' } });
expect(one).toHaveLength(1);
await engine2.close();
});
it('页面损坏(篡改 pg_ 文件)→ 打开自愈清理,其余数据可读', async () => {
resetOPFSMock();
const engine = new AriaEngine({
storageBackend: 'opfs',
memtableSizeThreshold: 64 * 1024 * 1024,
checkpointInterval: 100000,
});
await engine.open('page-db-3', 1);
await engine.createTable(SCHEMA());
for (let batch = 0; batch < 2; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 20; i++) {
rows.push({ id: `b${batch}-u${i}`, name: `U${i}`, note: 'n'.repeat(100) });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
await engine.close();
// 篡改第一个 SSTable 的第一页
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open('page-db-3', 1);
const metas = await (engine2 as any).lsm.sstableStore.listMeta();
expect(metas.length).toBeGreaterThanOrEqual(2);
const victim = metas[0];
const backend = (engine2 as any).backend;
const pageRaw = await backend.read(`pg_${victim.pageIds[0]}`);
const corrupted = new Uint8Array(pageRaw);
corrupted[100] ^= 0xff;
await backend.write(`pg_${victim.pageIds[0]}`, corrupted.buffer as ArrayBuffer);
await engine2.close();
// 重开:损坏页面的 SSTable 被清理,其余数据可读
const engine3 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine3.open('page-db-3', 1);
const rows3 = await engine3.find('users', { table: 'users' });
expect(rows3.length).toBeGreaterThan(0);
expect(rows3.length).toBeLessThan(40);
await engine3.close();
});
it('页面模式与非页面模式混合兼容(pageIds 缺失 → 整 value 读取)', async () => {
const opfs = resetOPFSMock();
// 阶段 1:非页面模式写入(pageStorage: false → 整 value SSTable
const engine = new AriaEngine({
storageBackend: 'opfs',
pageStorage: false,
memtableSizeThreshold: 64 * 1024 * 1024,
checkpointInterval: 100000,
});
await engine.open('page-db-4', 1);
await engine.createTable(SCHEMA());
await engine.insert('users', [{ id: 'legacy-1', name: 'Old', note: 'whole-value' }]);
await (engine as any).lsm.flush();
await engine.close();
const backendKeys1 = await opfs.listKeys();
expect(backendKeys1.some((k) => k.startsWith('sst_'))).toBe(true);
// 阶段 2:页面模式打开(默认 opfs → 启用),读旧数据 + 写新数据
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open('page-db-4', 1);
expect(await engine2.count('users')).toBe(1);
const legacy = await engine2.find('users', { table: 'users', where: { id: 'legacy-1' } });
expect(legacy[0].note).toBe('whole-value');
// 新写入 → 页面化
await engine2.insert('users', [{ id: 'new-1', name: 'New', note: 'paged' }]);
await (engine2 as any).lsm.flush();
await engine2.close();
// 阶段 3:混合库重开,新旧数据都在
const engine3 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine3.open('page-db-4', 1);
expect(await engine3.count('users')).toBe(2);
const both = await engine3.find('users', { table: 'users' });
const byId = Object.fromEntries(both.map((r) => [r.id, r]));
expect(byId['legacy-1'].note).toBe('whole-value');
expect(byId['new-1'].note).toBe('paged');
await engine3.close();
});
it('页面化 + 加密 + 压缩组合:往返完整', async () => {
resetOPFSMock();
const engine = new AriaEngine({
storageBackend: 'opfs',
compression: true,
encryption: { password: 'page-pass' },
memtableSizeThreshold: 64 * 1024 * 1024,
checkpointInterval: 100000,
});
await engine.open('page-db-5', 1);
await engine.createTable(SCHEMA());
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 20; i++) {
rows.push({ id: `e-${i}`, name: `E${i}`, note: '加密的页面化内容'.repeat(30) });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
await engine.close();
const engine2 = new AriaEngine({
storageBackend: 'opfs',
compression: true,
encryption: { password: 'page-pass' },
checkpointInterval: 100000,
});
await engine2.open('page-db-5', 1);
expect(await engine2.count('users')).toBe(20);
const one = await engine2.find('users', { table: 'users', where: { id: 'e-7' } });
expect(one[0].note).toBe('加密的页面化内容'.repeat(30));
await engine2.close();
});
});