428 lines
15 KiB
TypeScript
428 lines
15 KiB
TypeScript
|
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();
|