einvoice/test/suite/einvoice_validation/test.val-12.validation-performance.ts
2025-05-25 19:45:37 +00:00

504 lines
19 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../../ts/plugins.ts';
import { EInvoice } from '../../../ts/classes.xinvoice.ts';
import { CorpusLoader } from '../../helpers/corpus.loader.ts';
import { PerformanceTracker } from '../../helpers/performance.tracker.ts';
const testTimeout = 600000; // 10 minutes timeout for performance testing
// VAL-12: Validation Performance
// Tests validation performance characteristics including speed, memory usage,
// and scalability under various load conditions
tap.test('VAL-12: Validation Performance - Single Invoice Validation Speed', async (tools) => {
const startTime = Date.now();
// Test validation speed for different invoice sizes
const performanceTests = [
{
name: 'Minimal UBL Invoice',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>MIN-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`,
expectedMaxTime: 20 // 20ms max for minimal invoice
},
{
name: 'Standard UBL Invoice',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>STD-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty>
<Party>
<PartyName><Name>Test Supplier</Name></PartyName>
<PostalAddress>
<StreetName>Test Street 1</StreetName>
<CityName>Test City</CityName>
<PostalZone>12345</PostalZone>
<Country><IdentificationCode>DE</IdentificationCode></Country>
</PostalAddress>
</Party>
</AccountingSupplierParty>
<AccountingCustomerParty>
<Party>
<PartyName><Name>Test Customer</Name></PartyName>
<PostalAddress>
<StreetName>Customer Street 1</StreetName>
<CityName>Customer City</CityName>
<PostalZone>54321</PostalZone>
<Country><IdentificationCode>DE</IdentificationCode></Country>
</PostalAddress>
</Party>
</AccountingCustomerParty>
<InvoiceLine>
<ID>1</ID>
<InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">100.00</LineExtensionAmount>
<Item><Name>Test Item</Name></Item>
<Price><PriceAmount currencyID="EUR">100.00</PriceAmount></Price>
</InvoiceLine>
<TaxTotal>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
</TaxTotal>
<LegalMonetaryTotal>
<LineExtensionAmount currencyID="EUR">100.00</LineExtensionAmount>
<TaxExclusiveAmount currencyID="EUR">100.00</TaxExclusiveAmount>
<TaxInclusiveAmount currencyID="EUR">119.00</TaxInclusiveAmount>
<PayableAmount currencyID="EUR">119.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`,
expectedMaxTime: 50 // 50ms max for standard invoice
}
];
for (const test of performanceTests) {
const times = [];
const iterations = 10;
for (let i = 0; i < iterations; i++) {
const iterationStart = Date.now();
try {
const invoice = new EInvoice();
await invoice.fromXmlString(test.xml);
const validationResult = await invoice.validate();
const iterationTime = Date.now() - iterationStart;
times.push(iterationTime);
// Ensure validation actually worked
expect(validationResult).toBeTruthy();
} catch (error) {
tools.log(`Validation failed for ${test.name}: ${error.message}`);
throw error;
}
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const p95Time = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)];
tools.log(`${test.name} validation performance:`);
tools.log(` Average: ${avgTime.toFixed(1)}ms`);
tools.log(` Min: ${minTime}ms, Max: ${maxTime}ms`);
tools.log(` P95: ${p95Time}ms`);
// Performance expectations
expect(avgTime).toBeLessThan(test.expectedMaxTime);
expect(p95Time).toBeLessThan(test.expectedMaxTime * 2);
PerformanceTracker.recordMetric(`validation-performance-${test.name.toLowerCase().replace(/\s+/g, '-')}`, avgTime);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('validation-performance-single', duration);
});
tap.test('VAL-12: Validation Performance - Concurrent Validation', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
// Test concurrent validation performance
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CONCURRENT-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
const concurrencyLevels = [1, 5, 10, 20];
for (const concurrency of concurrencyLevels) {
const concurrentStart = Date.now();
const promises = [];
for (let i = 0; i < concurrency; i++) {
promises.push((async () => {
const invoice = new EInvoice();
await invoice.fromXmlString(testXml);
return await invoice.validate();
})());
}
try {
const results = await Promise.all(promises);
const concurrentDuration = Date.now() - concurrentStart;
// Verify all validations succeeded
for (const result of results) {
expect(result).toBeTruthy();
}
const avgTimePerValidation = concurrentDuration / concurrency;
tools.log(`Concurrent validation (${concurrency} parallel):`);
tools.log(` Total time: ${concurrentDuration}ms`);
tools.log(` Avg per validation: ${avgTimePerValidation.toFixed(1)}ms`);
tools.log(` Throughput: ${(1000 / avgTimePerValidation).toFixed(1)} validations/sec`);
// Performance expectations
expect(avgTimePerValidation).toBeLessThan(100); // 100ms max per validation
expect(concurrentDuration).toBeLessThan(5000); // 5 seconds max total
PerformanceTracker.recordMetric(`validation-performance-concurrent-${concurrency}`, avgTimePerValidation);
} catch (error) {
tools.log(`Concurrent validation failed at level ${concurrency}: ${error.message}`);
throw error;
}
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('validation-performance-concurrent', totalDuration);
});
tap.test('VAL-12: Validation Performance - Large Invoice Handling', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
// Test performance with large invoices (many line items)
const lineCounts = [1, 10, 50, 100];
for (const lineCount of lineCounts) {
const largeInvoiceStart = Date.now();
// Generate invoice with multiple lines
let invoiceLines = '';
for (let i = 1; i <= lineCount; i++) {
invoiceLines += `
<InvoiceLine>
<ID>${i}</ID>
<InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">10.00</LineExtensionAmount>
<Item><Name>Item ${i}</Name></Item>
<Price><PriceAmount currencyID="EUR">10.00</PriceAmount></Price>
</InvoiceLine>`;
}
const totalAmount = lineCount * 10;
const taxAmount = totalAmount * 0.19;
const totalWithTax = totalAmount + taxAmount;
const largeInvoiceXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>LARGE-${lineCount}-LINES</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
${invoiceLines}
<TaxTotal>
<TaxAmount currencyID="EUR">${taxAmount.toFixed(2)}</TaxAmount>
</TaxTotal>
<LegalMonetaryTotal>
<LineExtensionAmount currencyID="EUR">${totalAmount.toFixed(2)}</LineExtensionAmount>
<TaxExclusiveAmount currencyID="EUR">${totalAmount.toFixed(2)}</TaxExclusiveAmount>
<TaxInclusiveAmount currencyID="EUR">${totalWithTax.toFixed(2)}</TaxInclusiveAmount>
<PayableAmount currencyID="EUR">${totalWithTax.toFixed(2)}</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
try {
const invoice = new EInvoice();
await invoice.fromXmlString(largeInvoiceXml);
const validationResult = await invoice.validate();
const largeInvoiceDuration = Date.now() - largeInvoiceStart;
expect(validationResult).toBeTruthy();
const timePerLine = largeInvoiceDuration / lineCount;
tools.log(`Large invoice validation (${lineCount} lines):`);
tools.log(` Total time: ${largeInvoiceDuration}ms`);
tools.log(` Time per line: ${timePerLine.toFixed(2)}ms`);
// Performance expectations scale with line count
const maxExpectedTime = Math.max(100, lineCount * 2); // 2ms per line minimum
expect(largeInvoiceDuration).toBeLessThan(maxExpectedTime);
PerformanceTracker.recordMetric(`validation-performance-large-${lineCount}-lines`, largeInvoiceDuration);
} catch (error) {
tools.log(`Large invoice validation failed (${lineCount} lines): ${error.message}`);
throw error;
}
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('validation-performance-large', totalDuration);
});
tap.test('VAL-12: Validation Performance - Memory Usage Monitoring', async (tools) => {
const startTime = Date.now();
// Monitor memory usage during validation
const memoryBefore = process.memoryUsage();
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>MEMORY-TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
// Perform multiple validations and monitor memory
const iterations = 100;
for (let i = 0; i < iterations; i++) {
const invoice = new EInvoice();
await invoice.fromXmlString(testXml);
await invoice.validate();
// Force garbage collection periodically
if (i % 20 === 0 && global.gc) {
global.gc();
}
}
const memoryAfter = process.memoryUsage();
const heapGrowth = memoryAfter.heapUsed - memoryBefore.heapUsed;
const rssGrowth = memoryAfter.rss - memoryBefore.rss;
tools.log(`Memory usage for ${iterations} validations:`);
tools.log(` Heap growth: ${(heapGrowth / 1024 / 1024).toFixed(2)} MB`);
tools.log(` RSS growth: ${(rssGrowth / 1024 / 1024).toFixed(2)} MB`);
tools.log(` Heap per validation: ${(heapGrowth / iterations / 1024).toFixed(2)} KB`);
// Memory expectations
const heapPerValidation = heapGrowth / iterations;
expect(heapPerValidation).toBeLessThan(50 * 1024); // Less than 50KB per validation
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('validation-performance-memory', duration);
});
tap.test('VAL-12: Validation Performance - Corpus Performance Analysis', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
const performanceResults = [];
let totalValidations = 0;
let totalTime = 0;
try {
// Test performance across different corpus categories
const categories = ['UBL_XML_RECHNUNG', 'CII_XML_RECHNUNG'];
for (const category of categories) {
const categoryStart = Date.now();
let categoryValidations = 0;
try {
const files = await CorpusLoader.getFiles(category);
for (const filePath of files.slice(0, 5)) { // Test first 5 files per category
const fileStart = Date.now();
try {
const invoice = new EInvoice();
await invoice.fromFile(filePath);
await invoice.validate();
const fileTime = Date.now() - fileStart;
categoryValidations++;
totalValidations++;
totalTime += fileTime;
// Track file size impact on performance
const stats = await plugins.fs.stat(filePath);
const fileSizeKB = stats.size / 1024;
performanceResults.push({
category,
file: plugins.path.basename(filePath),
time: fileTime,
sizeKB: fileSizeKB,
timePerKB: fileTime / fileSizeKB
});
} catch (error) {
tools.log(`Failed to process ${plugins.path.basename(filePath)}: ${error.message}`);
}
}
const categoryTime = Date.now() - categoryStart;
const avgCategoryTime = categoryValidations > 0 ? categoryTime / categoryValidations : 0;
tools.log(`${category} performance:`);
tools.log(` Files processed: ${categoryValidations}`);
tools.log(` Total time: ${categoryTime}ms`);
tools.log(` Avg per file: ${avgCategoryTime.toFixed(1)}ms`);
} catch (error) {
tools.log(`Failed to process category ${category}: ${error.message}`);
}
}
// Analyze performance correlations
if (performanceResults.length > 0) {
const avgTime = totalTime / totalValidations;
const avgSize = performanceResults.reduce((sum, r) => sum + r.sizeKB, 0) / performanceResults.length;
const avgTimePerKB = performanceResults.reduce((sum, r) => sum + r.timePerKB, 0) / performanceResults.length;
tools.log(`Overall corpus performance analysis:`);
tools.log(` Total validations: ${totalValidations}`);
tools.log(` Average time: ${avgTime.toFixed(1)}ms`);
tools.log(` Average file size: ${avgSize.toFixed(1)}KB`);
tools.log(` Average time per KB: ${avgTimePerKB.toFixed(2)}ms/KB`);
// Performance expectations
expect(avgTime).toBeLessThan(200); // 200ms max average
expect(avgTimePerKB).toBeLessThan(10); // 10ms per KB max
// Find slowest files
const slowestFiles = performanceResults
.sort((a, b) => b.time - a.time)
.slice(0, 3);
tools.log(`Slowest files:`);
for (const file of slowestFiles) {
tools.log(` ${file.file}: ${file.time}ms (${file.sizeKB.toFixed(1)}KB)`);
}
}
} catch (error) {
tools.log(`Corpus performance analysis failed: ${error.message}`);
throw error;
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('validation-performance-corpus', totalDuration);
expect(totalDuration).toBeLessThan(300000); // 5 minutes max
tools.log(`Corpus performance analysis completed in ${totalDuration}ms`);
});
tap.test('VAL-12: Validation Performance - Stress Testing', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
// Stress test with rapid successive validations
const stressTestXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>STRESS-TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
const stressIterations = 200;
const stressTimes = [];
try {
for (let i = 0; i < stressIterations; i++) {
const iterationStart = Date.now();
const invoice = new EInvoice();
await invoice.fromXmlString(stressTestXml);
await invoice.validate();
const iterationTime = Date.now() - iterationStart;
stressTimes.push(iterationTime);
// Log progress every 50 iterations
if ((i + 1) % 50 === 0) {
const currentAvg = stressTimes.slice(-50).reduce((a, b) => a + b, 0) / 50;
tools.log(`Stress test progress: ${i + 1}/${stressIterations}, avg last 50: ${currentAvg.toFixed(1)}ms`);
}
}
// Analyze stress test results
const avgStressTime = stressTimes.reduce((a, b) => a + b, 0) / stressTimes.length;
const minStressTime = Math.min(...stressTimes);
const maxStressTime = Math.max(...stressTimes);
const stdDev = Math.sqrt(stressTimes.reduce((sum, time) => sum + Math.pow(time - avgStressTime, 2), 0) / stressTimes.length);
// Check for performance degradation over time
const firstHalf = stressTimes.slice(0, stressIterations / 2);
const secondHalf = stressTimes.slice(stressIterations / 2);
const firstHalfAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
const secondHalfAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
const degradation = ((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100;
tools.log(`Stress test results (${stressIterations} iterations):`);
tools.log(` Average time: ${avgStressTime.toFixed(1)}ms`);
tools.log(` Min: ${minStressTime}ms, Max: ${maxStressTime}ms`);
tools.log(` Standard deviation: ${stdDev.toFixed(1)}ms`);
tools.log(` Performance degradation: ${degradation.toFixed(1)}%`);
// Performance expectations
expect(avgStressTime).toBeLessThan(50); // 50ms average max
expect(degradation).toBeLessThan(20); // Less than 20% degradation
expect(stdDev).toBeLessThan(avgStressTime); // Standard deviation should be reasonable
} catch (error) {
tools.log(`Stress test failed: ${error.message}`);
throw error;
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('validation-performance-stress', totalDuration);
tools.log(`Stress test completed in ${totalDuration}ms`);
});
tap.test('VAL-12: Performance Summary', async (tools) => {
const operations = [
'validation-performance-single',
'validation-performance-concurrent',
'validation-performance-large',
'validation-performance-memory',
'validation-performance-corpus',
'validation-performance-stress'
];
tools.log(`\n=== Validation Performance Summary ===`);
for (const operation of operations) {
const summary = await PerformanceTracker.getSummary(operation);
if (summary) {
tools.log(`${operation}:`);
tools.log(` avg=${summary.average}ms, min=${summary.min}ms, max=${summary.max}ms, p95=${summary.p95}ms`);
}
}
tools.log(`\nValidation performance testing completed successfully.`);
});