/** * @file test.perf-02.validation-performance.ts * @description Performance tests for invoice validation operations */ import { tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { CorpusLoader } from '../../suite/corpus.loader.js'; import { PerformanceTracker } from '../../suite/performance.tracker.js'; const corpusLoader = new CorpusLoader(); const performanceTracker = new PerformanceTracker('PERF-02: Validation Performance'); tap.test('PERF-02: Validation Performance - should meet performance targets for validation operations', async (t) => { // Test 1: Syntax validation performance const syntaxValidation = await performanceTracker.measureAsync( 'syntax-validation-performance', async () => { const einvoice = new EInvoice(); const results = []; // Create test invoices of varying complexity const testInvoices = [ { name: 'Minimal Invoice', invoice: { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-VAL-001', issueDate: '2024-02-01', seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: [{ description: 'Item', quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 }], totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 } } } }, { name: 'Standard Invoice (10 items)', invoice: { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-VAL-002', issueDate: '2024-02-01', dueDate: '2024-03-01', currency: 'EUR', seller: { name: 'Complex Seller GmbH', address: 'Hauptstraße 123', city: 'Berlin', postalCode: '10115', country: 'DE', taxId: 'DE123456789', email: 'info@seller.de', phone: '+49 30 12345678' }, buyer: { name: 'Complex Buyer Ltd', address: 'Business Park 456', city: 'Munich', postalCode: '80331', country: 'DE', taxId: 'DE987654321', email: 'ap@buyer.de' }, items: Array.from({ length: 10 }, (_, i) => ({ description: `Product Line ${i + 1}`, quantity: i + 1, unitPrice: 50.00 + i * 10, vatRate: 19, lineTotal: (i + 1) * (50.00 + i * 10), itemId: `ITEM-${i + 1}` })), totals: { netAmount: 1650.00, vatAmount: 313.50, grossAmount: 1963.50 } } } }, { name: 'Complex Invoice (50 items)', invoice: { format: 'cii' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-VAL-003', issueDate: '2024-02-01', seller: { name: 'Mega Seller', address: 'Complex Street', country: 'FR', taxId: 'FR12345678901' }, buyer: { name: 'Mega Buyer', address: 'Complex Avenue', country: 'FR', taxId: 'FR98765432109' }, items: Array.from({ length: 50 }, (_, i) => ({ description: `Complex Item ${i + 1} with detailed specifications`, quantity: Math.floor(Math.random() * 10) + 1, unitPrice: Math.random() * 500, vatRate: [5.5, 10, 20][i % 3], lineTotal: 0 // Will be calculated })), totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 } } } } ]; // Calculate totals for complex invoice testInvoices[2].invoice.data.items.forEach(item => { item.lineTotal = item.quantity * item.unitPrice; testInvoices[2].invoice.data.totals.netAmount += item.lineTotal; testInvoices[2].invoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100); }); testInvoices[2].invoice.data.totals.grossAmount = testInvoices[2].invoice.data.totals.netAmount + testInvoices[2].invoice.data.totals.vatAmount; // Run validation benchmarks for (const test of testInvoices) { const times = []; const iterations = 50; for (let i = 0; i < iterations; i++) { const startTime = process.hrtime.bigint(); const validationResult = await einvoice.validateInvoice(test.invoice, { level: 'syntax' }); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; times.push(duration); } times.sort((a, b) => a - b); results.push({ name: test.name, itemCount: test.invoice.data.items.length, min: times[0], max: times[times.length - 1], avg: times.reduce((a, b) => a + b, 0) / times.length, median: times[Math.floor(times.length / 2)], p95: times[Math.floor(times.length * 0.95)] }); } return results; } ); // Test 2: Business rule validation performance const businessRuleValidation = await performanceTracker.measureAsync( 'business-rule-validation', async () => { const einvoice = new EInvoice(); const results = { ruleCategories: [], totalRulesChecked: 0, avgTimePerRule: 0 }; // Create test invoice with various business rule scenarios const testInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'BR-TEST-001', issueDate: '2024-02-01', dueDate: '2024-03-01', currency: 'EUR', seller: { name: 'Business Rule Test Seller', address: 'Test Street 1', city: 'Berlin', country: 'DE', taxId: 'DE123456789', registrationNumber: 'HRB12345' }, buyer: { name: 'Business Rule Test Buyer', address: 'Test Avenue 2', city: 'Paris', country: 'FR', taxId: 'FR98765432109' }, items: [ { description: 'Standard Product', quantity: 10, unitPrice: 100.00, vatRate: 19, lineTotal: 1000.00 }, { description: 'Reduced VAT Product', quantity: 5, unitPrice: 50.00, vatRate: 7, lineTotal: 250.00 }, { description: 'Zero VAT Export', quantity: 2, unitPrice: 200.00, vatRate: 0, lineTotal: 400.00 } ], totals: { netAmount: 1650.00, vatAmount: 207.50, grossAmount: 1857.50 }, paymentTerms: 'Net 30 days', paymentMeans: { iban: 'DE89370400440532013000', bic: 'COBADEFFXXX' } } }; // Test different validation rule sets const ruleSets = [ { name: 'BR-CO (Calculations)', rules: ['BR-CO-*'] }, { name: 'BR-CL (Codelists)', rules: ['BR-CL-*'] }, { name: 'BR-S (VAT)', rules: ['BR-S-*'] }, { name: 'BR-DE (Germany)', rules: ['BR-DE-*'] }, { name: 'All Rules', rules: ['*'] } ]; for (const ruleSet of ruleSets) { const times = []; const iterations = 20; for (let i = 0; i < iterations; i++) { const startTime = process.hrtime.bigint(); const validationResult = await einvoice.validateInvoice(testInvoice, { level: 'business', rules: ruleSet.rules }); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; times.push(duration); if (i === 0) { results.totalRulesChecked += validationResult.rulesChecked || 0; } } const avgTime = times.reduce((a, b) => a + b, 0) / times.length; results.ruleCategories.push({ name: ruleSet.name, avgTime: avgTime.toFixed(3), rulesPerMs: ((validationResult.rulesChecked || 1) / avgTime).toFixed(2) }); } return results; } ); // Test 3: Corpus validation performance const corpusValidation = await performanceTracker.measureAsync( 'corpus-validation-performance', async () => { const files = await corpusLoader.getFilesByPattern('**/*.xml'); const einvoice = new EInvoice(); const results = { totalFiles: 0, validationTimes: { syntax: [], semantic: [], business: [] }, formatPerformance: new Map(), errors: 0 }; // Sample corpus files const sampleFiles = files.slice(0, 50); for (const file of sampleFiles) { try { const content = await plugins.fs.readFile(file, 'utf-8'); // Detect format const format = await einvoice.detectFormat(content); if (!format || format === 'unknown') continue; // Parse invoice const invoice = await einvoice.parseInvoice(content, format); results.totalFiles++; // Initialize format stats if (!results.formatPerformance.has(format)) { results.formatPerformance.set(format, { count: 0, totalTime: 0 }); } // Measure validation at different levels const levels = ['syntax', 'semantic', 'business'] as const; for (const level of levels) { const startTime = process.hrtime.bigint(); await einvoice.validateInvoice(invoice, { level }); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; results.validationTimes[level].push(duration); if (level === 'business') { const formatStats = results.formatPerformance.get(format)!; formatStats.count++; formatStats.totalTime += duration; } } } catch (error) { results.errors++; } } // Calculate statistics const stats = {}; for (const level of Object.keys(results.validationTimes)) { const times = results.validationTimes[level]; if (times.length > 0) { times.sort((a, b) => a - b); stats[level] = { min: times[0], max: times[times.length - 1], avg: times.reduce((a, b) => a + b, 0) / times.length, median: times[Math.floor(times.length / 2)], p95: times[Math.floor(times.length * 0.95)] }; } } return { ...results, stats, formatPerformance: Array.from(results.formatPerformance.entries()).map(([format, data]) => ({ format, avgTime: data.count > 0 ? (data.totalTime / data.count).toFixed(3) : 'N/A' })) }; } ); // Test 4: Incremental validation performance const incrementalValidation = await performanceTracker.measureAsync( 'incremental-validation', async () => { const einvoice = new EInvoice(); const results = []; // Base invoice const baseInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'INCR-001', issueDate: '2024-02-01', seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: [], totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 } } }; // Measure validation time as we add items const itemCounts = [1, 5, 10, 20, 50, 100]; for (const count of itemCounts) { // Add items incrementally while (baseInvoice.data.items.length < count) { const item = { description: `Item ${baseInvoice.data.items.length + 1}`, quantity: 1, unitPrice: 100, vatRate: 19, lineTotal: 100 }; baseInvoice.data.items.push(item); baseInvoice.data.totals.netAmount += 100; baseInvoice.data.totals.vatAmount += 19; baseInvoice.data.totals.grossAmount += 119; } // Measure validation time const times = []; for (let i = 0; i < 30; i++) { const startTime = process.hrtime.bigint(); await einvoice.validateInvoice(baseInvoice); const endTime = process.hrtime.bigint(); times.push(Number(endTime - startTime) / 1_000_000); } const avgTime = times.reduce((a, b) => a + b, 0) / times.length; results.push({ itemCount: count, avgValidationTime: avgTime.toFixed(3), timePerItem: (avgTime / count).toFixed(4) }); } return results; } ); // Test 5: Parallel validation performance const parallelValidation = await performanceTracker.measureAsync( 'parallel-validation-performance', async () => { const einvoice = new EInvoice(); const results = []; // Create test invoice const testInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PARALLEL-001', issueDate: '2024-02-01', seller: { name: 'Parallel Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'Parallel Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: Array.from({ length: 20 }, (_, i) => ({ description: `Item ${i + 1}`, quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 })), totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 } } }; // Test different concurrency levels const concurrencyLevels = [1, 2, 5, 10, 20]; for (const concurrency of concurrencyLevels) { const startTime = Date.now(); // Create parallel validation tasks const tasks = Array(concurrency).fill(null).map(() => einvoice.validateInvoice(testInvoice) ); const results = await Promise.all(tasks); const endTime = Date.now(); const duration = endTime - startTime; const throughput = (concurrency / (duration / 1000)).toFixed(2); results.push({ concurrency, duration, throughput: `${throughput} validations/sec`, allValid: results.every(r => r.isValid) }); } return results; } ); // Summary t.comment('\n=== PERF-02: Validation Performance Test Summary ==='); t.comment('\nSyntax Validation Performance:'); syntaxValidation.result.forEach(result => { t.comment(` ${result.name} (${result.itemCount} items):`); t.comment(` - Min: ${result.min.toFixed(3)}ms, Max: ${result.max.toFixed(3)}ms`); t.comment(` - Avg: ${result.avg.toFixed(3)}ms, Median: ${result.median.toFixed(3)}ms`); t.comment(` - P95: ${result.p95.toFixed(3)}ms`); }); t.comment('\nBusiness Rule Validation:'); businessRuleValidation.result.ruleCategories.forEach(category => { t.comment(` ${category.name}: ${category.avgTime}ms avg (${category.rulesPerMs} rules/ms)`); }); t.comment(`\nCorpus Validation (${corpusValidation.result.totalFiles} files):`); Object.entries(corpusValidation.result.stats).forEach(([level, stats]: [string, any]) => { t.comment(` ${level} validation:`); t.comment(` - Min: ${stats.min.toFixed(3)}ms, Max: ${stats.max.toFixed(3)}ms`); t.comment(` - Avg: ${stats.avg.toFixed(3)}ms, Median: ${stats.median.toFixed(3)}ms`); }); t.comment(' By format:'); corpusValidation.result.formatPerformance.forEach(perf => { t.comment(` - ${perf.format}: ${perf.avgTime}ms avg`); }); t.comment('\nIncremental Validation Scaling:'); incrementalValidation.result.forEach(result => { t.comment(` ${result.itemCount} items: ${result.avgValidationTime}ms (${result.timePerItem}ms/item)`); }); t.comment('\nParallel Validation:'); parallelValidation.result.forEach(result => { t.comment(` ${result.concurrency} concurrent: ${result.duration}ms, ${result.throughput}`); }); // Performance targets check t.comment('\n=== Performance Targets Check ==='); const syntaxAvg = syntaxValidation.result[1].avg; // Standard invoice const businessAvg = businessRuleValidation.result.ruleCategories.find(r => r.name === 'All Rules')?.avgTime || 0; t.comment(`Syntax validation: ${syntaxAvg.toFixed(3)}ms ${syntaxAvg < 50 ? '✅' : '⚠️'} (target: <50ms)`); t.comment(`Business validation: ${businessAvg}ms ${parseFloat(businessAvg) < 200 ? '✅' : '⚠️'} (target: <200ms)`); // Overall performance summary t.comment('\n=== Overall Performance Summary ==='); performanceTracker.logSummary(); t.end(); }); tap.start();