einvoice/test/suite/einvoice_validation/test.val-07.performance-validation.ts

428 lines
15 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>LARGE-${Date.now()}</cbc:ID>
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Large Invoice Supplier Ltd</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>`;
for (let i = 1; i <= lineItems; i++) {
xml += `
<cac:InvoiceLine>
<cbc:ID>${i}</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">${i}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">${i * 100}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Product ${i}</cbc:Name>
<cbc:Description>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.</cbc:Description>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>`;
}
xml += '\n</Invoice>';
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();