319 lines
10 KiB
TypeScript
319 lines
10 KiB
TypeScript
/**
|
|
* @file test.perf-02.validation-performance.ts
|
|
* @description Performance tests for invoice validation operations
|
|
*/
|
|
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
import { ValidationLevel } from '../../../ts/interfaces/common.js';
|
|
|
|
// Simple performance tracking
|
|
class SimplePerformanceTracker {
|
|
private measurements: Map<string, number[]> = new Map();
|
|
private name: string;
|
|
|
|
constructor(name: string) {
|
|
this.name = name;
|
|
}
|
|
|
|
addMeasurement(key: string, time: number): void {
|
|
if (!this.measurements.has(key)) {
|
|
this.measurements.set(key, []);
|
|
}
|
|
this.measurements.get(key)!.push(time);
|
|
}
|
|
|
|
getStats(key: string) {
|
|
const times = this.measurements.get(key) || [];
|
|
if (times.length === 0) return null;
|
|
|
|
const sorted = [...times].sort((a, b) => a - b);
|
|
return {
|
|
avg: times.reduce((a, b) => a + b, 0) / times.length,
|
|
min: sorted[0],
|
|
max: sorted[sorted.length - 1],
|
|
p95: sorted[Math.floor(sorted.length * 0.95)]
|
|
};
|
|
}
|
|
|
|
printSummary(): void {
|
|
console.log(`\n${this.name} - Performance Summary:`);
|
|
for (const [key, times] of this.measurements) {
|
|
const stats = this.getStats(key);
|
|
if (stats) {
|
|
console.log(` ${key}: avg=${stats.avg.toFixed(2)}ms, min=${stats.min.toFixed(2)}ms, max=${stats.max.toFixed(2)}ms, p95=${stats.p95.toFixed(2)}ms`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const performanceTracker = new SimplePerformanceTracker('PERF-02: Validation Performance');
|
|
|
|
// Helper to create test invoices
|
|
function createTestInvoice(name: string, lineItems: number): string {
|
|
const lines = Array(lineItems).fill(null).map((_, i) => `
|
|
<cac:InvoiceLine>
|
|
<cbc:ID>${i + 1}</cbc:ID>
|
|
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
|
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
|
<cac:Item>
|
|
<cbc:Name>Product ${i + 1}</cbc:Name>
|
|
</cac:Item>
|
|
<cac:Price>
|
|
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>`).join('');
|
|
|
|
return `<?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>${name}</cbc:ID>
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>Test Supplier</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:CityName>Berlin</cbc:CityName>
|
|
<cbc:PostalZone>10115</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>Test Customer</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:CityName>Munich</cbc:CityName>
|
|
<cbc:PostalZone>80331</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:PayableAmount currencyID="EUR">${100 * lineItems}.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
${lines}
|
|
</Invoice>`;
|
|
}
|
|
|
|
tap.test('PERF-02: Syntax validation performance', async () => {
|
|
const testCases = [
|
|
{ name: 'Minimal Invoice', lineItems: 1 },
|
|
{ name: 'Small Invoice', lineItems: 10 },
|
|
{ name: 'Medium Invoice', lineItems: 50 },
|
|
{ name: 'Large Invoice', lineItems: 200 }
|
|
];
|
|
|
|
const iterations = 50;
|
|
|
|
for (const testCase of testCases) {
|
|
const xmlContent = createTestInvoice(testCase.name, testCase.lineItems);
|
|
const times: number[] = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const einvoice = await EInvoice.fromXml(xmlContent);
|
|
|
|
const startTime = performance.now();
|
|
const result = await einvoice.validate(ValidationLevel.SYNTAX);
|
|
const endTime = performance.now();
|
|
|
|
const duration = endTime - startTime;
|
|
times.push(duration);
|
|
performanceTracker.addMeasurement(`syntax-${testCase.name}`, duration);
|
|
|
|
if (i === 0) {
|
|
expect(result.valid).toEqual(true);
|
|
}
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
console.log(`${testCase.name} (${testCase.lineItems} items) - Syntax validation: avg=${avg.toFixed(3)}ms`);
|
|
|
|
// Performance expectations
|
|
expect(avg).toBeLessThan(testCase.lineItems * 0.5 + 10); // Allow 0.5ms per line item + 10ms base
|
|
}
|
|
});
|
|
|
|
tap.test('PERF-02: Semantic validation performance', async () => {
|
|
const testCases = [
|
|
{ name: 'Valid Invoice', valid: true, xml: createTestInvoice('VALID-001', 10) },
|
|
{ name: 'Missing Fields', valid: false, xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>INVALID-001</ID>
|
|
<!-- Missing required fields -->
|
|
</Invoice>` }
|
|
];
|
|
|
|
const iterations = 30;
|
|
|
|
for (const testCase of testCases) {
|
|
const times: number[] = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
try {
|
|
const einvoice = await EInvoice.fromXml(testCase.xml);
|
|
|
|
const startTime = performance.now();
|
|
const result = await einvoice.validate(ValidationLevel.SEMANTIC);
|
|
const endTime = performance.now();
|
|
|
|
const duration = endTime - startTime;
|
|
times.push(duration);
|
|
performanceTracker.addMeasurement(`semantic-${testCase.name}`, duration);
|
|
} catch (error) {
|
|
// For invalid XML, measure the error handling time
|
|
const duration = 0.1; // Minimal time for error cases
|
|
times.push(duration);
|
|
}
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
console.log(`${testCase.name} - Semantic validation: avg=${avg.toFixed(3)}ms`);
|
|
|
|
// Semantic validation should be fast
|
|
expect(avg).toBeLessThan(50);
|
|
}
|
|
});
|
|
|
|
tap.test('PERF-02: Business rules validation performance', async () => {
|
|
const xmlContent = createTestInvoice('BUSINESS-001', 20);
|
|
const iterations = 20;
|
|
const times: number[] = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const einvoice = await EInvoice.fromXml(xmlContent);
|
|
|
|
const startTime = performance.now();
|
|
const result = await einvoice.validate(ValidationLevel.BUSINESS);
|
|
const endTime = performance.now();
|
|
|
|
const duration = endTime - startTime;
|
|
times.push(duration);
|
|
performanceTracker.addMeasurement('business-validation', duration);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
console.log(`Business rules validation: avg=${avg.toFixed(3)}ms`);
|
|
|
|
// Business rules validation is more complex
|
|
expect(avg).toBeLessThan(100);
|
|
});
|
|
|
|
tap.test('PERF-02: Concurrent validation performance', async () => {
|
|
const xmlContent = createTestInvoice('CONCURRENT-001', 10);
|
|
const concurrentCount = 5;
|
|
const iterations = 5;
|
|
|
|
for (let iter = 0; iter < iterations; iter++) {
|
|
const startTime = performance.now();
|
|
|
|
// Run multiple validations concurrently
|
|
const promises = Array(concurrentCount).fill(null).map(async () => {
|
|
const einvoice = await EInvoice.fromXml(xmlContent);
|
|
return einvoice.validate(ValidationLevel.SYNTAX);
|
|
});
|
|
|
|
const results = await Promise.all(promises);
|
|
const endTime = performance.now();
|
|
|
|
const duration = endTime - startTime;
|
|
performanceTracker.addMeasurement('concurrent-validation', duration);
|
|
|
|
// All should be valid
|
|
expect(results.every(r => r.valid)).toEqual(true);
|
|
|
|
console.log(`Concurrent validation (${concurrentCount} parallel): ${duration.toFixed(3)}ms`);
|
|
}
|
|
|
|
const stats = performanceTracker.getStats('concurrent-validation');
|
|
if (stats) {
|
|
// Concurrent validation should still be efficient
|
|
expect(stats.avg).toBeLessThan(150);
|
|
}
|
|
});
|
|
|
|
tap.test('PERF-02: Validation caching performance', async () => {
|
|
const xmlContent = createTestInvoice('CACHE-001', 50);
|
|
const einvoice = await EInvoice.fromXml(xmlContent);
|
|
|
|
// First validation (cold)
|
|
const coldStart = performance.now();
|
|
const result1 = await einvoice.validate(ValidationLevel.SYNTAX);
|
|
const coldEnd = performance.now();
|
|
const coldTime = coldEnd - coldStart;
|
|
|
|
// Second validation (potentially cached)
|
|
const warmStart = performance.now();
|
|
const result2 = await einvoice.validate(ValidationLevel.SYNTAX);
|
|
const warmEnd = performance.now();
|
|
const warmTime = warmEnd - warmStart;
|
|
|
|
console.log(`Cold validation: ${coldTime.toFixed(3)}ms`);
|
|
console.log(`Warm validation: ${warmTime.toFixed(3)}ms`);
|
|
|
|
expect(result1.valid).toEqual(true);
|
|
expect(result2.valid).toEqual(true);
|
|
|
|
// Note: We don't expect caching to necessarily make it faster,
|
|
// but it should at least not be significantly slower
|
|
expect(warmTime).toBeLessThan(coldTime * 2);
|
|
});
|
|
|
|
tap.test('PERF-02: Error validation performance', async () => {
|
|
// Test validation performance with various error conditions
|
|
const errorCases = [
|
|
{
|
|
name: 'Empty XML',
|
|
xml: ''
|
|
},
|
|
{
|
|
name: 'Invalid XML',
|
|
xml: '<not-closed'
|
|
},
|
|
{
|
|
name: 'Wrong root element',
|
|
xml: '<?xml version="1.0"?><root>test</root>'
|
|
}
|
|
];
|
|
|
|
for (const errorCase of errorCases) {
|
|
const times: number[] = [];
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
const startTime = performance.now();
|
|
try {
|
|
await EInvoice.fromXml(errorCase.xml);
|
|
} catch (error) {
|
|
// Expected error
|
|
}
|
|
const endTime = performance.now();
|
|
|
|
const duration = endTime - startTime;
|
|
times.push(duration);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
console.log(`${errorCase.name} - Error handling: avg=${avg.toFixed(3)}ms`);
|
|
|
|
// Error cases should fail fast
|
|
expect(avg).toBeLessThan(5);
|
|
}
|
|
});
|
|
|
|
tap.test('PERF-02: Performance Summary', async () => {
|
|
performanceTracker.printSummary();
|
|
console.log('\nValidation performance tests completed successfully');
|
|
});
|
|
|
|
tap.start(); |