Files
MetonaSqlark/tests/engine/aria-wal-crc.test.ts
thzxx 334067d89e
CI / test (18.x) (push) Successful in 10m10s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m6s
CI / e2e (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 10m28s
release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
2026-08-10 12:07:00 +08:00

219 lines
8.1 KiB
TypeScript
Raw Permalink 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 WAL — CRC-32 校验升级测试
*
* 覆盖:
* 1. 新算法记录往返(append → recover
* 2. 篡改记录字节 → CRC 不匹配跳过
* 3. 旧版弱校验记录兼容(双算法探测)
* 4. appendBatch 组提交记录往返
* 5. 损坏记录跳过不影响后续完整记录
*/
import { WAL } from '../../src/engine/aria/wal/log';
import { WALRecordType } from '../../src/engine/aria/types';
// ===================================================================
// WAL 内存 Store(模拟 IStorageBackend 单记录语义)
// ===================================================================
class MemoryWALStore {
chunks: Uint8Array[] = [];
truncated = false;
async append(data: Uint8Array): Promise<void> {
this.chunks.push(data);
}
async readAll(): Promise<Uint8Array> {
const total = this.chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; }
return combined;
}
async truncate(): Promise<void> {
this.chunks = [];
this.truncated = true;
}
async exists(): Promise<boolean> {
return this.chunks.length > 0;
}
}
function makeRecord(over: Partial<import('../../src/engine/aria/types').WALRecord> = {}) {
return {
type: WALRecordType.INSERT,
txnId: 0,
tableName: 'users',
key: 'u-1',
data: { id: 'u-1', name: 'Alice' },
...over,
};
}
describe('AriaEngine — WAL CRC-32 校验', () => {
it('新算法记录 append → recover 往返完整', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.append(makeRecord({ key: 'u-1' }));
await wal.append(makeRecord({ key: 'u-2', data: { id: 'u-2', name: 'Bob' } }));
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(2);
expect(records[0].key).toBe('u-1');
expect(records[1].data).toEqual({ id: 'u-2', name: 'Bob' });
});
it('appendBatch 组提交记录往返完整', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.appendBatch([
makeRecord({ key: 'a' }),
makeRecord({ key: 'b' }),
makeRecord({ key: 'c' }),
]);
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records.map((r) => r.key)).toEqual(['a', 'b', 'c']);
expect(records.map((r) => r.lsn)).toEqual([1, 2, 3]);
});
it('篡改记录数据 → CRC 不匹配跳过', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.append(makeRecord({ key: 'good-1' }));
await wal.append(makeRecord({ key: 'evil' }));
await wal.append(makeRecord({ key: 'good-2' }));
// 篡改第二条记录的 data 字节(LSN=2 的记录)
const raw = await store.readAll();
const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
// 定位第二条记录起始:按记录格式顺序扫描直到 LSN=2
// [lsn:4][type:1][txnId:4][tblLen:2][table][keyLen:2][key][jsonLen:4][json][crc:4]
let offset = 0;
while (offset + 4 <= raw.byteLength && view.getUint32(offset, false) !== 2) {
const tblLen = view.getUint16(offset + 9, false);
const keyLenOff = offset + 11 + tblLen;
const keyLen = view.getUint16(keyLenOff, false);
const jsonLenOff = keyLenOff + 2 + keyLen;
const jsonLen = view.getUint32(jsonLenOff, false);
offset += 4 + 1 + 4 + 2 + tblLen + 2 + keyLen + 4 + jsonLen + 4;
}
expect(view.getUint32(offset, false)).toBe(2);
// 篡改 data 区(json 起始处第 4 字节)
const tblLen2 = view.getUint16(offset + 9, false);
const jsonStart = offset + 4 + 1 + 4 + 2 + tblLen2 + 2 + view.getUint16(offset + 11 + tblLen2, false) + 4;
raw[jsonStart + 4] ^= 0x01;
// 写回 storereadAll 返回的是合并副本)
store.chunks = [raw];
// 恢复:坏记录被跳过,前后好记录保留
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records.map((r) => r.key)).toEqual(['good-1', 'good-2']);
});
it('旧版弱校验记录兼容(双算法探测)', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
// 手工构造一条"旧算法"记录:与 encodeRecord 布局一致但 CRC 用旧算法计算
const record = {
lsn: 1,
type: WALRecordType.INSERT as number,
txnId: 0,
tableName: 'legacy',
key: 'old',
data: { id: 'old', v: 1 },
};
const encoder = new TextEncoder();
const tblB = encoder.encode(record.tableName);
const keyB = encoder.encode(record.key);
const jsonB = encoder.encode(JSON.stringify(record.data));
const size = 4 + 1 + 4 + 2 + tblB.length + 2 + keyB.length + 4 + jsonB.length + 4;
const buf = new ArrayBuffer(size);
const v = new DataView(buf);
let o = 0;
v.setUint32(o, record.lsn, false); o += 4;
v.setUint8(o, record.type); o += 1;
v.setUint32(o, record.txnId, false); o += 4;
v.setUint16(o, tblB.length, false); o += 2;
new Uint8Array(buf).set(tblB, o); o += tblB.length;
v.setUint16(o, keyB.length, false); o += 2;
new Uint8Array(buf).set(keyB, o); o += keyB.length;
v.setUint32(o, jsonB.length, false); o += 4;
new Uint8Array(buf).set(jsonB, o); o += jsonB.length;
// 旧算法 CRC
let crc = 0;
const u8 = new Uint8Array(buf, 0, o);
for (let i = 0; i < u8.length; i++) crc = ((crc << 5) - crc + u8[i]) | 0;
v.setUint32(o, crc >>> 0, false);
store.chunks.push(new Uint8Array(buf));
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records).toHaveLength(1);
expect(records[0].tableName).toBe('legacy');
expect(records[0].data).toEqual({ id: 'old', v: 1 });
});
it('旧算法记录 + 新算法记录混合 → 全部正确恢复', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
// 先放一条旧算法记录
const record = {
lsn: 1,
type: WALRecordType.DELETE as number,
txnId: 0,
tableName: 't',
key: 'old-key',
data: undefined as Record<string, unknown> | undefined,
};
const encoder = new TextEncoder();
const tblB = encoder.encode(record.tableName);
const keyB = encoder.encode(record.key);
const size = 4 + 1 + 4 + 2 + tblB.length + 2 + keyB.length + 4 + 4;
const buf = new ArrayBuffer(size);
const v = new DataView(buf);
let o = 0;
v.setUint32(o, record.lsn, false); o += 4;
v.setUint8(o, record.type); o += 1;
v.setUint32(o, record.txnId, false); o += 4;
v.setUint16(o, tblB.length, false); o += 2;
new Uint8Array(buf).set(tblB, o); o += tblB.length;
v.setUint16(o, keyB.length, false); o += 2;
new Uint8Array(buf).set(keyB, o); o += keyB.length;
v.setUint32(o, 0, false); o += 4; // jsonLen=0
let crc = 0;
const u8 = new Uint8Array(buf, 0, o);
for (let i = 0; i < u8.length; i++) crc = ((crc << 5) - crc + u8[i]) | 0;
v.setUint32(o, crc >>> 0, false);
store.chunks.push(new Uint8Array(buf));
// 再追加一条新算法记录(LSN=2)
await wal.append(makeRecord({ key: 'new-key', lsn: 2 } as never));
const records: import('../../src/engine/aria/types').WALRecord[] = [];
await wal.recover((r) => records.push(r));
expect(records.map((r) => r.key)).toEqual(['old-key', 'new-key']);
expect(records[0].type).toBe(WALRecordType.DELETE);
});
it('checkpoint 截断后 exists 返回 false', async () => {
const store = new MemoryWALStore();
const wal = new WAL(store as never, true, 'full');
await wal.append(makeRecord({ key: 'x' }));
expect(await store.exists()).toBe(true);
await wal.checkpoint();
expect(store.truncated).toBe(true);
expect(await store.exists()).toBe(false);
});
});