update
This commit is contained in:
@ -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();
|
Reference in New Issue
Block a user