工作流 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
124 lines
4.7 KiB
TypeScript
124 lines
4.7 KiB
TypeScript
/**
|
||
* v0.6.1-fix(P0) — 二级索引大数据量完整性回归测试
|
||
*
|
||
* 覆盖两类根因:
|
||
* 1. LSM.put 的 `!this.flushing` 条件竞态(修复:每次 freeze 一律挂链)
|
||
* 2. SSTable rangeScan 尾块漏读(索引键为块尾 key,locateBlockLE 排除
|
||
* 尾 key 越界的下一块 → 范围扫描丢尾部数据;修复:endBlockIdx 多扫一块)
|
||
*/
|
||
import { AriaEngine } from '../../src/engine/aria/index';
|
||
import { createSchema } from '../../src/table/schema';
|
||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||
|
||
let counter = 0;
|
||
function uniqueDB(): string {
|
||
return `idxrace-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
|
||
}
|
||
|
||
beforeEach(() => { resetOPFSMock(); });
|
||
|
||
describe('AriaEngine — 二级索引完整性(P0 回归)', () => {
|
||
it('5 万行写入:索引查询与主表一致(修复前丢 106~771 条)', async () => {
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 256 * 1024,
|
||
checkpointInterval: 2000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(createSchema('big', {
|
||
id: { type: 'string', primaryKey: true },
|
||
val: { type: 'number' },
|
||
tag: { type: 'string', index: true },
|
||
name: { type: 'string' },
|
||
}));
|
||
|
||
const TOTAL = 50000;
|
||
for (let batch = 0; batch < TOTAL / 1000; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 1000; i++) {
|
||
const idx = batch * 1000 + i;
|
||
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, name: `User${idx}` });
|
||
}
|
||
await engine.insert('big', rows);
|
||
}
|
||
|
||
expect(await engine.count('big')).toBe(TOTAL);
|
||
|
||
// 每个 tag 的索引查询都完整(修复前丢 106-771 条)
|
||
for (let t = 0; t < 10; t++) {
|
||
const viaIdx = await engine.find('big', { table: 'big', where: { tag: `t${t}` } });
|
||
expect(viaIdx.length).toBe(5000);
|
||
}
|
||
|
||
// 崩溃恢复后索引仍完整
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 256 * 1024,
|
||
checkpointInterval: 2000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine2.open((engine as any).dbName, 1);
|
||
for (let t = 0; t < 10; t++) {
|
||
const viaIdx = await engine2.find('big', { table: 'big', where: { tag: `t${t}` } });
|
||
expect(viaIdx.length).toBe(5000);
|
||
}
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('小批量高频写入(每批 50 行)触发极端 freeze 竞态', async () => {
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 32 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(createSchema('items', {
|
||
id: { type: 'string', primaryKey: true },
|
||
tag: { type: 'string', index: true },
|
||
}));
|
||
|
||
const TOTAL = 4000;
|
||
for (let i = 0; i < TOTAL; i++) {
|
||
await engine.insert('items', [{ id: `k${i}`, tag: i % 3 === 0 ? 'a' : 'b' }]);
|
||
}
|
||
expect(await engine.count('items')).toBe(TOTAL);
|
||
expect(await engine.find('items', { table: 'items', where: { tag: 'a' } })).toHaveLength(Math.ceil(TOTAL / 3));
|
||
expect(await engine.find('items', { table: 'items', where: { tag: 'b' } })).toHaveLength(TOTAL - Math.ceil(TOTAL / 3));
|
||
await engine.close();
|
||
}, 600000);
|
||
|
||
it('多索引列同时写入:每列索引都完整', async () => {
|
||
const engine = new AriaEngine({
|
||
storageBackend: 'opfs',
|
||
memtableSizeThreshold: 64 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(createSchema('multi', {
|
||
id: { type: 'string', primaryKey: true },
|
||
cat: { type: 'string', index: true },
|
||
grp: { type: 'number', index: true },
|
||
}));
|
||
|
||
const TOTAL = 6000;
|
||
for (let batch = 0; batch < TOTAL / 100; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 100; i++) {
|
||
const idx = batch * 100 + i;
|
||
rows.push({ id: `k${idx}`, cat: idx % 2 === 0 ? 'even' : 'odd', grp: idx % 5 });
|
||
}
|
||
await engine.insert('multi', rows);
|
||
}
|
||
expect(await engine.find('multi', { table: 'multi', where: { cat: 'even' } })).toHaveLength(TOTAL / 2);
|
||
expect(await engine.find('multi', { table: 'multi', where: { cat: 'odd' } })).toHaveLength(TOTAL / 2);
|
||
expect(await engine.find('multi', { table: 'multi', where: { grp: 0 } })).toHaveLength(TOTAL / 5);
|
||
expect(await engine.find('multi', { table: 'multi', where: { grp: 4 } })).toHaveLength(TOTAL / 5);
|
||
await engine.close();
|
||
}, 600000);
|
||
});
|