import { expect, tap } from '@git.zone/tstest/tapbundle'; import { promises as fs } from 'fs'; import * as path from 'path'; import { CorpusLoader } from '../../helpers/corpus.loader.js'; import { PerformanceTracker } from '../../helpers/performance.tracker.js'; tap.test('VAL-07: Validation Performance - should validate invoices within performance thresholds', async () => { // Test validation performance across different file sizes and formats const performanceCategories = [ { category: 'UBL_XMLRECHNUNG', description: 'UBL XML-Rechnung files', sizeThreshold: 50, // KB validationThreshold: 100 // ms }, { category: 'CII_XMLRECHNUNG', description: 'CII XML-Rechnung files', sizeThreshold: 50, // KB validationThreshold: 100 // ms }, { category: 'EN16931_UBL_EXAMPLES', description: 'EN16931 UBL examples', sizeThreshold: 30, // KB validationThreshold: 50 // ms } ] as const; console.log('Testing validation performance across different categories'); const { EInvoice } = await import('../../../ts/index.js'); const performanceResults: { category: string; avgTime: number; maxTime: number; fileCount: number; avgSize: number; }[] = []; for (const test of performanceCategories) { try { const files = await CorpusLoader.getFiles(test.category); const xmlFiles = files.filter(f => f.endsWith('.xml')).slice(0, 5); // Test 5 per category if (xmlFiles.length === 0) { console.log(`\n${test.category}: No XML files found, skipping`); continue; } console.log(`\n${test.category}: Testing ${xmlFiles.length} files`); console.log(` Expected: files <${test.sizeThreshold}KB, validation <${test.validationThreshold}ms`); const validationTimes: number[] = []; const fileSizes: number[] = []; let processedFiles = 0; for (const filePath of xmlFiles) { const fileName = path.basename(filePath); try { const xmlContent = await fs.readFile(filePath, 'utf-8'); const fileSize = xmlContent.length / 1024; // KB fileSizes.push(fileSize); const { result: einvoice } = await PerformanceTracker.track( 'perf-xml-loading', async () => await EInvoice.fromXml(xmlContent) ); const { metric } = await PerformanceTracker.track( 'validation-performance', async () => await einvoice.validate(), { category: test.category, file: fileName, size: fileSize } ); validationTimes.push(metric.duration); processedFiles++; const sizeStatus = fileSize <= test.sizeThreshold ? '✓' : '○'; const timeStatus = metric.duration <= test.validationThreshold ? '✓' : '○'; console.log(` ${sizeStatus}${timeStatus} ${fileName}: ${fileSize.toFixed(1)}KB, ${metric.duration.toFixed(2)}ms`); } catch (error) { console.log(` ✗ ${fileName}: Error - ${error.message}`); } } if (validationTimes.length > 0) { const avgTime = validationTimes.reduce((a, b) => a + b, 0) / validationTimes.length; const maxTime = Math.max(...validationTimes); const avgSize = fileSizes.reduce((a, b) => a + b, 0) / fileSizes.length; performanceResults.push({ category: test.category, avgTime, maxTime, fileCount: processedFiles, avgSize }); console.log(` Summary: avg ${avgTime.toFixed(2)}ms, max ${maxTime.toFixed(2)}ms, avg size ${avgSize.toFixed(1)}KB`); // Performance assertions expect(avgTime).toBeLessThan(test.validationThreshold * 1.5); // Allow 50% tolerance expect(maxTime).toBeLessThan(test.validationThreshold * 3); // Allow 3x for outliers } } catch (error) { console.log(`Error testing ${test.category}: ${error.message}`); } } // Overall performance summary console.log('\n=== VALIDATION PERFORMANCE SUMMARY ==='); performanceResults.forEach(result => { console.log(`${result.category}:`); console.log(` Files: ${result.fileCount}, Avg size: ${result.avgSize.toFixed(1)}KB`); console.log(` Avg time: ${result.avgTime.toFixed(2)}ms, Max time: ${result.maxTime.toFixed(2)}ms`); console.log(` Throughput: ${(result.avgSize / result.avgTime * 1000).toFixed(0)} KB/s`); }); // Performance summary from tracker const perfSummary = await PerformanceTracker.getSummary('validation-performance'); if (perfSummary) { console.log(`\nOverall Validation Performance:`); console.log(` Average: ${perfSummary.average.toFixed(2)}ms`); console.log(` Min: ${perfSummary.min.toFixed(2)}ms`); console.log(` Max: ${perfSummary.max.toFixed(2)}ms`); console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`); } expect(performanceResults.length).toBeGreaterThan(0); }); tap.test('VAL-07: Large Invoice Validation Performance - should handle large invoices efficiently', async () => { const { EInvoice } = await import('../../../ts/index.js'); // Generate large test invoices of different sizes function generateLargeUBLInvoice(lineItems: number): string { let xml = ` LARGE-${Date.now()} 2024-01-01 EUR Large Invoice Supplier Ltd `; for (let i = 1; i <= lineItems; i++) { xml += ` ${i} ${i} ${i * 100} Product ${i} Detailed description for product ${i} with extensive information about features, specifications, and usage instructions that make this line quite long to test performance with larger text content. S 19 VAT 100 `; } xml += '\n'; return xml; } const sizeTests = [ { name: 'Small invoice (10 lines)', lineItems: 10, maxTime: 50 }, { name: 'Medium invoice (100 lines)', lineItems: 100, maxTime: 200 }, { name: 'Large invoice (500 lines)', lineItems: 500, maxTime: 500 }, { name: 'Very large invoice (1000 lines)', lineItems: 1000, maxTime: 1000 } ]; console.log('Testing validation performance with large invoices'); for (const test of sizeTests) { const xml = generateLargeUBLInvoice(test.lineItems); const sizeKB = Math.round(xml.length / 1024); console.log(`\n${test.name} (${sizeKB}KB, ${test.lineItems} lines)`); try { const { metric } = await PerformanceTracker.track( 'large-invoice-validation', async () => { const einvoice = await EInvoice.fromXml(xml); return await einvoice.validate(); }, { lineItems: test.lineItems, sizeKB: sizeKB } ); console.log(` Validation time: ${metric.duration.toFixed(2)}ms`); console.log(` Memory used: ${metric.memory ? (metric.memory.used / 1024 / 1024).toFixed(2) : 'N/A'}MB`); console.log(` Processing rate: ${(test.lineItems / metric.duration * 1000).toFixed(0)} lines/sec`); // Performance assertions based on size expect(metric.duration).toBeLessThan(test.maxTime); // Memory usage should be reasonable if (metric.memory && metric.memory.used > 0) { const memoryMB = metric.memory.used / 1024 / 1024; expect(memoryMB).toBeLessThan(sizeKB); // Should not use more memory than file size } } catch (error) { console.log(` ✗ Error: ${error.message}`); // Large invoices should not crash expect(error.message).toContain('timeout'); // Only acceptable error is timeout } } }); tap.test('VAL-07: Concurrent Validation Performance - should handle concurrent validations', async () => { const { EInvoice } = await import('../../../ts/index.js'); // Get test files for concurrent validation const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG'); const testFiles = ublFiles.filter(f => f.endsWith('.xml')).slice(0, 8); // Test 8 files concurrently if (testFiles.length === 0) { console.log('No test files available for concurrent validation test'); return; } console.log(`Testing concurrent validation of ${testFiles.length} files`); const concurrencyLevels = [1, 2, 4, 8]; for (const concurrency of concurrencyLevels) { if (concurrency > testFiles.length) continue; console.log(`\nConcurrency level: ${concurrency}`); // Prepare validation tasks const tasks = testFiles.slice(0, concurrency).map(async (filePath, index) => { try { const xmlContent = await fs.readFile(filePath, 'utf-8'); const fileName = path.basename(filePath); return await PerformanceTracker.track( `concurrent-validation-${concurrency}`, async () => { const einvoice = await EInvoice.fromXml(xmlContent); return await einvoice.validate(); }, { concurrency, taskIndex: index, file: fileName } ); } catch (error) { return { error: error.message }; } }); // Execute all tasks concurrently const startTime = performance.now(); const results = await Promise.all(tasks); const totalTime = performance.now() - startTime; // Analyze results const successful = results.filter(r => !r.error).length; const validationTimes = results .filter(r => !r.error && r.metric) .map(r => r.metric.duration); if (validationTimes.length > 0) { const avgValidationTime = validationTimes.reduce((a, b) => a + b, 0) / validationTimes.length; const throughput = (successful / totalTime) * 1000; // validations per second console.log(` Total time: ${totalTime.toFixed(2)}ms`); console.log(` Successful validations: ${successful}/${concurrency}`); console.log(` Avg validation time: ${avgValidationTime.toFixed(2)}ms`); console.log(` Throughput: ${throughput.toFixed(1)} validations/sec`); // Performance expectations for concurrent validation expect(successful).toBeGreaterThan(0); expect(avgValidationTime).toBeLessThan(500); // Individual validations should still be fast expect(throughput).toBeGreaterThan(1); // Should handle at least 1 validation per second } else { console.log(` All validations failed`); } } }); tap.test('VAL-07: Memory Usage During Validation - should not consume excessive memory', async () => { const { EInvoice } = await import('../../../ts/index.js'); // Test memory usage with different validation scenarios const memoryTests = [ { name: 'Sequential validations', description: 'Validate multiple invoices sequentially' }, { name: 'Repeated validation', description: 'Validate the same invoice multiple times' } ]; console.log('Testing memory usage during validation'); // Get a test file const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG'); const testFile = ublFiles.find(f => f.endsWith('.xml')); if (!testFile) { console.log('No test file available for memory testing'); return; } const xmlContent = await fs.readFile(testFile, 'utf-8'); const einvoice = await EInvoice.fromXml(xmlContent); console.log(`Using test file: ${path.basename(testFile)} (${Math.round(xmlContent.length/1024)}KB)`); // Test 1: Sequential validations console.log('\nTesting sequential validations:'); const memoryBefore = process.memoryUsage(); for (let i = 0; i < 10; i++) { await PerformanceTracker.track( 'memory-test-sequential', async () => await einvoice.validate() ); } const memoryAfter = process.memoryUsage(); const memoryIncrease = (memoryAfter.heapUsed - memoryBefore.heapUsed) / 1024 / 1024; // MB console.log(` Memory increase: ${memoryIncrease.toFixed(2)}MB`); console.log(` Heap total: ${(memoryAfter.heapTotal / 1024 / 1024).toFixed(2)}MB`); // Memory increase should be reasonable expect(memoryIncrease).toBeLessThan(50); // Should not leak more than 50MB // Test 2: Validation with garbage collection (if available) if (global.gc) { console.log('\nTesting with garbage collection:'); global.gc(); // Force garbage collection const gcMemoryBefore = process.memoryUsage(); for (let i = 0; i < 5; i++) { await einvoice.validate(); if (i % 2 === 0) global.gc(); // GC every other iteration } const gcMemoryAfter = process.memoryUsage(); const gcMemoryIncrease = (gcMemoryAfter.heapUsed - gcMemoryBefore.heapUsed) / 1024 / 1024; console.log(` Memory increase with GC: ${gcMemoryIncrease.toFixed(2)}MB`); // With GC, memory increase should be even smaller expect(gcMemoryIncrease).toBeLessThan(20); } }); tap.test('VAL-07: Validation Performance Benchmarks - should meet benchmark targets', async () => { console.log('Validation Performance Benchmark Summary'); // Collect performance metrics from the session const benchmarkOperations = [ 'validation-performance', 'large-invoice-validation', 'concurrent-validation-1', 'concurrent-validation-4' ]; const benchmarkResults: { operation: string; metrics: any }[] = []; for (const operation of benchmarkOperations) { const summary = await PerformanceTracker.getSummary(operation); if (summary) { benchmarkResults.push({ operation, metrics: summary }); console.log(`\n${operation}:`); console.log(` Average: ${summary.average.toFixed(2)}ms`); console.log(` P95: ${summary.p95.toFixed(2)}ms`); console.log(` Min/Max: ${summary.min.toFixed(2)}ms / ${summary.max.toFixed(2)}ms`); } } // Overall benchmark results if (benchmarkResults.length > 0) { const overallAverage = benchmarkResults.reduce((sum, result) => sum + result.metrics.average, 0) / benchmarkResults.length; console.log(`\nOverall Validation Performance Benchmark:`); console.log(` Average across all operations: ${overallAverage.toFixed(2)}ms`); // Benchmark targets (from test/readme.md) expect(overallAverage).toBeLessThan(200); // Target: <200ms average for validation // Check that no operation is extremely slow benchmarkResults.forEach(result => { expect(result.metrics.p95).toBeLessThan(1000); // P95 should be under 1 second }); console.log(`✓ All validation performance benchmarks met`); } }); tap.start();