Files
MetonaSqlark/tests/engine/aria-wal-segment.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

279 lines
11 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 — SegmentedWALStore 分片 WAL 存储测试
*
* 覆盖:
* 1. append → readAll 往返(分片切换、字节顺序)
* 2. 空洞检测:序号不连续 → 空洞后的分片丢弃
* 3. 旧格式 __wal_N + __wal_count 兼容读取
* 4. truncate / exists
* 5. 后端无 append 接口时回退 read+write(正确性一致)
* 6. AriaEngine 集成:高频写入跨重启数据完整(分片 + WAL 重放)
*/
import { SegmentedWALStore } from '../../src/engine/aria/wal/segmented_store';
import { MemoryBackend } from '../../src/engine/aria/store/backend';
import { AriaEngine } from '../../src/engine/aria/index';
import { createSchema } from '../../src/table/schema';
import { resetOPFSMock } from '../helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
let idbCounter = 0;
function uniqueDB(): string {
return `seg-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
const enc = (s: string) => new Uint8Array(new TextEncoder().encode(s));
describe('AriaEngine — SegmentedWALStore 单元', () => {
it('append → readAll 往返,多批次字节顺序一致', async () => {
const backend = new MemoryBackend();
await backend.open('seg-unit-1');
const store = new SegmentedWALStore(backend, 1024);
expect(await store.exists()).toBe(false);
await store.append(enc('AAA'));
await store.append(enc('BBB'));
await store.append(enc('CCC'));
expect(await store.exists()).toBe(true);
const all = await store.readAll();
expect(new TextDecoder().decode(all)).toBe('AAABBBCCC');
// 后端是分片文件(单个 key
const keys = await backend.listKeys();
expect(keys).toEqual(['__wal_000000.bin']);
await backend.close();
});
it('超过分片阈值自动切换分片', async () => {
const backend = new MemoryBackend();
await backend.open('seg-unit-2');
const store = new SegmentedWALStore(backend, 16); // 每分片 16 字节
await store.append(enc('AAAAAAAAAA')); // 10B → 分片 0
await store.append(enc('BBBBBBBBBB')); // 10B → 分片 10 已 10B+10B > 16
await store.append(enc('CC')); // 2B → 分片 110+2 <= 16
const all = await store.readAll();
expect(new TextDecoder().decode(all)).toBe('AAAAAAAAAABBBBBBBBBBCC');
const keys = await backend.listKeys().then((ks) => ks.sort());
expect(keys).toEqual(['__wal_000000.bin', '__wal_000001.bin']);
await backend.close();
});
it('空洞检测:分片序号不连续 → 空洞后的分片整体丢弃', async () => {
const backend = new MemoryBackend();
await backend.open('seg-unit-3');
const store = new SegmentedWALStore(backend, 16);
await store.append(enc('SEG0-SEG0-S')); // 10B → 分片 0
await store.append(enc('SEG1-SEG1-S')); // 10B → 分片 10 已 10B+10B > 16
await store.append(enc('S2')); // 2B → 分片 110+2 <= 16
// 模拟 truncate 部分完成:删除分片 1(空洞)
await backend.delete('__wal_000001.bin');
const all = await store.readAll();
// 空洞在分片 1 → 1 及之后丢弃,只保留分片 0
expect(new TextDecoder().decode(all)).toBe('SEG0-SEG0-S');
await backend.close();
});
it('空洞在开头(分片 0 缺失)→ 全部丢弃(保守截断)', async () => {
const backend = new MemoryBackend();
await backend.open('seg-unit-4');
const store = new SegmentedWALStore(backend, 16);
await store.append(enc('X0'));
await store.append(enc('X1'));
await backend.delete('__wal_000000.bin');
const all = await store.readAll();
expect(all.byteLength).toBe(0);
await backend.close();
});
it('旧格式 __wal_N + __wal_count 兼容读取(迁移前数据)', async () => {
const backend = new MemoryBackend();
await backend.open('seg-unit-5');
// 构造旧格式数据
await backend.write('__wal_0', enc('LEGACY0').buffer);
await backend.write('__wal_1', enc('LEGACY1').buffer);
await backend.write('__wal_count', enc('2').buffer);
const store = new SegmentedWALStore(backend, 1024);
expect(await store.exists()).toBe(true);
const all = await store.readAll();
expect(new TextDecoder().decode(all)).toBe('LEGACY0LEGACY1');
// 迁移中追加新格式:新记录写入分片文件(不与旧键冲突)
await store.append(enc('NEW'));
const keys = await backend.listKeys().then((ks) => ks.sort());
expect(keys).toContain('__wal_000000.bin');
expect(keys).toContain('__wal_0');
// truncate 清空新旧全部
await store.truncate();
expect(await store.exists()).toBe(false);
expect((await backend.listKeys()).length).toBe(0);
await backend.close();
});
it('truncate 清空分片并重置状态', async () => {
const backend = new MemoryBackend();
await backend.open('seg-unit-6');
const store = new SegmentedWALStore(backend, 16);
await store.append(enc('AAAAAA'));
await store.append(enc('BBBBBB'));
await store.truncate();
expect(await store.exists()).toBe(false);
expect((await backend.listKeys()).length).toBe(0);
// 截断后继续追加从分片 0 重新开始
await store.append(enc('CCC'));
const all = await store.readAll();
expect(new TextDecoder().decode(all)).toBe('CCC');
await backend.close();
});
it('后端无 append 接口 → 回退 read+write 语义一致', async () => {
// MemoryBackend 没有 append 接口 → 走回退路径
const backend = new MemoryBackend();
await backend.open('seg-unit-7');
const store = new SegmentedWALStore(backend, 1024);
await store.append(enc('PART1'));
await store.append(enc('PART2'));
const all = await store.readAll();
expect(new TextDecoder().decode(all)).toBe('PART1PART2');
await backend.close();
});
});
// ===================================================================
// AriaEngine 集成 — 分片 WAL 跨重启
// ===================================================================
describe('AriaEngine — 分片 WAL 集成', () => {
it('高频写入(多条 WAL 记录)→ close → reopen 数据完整', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 100000,
walSyncMode: 'full',
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('logs', {
id: { type: 'string', primaryKey: true },
msg: { type: 'string' },
}));
const total = 300;
for (let i = 0; i < total; i++) {
await engine.insert('logs', [{ id: `log-${i}`, msg: `message ${i}` }]);
}
// 不 flush 不 checkpoint,全部留在 WAL → 模拟崩溃后重放
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 100000,
walSyncMode: 'full',
});
await engine2.open(dbName, 1);
const rows = await engine2.find('logs', { table: 'logs' });
expect(rows).toHaveLength(total);
expect(rows.some((r) => r.id === 'log-299')).toBe(true);
await engine2.close();
});
it('分片结构落盘验证(backend 中是分片文件而非旧单记录键)', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 100000,
walSyncMode: 'full',
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('t', [{ id: '1' }, { id: '2' }, { id: '3' }]);
const backend = (engine as any).backend;
const keys = await backend.listKeys();
const walKeys = keys.filter((k: string) => k.startsWith('__wal_'));
// 新格式:__wal_000000.bin;无旧格式单记录键与 count 键
expect(walKeys).toContain('__wal_000000.bin');
expect(walKeys.some((k: string) => /^__wal_\d+$/.test(k) && !k.endsWith('.bin'))).toBe(false);
expect(walKeys).not.toContain('__wal_count');
await engine.close();
});
it('WAL 分片部分残留(空洞)→ 重开恢复已落盘数据,不丢已 checkpoint 数据', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 100000,
walSyncMode: 'full',
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
}));
// 第一批:flush 落盘 + checkpoint 清 WAL
await engine.insert('t', [{ id: 'a' }, { id: 'b' }]);
await (engine as any).lsm.flush();
await (engine as any).wal.checkpoint();
// 第二批:只进 WAL(不 flush
await engine.insert('t', [{ id: 'c' }, { id: 'd' }]);
// 模拟 WAL 分片文件被外部删掉(空洞场景)
const backend = (engine as any).backend;
await backend.delete('__wal_000000.bin');
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
// 已落盘的 a/b 必须保留;c/d 在 WAL 中且文件被删 → 恢复不到(可接受的保守丢弃)
const rows = await engine2.find('t', { table: 't' });
expect(rows.some((r) => r.id === 'a')).toBe(true);
expect(rows.some((r) => r.id === 'b')).toBe(true);
await engine2.close();
});
it('旧格式 WAL 升级迁移:旧键重放后 checkpoint 清空', async () => {
const dbName = uniqueDB();
// 手工构造旧格式 WAL 库:直接写旧键(模拟 v0.4.4 库崩溃现场)
const engine = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 100000,
walSyncMode: 'full',
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
}));
// 写入一批数据但只保留在 WAL(新格式)→ close 前手工转成旧格式
await engine.insert('t', [{ id: 'x' }]);
const backend = (engine as any).backend;
const raw = await backend.read('__wal_000000.bin');
expect(raw).not.toBeNull();
// 清掉新格式,写成旧格式(单记录键)
await backend.delete('__wal_000000.bin');
await backend.write('__wal_0', raw);
await backend.write('__wal_count', new TextEncoder().encode('1').buffer);
// 同时清掉内存里的 WAL 状态,模拟"重启"
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
const rows = await engine2.find('t', { table: 't' });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('x');
// 恢复后 checkpoint 清空旧键
const keys = await (engine2 as any).backend.listKeys();
expect(keys.some((k: string) => k.startsWith('__wal_'))).toBe(false);
await engine2.close();
});
});