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 ;
2025-05-30 18:08:27 +00:00
expect ( memoryMB ) . toBeLessThan ( 50 ) ; // Should not use more than 50MB for validation
2025-05-25 19:45:37 +00:00
}
} catch ( error ) {
console . log ( ` ✗ Error: ${ error . message } ` ) ;
2025-05-30 18:08:27 +00:00
// Large invoices may fail but should fail gracefully
expect ( error ) . toBeTruthy ( ) ; // Any error is acceptable as long as it doesn't crash
2025-05-25 19:45:37 +00:00
}
}
} ) ;
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 ( ) ;