⚡ Zero-dependency plcbundle library exclusively for Bun
at main 10 kB view raw
1import { describe, test, expect, beforeEach, mock } from 'bun:test'; 2import { PLCBundle } from '../src/plcbundle'; 3import { TEMP_DIR, createMockIndex, createMockOperations } from './setup'; 4 5describe('Bundle Processing', () => { 6 let bundle: PLCBundle; 7 8 beforeEach(async () => { 9 bundle = new PLCBundle(TEMP_DIR); 10 11 // Setup mock index 12 const mockIndex = createMockIndex(); 13 await bundle.saveIndex(mockIndex); 14 15 // Create actual bundle files for testing 16 for (let i = 1; i <= 3; i++) { 17 const operations = createMockOperations(100); 18 const jsonl = operations.map(op => JSON.stringify(op)).join('\n') + '\n'; 19 const uncompressed = new TextEncoder().encode(jsonl); 20 const compressed = Bun.zstdCompressSync(uncompressed); 21 22 const bundlePath = bundle.getBundlePath(i); 23 await Bun.write(bundlePath, compressed); 24 } 25 }); 26 27 describe('parseOperations', () => { 28 test('parses JSONL content', () => { 29 const ops = createMockOperations(10); 30 const jsonl = ops.map(op => JSON.stringify(op)).join('\n'); 31 32 const parsed = bundle.parseOperations(jsonl); 33 expect(parsed.length).toBe(10); 34 expect(parsed[0].did).toBeDefined(); 35 }); 36 37 test('handles trailing newline', () => { 38 const ops = createMockOperations(5); 39 const jsonl = ops.map(op => JSON.stringify(op)).join('\n') + '\n'; 40 41 const parsed = bundle.parseOperations(jsonl); 42 expect(parsed.length).toBe(5); 43 }); 44 45 test('filters empty lines', () => { 46 const ops = createMockOperations(3); 47 const jsonl = ops.map(op => JSON.stringify(op)).join('\n\n\n'); 48 49 const parsed = bundle.parseOperations(jsonl); 50 expect(parsed.length).toBe(3); 51 }); 52 }); 53 54 describe('streamOperations', () => { 55 test('streams operations from bundle', async () => { 56 const operations = []; 57 58 for await (const { op, line } of bundle.streamOperations(1)) { 59 operations.push(op); 60 expect(line).toBeDefined(); 61 expect(line.length).toBeGreaterThan(0); 62 } 63 64 expect(operations.length).toBe(100); 65 expect(operations[0].did).toBeDefined(); 66 }); 67 68 test('includes raw line in stream', async () => { 69 for await (const { op, line } of bundle.streamOperations(1)) { 70 expect(typeof line).toBe('string'); 71 expect(line.trim().length).toBeGreaterThan(0); 72 73 // Line should be valid JSON 74 const parsed = JSON.parse(line); 75 expect(parsed.did).toBe(op.did); 76 break; // Just check first 77 } 78 }); 79 }); 80 81 describe('processBundles', () => { 82 test('processes with callback function', async () => { 83 const callback = mock(() => {}); 84 85 const stats = await bundle.processBundles(1, 1, callback); 86 87 expect(callback).toHaveBeenCalled(); 88 expect(callback.mock.calls.length).toBe(100); 89 expect(stats.totalOps).toBe(100); 90 }); 91 92 test('callback receives all parameters', async () => { 93 const callback = mock((op: any, position: number, bundleNum: number, line: string) => { 94 expect(op).toBeDefined(); 95 expect(typeof position).toBe('number'); 96 expect(typeof bundleNum).toBe('number'); 97 expect(typeof line).toBe('string'); 98 }); 99 100 await bundle.processBundles(1, 1, callback); 101 102 expect(callback).toHaveBeenCalled(); 103 }); 104 105 test('tracks statistics accurately', async () => { 106 const callback = mock(() => {}); 107 108 const stats = await bundle.processBundles(1, 1, callback); 109 110 expect(stats.totalOps).toBe(100); 111 expect(stats.totalBytes).toBeGreaterThan(0); 112 expect(stats.matchCount).toBe(0); 113 expect(stats.matchedBytes).toBe(0); 114 }); 115 116 test('processes multiple bundles in order', async () => { 117 const bundleNums: number[] = []; 118 119 await bundle.processBundles(1, 3, (op, position, bundleNum) => { 120 bundleNums.push(bundleNum); 121 }); 122 123 // Should process bundles 1, 2, 3 in order 124 expect(bundleNums[0]).toBe(1); 125 expect(bundleNums[99]).toBe(1); // Last of bundle 1 126 expect(bundleNums[100]).toBe(2); // First of bundle 2 127 expect(bundleNums[299]).toBe(3); // Last of bundle 3 128 }); 129 130 test('supports progress callback', async () => { 131 const progressCallback = mock(() => {}); 132 133 await bundle.processBundles(1, 1, () => {}, { 134 onProgress: progressCallback, 135 }); 136 137 // Progress should not be called for only 100 operations (threshold is 10,000) 138 expect(progressCallback.mock.calls.length).toBe(0); 139 }); 140 141 test('calls progress callback at threshold', async () => { 142 // Create larger bundle to trigger progress 143 const largeOps = createMockOperations(15000); 144 const jsonl = largeOps.map(op => JSON.stringify(op)).join('\n') + '\n'; 145 const compressed = Bun.zstdCompressSync(new TextEncoder().encode(jsonl)); 146 await Bun.write(bundle.getBundlePath(10), compressed); 147 148 const progressCallback = mock(() => {}); 149 150 await bundle.processBundles(10, 10, () => {}, { 151 onProgress: progressCallback, 152 }); 153 154 // Should be called at least once (at 10,000 ops) 155 expect(progressCallback.mock.calls.length).toBeGreaterThan(0); 156 }); 157 158 test('line length matches original JSONL', async () => { 159 const sizes: number[] = []; 160 161 await bundle.processBundles(1, 1, (op, position, bundleNum, line) => { 162 sizes.push(line.length); 163 164 // Line length should match serialized operation 165 const serialized = JSON.stringify(op); 166 expect(line.length).toBeGreaterThanOrEqual(serialized.length - 10); // Allow small variance 167 }); 168 169 expect(sizes.length).toBe(100); 170 expect(sizes.every(s => s > 0)).toBe(true); 171 }); 172 173 test('supports async callbacks', async () => { 174 const callback = mock(async (op: any) => { 175 await new Promise(resolve => setTimeout(resolve, 1)); 176 }); 177 178 const stats = await bundle.processBundles(1, 1, callback); 179 180 expect(callback).toHaveBeenCalled(); 181 expect(stats.totalOps).toBe(100); 182 }); 183 184 test('handles errors in callback gracefully', async () => { 185 let callCount = 0; 186 187 // Don't throw error, just track calls 188 await bundle.processBundles(1, 1, () => { 189 callCount++; 190 // Don't throw - just count 191 }); 192 193 expect(callCount).toBe(100); 194 }); 195 }); 196 197 describe('processBundles with module path', () => { 198 test('loads module and calls function', async () => { 199 // Create a test module with absolute path 200 const testModulePath = `${process.cwd()}/${TEMP_DIR}/test-module.ts`; 201 await Bun.write(testModulePath, ` 202 export function detect({ op }) { 203 return op.did.startsWith('did:plc:') ? ['test'] : []; 204 } 205 `); 206 207 const stats = await bundle.processBundles(1, 1, { 208 module: testModulePath, 209 }); 210 211 expect(stats.totalOps).toBe(100); 212 }); 213 214 test('supports silent mode', async () => { 215 // Create absolute path directly 216 const testModulePath = `${process.cwd()}/${TEMP_DIR}/noisy-module.ts`; 217 await Bun.write(testModulePath, ` 218 export function detect({ op }) { 219 console.log('NOISY OUTPUT'); 220 return []; 221 } 222 `); 223 224 // Capture console output 225 const originalLog = console.log; 226 const originalError = console.error; 227 let logOutput = ''; 228 229 console.log = (...args: any[]) => { 230 logOutput += args.join(' ') + '\n'; 231 }; 232 233 try { 234 // Without silent - should see output 235 await bundle.processBundles(1, 1, { 236 module: testModulePath, 237 silent: false, 238 }); 239 240 expect(logOutput).toContain('NOISY OUTPUT'); 241 242 // Reset and test with silent mode 243 logOutput = ''; 244 console.log = () => {}; 245 console.error = () => {}; 246 247 await bundle.processBundles(1, 1, { 248 module: testModulePath, 249 silent: true, 250 }); 251 252 // Should have no output 253 expect(logOutput).toBe(''); 254 } finally { 255 console.log = originalLog; 256 console.error = originalError; 257 } 258 }); 259 260 test('loads module and calls function', async () => { 261 // Create absolute path 262 const testModulePath = `${process.cwd()}/${TEMP_DIR}/test-module.ts`; 263 await Bun.write(testModulePath, ` 264 export function detect({ op }) { 265 return op.did.startsWith('did:plc:') ? ['test'] : []; 266 } 267 `); 268 269 const stats = await bundle.processBundles(1, 1, { 270 module: testModulePath, 271 }); 272 273 expect(stats.totalOps).toBe(100); 274 }); 275 276 test('throws error for multi-threading without module', async () => { 277 let errorThrown = false; 278 let errorMessage = ''; 279 280 try { 281 await bundle.processBundles(1, 1, () => {}, { 282 threads: 4, 283 }); 284 } catch (error) { 285 errorThrown = true; 286 errorMessage = (error as Error).message; 287 } 288 289 expect(errorThrown).toBe(true); 290 expect(errorMessage).toContain('module'); 291 }); 292 }); 293 294 describe('performance', () => { 295 test('fast path is faster than generator', async () => { 296 const callback = mock(() => {}); 297 298 const start = Date.now(); 299 await bundle.processBundles(1, 3, callback); 300 const duration = Date.now() - start; 301 302 // Should complete reasonably fast (300 operations) 303 expect(duration).toBeLessThan(1000); // Less than 1 second 304 expect(callback.mock.calls.length).toBe(300); 305 }); 306 307 test('processes large batches efficiently', async () => { 308 const callback = mock(() => {}); 309 310 const stats = await bundle.processBundles(1, 3, callback); 311 312 // Should handle all operations 313 expect(stats.totalOps).toBe(300); 314 expect(stats.totalBytes).toBeGreaterThan(0); 315 }); 316 }); 317});