import * as plugins from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../server.loader.js'; const TEST_PORT = 2525; interface ResourceMetrics { timestamp: number; memoryUsage: { rss: number; heapTotal: number; heapUsed: number; external: number; }; processInfo: { pid: number; uptime: number; cpuUsage: NodeJS.CpuUsage; }; } interface LeakAnalysis { memoryGrowthMB: number; memoryTrend: number; stabilityScore: number; memoryLeakDetected: boolean; resourcesStable: boolean; samplesAnalyzed: number; initialMemoryMB: number; finalMemoryMB: number; } const captureResourceMetrics = async (): Promise => { // Force GC if available before measurement if (global.gc) { global.gc(); await new Promise(resolve => setTimeout(resolve, 100)); } const memUsage = process.memoryUsage(); return { timestamp: Date.now(), memoryUsage: { rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100, heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100, heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100, external: Math.round(memUsage.external / 1024 / 1024 * 100) / 100 }, processInfo: { pid: process.pid, uptime: process.uptime(), cpuUsage: process.cpuUsage() } }; }; const analyzeResourceLeaks = (initial: ResourceMetrics, samples: Array<{ operation: number; metrics: ResourceMetrics }>, final: ResourceMetrics): LeakAnalysis => { const memoryGrowthMB = final.memoryUsage.heapUsed - initial.memoryUsage.heapUsed; // Analyze memory trend over samples let memoryTrend = 0; if (samples.length > 1) { const firstSample = samples[0].metrics.memoryUsage.heapUsed; const lastSample = samples[samples.length - 1].metrics.memoryUsage.heapUsed; memoryTrend = lastSample - firstSample; } // Calculate stability score based on memory variance let stabilityScore = 1.0; if (samples.length > 2) { const memoryValues = samples.map(s => s.metrics.memoryUsage.heapUsed); const average = memoryValues.reduce((a, b) => a + b, 0) / memoryValues.length; const variance = memoryValues.reduce((acc, val) => acc + Math.pow(val - average, 2), 0) / memoryValues.length; const stdDev = Math.sqrt(variance); stabilityScore = Math.max(0, 1 - (stdDev / average)); } return { memoryGrowthMB: Math.round(memoryGrowthMB * 100) / 100, memoryTrend: Math.round(memoryTrend * 100) / 100, stabilityScore: Math.round(stabilityScore * 100) / 100, memoryLeakDetected: memoryGrowthMB > 50, resourcesStable: stabilityScore > 0.8 && memoryGrowthMB < 25, samplesAnalyzed: samples.length, initialMemoryMB: initial.memoryUsage.heapUsed, finalMemoryMB: final.memoryUsage.heapUsed }; }; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('REL-03: Resource leak detection - Memory leak analysis', async (tools) => { const done = tools.defer(); const operationCount = 20; const connections: net.Socket[] = []; const samples: Array<{ operation: number; metrics: ResourceMetrics }> = []; try { const initialMetrics = await captureResourceMetrics(); console.log(`šŸ“Š Initial memory: ${initialMetrics.memoryUsage.heapUsed}MB`); for (let i = 0; i < operationCount; i++) { console.log(`šŸ”„ Operation ${i + 1}/${operationCount}...`); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); connections.push(socket); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write(`EHLO leaktest-${i}\r\n`); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); // Complete email transaction socket.write(`MAIL FROM:\r\n`); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write(`RCPT TO:\r\n`); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write('DATA\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('354'); resolve(); }); }); const emailContent = [ `From: sender${i}@example.com`, `To: recipient${i}@example.com`, `Subject: Resource Leak Test ${i + 1}`, `Message-ID: `, '', `This is resource leak test iteration ${i + 1}.`, '.', '' ].join('\r\n'); socket.write(emailContent); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write('QUIT\r\n'); await new Promise((resolve) => { socket.once('data', () => { socket.end(); resolve(); }); }); // Capture metrics every 5 operations if ((i + 1) % 5 === 0) { const metrics = await captureResourceMetrics(); samples.push({ operation: i + 1, metrics }); console.log(`šŸ“ˆ Sample ${samples.length}: Memory ${metrics.memoryUsage.heapUsed}MB`); } // Small delay between operations await new Promise(resolve => setTimeout(resolve, 100)); } // Clean up all connections connections.forEach(conn => { if (!conn.destroyed) { conn.destroy(); } }); // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 2000)); const finalMetrics = await captureResourceMetrics(); const leakAnalysis = analyzeResourceLeaks(initialMetrics, samples, finalMetrics); console.log('\nšŸ“Š Resource Leak Analysis:'); console.log(`Initial memory: ${leakAnalysis.initialMemoryMB}MB`); console.log(`Final memory: ${leakAnalysis.finalMemoryMB}MB`); console.log(`Memory growth: ${leakAnalysis.memoryGrowthMB}MB`); console.log(`Memory trend: ${leakAnalysis.memoryTrend}MB`); console.log(`Stability score: ${leakAnalysis.stabilityScore}`); console.log(`Memory leak detected: ${leakAnalysis.memoryLeakDetected}`); console.log(`Resources stable: ${leakAnalysis.resourcesStable}`); expect(leakAnalysis.memoryLeakDetected).toBeFalse(); expect(leakAnalysis.resourcesStable).toBeTrue(); done.resolve(); } catch (error) { connections.forEach(conn => conn.destroy()); done.reject(error); } }); tap.test('REL-03: Resource leak detection - Connection leak test', async (tools) => { const done = tools.defer(); const abandonedConnections: net.Socket[] = []; try { console.log('\nTesting for connection resource leaks...'); // Create connections that are abandoned without proper cleanup for (let i = 0; i < 10; i++) { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); abandonedConnections.push(socket); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting but don't complete transaction await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Start but don't complete EHLO socket.write(`EHLO abandoned-${i}\r\n`); // Don't wait for response, just move to next await new Promise(resolve => setTimeout(resolve, 50)); } console.log('Created 10 abandoned connections'); // Wait a bit await new Promise(resolve => setTimeout(resolve, 2000)); // Try to create new connections - should still work let newConnectionsSuccessful = 0; for (let i = 0; i < 5; i++) { try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 5000 }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { socket.destroy(); reject(new Error('Connection timeout')); }, 5000); socket.once('connect', () => { clearTimeout(timeout); resolve(); }); socket.once('error', (err) => { clearTimeout(timeout); reject(err); }); }); // Verify connection works const greeting = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); if (greeting.includes('220')) { newConnectionsSuccessful++; socket.write('QUIT\r\n'); socket.end(); } } catch (error) { console.log(`New connection ${i + 1} failed:`, error.message); } } // Clean up abandoned connections abandonedConnections.forEach(conn => conn.destroy()); console.log(`New connections successful: ${newConnectionsSuccessful}/5`); // Server should still accept new connections despite abandoned ones expect(newConnectionsSuccessful).toBeGreaterThanOrEqual(4); done.resolve(); } catch (error) { abandonedConnections.forEach(conn => conn.destroy()); done.reject(error); } }); tap.test('REL-03: Resource leak detection - Rapid create/destroy cycles', async (tools) => { const done = tools.defer(); const cycles = 30; const initialMetrics = await captureResourceMetrics(); try { console.log('\nTesting rapid connection create/destroy cycles...'); for (let i = 0; i < cycles; i++) { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 5000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Immediately destroy after connect socket.destroy(); // Very short delay await new Promise(resolve => setTimeout(resolve, 20)); if ((i + 1) % 10 === 0) { console.log(`Completed ${i + 1} cycles`); } } // Wait for resources to be released await new Promise(resolve => setTimeout(resolve, 3000)); const finalMetrics = await captureResourceMetrics(); const memoryGrowth = finalMetrics.memoryUsage.heapUsed - initialMetrics.memoryUsage.heapUsed; console.log(`Memory growth after ${cycles} cycles: ${memoryGrowth.toFixed(2)}MB`); // Memory growth should be minimal expect(memoryGrowth).toBeLessThan(10); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('cleanup server', async () => { await stopTestServer(); }); tap.start();