fix(tsmdb): add comprehensive unit tests for tsmdb components: checksum, query planner, index engine, session, and WAL
This commit is contained in:
411
test/test.tsmdb.wal.ts
Normal file
411
test/test.tsmdb.wal.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartmongo from '../ts/index.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
const { WAL, ObjectId } = smartmongo.tsmdb;
|
||||
|
||||
let wal: InstanceType<typeof WAL>;
|
||||
const TEST_WAL_PATH = '/tmp/tsmdb-test-wal/test.wal';
|
||||
|
||||
// Helper to clean up test files
|
||||
async function cleanupTestFiles() {
|
||||
try {
|
||||
await fs.rm('/tmp/tsmdb-test-wal', { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore if doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Setup
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: cleanup before tests', async () => {
|
||||
await cleanupTestFiles();
|
||||
});
|
||||
|
||||
tap.test('wal: should create WAL instance', async () => {
|
||||
wal = new WAL(TEST_WAL_PATH, { checkpointInterval: 100 });
|
||||
expect(wal).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('wal: should initialize WAL', async () => {
|
||||
const result = await wal.initialize();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.recoveredEntries).toBeArray();
|
||||
expect(result.recoveredEntries.length).toEqual(0);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// LSN Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: getCurrentLsn should return 0 initially', async () => {
|
||||
const lsn = wal.getCurrentLsn();
|
||||
expect(lsn).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('wal: LSN should increment after logging', async () => {
|
||||
const doc = { _id: new ObjectId(), name: 'Test' };
|
||||
const lsn = await wal.logInsert('testdb', 'testcoll', doc as any);
|
||||
|
||||
expect(lsn).toEqual(1);
|
||||
expect(wal.getCurrentLsn()).toEqual(1);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Insert Logging Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: logInsert should create entry with correct structure', async () => {
|
||||
const doc = { _id: new ObjectId(), name: 'InsertTest', value: 42 };
|
||||
const lsn = await wal.logInsert('testdb', 'insertcoll', doc as any);
|
||||
|
||||
expect(lsn).toBeGreaterThan(0);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.operation).toEqual('insert');
|
||||
expect(entry!.dbName).toEqual('testdb');
|
||||
expect(entry!.collName).toEqual('insertcoll');
|
||||
expect(entry!.documentId).toEqual(doc._id.toHexString());
|
||||
expect(entry!.data).toBeTruthy();
|
||||
expect(entry!.timestamp).toBeGreaterThan(0);
|
||||
expect(entry!.checksum).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('wal: logInsert with transaction ID', async () => {
|
||||
const doc = { _id: new ObjectId(), name: 'TxnInsertTest' };
|
||||
const lsn = await wal.logInsert('testdb', 'insertcoll', doc as any, 'txn-123');
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry!.txnId).toEqual('txn-123');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Update Logging Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: logUpdate should store old and new document', async () => {
|
||||
const oldDoc = { _id: new ObjectId(), name: 'OldName', value: 1 };
|
||||
const newDoc = { ...oldDoc, name: 'NewName', value: 2 };
|
||||
|
||||
const lsn = await wal.logUpdate('testdb', 'updatecoll', oldDoc as any, newDoc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.operation).toEqual('update');
|
||||
expect(entry!.data).toBeTruthy();
|
||||
expect(entry!.previousData).toBeTruthy();
|
||||
expect(entry!.documentId).toEqual(oldDoc._id.toHexString());
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Delete Logging Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: logDelete should record deleted document', async () => {
|
||||
const doc = { _id: new ObjectId(), name: 'ToDelete' };
|
||||
|
||||
const lsn = await wal.logDelete('testdb', 'deletecoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.operation).toEqual('delete');
|
||||
expect(entry!.previousData).toBeTruthy();
|
||||
expect(entry!.data).toBeUndefined();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Transaction Logging Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: logBeginTransaction should create begin entry', async () => {
|
||||
const lsn = await wal.logBeginTransaction('txn-begin-test');
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.operation).toEqual('begin');
|
||||
expect(entry!.txnId).toEqual('txn-begin-test');
|
||||
});
|
||||
|
||||
tap.test('wal: logCommitTransaction should create commit entry', async () => {
|
||||
const lsn = await wal.logCommitTransaction('txn-commit-test');
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.operation).toEqual('commit');
|
||||
expect(entry!.txnId).toEqual('txn-commit-test');
|
||||
});
|
||||
|
||||
tap.test('wal: logAbortTransaction should create abort entry', async () => {
|
||||
const lsn = await wal.logAbortTransaction('txn-abort-test');
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry).toBeTruthy();
|
||||
expect(entry!.operation).toEqual('abort');
|
||||
expect(entry!.txnId).toEqual('txn-abort-test');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// getTransactionEntries Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: getTransactionEntries should return entries for transaction', async () => {
|
||||
// Log a complete transaction
|
||||
const txnId = 'txn-entries-test';
|
||||
await wal.logBeginTransaction(txnId);
|
||||
|
||||
const doc1 = { _id: new ObjectId(), name: 'TxnDoc1' };
|
||||
await wal.logInsert('testdb', 'txncoll', doc1 as any, txnId);
|
||||
|
||||
const doc2 = { _id: new ObjectId(), name: 'TxnDoc2' };
|
||||
await wal.logInsert('testdb', 'txncoll', doc2 as any, txnId);
|
||||
|
||||
await wal.logCommitTransaction(txnId);
|
||||
|
||||
const entries = wal.getTransactionEntries(txnId);
|
||||
|
||||
expect(entries.length).toEqual(4); // begin + 2 inserts + commit
|
||||
expect(entries.every(e => e.txnId === txnId)).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('wal: getTransactionEntries should return empty for unknown transaction', async () => {
|
||||
const entries = wal.getTransactionEntries('unknown-txn-id');
|
||||
expect(entries.length).toEqual(0);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// getEntriesAfter Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: getEntriesAfter should filter by LSN', async () => {
|
||||
const currentLsn = wal.getCurrentLsn();
|
||||
|
||||
// Add more entries
|
||||
const doc = { _id: new ObjectId(), name: 'AfterTest' };
|
||||
await wal.logInsert('testdb', 'aftercoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(currentLsn);
|
||||
expect(entries.length).toEqual(1);
|
||||
expect(entries[0].lsn).toBeGreaterThan(currentLsn);
|
||||
});
|
||||
|
||||
tap.test('wal: getEntriesAfter with LSN 0 should return all entries', async () => {
|
||||
const entries = wal.getEntriesAfter(0);
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
expect(entries.length).toEqual(wal.getCurrentLsn());
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Checkpoint Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: checkpoint should create checkpoint entry', async () => {
|
||||
const lsn = await wal.checkpoint();
|
||||
|
||||
expect(lsn).toBeGreaterThan(0);
|
||||
|
||||
// After checkpoint, getEntriesAfter(checkpoint) should be limited
|
||||
const entries = wal.getEntriesAfter(0);
|
||||
expect(entries.some(e => e.operation === 'checkpoint')).toBeTrue();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Document Recovery Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: recoverDocument should deserialize document from entry', async () => {
|
||||
const originalDoc = { _id: new ObjectId(), name: 'RecoverTest', nested: { a: 1, b: 2 } };
|
||||
const lsn = await wal.logInsert('testdb', 'recovercoll', originalDoc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const recovered = wal.recoverDocument(entry!);
|
||||
|
||||
expect(recovered).toBeTruthy();
|
||||
expect(recovered!.name).toEqual('RecoverTest');
|
||||
expect(recovered!.nested.a).toEqual(1);
|
||||
expect(recovered!.nested.b).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('wal: recoverDocument should return null for entry without data', async () => {
|
||||
const lsn = await wal.logBeginTransaction('recover-no-data');
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const recovered = wal.recoverDocument(entry!);
|
||||
expect(recovered).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('wal: recoverPreviousDocument should deserialize previous state', async () => {
|
||||
const oldDoc = { _id: new ObjectId(), name: 'Old', value: 100 };
|
||||
const newDoc = { ...oldDoc, name: 'New', value: 200 };
|
||||
|
||||
const lsn = await wal.logUpdate('testdb', 'recovercoll', oldDoc as any, newDoc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const previous = wal.recoverPreviousDocument(entry!);
|
||||
|
||||
expect(previous).toBeTruthy();
|
||||
expect(previous!.name).toEqual('Old');
|
||||
expect(previous!.value).toEqual(100);
|
||||
});
|
||||
|
||||
tap.test('wal: recoverPreviousDocument should return null for insert entry', async () => {
|
||||
const doc = { _id: new ObjectId(), name: 'NoPrevious' };
|
||||
const lsn = await wal.logInsert('testdb', 'recovercoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const previous = wal.recoverPreviousDocument(entry!);
|
||||
expect(previous).toBeNull();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// WAL Persistence and Recovery Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: should persist and recover entries', async () => {
|
||||
// Close current WAL
|
||||
await wal.close();
|
||||
|
||||
// Create new WAL instance and initialize (should recover)
|
||||
const wal2 = new WAL(TEST_WAL_PATH, { checkpointInterval: 100 });
|
||||
const result = await wal2.initialize();
|
||||
|
||||
// Should have recovered entries
|
||||
expect(result.recoveredEntries).toBeArray();
|
||||
// After checkpoint, there might not be many recoverable entries
|
||||
// but getCurrentLsn should be preserved or reset
|
||||
|
||||
await wal2.close();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Entry Checksum Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: entries should have valid checksums', async () => {
|
||||
wal = new WAL(TEST_WAL_PATH + '.checksum', { checkpointInterval: 100 });
|
||||
await wal.initialize();
|
||||
|
||||
const doc = { _id: new ObjectId(), name: 'ChecksumTest' };
|
||||
const lsn = await wal.logInsert('testdb', 'checksumcoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
expect(entry!.checksum).toBeGreaterThan(0);
|
||||
expect(typeof entry!.checksum).toEqual('number');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: should handle special characters in document', async () => {
|
||||
const doc = {
|
||||
_id: new ObjectId(),
|
||||
name: 'Test\nWith\tSpecial\r\nChars',
|
||||
emoji: '🎉',
|
||||
unicode: '日本語',
|
||||
};
|
||||
|
||||
const lsn = await wal.logInsert('testdb', 'specialcoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const recovered = wal.recoverDocument(entry!);
|
||||
expect(recovered!.name).toEqual('Test\nWith\tSpecial\r\nChars');
|
||||
expect(recovered!.emoji).toEqual('🎉');
|
||||
expect(recovered!.unicode).toEqual('日本語');
|
||||
});
|
||||
|
||||
tap.test('wal: should handle binary data in documents', async () => {
|
||||
const doc = {
|
||||
_id: new ObjectId(),
|
||||
binaryField: Buffer.from([0x00, 0xFF, 0x7F, 0x80]),
|
||||
};
|
||||
|
||||
const lsn = await wal.logInsert('testdb', 'binarycoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const recovered = wal.recoverDocument(entry!);
|
||||
expect(recovered).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('wal: should handle nested documents', async () => {
|
||||
const doc = {
|
||||
_id: new ObjectId(),
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
value: 'deep',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const lsn = await wal.logInsert('testdb', 'nestedcoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const recovered = wal.recoverDocument(entry!);
|
||||
expect(recovered!.level1.level2.level3.value).toEqual('deep');
|
||||
});
|
||||
|
||||
tap.test('wal: should handle arrays in documents', async () => {
|
||||
const doc = {
|
||||
_id: new ObjectId(),
|
||||
tags: ['a', 'b', 'c'],
|
||||
numbers: [1, 2, 3],
|
||||
mixed: [1, 'two', { three: 3 }],
|
||||
};
|
||||
|
||||
const lsn = await wal.logInsert('testdb', 'arraycoll', doc as any);
|
||||
|
||||
const entries = wal.getEntriesAfter(lsn - 1);
|
||||
const entry = entries.find(e => e.lsn === lsn);
|
||||
|
||||
const recovered = wal.recoverDocument(entry!);
|
||||
expect(recovered!.tags).toEqual(['a', 'b', 'c']);
|
||||
expect(recovered!.numbers).toEqual([1, 2, 3]);
|
||||
expect(recovered!.mixed[2].three).toEqual(3);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
|
||||
tap.test('wal: cleanup', async () => {
|
||||
await wal.close();
|
||||
await cleanupTestFiles();
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user