einvoice/test/suite/einvoice_performance/test.perf-02.validation-performance.ts

518 lines
18 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
/**
* @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<string, { count: number; totalTime: number }>(),
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();