update
This commit is contained in:
@ -1,523 +1,140 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as einvoice from '../../../ts/index.js';
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||||
|
||||
tap.test('ERR-05: Memory/Resource Errors - Handle memory and resource constraints', async (t) => {
|
||||
const performanceTracker = new PerformanceTracker('ERR-05');
|
||||
tap.test('ERR-05: Memory Errors - should handle memory constraints', async () => {
|
||||
// ERR-05: Test error handling for memory errors
|
||||
|
||||
await t.test('Memory allocation errors', async () => {
|
||||
performanceTracker.startOperation('memory-allocation');
|
||||
|
||||
const memoryScenarios = [
|
||||
{
|
||||
name: 'Large XML parsing',
|
||||
size: 50 * 1024 * 1024, // 50MB
|
||||
operation: 'XML parsing',
|
||||
expectedError: /memory|heap|allocation failed/i
|
||||
},
|
||||
{
|
||||
name: 'Multiple concurrent operations',
|
||||
concurrency: 100,
|
||||
operation: 'Concurrent processing',
|
||||
expectedError: /memory|resource|too many/i
|
||||
},
|
||||
{
|
||||
name: 'Buffer overflow protection',
|
||||
size: 100 * 1024 * 1024, // 100MB
|
||||
operation: 'Buffer allocation',
|
||||
expectedError: /buffer.*too large|memory limit|overflow/i
|
||||
}
|
||||
];
|
||||
|
||||
for (const scenario of memoryScenarios) {
|
||||
const startTime = performance.now();
|
||||
// Test 1: Basic error handling
|
||||
console.log('\nTest 1: Basic memory errors handling');
|
||||
const { result: basicResult, metric: basicMetric } = await PerformanceTracker.track(
|
||||
'err05-basic',
|
||||
async () => {
|
||||
let errorCaught = false;
|
||||
let errorMessage = '';
|
||||
|
||||
try {
|
||||
if (scenario.name === 'Large XML parsing') {
|
||||
// Simulate large XML that could cause memory issues
|
||||
const largeXml = '<invoice>' + 'x'.repeat(scenario.size) + '</invoice>';
|
||||
|
||||
// Check memory usage before attempting parse
|
||||
const memUsage = process.memoryUsage();
|
||||
if (memUsage.heapUsed + scenario.size > memUsage.heapTotal * 0.9) {
|
||||
throw new Error('Insufficient memory for XML parsing operation');
|
||||
}
|
||||
} else if (scenario.name === 'Buffer overflow protection') {
|
||||
// Simulate buffer size check
|
||||
const MAX_BUFFER_SIZE = 50 * 1024 * 1024; // 50MB limit
|
||||
if (scenario.size > MAX_BUFFER_SIZE) {
|
||||
throw new Error(`Buffer size ${scenario.size} exceeds maximum allowed size of ${MAX_BUFFER_SIZE}`);
|
||||
}
|
||||
}
|
||||
// Simulate error scenario
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Try to load invalid content based on test type
|
||||
// Simulate large document
|
||||
const largeXml = '<?xml version="1.0"?><Invoice>' + 'x'.repeat(1000000) + '</Invoice>';
|
||||
await einvoice.fromXmlString(largeXml);
|
||||
|
||||
} catch (error) {
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message.toLowerCase()).toMatch(scenario.expectedError);
|
||||
console.log(`✓ ${scenario.name}: ${error.message}`);
|
||||
errorCaught = true;
|
||||
errorMessage = error.message || 'Unknown error';
|
||||
console.log(` Error caught: ${errorMessage}`);
|
||||
}
|
||||
|
||||
performanceTracker.recordMetric('memory-error-handling', performance.now() - startTime);
|
||||
return {
|
||||
success: errorCaught,
|
||||
errorMessage,
|
||||
gracefulHandling: errorCaught && !errorMessage.includes('FATAL')
|
||||
};
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('memory-allocation');
|
||||
});
|
||||
);
|
||||
|
||||
await t.test('Resource exhaustion handling', async () => {
|
||||
performanceTracker.startOperation('resource-exhaustion');
|
||||
|
||||
class ResourcePool {
|
||||
private available: number;
|
||||
private inUse = 0;
|
||||
private waitQueue: Array<(value: any) => void> = [];
|
||||
|
||||
constructor(private maxResources: number) {
|
||||
this.available = maxResources;
|
||||
}
|
||||
|
||||
async acquire(): Promise<{ id: number; release: () => void }> {
|
||||
if (this.available > 0) {
|
||||
this.available--;
|
||||
this.inUse++;
|
||||
const resourceId = this.inUse;
|
||||
|
||||
return {
|
||||
id: resourceId,
|
||||
release: () => this.release()
|
||||
};
|
||||
}
|
||||
|
||||
// Resource exhausted - wait or throw
|
||||
if (this.waitQueue.length > 10) {
|
||||
throw new Error('Resource pool exhausted - too many pending requests');
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.waitQueue.push(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.available++;
|
||||
this.inUse--;
|
||||
|
||||
if (this.waitQueue.length > 0) {
|
||||
const waiting = this.waitQueue.shift();
|
||||
waiting(this.acquire());
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
available: this.available,
|
||||
inUse: this.inUse,
|
||||
waiting: this.waitQueue.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const pool = new ResourcePool(5);
|
||||
const acquiredResources = [];
|
||||
|
||||
// Acquire all resources
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const resource = await pool.acquire();
|
||||
acquiredResources.push(resource);
|
||||
console.log(` Acquired resource ${resource.id}`);
|
||||
}
|
||||
|
||||
console.log(` Pool status:`, pool.getStatus());
|
||||
|
||||
// Try to acquire when exhausted
|
||||
try {
|
||||
// Create many waiting requests
|
||||
const promises = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
promises.push(pool.acquire());
|
||||
}
|
||||
await Promise.race([
|
||||
Promise.all(promises),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Resource pool exhausted')), 100))
|
||||
]);
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch(/resource pool exhausted/i);
|
||||
console.log(`✓ Resource exhaustion detected: ${error.message}`);
|
||||
}
|
||||
|
||||
// Release resources
|
||||
for (const resource of acquiredResources) {
|
||||
resource.release();
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('resource-exhaustion');
|
||||
});
|
||||
console.log(` Basic error handling completed in ${basicMetric.duration}ms`);
|
||||
console.log(` Error was caught: ${basicResult.success}`);
|
||||
console.log(` Graceful handling: ${basicResult.gracefulHandling}`);
|
||||
|
||||
await t.test('File handle management', async () => {
|
||||
performanceTracker.startOperation('file-handles');
|
||||
|
||||
class FileHandleManager {
|
||||
private openHandles = new Map<string, any>();
|
||||
private readonly maxHandles = 100;
|
||||
// Test 2: Recovery mechanism
|
||||
console.log('\nTest 2: Recovery after error');
|
||||
const { result: recoveryResult, metric: recoveryMetric } = await PerformanceTracker.track(
|
||||
'err05-recovery',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
async open(filename: string): Promise<any> {
|
||||
if (this.openHandles.size >= this.maxHandles) {
|
||||
// Try to close least recently used
|
||||
const lru = this.openHandles.keys().next().value;
|
||||
if (lru) {
|
||||
await this.close(lru);
|
||||
console.log(` Auto-closed LRU file: ${lru}`);
|
||||
} else {
|
||||
throw new Error(`Too many open files (${this.maxHandles} limit reached)`);
|
||||
}
|
||||
// First cause an error
|
||||
try {
|
||||
// Simulate large document
|
||||
const largeXml = '<?xml version="1.0"?><Invoice>' + 'x'.repeat(1000000) + '</Invoice>';
|
||||
await einvoice.fromXmlString(largeXml);
|
||||
} catch (error) {
|
||||
// Expected error
|
||||
}
|
||||
|
||||
// Now try normal operation
|
||||
einvoice.id = 'RECOVERY-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'RECOVERY-TEST';
|
||||
einvoice.accountingDocId = 'RECOVERY-TEST';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing error recovery',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
|
||||
// Simulate file open
|
||||
const handle = {
|
||||
filename,
|
||||
opened: Date.now(),
|
||||
read: async () => `Content of ${filename}`
|
||||
};
|
||||
|
||||
this.openHandles.set(filename, handle);
|
||||
return handle;
|
||||
}
|
||||
};
|
||||
|
||||
async close(filename: string): Promise<void> {
|
||||
if (this.openHandles.has(filename)) {
|
||||
this.openHandles.delete(filename);
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'TEST-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Try to export after error
|
||||
let canRecover = false;
|
||||
try {
|
||||
const xml = await einvoice.toXmlString('ubl');
|
||||
canRecover = xml.includes('RECOVERY-TEST');
|
||||
} catch (error) {
|
||||
canRecover = false;
|
||||
}
|
||||
|
||||
async closeAll(): Promise<void> {
|
||||
for (const filename of this.openHandles.keys()) {
|
||||
await this.close(filename);
|
||||
}
|
||||
}
|
||||
|
||||
getOpenCount(): number {
|
||||
return this.openHandles.size;
|
||||
}
|
||||
return { success: canRecover };
|
||||
}
|
||||
|
||||
const fileManager = new FileHandleManager();
|
||||
|
||||
// Test normal operations
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await fileManager.open(`file${i}.xml`);
|
||||
}
|
||||
console.log(` Opened ${fileManager.getOpenCount()} files`);
|
||||
|
||||
// Test approaching limit
|
||||
for (let i = 50; i < 100; i++) {
|
||||
await fileManager.open(`file${i}.xml`);
|
||||
}
|
||||
console.log(` At limit: ${fileManager.getOpenCount()} files`);
|
||||
|
||||
// Test exceeding limit (should auto-close LRU)
|
||||
await fileManager.open('file100.xml');
|
||||
console.log(` After LRU eviction: ${fileManager.getOpenCount()} files`);
|
||||
|
||||
// Clean up
|
||||
await fileManager.closeAll();
|
||||
expect(fileManager.getOpenCount()).toEqual(0);
|
||||
console.log('✓ File handle management working correctly');
|
||||
|
||||
performanceTracker.endOperation('file-handles');
|
||||
});
|
||||
);
|
||||
|
||||
await t.test('Memory leak detection', async () => {
|
||||
performanceTracker.startOperation('memory-leak-detection');
|
||||
|
||||
class MemoryMonitor {
|
||||
private samples: Array<{ time: number; usage: NodeJS.MemoryUsage }> = [];
|
||||
private leakThreshold = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
recordSample(): void {
|
||||
this.samples.push({
|
||||
time: Date.now(),
|
||||
usage: process.memoryUsage()
|
||||
});
|
||||
|
||||
// Keep only recent samples
|
||||
if (this.samples.length > 10) {
|
||||
this.samples.shift();
|
||||
}
|
||||
}
|
||||
|
||||
detectLeak(): { isLeaking: boolean; growth?: number; message?: string } {
|
||||
if (this.samples.length < 3) {
|
||||
return { isLeaking: false };
|
||||
}
|
||||
|
||||
const first = this.samples[0];
|
||||
const last = this.samples[this.samples.length - 1];
|
||||
const heapGrowth = last.usage.heapUsed - first.usage.heapUsed;
|
||||
|
||||
if (heapGrowth > this.leakThreshold) {
|
||||
return {
|
||||
isLeaking: true,
|
||||
growth: heapGrowth,
|
||||
message: `Potential memory leak detected: ${Math.round(heapGrowth / 1024 / 1024)}MB heap growth`
|
||||
};
|
||||
}
|
||||
|
||||
return { isLeaking: false, growth: heapGrowth };
|
||||
}
|
||||
|
||||
getReport(): string {
|
||||
const current = process.memoryUsage();
|
||||
return [
|
||||
`Memory Usage Report:`,
|
||||
` Heap Used: ${Math.round(current.heapUsed / 1024 / 1024)}MB`,
|
||||
` Heap Total: ${Math.round(current.heapTotal / 1024 / 1024)}MB`,
|
||||
` RSS: ${Math.round(current.rss / 1024 / 1024)}MB`,
|
||||
` Samples: ${this.samples.length}`
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
const monitor = new MemoryMonitor();
|
||||
|
||||
// Simulate operations that might leak memory
|
||||
const operations = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
monitor.recordSample();
|
||||
|
||||
// Simulate memory usage
|
||||
const data = new Array(1000).fill('x'.repeat(1000));
|
||||
operations.push(data);
|
||||
|
||||
// Small delay
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
const leakCheck = monitor.detectLeak();
|
||||
console.log(monitor.getReport());
|
||||
|
||||
if (leakCheck.isLeaking) {
|
||||
console.log(`⚠️ ${leakCheck.message}`);
|
||||
} else {
|
||||
console.log(`✓ No memory leak detected (growth: ${Math.round(leakCheck.growth / 1024)}KB)`);
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('memory-leak-detection');
|
||||
});
|
||||
console.log(` Recovery test completed in ${recoveryMetric.duration}ms`);
|
||||
console.log(` Can recover after error: ${recoveryResult.success}`);
|
||||
|
||||
await t.test('Stream processing for large files', async () => {
|
||||
performanceTracker.startOperation('stream-processing');
|
||||
|
||||
class StreamProcessor {
|
||||
async processLargeXml(stream: any, options: { chunkSize?: number } = {}): Promise<void> {
|
||||
const chunkSize = options.chunkSize || 16 * 1024; // 16KB chunks
|
||||
let processedBytes = 0;
|
||||
let chunkCount = 0;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
// Simulate stream processing
|
||||
const processChunk = (chunk: Buffer) => {
|
||||
processedBytes += chunk.length;
|
||||
chunkCount++;
|
||||
|
||||
// Check memory pressure
|
||||
const memUsage = process.memoryUsage();
|
||||
if (memUsage.heapUsed > memUsage.heapTotal * 0.8) {
|
||||
reject(new Error('Memory pressure too high during stream processing'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process chunk (e.g., partial XML parsing)
|
||||
chunks.push(chunk);
|
||||
|
||||
// Limit buffered chunks
|
||||
if (chunks.length > 100) {
|
||||
chunks.shift(); // Remove oldest
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Simulate streaming
|
||||
const simulateStream = () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const chunk = Buffer.alloc(chunkSize, 'x');
|
||||
if (!processChunk(chunk)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` Processed ${chunkCount} chunks (${Math.round(processedBytes / 1024)}KB)`);
|
||||
resolve();
|
||||
};
|
||||
|
||||
simulateStream();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const processor = new StreamProcessor();
|
||||
|
||||
try {
|
||||
await processor.processLargeXml({}, { chunkSize: 8 * 1024 });
|
||||
console.log('✓ Stream processing completed successfully');
|
||||
} catch (error) {
|
||||
console.log(`✗ Stream processing failed: ${error.message}`);
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('stream-processing');
|
||||
});
|
||||
// Summary
|
||||
console.log('\n=== Memory Errors Error Handling Summary ===');
|
||||
console.log(`Error Detection: ${basicResult.success ? 'Working' : 'Failed'}`);
|
||||
console.log(`Graceful Handling: ${basicResult.gracefulHandling ? 'Yes' : 'No'}`);
|
||||
console.log(`Recovery: ${recoveryResult.success ? 'Successful' : 'Failed'}`);
|
||||
|
||||
await t.test('Resource cleanup patterns', async () => {
|
||||
performanceTracker.startOperation('resource-cleanup');
|
||||
|
||||
class ResourceManager {
|
||||
private cleanupHandlers: Array<() => Promise<void>> = [];
|
||||
|
||||
register(cleanup: () => Promise<void>): void {
|
||||
this.cleanupHandlers.push(cleanup);
|
||||
}
|
||||
|
||||
async executeWithCleanup<T>(operation: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} finally {
|
||||
// Always cleanup, even on error
|
||||
for (const handler of this.cleanupHandlers.reverse()) {
|
||||
try {
|
||||
await handler();
|
||||
} catch (cleanupError) {
|
||||
console.error(` Cleanup error: ${cleanupError.message}`);
|
||||
}
|
||||
}
|
||||
this.cleanupHandlers = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new ResourceManager();
|
||||
|
||||
// Register cleanup handlers
|
||||
manager.register(async () => {
|
||||
console.log(' Closing file handles...');
|
||||
});
|
||||
|
||||
manager.register(async () => {
|
||||
console.log(' Releasing memory buffers...');
|
||||
});
|
||||
|
||||
manager.register(async () => {
|
||||
console.log(' Clearing temporary files...');
|
||||
});
|
||||
|
||||
// Test successful operation
|
||||
try {
|
||||
await manager.executeWithCleanup(async () => {
|
||||
console.log(' Executing operation...');
|
||||
return 'Success';
|
||||
});
|
||||
console.log('✓ Operation with cleanup completed');
|
||||
} catch (error) {
|
||||
console.log(`✗ Operation failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test failed operation (cleanup should still run)
|
||||
try {
|
||||
await manager.executeWithCleanup(async () => {
|
||||
console.log(' Executing failing operation...');
|
||||
throw new Error('Operation failed');
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('✓ Cleanup ran despite error');
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('resource-cleanup');
|
||||
});
|
||||
|
||||
await t.test('Memory usage optimization strategies', async () => {
|
||||
performanceTracker.startOperation('memory-optimization');
|
||||
|
||||
const optimizationStrategies = [
|
||||
{
|
||||
name: 'Lazy loading',
|
||||
description: 'Load data only when needed',
|
||||
implementation: () => {
|
||||
let _data: any = null;
|
||||
return {
|
||||
get data() {
|
||||
if (!_data) {
|
||||
console.log(' Loading data on first access...');
|
||||
_data = { loaded: true };
|
||||
}
|
||||
return _data;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Object pooling',
|
||||
description: 'Reuse objects instead of creating new ones',
|
||||
implementation: () => {
|
||||
const pool: any[] = [];
|
||||
return {
|
||||
acquire: () => pool.pop() || { reused: false },
|
||||
release: (obj: any) => {
|
||||
obj.reused = true;
|
||||
pool.push(obj);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Weak references',
|
||||
description: 'Allow garbage collection of cached objects',
|
||||
implementation: () => {
|
||||
const cache = new WeakMap();
|
||||
return {
|
||||
set: (key: object, value: any) => cache.set(key, value),
|
||||
get: (key: object) => cache.get(key)
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const strategy of optimizationStrategies) {
|
||||
console.log(`\n Testing ${strategy.name}:`);
|
||||
console.log(` ${strategy.description}`);
|
||||
|
||||
const impl = strategy.implementation();
|
||||
|
||||
if (strategy.name === 'Lazy loading') {
|
||||
// Access data multiple times
|
||||
const obj = impl as any;
|
||||
obj.data; // First access
|
||||
obj.data; // Second access (no reload)
|
||||
} else if (strategy.name === 'Object pooling') {
|
||||
const pool = impl as any;
|
||||
const obj1 = pool.acquire();
|
||||
console.log(` First acquire: reused=${obj1.reused}`);
|
||||
pool.release(obj1);
|
||||
const obj2 = pool.acquire();
|
||||
console.log(` Second acquire: reused=${obj2.reused}`);
|
||||
}
|
||||
|
||||
console.log(` ✓ ${strategy.name} implemented`);
|
||||
}
|
||||
|
||||
performanceTracker.endOperation('memory-optimization');
|
||||
});
|
||||
|
||||
// Performance summary
|
||||
console.log('\n' + performanceTracker.getSummary());
|
||||
|
||||
// Memory error handling best practices
|
||||
console.log('\nMemory/Resource Error Handling Best Practices:');
|
||||
console.log('1. Implement resource pooling for frequently used objects');
|
||||
console.log('2. Use streaming for large file processing');
|
||||
console.log('3. Monitor memory usage and implement early warning systems');
|
||||
console.log('4. Always clean up resources in finally blocks');
|
||||
console.log('5. Set reasonable limits on buffer sizes and concurrent operations');
|
||||
console.log('6. Implement graceful degradation when resources are constrained');
|
||||
console.log('7. Use weak references for caches that can be garbage collected');
|
||||
// Test passes if errors are caught gracefully
|
||||
expect(basicResult.success).toBeTrue();
|
||||
expect(recoveryResult.success).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
// Run the test
|
||||
tap.start();
|
||||
|
Reference in New Issue
Block a user