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

196 lines
8.2 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 CRC-32 损坏检测集成测试
*
* 覆盖:
* 1. 打开时整文件 CRC 校验失败 → 损坏 SSTable 被清理(自愈),打开不阻塞
* 2. 运行期预加载 CRC 校验失败 → 损坏文件不缓存并被清理
* 3. 旧版无校验文件(checksum=0)在 LSM 层仍正常加载
*/
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 `crc-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
const SCHEMA = () => createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
});
/** 篡改指定 pg_ 页面文件的一个字节 */
async function corruptKey(engine: AriaEngine, key: string, byteOffset: number): Promise<void> {
const backend = (engine as any).backend;
const raw = await backend.read(key);
expect(raw).not.toBeNull();
const buf = new Uint8Array(raw as ArrayBuffer);
buf[Math.min(byteOffset, buf.length - 1)] ^= 0xff;
await backend.write(key, buf.buffer as ArrayBuffer);
}
/** 列出全部 SSTable 的页面文件 key(页面化存储:pg_ 前缀) */
async function listSSTKeys(engine: AriaEngine): Promise<string[]> {
const backend = (engine as any).backend;
const keys = (await backend.listKeys()) as string[];
return keys.filter((k) => k.startsWith('pg_'));
}
/** 读取主 LSM 的 SSTable meta 列表 */
async function listSSTMetas(engine: AriaEngine): Promise<{ id: number; pageIds?: number[] }[]> {
const backend = (engine as any).backend;
const raw = await backend.read('__aria_lsm_meta');
if (!raw) return [];
return JSON.parse(new TextDecoder().decode(raw)) as { id: number; pageIds?: number[] }[];
}
describe('AriaEngine — SSTable CRC 损坏检测(集成)', () => {
it('打开时 CRC 损坏的 SSTable 被清理,其余数据可读', async () => {
const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
// 写入 200 行(触发多次 flush → 多个 SSTable
for (let batch = 0; batch < 3; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 50; i++) {
rows.push({ id: `u-${batch * 50 + i}`, name: `User${batch * 50 + i}`, age: batch * 50 + i });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
await engine.close();
// 重新打开,篡改第一个 SSTable 文件的数据区
const engine2 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine2.open(dbName, 1);
const sstKeys = await listSSTKeys(engine2);
expect(sstKeys.length).toBeGreaterThanOrEqual(2);
await corruptKey(engine2, sstKeys[0], 64);
// 再次打开:损坏文件应被跳过并清理,打开不抛错
await engine2.close();
const engine3 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine3.open(dbName, 1);
// 剩余未损坏文件的数据应可查询
const remaining = await engine3.find('users', { table: 'users' });
expect(remaining.length).toBeGreaterThan(0);
expect(remaining.length).toBeLessThan(200);
// 损坏文件已被清理(页面 + meta 移除)
const afterKeys = await listSSTKeys(engine3);
expect(afterKeys).not.toContain(sstKeys[0]);
const metas = await listSSTMetas(engine3);
// 被篡改页面所属的 SSTable meta 应被移除
const victimMetas = await listSSTMetas(engine2);
const victimMeta = victimMetas.find((m) => m.pageIds?.includes(Number(sstKeys[0].slice(3))));
expect(metas.some((m) => m.id === victimMeta?.id)).toBe(false);
await engine3.close();
});
it('打开时损坏全部 SSTable → 库仍可打开,数据为空但不崩溃', async () => {
const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
for (let batch = 0; batch < 2; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 50; i++) {
rows.push({ id: `u-${batch * 50 + i}`, name: `User${batch * 50 + i}`, age: batch * 50 + i });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine2.open(dbName, 1);
for (const key of await listSSTKeys(engine2)) {
await corruptKey(engine2, key, 16);
}
await engine2.close();
const engine3 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
// 不应抛 ARIA_OPEN_ERROR
await engine3.open(dbName, 1);
const rows3 = await engine3.find('users', { table: 'users' });
expect(rows3.length).toBe(0);
await engine3.close();
});
it('运行期 CRC 损坏 → 预加载不缓存损坏文件并清理(自愈)', async () => {
const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
for (let batch = 0; batch < 2; batch++) {
const rows = [] as Record<string, unknown>[];
for (let i = 0; i < 50; i++) {
rows.push({ id: `u-${batch * 50 + i}`, name: `User${batch * 50 + i}`, age: batch * 50 + i });
}
await engine.insert('users', rows);
await (engine as any).lsm.flush();
}
// 运行期:直接篡改后端中的某个 SSTable 文件(不触发打开校验路径)
const sstKeys = await listSSTKeys(engine);
expect(sstKeys.length).toBeGreaterThanOrEqual(2);
const victim = sstKeys[sstKeys.length - 1]; // 篡改最新(memtable 已 flush 后的文件)
await corruptKey(engine, victim, 100);
// 缓存里已有该文件(flush 时缓存)→ 清 LSM 层与 BufferPool 页面缓存模拟运行期磁盘损坏
const lsm = (engine as any).lsm;
lsm.sstableCache.clear();
await (engine as any).bufferPool.clear();
// 查询触发 prefetchRange → preloadSSTable 发现 CRC 失败 → 清理 + 不缓存
const remaining = await engine.find('users', { table: 'users' });
expect(remaining.length).toBeLessThan(100);
expect(lsm.sstableCache.has(Number(victim.slice(3)))).toBe(false);
const metas = await lsm.sstableStore.listMeta();
expect(metas.some((m: { id: number }) => m.id === Number(victim.slice(3)))).toBe(false);
await engine.close();
});
it('旧版无校验文件(checksum=0)在 LSM 中正常加载', async () => {
const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
const dbName = uniqueDB();
await engine.open(dbName, 1);
await engine.createTable(SCHEMA());
await engine.insert('users', [
{ id: 'a', name: 'Alice', age: 30 },
{ id: 'b', name: 'Bob', age: 25 },
]);
await (engine as any).lsm.flush();
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine2.open(dbName, 1);
const sstKeys = await listSSTKeys(engine2);
expect(sstKeys.length).toBe(1);
const backend = (engine2 as any).backend;
const raw = await backend.read(sstKeys[0]);
const buf = new Uint8Array(raw as ArrayBuffer);
new DataView(buf.buffer).setUint32(buf.byteLength - 4, 0, false); // checksum 清零 → 旧版
await backend.write(sstKeys[0], buf.buffer as ArrayBuffer);
await engine2.close();
// 重开:checksum=0 跳过校验,数据完整可读
const engine3 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024 });
await engine3.open(dbName, 1);
const rows3 = await engine3.find('users', { table: 'users' });
expect(rows3.length).toBe(2);
expect(rows3.map((r) => r.id).sort()).toEqual(['a', 'b']);
await engine3.close();
});
});