This commit is contained in:
2025-05-26 14:50:55 +00:00
parent 20583beb35
commit a3721f7a74
22 changed files with 2820 additions and 8112 deletions

View File

@ -1,501 +1,503 @@
import { test } from '@git.zone/tstest/tapbundle';
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
test('CREL-05: Memory Leak Prevention Reliability Tests', async () => {
// Helper function to get memory usage
const getMemoryUsage = () => {
const usage = process.memoryUsage();
return {
heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, // MB
external: Math.round(usage.external / 1024 / 1024 * 100) / 100, // MB
rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100 // MB
};
};
// Force garbage collection if available
const forceGC = () => {
if (global.gc) {
global.gc();
global.gc(); // Run twice for thoroughness
}
};
tap.test('CREL-05: Connection Pool Memory Management', async () => {
console.log('\n🧠 Testing SMTP Client Memory Leak Prevention');
console.log('=' .repeat(60));
// Helper function to get memory usage
const getMemoryUsage = () => {
const usage = process.memoryUsage();
return {
heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, // MB
external: Math.round(usage.external / 1024 / 1024 * 100) / 100, // MB
rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100 // MB
};
};
// Force garbage collection if available
const forceGC = () => {
if (global.gc) {
global.gc();
global.gc(); // Run twice for thoroughness
}
};
// Scenario 1: Connection Pool Memory Management
await test.test('Scenario 1: Connection Pool Memory Management', async () => {
console.log('\n🏊 Testing connection pool memory management...');
console.log('\n🏊 Testing connection pool memory management...');
// Create test server
const server = net.createServer(socket => {
socket.write('220 localhost SMTP Test Server\r\n');
const testServer = await createTestServer({
responseDelay: 20,
onConnect: () => {
console.log(' [Server] Connection established for memory test');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server.address() as net.AddressInfo).port;
try {
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap, ${initialMemory.rss}MB RSS`);
console.log(' Phase 1: Creating and using multiple connection pools...');
const memorySnapshots = [];
for (let poolIndex = 0; poolIndex < 5; poolIndex++) {
console.log(` Creating connection pool ${poolIndex + 1}...`);
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 3,
maxMessages: 20,
connectionTimeout: 1000
});
// Send emails through this pool
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: `sender${poolIndex}@memoryleak.test`,
to: [`recipient${i}@memoryleak.test`],
subject: `Memory Pool Test ${poolIndex + 1}-${i + 1}`,
text: `Testing memory management in pool ${poolIndex + 1}, email ${i + 1}`
}));
}
// Send emails concurrently
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
return { success: true, result };
}).catch(error => {
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
// Close the pool
smtpClient.close();
console.log(` Pool ${poolIndex + 1} closed`);
// Force garbage collection and measure memory
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const currentMemory = getMemoryUsage();
memorySnapshots.push({
pool: poolIndex + 1,
heap: currentMemory.heapUsed,
rss: currentMemory.rss,
external: currentMemory.external
});
console.log(` Memory after pool ${poolIndex + 1}: ${currentMemory.heapUsed}MB heap`);
}
console.log('\n Memory analysis:');
memorySnapshots.forEach((snapshot, index) => {
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
console.log(` Pool ${snapshot.pool}: +${memoryIncrease.toFixed(2)}MB heap increase`);
});
try {
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap, ${initialMemory.rss}MB RSS`);
// Check for memory leaks (memory should not continuously increase)
const firstIncrease = memorySnapshots[0].heap - initialMemory.heapUsed;
const lastIncrease = memorySnapshots[memorySnapshots.length - 1].heap - initialMemory.heapUsed;
const leakGrowth = lastIncrease - firstIncrease;
console.log(` Memory leak assessment:`);
console.log(` First pool increase: +${firstIncrease.toFixed(2)}MB`);
console.log(` Final memory increase: +${lastIncrease.toFixed(2)}MB`);
console.log(` Memory growth across pools: +${leakGrowth.toFixed(2)}MB`);
console.log(` Memory management: ${leakGrowth < 3.0 ? 'Good (< 3MB growth)' : 'Potential leak detected'}`);
console.log(' Phase 1: Creating and using multiple connection pools...');
const memorySnapshots = [];
expect(leakGrowth).toBeLessThan(5.0); // Allow some memory growth but detect major leaks
} finally {
server.close();
}
});
tap.test('CREL-05: Email Object Memory Lifecycle', async () => {
console.log('\n📧 Testing email object memory lifecycle...');
// Create test server
const server = net.createServer(socket => {
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
for (let poolIndex = 0; poolIndex < 5; poolIndex++) {
console.log(` Creating connection pool ${poolIndex + 1}...`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 3,
maxMessages: 20,
connectionTimeout: 1000
});
// Send emails through this pool
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: `sender${poolIndex}@memoryleak.test`,
to: [`recipient${i}@memoryleak.test`],
subject: `Memory Pool Test ${poolIndex + 1}-${i + 1}`,
text: `Testing memory management in pool ${poolIndex + 1}, email ${i + 1}`,
messageId: `memory-pool-${poolIndex}-${i}@memoryleak.test`
}));
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
// Send emails concurrently
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
return { success: true, result };
}).catch(error => {
return { success: false, error };
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server.address() as net.AddressInfo).port;
try {
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 2
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Phase 1: Creating large batches of email objects...');
const batchSizes = [50, 100, 150, 100, 50]; // Varying batch sizes
const memorySnapshots = [];
for (let batchIndex = 0; batchIndex < batchSizes.length; batchIndex++) {
const batchSize = batchSizes[batchIndex];
console.log(` Creating batch ${batchIndex + 1} with ${batchSize} emails...`);
const emails = [];
for (let i = 0; i < batchSize; i++) {
emails.push(new Email({
from: 'sender@emailmemory.test',
to: [`recipient${i}@emailmemory.test`],
subject: `Memory Lifecycle Test Batch ${batchIndex + 1} Email ${i + 1}`,
text: `Testing email object memory lifecycle. This is a moderately long email body to test memory usage patterns. Email ${i + 1} in batch ${batchIndex + 1} of ${batchSize} emails.`,
html: `<h1>Email ${i + 1}</h1><p>Testing memory patterns with HTML content. Batch ${batchIndex + 1}.</p>`
}));
}
console.log(` Sending batch ${batchIndex + 1}...`);
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
return { success: true };
}).catch(error => {
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Batch ${batchIndex + 1}: ${successful}/${batchSize} emails sent`);
// Close the pool
smtpClient.close();
console.log(` Pool ${poolIndex + 1} closed`);
// Clear email references
emails.length = 0;
// Force garbage collection and measure memory
// Force garbage collection
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const currentMemory = getMemoryUsage();
memorySnapshots.push({
batch: batchIndex + 1,
size: batchSize,
heap: currentMemory.heapUsed,
external: currentMemory.external
});
console.log(` Memory after batch ${batchIndex + 1}: ${currentMemory.heapUsed}MB heap`);
}
console.log('\n Email object memory analysis:');
memorySnapshots.forEach((snapshot, index) => {
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
console.log(` Batch ${snapshot.batch} (${snapshot.size} emails): +${memoryIncrease.toFixed(2)}MB`);
});
// Check if memory scales reasonably with email batch size
const maxMemoryIncrease = Math.max(...memorySnapshots.map(s => s.heap - initialMemory.heapUsed));
const avgBatchSize = batchSizes.reduce((a, b) => a + b, 0) / batchSizes.length;
console.log(` Maximum memory increase: +${maxMemoryIncrease.toFixed(2)}MB`);
console.log(` Average batch size: ${avgBatchSize} emails`);
console.log(` Memory per email: ~${(maxMemoryIncrease / avgBatchSize * 1024).toFixed(1)}KB`);
console.log(` Email object lifecycle: ${maxMemoryIncrease < 10 ? 'Efficient' : 'Needs optimization'}`);
expect(maxMemoryIncrease).toBeLessThan(15); // Allow reasonable memory usage
smtpClient.close();
} finally {
server.close();
}
});
tap.test('CREL-05: Long-Running Client Memory Stability', async () => {
console.log('\n⏱ Testing long-running client memory stability...');
// Create test server
const server = net.createServer(socket => {
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server.address() as net.AddressInfo).port;
try {
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 2,
maxMessages: 1000
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Starting sustained email sending operation...');
const memoryMeasurements = [];
const totalEmails = 100; // Reduced for test efficiency
const measurementInterval = 20; // Measure every 20 emails
let emailsSent = 0;
let emailsFailed = 0;
for (let i = 0; i < totalEmails; i++) {
const email = new Email({
from: 'sender@longrunning.test',
to: [`recipient${i}@longrunning.test`],
subject: `Long Running Test ${i + 1}`,
text: `Sustained operation test email ${i + 1}`
});
try {
await smtpClient.sendMail(email);
emailsSent++;
} catch (error) {
emailsFailed++;
}
// Measure memory at intervals
if ((i + 1) % measurementInterval === 0) {
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const currentMemory = getMemoryUsage();
memorySnapshots.push({
pool: poolIndex + 1,
memoryMeasurements.push({
emailCount: i + 1,
heap: currentMemory.heapUsed,
rss: currentMemory.rss,
external: currentMemory.external
timestamp: Date.now()
});
console.log(` Memory after pool ${poolIndex + 1}: ${currentMemory.heapUsed}MB heap`);
console.log(` ${i + 1}/${totalEmails} emails: ${currentMemory.heapUsed}MB heap`);
}
console.log('\n Memory analysis:');
memorySnapshots.forEach((snapshot, index) => {
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
console.log(` Pool ${snapshot.pool}: +${memoryIncrease.toFixed(2)}MB heap increase`);
});
// Check for memory leaks (memory should not continuously increase)
const firstIncrease = memorySnapshots[0].heap - initialMemory.heapUsed;
const lastIncrease = memorySnapshots[memorySnapshots.length - 1].heap - initialMemory.heapUsed;
const leakGrowth = lastIncrease - firstIncrease;
console.log(` Memory leak assessment:`);
console.log(` First pool increase: +${firstIncrease.toFixed(2)}MB`);
console.log(` Final memory increase: +${lastIncrease.toFixed(2)}MB`);
console.log(` Memory growth across pools: +${leakGrowth.toFixed(2)}MB`);
console.log(` Memory management: ${leakGrowth < 2.0 ? 'Good (< 2MB growth)' : 'Potential leak detected'}`);
} finally {
testServer.close();
}
});
// Scenario 2: Email Object Memory Lifecycle
await test.test('Scenario 2: Email Object Memory Lifecycle', async () => {
console.log('\n📧 Testing email object memory lifecycle...');
console.log('\n Long-running memory analysis:');
console.log(` Emails sent: ${emailsSent}, Failed: ${emailsFailed}`);
const testServer = await createTestServer({
responseDelay: 10
memoryMeasurements.forEach((measurement, index) => {
const memoryIncrease = measurement.heap - initialMemory.heapUsed;
console.log(` After ${measurement.emailCount} emails: +${memoryIncrease.toFixed(2)}MB heap`);
});
try {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Phase 1: Creating large batches of email objects...');
const batchSizes = [50, 100, 150, 100, 50]; // Varying batch sizes
const memorySnapshots = [];
for (let batchIndex = 0; batchIndex < batchSizes.length; batchIndex++) {
const batchSize = batchSizes[batchIndex];
console.log(` Creating batch ${batchIndex + 1} with ${batchSize} emails...`);
const emails = [];
for (let i = 0; i < batchSize; i++) {
emails.push(new Email({
from: 'sender@emailmemory.test',
to: [`recipient${i}@emailmemory.test`],
subject: `Memory Lifecycle Test Batch ${batchIndex + 1} Email ${i + 1}`,
text: `Testing email object memory lifecycle. This is a moderately long email body to test memory usage patterns. Email ${i + 1} in batch ${batchIndex + 1} of ${batchSize} emails.`,
html: `<h1>Email ${i + 1}</h1><p>Testing memory patterns with HTML content. Batch ${batchIndex + 1}.</p>`,
messageId: `email-memory-${batchIndex}-${i}@emailmemory.test`
}));
}
console.log(` Sending batch ${batchIndex + 1}...`);
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
return { success: true };
}).catch(error => {
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Batch ${batchIndex + 1}: ${successful}/${batchSize} emails sent`);
// Clear email references
emails.length = 0;
// Force garbage collection
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const currentMemory = getMemoryUsage();
memorySnapshots.push({
batch: batchIndex + 1,
size: batchSize,
heap: currentMemory.heapUsed,
external: currentMemory.external
});
console.log(` Memory after batch ${batchIndex + 1}: ${currentMemory.heapUsed}MB heap`);
}
console.log('\n Email object memory analysis:');
memorySnapshots.forEach((snapshot, index) => {
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
console.log(` Batch ${snapshot.batch} (${snapshot.size} emails): +${memoryIncrease.toFixed(2)}MB`);
});
// Check if memory scales reasonably with email batch size
const maxMemoryIncrease = Math.max(...memorySnapshots.map(s => s.heap - initialMemory.heapUsed));
const avgBatchSize = batchSizes.reduce((a, b) => a + b, 0) / batchSizes.length;
// Analyze memory growth trend
if (memoryMeasurements.length >= 2) {
const firstMeasurement = memoryMeasurements[0];
const lastMeasurement = memoryMeasurements[memoryMeasurements.length - 1];
console.log(` Maximum memory increase: +${maxMemoryIncrease.toFixed(2)}MB`);
console.log(` Average batch size: ${avgBatchSize} emails`);
console.log(` Memory per email: ~${(maxMemoryIncrease / avgBatchSize * 1024).toFixed(1)}KB`);
console.log(` Email object lifecycle: ${maxMemoryIncrease < 10 ? 'Efficient' : 'Needs optimization'}`);
smtpClient.close();
} finally {
testServer.close();
const memoryGrowth = lastMeasurement.heap - firstMeasurement.heap;
const emailsProcessed = lastMeasurement.emailCount - firstMeasurement.emailCount;
const growthRate = (memoryGrowth / emailsProcessed) * 1000; // KB per email
console.log(` Memory growth over operation: +${memoryGrowth.toFixed(2)}MB`);
console.log(` Growth rate: ~${growthRate.toFixed(2)}KB per email`);
console.log(` Memory stability: ${growthRate < 10 ? 'Excellent' : growthRate < 25 ? 'Good' : 'Concerning'}`);
expect(growthRate).toBeLessThan(50); // Allow reasonable growth but detect major leaks
}
expect(emailsSent).toBeGreaterThanOrEqual(totalEmails - 5); // Most emails should succeed
smtpClient.close();
} finally {
server.close();
}
});
tap.test('CREL-05: Large Content Memory Management', async () => {
console.log('\n🌊 Testing large content memory management...');
// Create test server
const server = net.createServer(socket => {
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
// Scenario 3: Long-Running Client Memory Stability
await test.test('Scenario 3: Long-Running Client Memory Stability', async () => {
console.log('\n⏱ Testing long-running client memory stability...');
const testServer = await createTestServer({
responseDelay: 5 // Fast responses for sustained operation
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
try {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
maxMessages: 1000
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Starting sustained email sending operation...');
const memoryMeasurements = [];
const totalEmails = 200; // Reduced for test efficiency
const measurementInterval = 40; // Measure every 40 emails
let emailsSent = 0;
let emailsFailed = 0;
for (let i = 0; i < totalEmails; i++) {
const email = new Email({
from: 'sender@longrunning.test',
to: [`recipient${i}@longrunning.test`],
subject: `Long Running Test ${i + 1}`,
text: `Sustained operation test email ${i + 1}`,
messageId: `longrunning-${i}@longrunning.test`
});
try {
await smtpClient.sendMail(email);
emailsSent++;
} catch (error) {
emailsFailed++;
}
// Measure memory at intervals
if ((i + 1) % measurementInterval === 0) {
forceGC();
const currentMemory = getMemoryUsage();
memoryMeasurements.push({
emailCount: i + 1,
heap: currentMemory.heapUsed,
rss: currentMemory.rss,
timestamp: Date.now()
});
console.log(` ${i + 1}/${totalEmails} emails: ${currentMemory.heapUsed}MB heap`);
}
}
console.log('\n Long-running memory analysis:');
console.log(` Emails sent: ${emailsSent}, Failed: ${emailsFailed}`);
memoryMeasurements.forEach((measurement, index) => {
const memoryIncrease = measurement.heap - initialMemory.heapUsed;
console.log(` After ${measurement.emailCount} emails: +${memoryIncrease.toFixed(2)}MB heap`);
});
// Analyze memory growth trend
if (memoryMeasurements.length >= 2) {
const firstMeasurement = memoryMeasurements[0];
const lastMeasurement = memoryMeasurements[memoryMeasurements.length - 1];
const memoryGrowth = lastMeasurement.heap - firstMeasurement.heap;
const emailsProcessed = lastMeasurement.emailCount - firstMeasurement.emailCount;
const growthRate = (memoryGrowth / emailsProcessed) * 1000; // KB per email
console.log(` Memory growth over operation: +${memoryGrowth.toFixed(2)}MB`);
console.log(` Growth rate: ~${growthRate.toFixed(2)}KB per email`);
console.log(` Memory stability: ${growthRate < 5 ? 'Excellent' : growthRate < 15 ? 'Good' : 'Concerning'}`);
}
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 4: Buffer and Stream Memory Management
await test.test('Scenario 4: Buffer and Stream Memory Management', async () => {
console.log('\n🌊 Testing buffer and stream memory management...');
const testServer = await createTestServer({
responseDelay: 20,
onData: (data: string) => {
if (data.includes('large-attachment')) {
console.log(' [Server] Processing large attachment email');
}
}
const port = (server.address() as net.AddressInfo).port;
try {
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 1
});
try {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
streamingMode: true, // Enable streaming for large content
bufferManagement: true
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Testing with various content sizes...');
const contentSizes = [
{ size: 1024, name: '1KB' },
{ size: 10240, name: '10KB' },
{ size: 102400, name: '100KB' },
{ size: 256000, name: '250KB' }
];
for (const contentTest of contentSizes) {
console.log(` Testing ${contentTest.name} content size...`);
const beforeMemory = getMemoryUsage();
// Create large text content
const largeText = 'X'.repeat(contentTest.size);
const email = new Email({
from: 'sender@largemem.test',
to: ['recipient@largemem.test'],
subject: `Large Content Test - ${contentTest.name}`,
text: largeText
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Testing with various buffer sizes...');
const bufferSizes = [
{ size: 1024, name: '1KB' },
{ size: 10240, name: '10KB' },
{ size: 102400, name: '100KB' },
{ size: 512000, name: '500KB' },
{ size: 1048576, name: '1MB' }
];
for (const bufferTest of bufferSizes) {
console.log(` Testing ${bufferTest.name} buffer size...`);
const beforeMemory = getMemoryUsage();
// Create large text content
const largeText = 'X'.repeat(bufferTest.size);
const email = new Email({
from: 'sender@buffermem.test',
to: ['recipient@buffermem.test'],
subject: `Buffer Memory Test - ${bufferTest.name}`,
text: largeText,
messageId: `large-attachment-${bufferTest.size}@buffermem.test`
});
try {
const result = await smtpClient.sendMail(email);
console.log(`${bufferTest.name} email sent successfully`);
} catch (error) {
console.log(`${bufferTest.name} email failed: ${error.message}`);
}
// Force cleanup
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const afterMemory = getMemoryUsage();
const memoryDiff = afterMemory.heap - beforeMemory.heap;
console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`);
console.log(` Buffer efficiency: ${Math.abs(memoryDiff) < (bufferTest.size / 1024 / 1024) ? 'Good' : 'High memory usage'}`);
try {
await smtpClient.sendMail(email);
console.log(`${contentTest.name} email sent successfully`);
} catch (error) {
console.log(`${contentTest.name} email failed: ${error.message}`);
}
const finalMemory = getMemoryUsage();
const totalMemoryIncrease = finalMemory.heap - initialMemory.heapUsed;
console.log(`\n Buffer memory management summary:`);
console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`);
console.log(` Buffer management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 5: Event Listener Memory Management
await test.test('Scenario 5: Event Listener Memory Management', async () => {
console.log('\n🎧 Testing event listener memory management...');
const testServer = await createTestServer({
responseDelay: 15
});
try {
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Phase 1: Creating clients with many event listeners...');
const clients = [];
const memorySnapshots = [];
for (let clientIndex = 0; clientIndex < 10; clientIndex++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1
});
// Add multiple event listeners to test memory management
const eventHandlers = [];
for (let i = 0; i < 5; i++) {
const handler = (data: any) => {
// Event handler logic
};
eventHandlers.push(handler);
// Simulate adding event listeners (using mock events)
if (smtpClient.on) {
smtpClient.on('connect', handler);
smtpClient.on('error', handler);
smtpClient.on('close', handler);
}
}
clients.push({ client: smtpClient, handlers: eventHandlers });
// Send a test email through each client
const email = new Email({
from: 'sender@eventmem.test',
to: ['recipient@eventmem.test'],
subject: `Event Memory Test ${clientIndex + 1}`,
text: `Testing event listener memory management ${clientIndex + 1}`,
messageId: `event-memory-${clientIndex}@eventmem.test`
});
try {
await smtpClient.sendMail(email);
console.log(` Client ${clientIndex + 1}: Email sent`);
} catch (error) {
console.log(` Client ${clientIndex + 1}: Failed - ${error.message}`);
}
// Measure memory after each client
if ((clientIndex + 1) % 3 === 0) {
forceGC();
const currentMemory = getMemoryUsage();
memorySnapshots.push({
clientCount: clientIndex + 1,
heap: currentMemory.heapUsed
});
console.log(` Memory after ${clientIndex + 1} clients: ${currentMemory.heapUsed}MB`);
}
}
console.log(' Phase 2: Closing all clients and removing listeners...');
for (let i = 0; i < clients.length; i++) {
const { client, handlers } = clients[i];
// Remove event listeners
if (client.removeAllListeners) {
client.removeAllListeners();
}
// Close client
client.close();
if ((i + 1) % 3 === 0) {
forceGC();
const currentMemory = getMemoryUsage();
console.log(` Memory after closing ${i + 1} clients: ${currentMemory.heapUsed}MB`);
}
}
// Final memory check
// Force cleanup
forceGC();
await new Promise(resolve => setTimeout(resolve, 200));
const finalMemory = getMemoryUsage();
await new Promise(resolve => setTimeout(resolve, 100));
const afterMemory = getMemoryUsage();
const memoryDiff = afterMemory.heapUsed - beforeMemory.heapUsed;
console.log('\n Event listener memory analysis:');
memorySnapshots.forEach(snapshot => {
const increase = snapshot.heap - initialMemory.heapUsed;
console.log(` ${snapshot.clientCount} clients: +${increase.toFixed(2)}MB`);
});
const finalIncrease = finalMemory.heap - initialMemory.heapUsed;
console.log(` Final memory after cleanup: +${finalIncrease.toFixed(2)}MB`);
console.log(` Event listener cleanup: ${finalIncrease < 1 ? 'Excellent' : 'Memory retained'}`);
} finally {
testServer.close();
console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`);
console.log(` Efficiency: ${Math.abs(memoryDiff) < (contentTest.size / 1024 / 1024) * 2 ? 'Good' : 'High memory usage'}`);
}
});
const finalMemory = getMemoryUsage();
const totalMemoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;
console.log(`\n Large content memory summary:`);
console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`);
console.log(` Memory management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`);
expect(totalMemoryIncrease).toBeLessThan(20); // Allow reasonable memory usage for large content
smtpClient.close();
} finally {
server.close();
}
});
tap.test('CREL-05: Test Summary', async () => {
console.log('\n✅ CREL-05: Memory Leak Prevention Reliability Tests completed');
console.log('🧠 All memory management scenarios tested successfully');
});
});
tap.start();

View File

@ -1,586 +1,291 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import { createTestServer } from '../../helpers/server.loader.js';
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as fs from 'fs';
import * as path from 'path';
tap.test('CREL-07: Resource Cleanup Reliability Tests', async () => {
console.log('\n🧹 Testing SMTP Client Resource Cleanup Reliability');
console.log('=' .repeat(60));
const tempDir = path.join(process.cwd(), '.nogit', 'test-resource-cleanup');
// Ensure test directory exists
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Helper function to count active resources
const getResourceCounts = () => {
const usage = process.memoryUsage();
return {
memory: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
handles: process._getActiveHandles ? process._getActiveHandles().length : 0,
requests: process._getActiveRequests ? process._getActiveRequests().length : 0
};
// Helper function to count active resources
const getResourceCounts = () => {
const usage = process.memoryUsage();
return {
memory: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
handles: (process as any)._getActiveHandles ? (process as any)._getActiveHandles().length : 0,
requests: (process as any)._getActiveRequests ? (process as any)._getActiveRequests().length : 0
};
};
// Scenario 1: Connection Pool Cleanup
await test.test('Scenario 1: Connection Pool Cleanup', async () => {
console.log('\n🏊 Testing connection pool resource cleanup...');
let openConnections = 0;
let closedConnections = 0;
const connectionIds: string[] = [];
const testServer = await createTestServer({
responseDelay: 20,
onConnect: (socket: any) => {
openConnections++;
const connId = `CONN-${openConnections}`;
connectionIds.push(connId);
console.log(` [Server] ${connId} opened (total open: ${openConnections})`);
socket.on('close', () => {
closedConnections++;
console.log(` [Server] Connection closed (total closed: ${closedConnections})`);
});
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Phase 1: Creating and using connection pools...');
const clients = [];
// Scenario 1: Basic Resource Cleanup
tap.test('CREL-07: Basic Resource Cleanup', async () => {
console.log('\n🧹 Testing SMTP Client Resource Cleanup');
console.log('=' .repeat(60));
let connections = 0;
let disconnections = 0;
const testServer = await createTestServer({
onConnection: (socket: any) => {
connections++;
console.log(` [Server] Connection opened (total: ${connections})`);
for (let poolIndex = 0; poolIndex < 4; poolIndex++) {
console.log(` Creating connection pool ${poolIndex + 1}...`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 3,
maxMessages: 20,
resourceCleanup: true,
autoCleanupInterval: 1000
});
clients.push(smtpClient);
// Send emails through this pool
const emails = [];
for (let i = 0; i < 5; i++) {
emails.push(new Email({
from: `sender${poolIndex}@cleanup.test`,
to: [`recipient${i}@cleanup.test`],
subject: `Pool Cleanup Test ${poolIndex + 1}-${i + 1}`,
text: `Testing connection pool cleanup ${poolIndex + 1}-${i + 1}`,
messageId: `pool-cleanup-${poolIndex}-${i}@cleanup.test`
}));
}
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Pool ${poolIndex + 1} Email ${index + 1} sent`);
return { success: true };
}).catch(error => {
console.log(` ✗ Pool ${poolIndex + 1} Email ${index + 1} failed`);
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
}
const afterCreation = getResourceCounts();
console.log(` After pool creation: ${afterCreation.memory}MB memory, ${afterCreation.handles} handles`);
console.log(' Phase 2: Closing all pools and testing cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing pool ${i + 1}...`);
clients[i].close();
// Wait for cleanup to occur
await new Promise(resolve => setTimeout(resolve, 200));
const currentResources = getResourceCounts();
console.log(` Resources after closing pool ${i + 1}: ${currentResources.memory}MB, ${currentResources.handles} handles`);
}
// Wait for all cleanup to complete
await new Promise(resolve => setTimeout(resolve, 1000));
const finalResources = getResourceCounts();
console.log(`\n Resource cleanup assessment:`);
console.log(` Initial: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(` Final: ${finalResources.memory}MB memory, ${finalResources.handles} handles`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Good' : 'Memory retained'}`);
console.log(` Handle cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Good' : 'Handles remaining'}`);
console.log(` Connection cleanup: ${closedConnections >= openConnections - 1 ? 'Complete' : 'Incomplete'}`);
} finally {
testServer.close();
socket.on('close', () => {
disconnections++;
console.log(` [Server] Connection closed (total closed: ${disconnections})`);
});
}
});
// Scenario 2: File Handle and Stream Cleanup
await test.test('Scenario 2: File Handle and Stream Cleanup', async () => {
console.log('\n📁 Testing file handle and stream cleanup...');
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating SMTP clients and sending emails...');
const clients = [];
const testServer = await createTestServer({
responseDelay: 30,
onData: (data: string) => {
if (data.includes('Attachment Test')) {
console.log(' [Server] Processing attachment email');
}
// Create multiple clients
for (let i = 0; i < 3; i++) {
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port
});
clients.push(smtpClient);
// Send a test email
const email = new Email({
from: `sender${i}@cleanup.test`,
to: [`recipient${i}@cleanup.test`],
subject: `Cleanup Test ${i + 1}`,
text: `Testing connection cleanup ${i + 1}`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${i + 1} email sent`);
} catch (error) {
console.log(` ✗ Client ${i + 1} failed: ${error.message}`);
}
}
const afterSending = getResourceCounts();
console.log(` After sending: ${afterSending.memory}MB memory, ${afterSending.handles} handles`);
console.log(' Closing all clients...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1}...`);
clients[i].close();
await new Promise(resolve => setTimeout(resolve, 100));
}
// Wait for cleanup to complete
await new Promise(resolve => setTimeout(resolve, 500));
const finalResources = getResourceCounts();
console.log(`\n Resource cleanup assessment:`);
console.log(` Initial: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(` Final: ${finalResources.memory}MB memory, ${finalResources.handles} handles`);
console.log(` Connections opened: ${connections}`);
console.log(` Connections closed: ${disconnections}`);
console.log(` Cleanup: ${disconnections >= connections - 1 ? 'Complete' : 'Incomplete'}`);
expect(disconnections).toBeGreaterThanOrEqual(connections - 1);
} finally {
testServer.server.close();
}
});
// Scenario 2: Multiple Close Safety
tap.test('CREL-07: Multiple Close Safety', async () => {
console.log('\n🔁 Testing multiple close calls safety...');
const testServer = await createTestServer({});
try {
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
// Send a test email
const email = new Email({
from: 'sender@multiclose.test',
to: ['recipient@multiclose.test'],
subject: 'Multiple Close Test',
text: 'Testing multiple close calls'
});
console.log(' Creating temporary files for attachment testing...');
const tempFiles: string[] = [];
for (let i = 0; i < 8; i++) {
const fileName = path.join(tempDir, `attachment-${i}.txt`);
const content = `Attachment content ${i + 1}\n${'X'.repeat(1000)}`; // 1KB files
fs.writeFileSync(fileName, content);
tempFiles.push(fileName);
console.log(` Created temp file: ${fileName}`);
console.log(' Sending test email...');
await smtpClient.sendMail(email);
console.log(' ✓ Email sent successfully');
console.log(' Attempting multiple close calls...');
let closeErrors = 0;
for (let i = 0; i < 5; i++) {
try {
smtpClient.close();
console.log(` ✓ Close call ${i + 1} completed`);
} catch (error) {
closeErrors++;
console.log(` ✗ Close call ${i + 1} error: ${error.message}`);
}
}
const smtpClient = createSmtpClient({
console.log(` Close errors: ${closeErrors}`);
console.log(` Safety: ${closeErrors === 0 ? 'Safe' : 'Issues detected'}`);
expect(closeErrors).toEqual(0);
} finally {
testServer.server.close();
}
});
// Scenario 3: Error Recovery and Cleanup
tap.test('CREL-07: Error Recovery and Cleanup', async () => {
console.log('\n❌ Testing error recovery and cleanup...');
let errorMode = false;
let requestCount = 0;
const testServer = await createTestServer({
onConnection: (socket: any) => {
requestCount++;
if (errorMode && requestCount % 2 === 0) {
console.log(` [Server] Simulating connection error`);
setTimeout(() => socket.destroy(), 50);
}
}
});
try {
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
connectionTimeout: 2000
});
console.log(' Phase 1: Normal operation...');
const normalEmail = new Email({
from: 'sender@test.com',
to: ['recipient@test.com'],
subject: 'Normal Test',
text: 'Testing normal operation'
});
let normalResult = false;
try {
await smtpClient.sendMail(normalEmail);
normalResult = true;
console.log(' ✓ Normal operation successful');
} catch (error) {
console.log(' ✗ Normal operation failed');
}
console.log(' Phase 2: Error injection...');
errorMode = true;
let errorCount = 0;
for (let i = 0; i < 3; i++) {
try {
const errorEmail = new Email({
from: 'sender@error.test',
to: ['recipient@error.test'],
subject: `Error Test ${i + 1}`,
text: 'Testing error handling'
});
await smtpClient.sendMail(errorEmail);
console.log(` ✓ Email ${i + 1} sent (recovered)`);
} catch (error) {
errorCount++;
console.log(` ✗ Email ${i + 1} failed as expected`);
}
}
console.log(' Phase 3: Recovery...');
errorMode = false;
const recoveryEmail = new Email({
from: 'sender@recovery.test',
to: ['recipient@recovery.test'],
subject: 'Recovery Test',
text: 'Testing recovery'
});
let recovered = false;
try {
await smtpClient.sendMail(recoveryEmail);
recovered = true;
console.log(' ✓ Recovery successful');
} catch (error) {
console.log(' ✗ Recovery failed');
}
// Close and cleanup
smtpClient.close();
console.log(`\n Error recovery assessment:`);
console.log(` Normal operation: ${normalResult ? 'Success' : 'Failed'}`);
console.log(` Errors encountered: ${errorCount}`);
console.log(` Recovery: ${recovered ? 'Successful' : 'Failed'}`);
expect(normalResult).toEqual(true);
expect(errorCount).toBeGreaterThan(0);
} finally {
testServer.server.close();
}
});
// Scenario 4: Rapid Connect/Disconnect
tap.test('CREL-07: Rapid Connect/Disconnect Cycles', async () => {
console.log('\n⚡ Testing rapid connect/disconnect cycles...');
const testServer = await createTestServer({});
try {
console.log(' Performing rapid connect/disconnect cycles...');
let successful = 0;
let failed = 0;
for (let cycle = 0; cycle < 5; cycle++) {
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
streamCleanup: true,
fileHandleManagement: true
connectionTimeout: 1000
});
console.log(' Sending emails with file attachments...');
const emailPromises = tempFiles.map((filePath, index) => {
const email = new Email({
from: 'sender@filehandle.test',
to: [`recipient${index}@filehandle.test`],
subject: `File Handle Cleanup Test ${index + 1}`,
text: `Testing file handle cleanup with attachment ${index + 1}`,
attachments: [{
filename: `attachment-${index}.txt`,
path: filePath
}],
messageId: `filehandle-${index}@filehandle.test`
});
try {
// Quick verify to establish connection
await smtpClient.verify();
successful++;
console.log(` ✓ Cycle ${cycle + 1}: Connected`);
} catch (error) {
failed++;
console.log(` ✗ Cycle ${cycle + 1}: Failed`);
}
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Email ${index + 1} with attachment sent`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
return { success: false, index, error };
});
});
const results = await Promise.all(emailPromises);
const successful = results.filter(r => r.success).length;
console.log(` Email sending completed: ${successful}/${tempFiles.length} successful`);
const afterSending = getResourceCounts();
console.log(` Resources after sending: ${afterSending.memory}MB memory, ${afterSending.handles} handles`);
console.log(' Closing client and testing file handle cleanup...');
// Immediately close
smtpClient.close();
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 500));
// Clean up temp files
tempFiles.forEach(filePath => {
try {
fs.unlinkSync(filePath);
console.log(` Cleaned up: ${filePath}`);
} catch (error) {
console.log(` Failed to clean up ${filePath}: ${error.message}`);
}
});
const finalResources = getResourceCounts();
console.log(`\n File handle cleanup assessment:`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` After sending: ${afterSending.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Handle management: ${finalResources.handles <= initialResources.handles + 2 ? 'Effective' : 'Handles leaked'}`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 3 ? 'Good' : 'Memory retained'}`);
} finally {
testServer.close();
// Brief pause between cycles
await new Promise(resolve => setTimeout(resolve, 50));
}
});
// Scenario 3: Timer and Interval Cleanup
await test.test('Scenario 3: Timer and Interval Cleanup', async () => {
console.log('\n⏰ Testing timer and interval cleanup...');
const testServer = await createTestServer({
responseDelay: 40
});
console.log(`\n Rapid cycle results:`);
console.log(` Successful connections: ${successful}`);
console.log(` Failed connections: ${failed}`);
console.log(` Success rate: ${(successful / (successful + failed) * 100).toFixed(1)}%`);
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
expect(successful).toBeGreaterThan(0);
console.log(' Creating clients with various timer configurations...');
const clients = [];
for (let i = 0; i < 3; i++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
// Timer configurations
connectionTimeout: 2000,
keepAlive: true,
keepAliveInterval: 1000,
retryDelay: 500,
healthCheckInterval: 800,
timerCleanup: true
});
clients.push(smtpClient);
// Send an email to activate timers
const email = new Email({
from: `sender${i}@timer.test`,
to: [`recipient${i}@timer.test`],
subject: `Timer Cleanup Test ${i + 1}`,
text: `Testing timer cleanup ${i + 1}`,
messageId: `timer-${i}@timer.test`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${i + 1} email sent (timers active)`);
} catch (error) {
console.log(` ✗ Client ${i + 1} failed: ${error.message}`);
}
}
// Let timers run for a while
console.log(' Allowing timers to run...');
await new Promise(resolve => setTimeout(resolve, 2000));
const withTimers = getResourceCounts();
console.log(` Resources with active timers: ${withTimers.memory}MB memory, ${withTimers.handles} handles`);
console.log(' Closing clients and testing timer cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1}...`);
clients[i].close();
// Wait for timer cleanup
await new Promise(resolve => setTimeout(resolve, 300));
const currentResources = getResourceCounts();
console.log(` Resources after closing client ${i + 1}: ${currentResources.handles} handles`);
}
// Wait for all timer cleanup to complete
await new Promise(resolve => setTimeout(resolve, 1500));
const finalResources = getResourceCounts();
console.log(`\n Timer cleanup assessment:`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` With timers: ${withTimers.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Timer cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Timers remaining'}`);
console.log(` Resource management: ${finalResources.handles < withTimers.handles ? 'Effective' : 'Incomplete'}`);
} finally {
testServer.close();
}
});
// Scenario 4: Event Listener and Callback Cleanup
await test.test('Scenario 4: Event Listener and Callback Cleanup', async () => {
console.log('\n🎧 Testing event listener and callback cleanup...');
const testServer = await createTestServer({
responseDelay: 25,
onConnect: () => {
console.log(' [Server] Connection for event cleanup test');
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory`);
console.log(' Creating clients with extensive event listeners...');
const clients = [];
const eventHandlers: any[] = [];
for (let clientIndex = 0; clientIndex < 5; clientIndex++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
eventCleanup: true
});
clients.push(smtpClient);
// Add multiple event listeners
const handlers = [];
for (let eventIndex = 0; eventIndex < 6; eventIndex++) {
const handler = (data: any) => {
// Event handler with closure
console.log(` Event ${eventIndex} from client ${clientIndex}: ${typeof data}`);
};
handlers.push(handler);
// Add event listeners (simulated)
if (smtpClient.on) {
smtpClient.on('connect', handler);
smtpClient.on('error', handler);
smtpClient.on('close', handler);
smtpClient.on('data', handler);
}
}
eventHandlers.push(handlers);
// Send test email to trigger events
const email = new Email({
from: `sender${clientIndex}@eventcleanup.test`,
to: [`recipient${clientIndex}@eventcleanup.test`],
subject: `Event Cleanup Test ${clientIndex + 1}`,
text: `Testing event listener cleanup ${clientIndex + 1}`,
messageId: `event-cleanup-${clientIndex}@eventcleanup.test`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${clientIndex + 1} email sent (events active)`);
} catch (error) {
console.log(` ✗ Client ${clientIndex + 1} failed: ${error.message}`);
}
}
const withEvents = getResourceCounts();
console.log(` Resources with event listeners: ${withEvents.memory}MB memory`);
console.log(' Closing clients and testing event listener cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1} and removing ${eventHandlers[i].length} event listeners...`);
// Remove event listeners manually first
if (clients[i].removeAllListeners) {
clients[i].removeAllListeners();
}
// Close client
clients[i].close();
// Clear handler references
eventHandlers[i].length = 0;
await new Promise(resolve => setTimeout(resolve, 100));
}
// Force garbage collection if available
if (global.gc) {
global.gc();
global.gc();
}
await new Promise(resolve => setTimeout(resolve, 500));
const finalResources = getResourceCounts();
console.log(`\n Event listener cleanup assessment:`);
console.log(` Initial memory: ${initialResources.memory}MB`);
console.log(` With events: ${withEvents.memory}MB`);
console.log(` Final memory: ${finalResources.memory}MB`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Effective' : 'Memory retained'}`);
console.log(` Event cleanup: ${finalResources.memory < withEvents.memory ? 'Successful' : 'Partial'}`);
} finally {
testServer.close();
}
});
// Scenario 5: Error State Cleanup
await test.test('Scenario 5: Error State Cleanup', async () => {
console.log('\n💥 Testing error state cleanup...');
let connectionCount = 0;
let errorInjectionActive = false;
const testServer = await createTestServer({
responseDelay: 30,
onConnect: (socket: any) => {
connectionCount++;
console.log(` [Server] Connection ${connectionCount}`);
if (errorInjectionActive && connectionCount > 2) {
console.log(` [Server] Injecting connection error ${connectionCount}`);
setTimeout(() => socket.destroy(), 50);
}
},
onData: (data: string, socket: any) => {
if (errorInjectionActive && data.includes('MAIL FROM')) {
socket.write('500 Internal server error\r\n');
return false;
}
return true;
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating clients for error state testing...');
const clients = [];
for (let i = 0; i < 4; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
retryDelay: 200,
retries: 2,
errorStateCleanup: true,
gracefulErrorHandling: true
}));
}
console.log(' Phase 1: Normal operation...');
const email1 = new Email({
from: 'sender@errorstate.test',
to: ['recipient1@errorstate.test'],
subject: 'Error State Test - Normal',
text: 'Normal operation before errors',
messageId: 'error-normal@errorstate.test'
});
try {
await clients[0].sendMail(email1);
console.log(' ✓ Normal operation successful');
} catch (error) {
console.log(' ✗ Normal operation failed');
}
const afterNormal = getResourceCounts();
console.log(` Resources after normal operation: ${afterNormal.handles} handles`);
console.log(' Phase 2: Error injection phase...');
errorInjectionActive = true;
const errorEmails = [];
for (let i = 0; i < 6; i++) {
errorEmails.push(new Email({
from: 'sender@errorstate.test',
to: [`recipient${i}@errorstate.test`],
subject: `Error State Test ${i + 1}`,
text: `Testing error state cleanup ${i + 1}`,
messageId: `error-state-${i}@errorstate.test`
}));
}
const errorPromises = errorEmails.map((email, index) => {
const client = clients[index % clients.length];
return client.sendMail(email).then(result => {
console.log(` ✓ Error email ${index + 1} unexpectedly succeeded`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Error email ${index + 1} failed as expected`);
return { success: false, index, error: error.message };
});
});
const errorResults = await Promise.all(errorPromises);
const afterErrors = getResourceCounts();
console.log(` Resources after error phase: ${afterErrors.handles} handles`);
console.log(' Phase 3: Recovery and cleanup...');
errorInjectionActive = false;
// Test recovery
const recoveryEmail = new Email({
from: 'sender@errorstate.test',
to: ['recovery@errorstate.test'],
subject: 'Error State Test - Recovery',
text: 'Testing recovery after errors',
messageId: 'error-recovery@errorstate.test'
});
try {
await clients[0].sendMail(recoveryEmail);
console.log(' ✓ Recovery successful');
} catch (error) {
console.log(' ✗ Recovery failed');
}
console.log(' Closing all clients...');
clients.forEach((client, index) => {
console.log(` Closing client ${index + 1}...`);
client.close();
});
await new Promise(resolve => setTimeout(resolve, 1000));
const finalResources = getResourceCounts();
const errorSuccessful = errorResults.filter(r => r.success).length;
const errorFailed = errorResults.filter(r => !r.success).length;
console.log(`\n Error state cleanup assessment:`);
console.log(` Error phase results: ${errorSuccessful} succeeded, ${errorFailed} failed`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` After errors: ${afterErrors.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Error state cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Incomplete'}`);
console.log(` Recovery capability: ${errorFailed > 0 ? 'Error handling active' : 'No errors detected'}`);
console.log(` Resource management: ${finalResources.handles < afterErrors.handles ? 'Effective' : 'Needs improvement'}`);
} finally {
testServer.close();
}
});
// Cleanup test directory
try {
if (fs.existsSync(tempDir)) {
const files = fs.readdirSync(tempDir);
for (const file of files) {
const filePath = path.join(tempDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
fs.rmdirSync(tempDir);
console.log('\n🧹 Test directory cleaned up successfully');
}
} catch (error) {
console.log(`\n⚠ Warning: Could not clean up test directory: ${error.message}`);
} finally {
testServer.server.close();
}
});
tap.test('CREL-07: Test Summary', async () => {
console.log('\n✅ CREL-07: Resource Cleanup Reliability Tests completed');
console.log('🧹 All resource cleanup scenarios tested successfully');
});
});
tap.start();