memory-leak-detector.test.ts•13.1 kB
/**
* Comprehensive Test Suite for Memory Leak Detection System
*/
import { MemoryLeakDetector, MemoryLeakIntegration, ComponentMemoryTracker } from './memory-leak-detector';
describe('MemoryLeakDetector', () => {
let detector: MemoryLeakDetector;
beforeEach(() => {
detector = new MemoryLeakDetector({
heapGrowthRate: 5, // Lower threshold for testing
maxHeapSize: 100,
maxObjectCount: 1000,
monitoringWindow: 10000,
snapshotInterval: 5000,
memoryIncreaseThreshold: 25,
});
});
afterEach(() => {
detector.shutdown();
});
describe('Component Registration', () => {
it('should register components for monitoring', () => {
const tracker = detector.registerComponent('test-component', {
maxObjectCount: 500,
});
expect(tracker.name).toBe('test-component');
expect(tracker.objectCount).toBe(0);
expect(tracker.thresholds.maxObjectCount).toBe(500);
});
it('should emit componentRegistered event', (done) => {
detector.on('componentRegistered', ({ name, tracker }) => {
expect(name).toBe('test-component');
expect(tracker).toBeDefined();
done();
});
detector.registerComponent('test-component');
});
});
describe('Object Tracking', () => {
let tracker: ComponentMemoryTracker;
beforeEach(() => {
tracker = detector.registerComponent('test-component');
});
it('should track object allocation', () => {
const testObject = { data: 'test' };
detector.trackObjectAllocation('test-component', testObject, 100);
expect(tracker.objectCount).toBe(1);
expect(tracker.memoryUsage).toBe(100);
expect(tracker.weakRefs.size).toBe(1);
});
it('should track object deallocation', () => {
detector.trackObjectAllocation('test-component', { data: 'test' }, 100);
detector.trackObjectDeallocation('test-component', 50);
expect(tracker.objectCount).toBe(0);
expect(tracker.memoryUsage).toBe(50);
});
it('should not allow negative object counts', () => {
detector.trackObjectDeallocation('test-component', 100);
expect(tracker.objectCount).toBe(0);
expect(tracker.memoryUsage).toBe(0);
});
});
describe('Memory Leak Detection', () => {
it('should detect object accumulation leaks', async () => {
const tracker = detector.registerComponent('leak-component', {
maxObjectCount: 5,
});
// Allocate more objects than threshold
for (let i = 0; i < 10; i++) {
detector.trackObjectAllocation('leak-component', { id: i });
}
const detections = await detector.detectMemoryLeaks();
const objectLeak = detections.find(d => d.leakType === 'object_accumulation');
expect(objectLeak).toBeDefined();
expect(objectLeak?.component).toBe('leak-component');
expect(objectLeak?.severity).toBe('high');
});
it('should detect weak reference buildup', async () => {
const tracker = detector.registerComponent('weakref-component', {
maxObjectCount: 10,
});
// Create many weak references
for (let i = 0; i < 20; i++) {
detector.trackObjectAllocation('weakref-component', { id: i });
}
const detections = await detector.detectMemoryLeaks();
const weakRefLeak = detections.find(d => d.leakType === 'weak_ref_buildup');
expect(weakRefLeak).toBeDefined();
expect(weakRefLeak?.component).toBe('weakref-component');
});
it('should emit memoryLeakDetected events', (done) => {
detector.on('memoryLeakDetected', (detection) => {
expect(detection.component).toBeDefined();
expect(detection.leakType).toBeDefined();
expect(detection.severity).toBeDefined();
done();
});
detector.registerComponent('event-component', {
maxObjectCount: 1,
});
// Trigger leak detection
detector.trackObjectAllocation('event-component', { data: 'test' });
detector.trackObjectAllocation('event-component', { data: 'test2' });
detector.detectMemoryLeaks();
});
});
describe('Heap Snapshots', () => {
it('should create heap snapshots', async () => {
const snapshot = await detector.createHeapSnapshot();
expect(snapshot).toBeDefined();
expect(snapshot.timestamp).toBeDefined();
expect(snapshot.memoryUsage).toBeDefined();
});
it('should compare heap snapshots', async () => {
const snapshot1 = await detector.createHeapSnapshot();
// Allocate some memory
Array.from({ length: 100 }, () => Buffer.alloc(1024));
const snapshot2 = await detector.createHeapSnapshot();
const comparison = await detector.compareHeapSnapshots(snapshot1, snapshot2);
expect(comparison.sizeIncrement).toBeGreaterThan(0);
expect(comparison.added).toBeGreaterThanOrEqual(0);
});
});
describe('Cleanup Operations', () => {
it('should force cleanup and remove dead weak references', async () => {
const tracker = detector.registerComponent('cleanup-component');
// Create objects and let them go out of scope
(() => {
for (let i = 0; i < 10; i++) {
detector.trackObjectAllocation('cleanup-component', { id: i });
}
})();
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const result = await detector.forceCleanup();
expect(result.cleaned).toBeGreaterThanOrEqual(0);
});
it('should emit cleanup events', (done) => {
detector.on('forceCleanup', ({ cleaned, memoryFreed }) => {
expect(typeof cleaned).toBe('number');
expect(typeof memoryFreed).toBe('number');
done();
});
detector.forceCleanup();
});
});
describe('Memory Pressure Simulation', () => {
it('should simulate memory pressure', async () => {
const config = {
enabled: true,
targetMemoryMB: 10,
duration: 1000,
escalationSteps: 2,
};
const events: string[] = [];
detector.on('memoryPressureStart', () => events.push('start'));
detector.on('memoryPressureStep', () => events.push('step'));
detector.on('memoryPressureEnd', () => events.push('end'));
await detector.simulateMemoryPressure(config);
expect(events).toContain('start');
expect(events).toContain('step');
expect(events).toContain('end');
});
it('should not simulate when disabled', async () => {
const config = {
enabled: false,
targetMemoryMB: 10,
duration: 1000,
escalationSteps: 2,
};
let eventCount = 0;
detector.on('memoryPressureStart', () => eventCount++);
await detector.simulateMemoryPressure(config);
expect(eventCount).toBe(0);
});
});
describe('Statistics and Reporting', () => {
it('should provide comprehensive statistics', () => {
const tracker = detector.registerComponent('stats-component');
detector.trackObjectAllocation('stats-component', { data: 'test' }, 100);
const stats = detector.getStatistics();
expect(stats.detections).toBeDefined();
expect(stats.components).toHaveLength(6); // 5 default + 1 test component
expect(stats.recentDetections).toBeDefined();
expect(stats.memoryTrend).toBeDefined();
const testComponent = stats.components.find(c => c.name === 'stats-component');
expect(testComponent?.objectCount).toBe(1);
expect(testComponent?.memoryUsage).toBe(100);
});
it('should track detection history', async () => {
const tracker = detector.registerComponent('history-component', {
maxObjectCount: 1,
});
// Trigger multiple detections
detector.trackObjectAllocation('history-component', { id: 1 });
detector.trackObjectAllocation('history-component', { id: 2 });
await detector.detectMemoryLeaks();
const stats = detector.getStatistics();
expect(stats.detections.totalDetections).toBeGreaterThan(0);
});
});
describe('Auto-Fix Functionality', () => {
it('should apply auto-fixes for heap growth', async () => {
let autoFixApplied = false;
detector.on('autoFixApplied', () => {
autoFixApplied = true;
});
// Simulate heap growth detection with auto-fix
const detection = {
timestamp: Date.now(),
component: 'system',
leakType: 'heap_growth' as const,
severity: 'high' as const,
currentUsage: 1000000,
growthRate: 15,
recommendation: 'Force garbage collection',
autoFixAvailable: true,
};
await (detector as any).applyAutoFix(detection);
// Check if global.gc was called (if available)
if (global.gc) {
expect(autoFixApplied).toBe(true);
}
});
});
});
describe('MemoryLeakIntegration', () => {
beforeEach(() => {
MemoryLeakIntegration.initialize();
});
afterEach(() => {
MemoryLeakIntegration.shutdown();
});
describe('Integration Hooks', () => {
it('should track cache operations', () => {
const detector = MemoryLeakIntegration.getDetector();
expect(detector).toBeDefined();
// These should not throw
MemoryLeakIntegration.trackCacheOperation('set', 'test-key', 100);
MemoryLeakIntegration.trackCacheOperation('get', 'test-key');
MemoryLeakIntegration.trackCacheOperation('delete', 'test-key', 100);
});
it('should track Pareto frontier operations', () => {
MemoryLeakIntegration.trackParetoOperation('add', 'candidate-1', 200);
MemoryLeakIntegration.trackParetoOperation('remove', 'candidate-1', 200);
});
it('should track LLM process operations', () => {
MemoryLeakIntegration.trackLLMProcess('spawn', 'process-1', 1024);
MemoryLeakIntegration.trackLLMProcess('exit', 'process-1', 1024);
});
it('should handle operations when detector is not initialized', () => {
MemoryLeakIntegration.shutdown();
// These should not throw even when detector is null
expect(() => {
MemoryLeakIntegration.trackCacheOperation('set', 'test-key');
MemoryLeakIntegration.trackParetoOperation('add', 'candidate-1');
MemoryLeakIntegration.trackLLMProcess('spawn', 'process-1');
}).not.toThrow();
});
});
describe('Lifecycle Management', () => {
it('should initialize detector only once', () => {
const detector1 = MemoryLeakIntegration.initialize();
const detector2 = MemoryLeakIntegration.initialize();
expect(detector1).toBe(detector2);
});
it('should properly shutdown detector', () => {
const detector = MemoryLeakIntegration.getDetector();
expect(detector).toBeDefined();
MemoryLeakIntegration.shutdown();
expect(MemoryLeakIntegration.getDetector()).toBeNull();
});
});
});
describe('Memory Leak Stress Tests', () => {
let detector: MemoryLeakDetector;
beforeEach(() => {
detector = new MemoryLeakDetector({
heapGrowthRate: 1,
maxObjectCount: 100,
monitoringWindow: 5000,
});
});
afterEach(() => {
detector.shutdown();
});
it('should handle rapid object allocation/deallocation', () => {
const tracker = detector.registerComponent('stress-component');
// Rapidly allocate and deallocate objects
for (let i = 0; i < 1000; i++) {
detector.trackObjectAllocation('stress-component', { id: i });
if (i % 2 === 0) {
detector.trackObjectDeallocation('stress-component');
}
}
// Should not crash or corrupt state
expect(tracker.objectCount).toBeGreaterThanOrEqual(0);
expect(tracker.memoryUsage).toBeGreaterThanOrEqual(0);
});
it('should handle concurrent operations', async () => {
const tracker = detector.registerComponent('concurrent-component');
// Simulate concurrent operations
const promises = Array.from({ length: 10 }, async (_, i) => {
for (let j = 0; j < 100; j++) {
detector.trackObjectAllocation('concurrent-component', { id: `${i}-${j}` });
if (j % 10 === 0) {
await detector.detectMemoryLeaks();
}
}
});
await Promise.all(promises);
// Verify final state is consistent
expect(tracker.objectCount).toBe(1000);
expect(tracker.weakRefs.size).toBe(1000);
});
it('should maintain performance under load', async () => {
const tracker = detector.registerComponent('performance-component');
const startTime = performance.now();
// Allocate many objects
for (let i = 0; i < 10000; i++) {
detector.trackObjectAllocation('performance-component', { id: i });
}
// Run detection
await detector.detectMemoryLeaks();
const duration = performance.now() - startTime;
// Should complete within reasonable time (adjust threshold as needed)
expect(duration).toBeLessThan(5000); // 5 seconds
});
});