/** * @file test.conv-10.batch-conversion.ts * @description Tests for batch conversion operations and performance */ import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../plugins.js'; import { EInvoice } from '../../../ts/index.js'; tap.test('CONV-10: Batch Conversion - should handle sequential batch loading', async (t) => { const einvoice = new EInvoice(); const batchSize = 10; const results = { processed: 0, successful: 0, failed: 0, totalTime: 0, averageTime: 0 }; // Create test UBL invoices const ublInvoices = Array.from({ length: batchSize }, (_, i) => { const invoiceNumber = `BATCH-SEQ-2024-${String(i + 1).padStart(3, '0')}`; return ` ${invoiceNumber} 2024-01-25 380 EUR Seller Company ${i + 1} Address ${i + 1} Berlin 10115 DE DE${String(123456789 + i).padStart(9, '0')} VAT Buyer Company ${i + 1} Buyer Address ${i + 1} Munich 80331 DE 1 ${i + 1} ${(i + 1) * (100.00 + (i * 10))} Product ${i + 1} ${100.00 + (i * 10)} ${(i + 1) * (100.00 + (i * 10))} ${(i + 1) * (100.00 + (i * 10))} ${((i + 1) * (100.00 + (i * 10)) * 1.19).toFixed(2)} ${((i + 1) * (100.00 + (i * 10)) * 1.19).toFixed(2)} `; }); // Process sequentially const startTime = Date.now(); for (const xmlContent of ublInvoices) { results.processed++; try { const loaded = await einvoice.loadXml(xmlContent); if (loaded && loaded.id) { results.successful++; } else { console.log('Loaded but no id:', loaded?.id); } } catch (error) { console.log('Error loading invoice:', error); results.failed++; } } results.totalTime = Date.now() - startTime; results.averageTime = results.totalTime / results.processed; console.log(`Sequential Batch (${results.processed} invoices):`); console.log(` - Successful: ${results.successful}`); console.log(` - Failed: ${results.failed}`); console.log(` - Total time: ${results.totalTime}ms`); console.log(` - Average time per invoice: ${results.averageTime.toFixed(2)}ms`); expect(results.successful).toEqual(batchSize); expect(results.failed).toEqual(0); }); tap.test('CONV-10: Batch Conversion - should handle parallel batch loading', async (t) => { const einvoice = new EInvoice(); const batchSize = 10; const results = { processed: 0, successful: 0, failed: 0, totalTime: 0, averageTime: 0 }; // Create test CII invoices const ciiInvoices = Array.from({ length: batchSize }, (_, i) => { const invoiceNumber = `BATCH-PAR-2024-${String(i + 1).padStart(3, '0')}`; return ` urn:cen.eu:en16931:2017 ${invoiceNumber} 380 20240125 Parallel Seller ${i + 1} Parallel Address ${i + 1} Paris 75001 FR FR${String(12345678901 + i).padStart(11, '0')} Parallel Buyer ${i + 1} Parallel Buyer Address ${i + 1} Lyon 69001 FR EUR 500.00 500.00 100.00 600.00 `; }); // Process in parallel const startTime = Date.now(); const loadingPromises = ciiInvoices.map(async (xmlContent) => { try { const loaded = await einvoice.loadXml(xmlContent); return { success: true, loaded }; } catch (error) { return { success: false, error }; } }); const loadingResults = await Promise.all(loadingPromises); results.processed = loadingResults.length; results.successful = loadingResults.filter(r => r.success && r.loaded?.id).length; results.failed = loadingResults.filter(r => !r.success).length; results.totalTime = Date.now() - startTime; results.averageTime = results.totalTime / results.processed; console.log(`\nParallel Batch (${results.processed} invoices):`); console.log(` - Successful: ${results.successful}`); console.log(` - Failed: ${results.failed}`); console.log(` - Total time: ${results.totalTime}ms`); console.log(` - Average time per invoice: ${results.averageTime.toFixed(2)}ms`); expect(results.successful).toEqual(batchSize); expect(results.failed).toEqual(0); }); tap.test('CONV-10: Batch Conversion - should handle mixed format batch loading', async (t) => { const einvoice = new EInvoice(); const results = { byFormat: new Map(), totalProcessed: 0, totalSuccessful: 0 }; // Create mixed format invoices (3 of each) const mixedInvoices = [ // UBL invoices ...Array.from({ length: 3 }, (_, i) => ({ format: 'ubl', content: ` MIXED-UBL-${i + 1} 2024-01-26 380 EUR UBL Seller ${i + 1} UBL Buyer ${i + 1} 297.50 ` })), // CII invoices ...Array.from({ length: 3 }, (_, i) => ({ format: 'cii', content: ` MIXED-CII-${i + 1} 380 20240126 CII Seller ${i + 1} CII Buyer ${i + 1} EUR ` })) ]; // Process mixed batch for (const invoice of mixedInvoices) { const format = invoice.format; if (!results.byFormat.has(format)) { results.byFormat.set(format, { processed: 0, successful: 0, failed: 0 }); } const formatStats = results.byFormat.get(format)!; formatStats.processed++; results.totalProcessed++; try { const loaded = await einvoice.loadXml(invoice.content); if (loaded && loaded.id) { formatStats.successful++; results.totalSuccessful++; } } catch (error) { formatStats.failed++; } } const successRate = (results.totalSuccessful / results.totalProcessed * 100).toFixed(2) + '%'; console.log(`\nMixed Format Batch:`); console.log(` - Total processed: ${results.totalProcessed}`); console.log(` - Success rate: ${successRate}`); console.log(` - Format statistics:`); results.byFormat.forEach((stats, format) => { console.log(` * ${format}: ${stats.successful}/${stats.processed} successful`); }); expect(results.totalSuccessful).toEqual(results.totalProcessed); }); tap.test('CONV-10: Batch Conversion - should handle large batch with memory monitoring', async (t) => { const einvoice = new EInvoice(); const batchSize = 50; const memorySnapshots = []; // Capture initial memory if (global.gc) global.gc(); const initialMemory = process.memoryUsage(); // Create large batch of simple UBL invoices const largeBatch = Array.from({ length: batchSize }, (_, i) => { const invoiceNumber = `LARGE-BATCH-${String(i + 1).padStart(4, '0')}`; return ` ${invoiceNumber} 2024-01-27 380 EUR Large Batch Seller ${i + 1} Street ${i + 1}, Building ${i % 10 + 1} Berlin ${10000 + i} DE DE${String(100000000 + i).padStart(9, '0')} VAT Large Batch Buyer ${i + 1} Avenue ${i + 1}, Suite ${i % 20 + 1} Munich ${80000 + i} DE ${Array.from({ length: 5 }, (_, j) => ` ${j + 1} ${j + 1} ${(j + 1) * (50.00 + j * 10)} Product ${i + 1}-${j + 1} with detailed description ${50.00 + j * 10} `).join('')} ${Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0)} ${Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0)} ${(Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0) * 1.19).toFixed(2)} ${(Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0) * 1.19).toFixed(2)} `; }); // Process in chunks and monitor memory const chunkSize = 10; let processed = 0; let successful = 0; for (let i = 0; i < largeBatch.length; i += chunkSize) { const chunk = largeBatch.slice(i, i + chunkSize); // Process chunk const chunkResults = await Promise.all( chunk.map(async (xmlContent) => { try { const loaded = await einvoice.loadXml(xmlContent); return loaded && loaded.id; } catch { return false; } }) ); processed += chunk.length; successful += chunkResults.filter(r => r).length; // Capture memory snapshot const currentMemory = process.memoryUsage(); memorySnapshots.push({ processed, heapUsed: Math.round((currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024 * 100) / 100, external: Math.round((currentMemory.external - initialMemory.external) / 1024 / 1024 * 100) / 100 }); } // Force garbage collection if available if (global.gc) global.gc(); const finalMemory = process.memoryUsage(); const results = { processed, successful, successRate: (successful / processed * 100).toFixed(2) + '%', memoryIncrease: { heapUsed: Math.round((finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024 * 100) / 100, external: Math.round((finalMemory.external - initialMemory.external) / 1024 / 1024 * 100) / 100 }, averageMemoryPerInvoice: Math.round((finalMemory.heapUsed - initialMemory.heapUsed) / processed / 1024 * 100) / 100 }; console.log(`\nLarge Batch Memory Analysis (${results.processed} invoices):`); console.log(` - Success rate: ${results.successRate}`); console.log(` - Memory increase: ${results.memoryIncrease.heapUsed}MB heap`); console.log(` - Average memory per invoice: ${results.averageMemoryPerInvoice}KB`); expect(results.successful).toEqual(batchSize); expect(results.memoryIncrease.heapUsed).toBeLessThan(100); // Should use less than 100MB for 50 invoices }); tap.test('CONV-10: Batch Conversion - should handle corpus batch loading', async (t) => { const einvoice = new EInvoice(); const batchStats = { totalFiles: 0, processed: 0, successful: 0, failedParsing: 0, formats: new Set(), processingTimes: [] as number[] }; // Get a few corpus files for testing const corpusDir = plugins.path.join(process.cwd(), 'test/assets/corpus'); const xmlFiles: string[] = []; // Manually check a few known corpus files const testFiles = [ 'XML-Rechnung/UBL/EN16931_Einfach.ubl.xml', 'XML-Rechnung/CII/EN16931_Einfach.cii.xml', 'PEPPOL/Valid/billing-3.0-invoice-full-sample.xml' ]; for (const file of testFiles) { const fullPath = plugins.path.join(corpusDir, file); try { await plugins.fs.access(fullPath); xmlFiles.push(fullPath); } catch { // File doesn't exist, skip } } batchStats.totalFiles = xmlFiles.length; if (xmlFiles.length > 0) { // Process files for (const file of xmlFiles) { const startTime = Date.now(); try { const content = await plugins.fs.readFile(file, 'utf-8'); const loaded = await einvoice.loadXml(content); if (loaded && loaded.id) { batchStats.processed++; batchStats.successful++; // Track format from filename if (file.includes('.ubl.')) batchStats.formats.add('ubl'); else if (file.includes('.cii.')) batchStats.formats.add('cii'); else if (file.includes('PEPPOL')) batchStats.formats.add('ubl'); } else { batchStats.failedParsing++; } batchStats.processingTimes.push(Date.now() - startTime); } catch (error) { batchStats.failedParsing++; } } // Calculate statistics const avgProcessingTime = batchStats.processingTimes.length > 0 ? batchStats.processingTimes.reduce((a, b) => a + b, 0) / batchStats.processingTimes.length : 0; console.log(`\nCorpus Batch Loading (${batchStats.totalFiles} files):`); console.log(` - Successfully parsed: ${batchStats.processed}`); console.log(` - Failed parsing: ${batchStats.failedParsing}`); console.log(` - Average processing time: ${Math.round(avgProcessingTime)}ms`); console.log(` - Formats found: ${Array.from(batchStats.formats).join(', ')}`); expect(batchStats.successful).toBeGreaterThan(0); } else { console.log('\nCorpus Batch Loading: No test files found, skipping test'); expect(true).toEqual(true); // Pass the test if no files found } }); tap.start();