import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as einvoice from '../../../ts/index.js'; import * as plugins from '../../plugins.js'; tap.test('PARSE-06: Memory-efficient parsing strategies', async () => { console.log('Testing memory-efficient parsing of large e-invoices...\n'); // Generate different sized test documents const generateLargeInvoice = (lineItems: number): string => { const lines = []; for (let i = 1; i <= lineItems; i++) { lines.push(` ${i} ${i} ${(i * 10).toFixed(2)} Product Item ${i} Product Item ${i} with a reasonably long description to increase document size for streaming test purposes ${(Math.random() * 100).toFixed(2)} `); } return ` LARGE-${lineItems} 2024-01-01 Large Invoice Supplier Large Invoice Customer ${lines.join('')} `; }; const testSizes = [ { items: 100, expectedSize: '~50KB' }, { items: 1000, expectedSize: '~500KB' }, { items: 5000, expectedSize: '~2.5MB' } ]; for (const test of testSizes) { const startTime = Date.now(); const startMemory = process.memoryUsage(); const largeXml = generateLargeInvoice(test.items); const xmlSize = Buffer.byteLength(largeXml, 'utf8'); console.log(`\nTesting ${test.items} line items (${test.expectedSize}, actual: ${(xmlSize/1024).toFixed(1)}KB):`); try { const invoice = new einvoice.EInvoice(); await invoice.fromXmlString(largeXml); const endMemory = process.memoryUsage(); const memoryDelta = { heapUsed: (endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024, external: (endMemory.external - startMemory.external) / 1024 / 1024 }; const parseTime = Date.now() - startTime; console.log(` Parse time: ${parseTime}ms`); console.log(` Memory delta: ${memoryDelta.heapUsed.toFixed(2)}MB heap, ${memoryDelta.external.toFixed(2)}MB external`); console.log(` Parse rate: ${(xmlSize / parseTime * 1000 / 1024 / 1024).toFixed(2)}MB/s`); // Check if memory usage is reasonable const memoryRatio = memoryDelta.heapUsed / (xmlSize / 1024 / 1024); console.log(` Memory ratio: ${memoryRatio.toFixed(2)}x document size`); if (memoryRatio > 10) { console.log(' ⚠️ High memory usage detected'); } else { console.log(' ✓ Memory usage acceptable'); } // Verify the invoice was parsed correctly expect(invoice.id).toEqual(`LARGE-${test.items}`); expect(invoice.items?.length).toEqual(test.items); } catch (error) { console.log(` ✗ Parse error: ${error.message}`); } // Force garbage collection if available if (global.gc) { global.gc(); } } }); tap.test('PARSE-06: Streaming parse simulation', async () => { console.log('\nTesting streaming parse behavior...\n'); // Test parsing in chunks (simulating streaming) const chunkTests = [ { name: 'Parse partial invoice (incomplete)', xml: ` PARTIAL-001 `, expectError: true }, { name: 'Parse complete minimal invoice', xml: ` MINIMAL-001 `, expectError: false } ]; for (const test of chunkTests) { console.log(`${test.name}:`); try { const invoice = new einvoice.EInvoice(); await invoice.fromXmlString(test.xml); if (test.expectError) { console.log(' ✗ Expected error but parsed successfully'); } else { console.log(' ✓ Parsed successfully'); console.log(` ID: ${invoice.id}`); } } catch (error) { if (test.expectError) { console.log(' ✓ Expected error occurred'); console.log(` Error: ${error.message}`); } else { console.log(` ✗ Unexpected error: ${error.message}`); } } } }); tap.test('PARSE-06: Progressive parsing performance', async () => { console.log('\nTesting progressive parsing performance...\n'); // Test parsing increasingly complex documents const complexityLevels = [ { name: 'Simple', lineItems: 10, additionalElements: 0 }, { name: 'Moderate', lineItems: 50, additionalElements: 10 }, { name: 'Complex', lineItems: 100, additionalElements: 20 }, { name: 'Very Complex', lineItems: 500, additionalElements: 50 } ]; const results = []; for (const level of complexityLevels) { const invoice = ` ${level.name}-INVOICE 2024-01-01 2024-02-01 ${Array.from({length: level.additionalElements}, (_, i) => ` Additional note ${i + 1} for complexity testing`).join('')} Complex Supplier ${Array.from({length: level.lineItems}, (_, i) => ` ${i + 1} 1 100.00 Item ${i + 1} `).join('')} `; const startTime = Date.now(); const xmlSize = Buffer.byteLength(invoice, 'utf8'); try { const einvoiceObj = new einvoice.EInvoice(); await einvoiceObj.fromXmlString(invoice); const parseTime = Date.now() - startTime; const parseRate = (xmlSize / parseTime * 1000 / 1024).toFixed(2); results.push({ level: level.name, size: xmlSize, time: parseTime, rate: parseRate }); console.log(`${level.name} (${level.lineItems} items, ${(xmlSize/1024).toFixed(1)}KB):`); console.log(` ✓ Parsed in ${parseTime}ms (${parseRate}KB/s)`); } catch (error) { console.log(`${level.name}: ✗ Error - ${error.message}`); } } // Performance summary console.log('\nPerformance Summary:'); results.forEach(r => { console.log(` ${r.level}: ${r.time}ms for ${(r.size/1024).toFixed(1)}KB (${r.rate}KB/s)`); }); }); tap.test('PARSE-06: Memory cleanup verification', async () => { console.log('\nTesting memory cleanup after parsing...\n'); // Parse a large document and verify memory is released const largeXml = ` MEMORY-TEST 2024-01-01 ${Array.from({length: 1000}, (_, i) => ` ${i + 1} 1 100.00 Memory test item ${i + 1} with additional description `).join('')} `; // Initial memory if (global.gc) global.gc(); const initialMemory = process.memoryUsage().heapUsed; // Parse multiple times console.log('Parsing 5 large invoices sequentially...'); for (let i = 0; i < 5; i++) { const invoice = new einvoice.EInvoice(); await invoice.fromXmlString(largeXml); console.log(` Parse ${i + 1} complete`); } // Force GC and check memory if (global.gc) { global.gc(); await new Promise(resolve => setTimeout(resolve, 100)); const finalMemory = process.memoryUsage().heapUsed; const memoryIncrease = (finalMemory - initialMemory) / 1024 / 1024; console.log(`\nMemory increase after 5 parses: ${memoryIncrease.toFixed(2)}MB`); if (memoryIncrease > 50) { console.log('⚠️ Possible memory leak detected'); } else { console.log('✓ Memory usage within acceptable range'); } } else { console.log('⚠️ Manual GC not available - memory leak test skipped'); } }); // Run the tests tap.start();