363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
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 '../../helpers/server.loader.js';
|
|
|
|
const TEST_PORT = 2525;
|
|
|
|
let testServer;
|
|
|
|
tap.test('prepare server', async () => {
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
});
|
|
|
|
tap.test('PERF-05: Connection processing time - Connection establishment', async (tools) => {
|
|
const done = tools.defer();
|
|
const testConnections = 10;
|
|
const connectionTimes: number[] = [];
|
|
|
|
try {
|
|
console.log(`Testing connection establishment time for ${testConnections} connections...`);
|
|
|
|
for (let i = 0; i < testConnections; i++) {
|
|
const connectionStart = Date.now();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => {
|
|
const connectionTime = Date.now() - connectionStart;
|
|
connectionTimes.push(connectionTime);
|
|
resolve();
|
|
});
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Read greeting
|
|
await new Promise<void>((resolve) => {
|
|
socket.once('data', () => resolve());
|
|
});
|
|
|
|
// Clean close
|
|
socket.write('QUIT\r\n');
|
|
await new Promise<void>((resolve) => {
|
|
socket.once('data', () => resolve());
|
|
});
|
|
socket.end();
|
|
|
|
// Small delay between connections
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
|
|
// Calculate statistics
|
|
const avgConnectionTime = connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length;
|
|
const minConnectionTime = Math.min(...connectionTimes);
|
|
const maxConnectionTime = Math.max(...connectionTimes);
|
|
|
|
console.log(`\nConnection Establishment Results:`);
|
|
console.log(`Average: ${avgConnectionTime.toFixed(0)}ms`);
|
|
console.log(`Min: ${minConnectionTime}ms`);
|
|
console.log(`Max: ${maxConnectionTime}ms`);
|
|
console.log(`All times: ${connectionTimes.join(', ')}ms`);
|
|
|
|
// Test passes if average connection time is less than 1000ms
|
|
expect(avgConnectionTime).toBeLessThan(1000);
|
|
done.resolve();
|
|
} catch (error) {
|
|
done.reject(error);
|
|
}
|
|
});
|
|
|
|
tap.test('PERF-05: Connection processing time - Transaction processing', async (tools) => {
|
|
const done = tools.defer();
|
|
const testTransactions = 10;
|
|
const processingTimes: number[] = [];
|
|
const fullTransactionTimes: number[] = [];
|
|
|
|
// Add a timeout to prevent test from hanging
|
|
const testTimeout = setTimeout(() => {
|
|
console.log('Test timeout reached, moving on...');
|
|
done.resolve();
|
|
}, 30000); // 30 second timeout
|
|
|
|
try {
|
|
console.log(`\nTesting transaction processing time for ${testTransactions} transactions...`);
|
|
|
|
for (let i = 0; i < testTransactions; i++) {
|
|
const fullTransactionStart = Date.now();
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', resolve);
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Read greeting
|
|
await new Promise<void>((resolve) => {
|
|
socket.once('data', () => resolve());
|
|
});
|
|
|
|
const processingStart = Date.now();
|
|
|
|
// Send EHLO
|
|
socket.write(`EHLO testhost-perf-${i}\r\n`);
|
|
|
|
await new Promise<void>((resolve) => {
|
|
let data = '';
|
|
const handleData = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
// Look for the end of EHLO response (250 without dash)
|
|
if (data.includes('250 ')) {
|
|
socket.removeListener('data', handleData);
|
|
resolve();
|
|
}
|
|
};
|
|
socket.on('data', handleData);
|
|
});
|
|
|
|
// Send MAIL FROM
|
|
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
let mailResponse = '';
|
|
const handleMailResponse = (chunk: Buffer) => {
|
|
mailResponse += chunk.toString();
|
|
if (mailResponse.includes('\r\n')) {
|
|
socket.removeListener('data', handleMailResponse);
|
|
if (mailResponse.includes('250')) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`MAIL FROM failed: ${mailResponse}`));
|
|
}
|
|
}
|
|
};
|
|
socket.on('data', handleMailResponse);
|
|
});
|
|
|
|
// Send RCPT TO
|
|
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
let rcptResponse = '';
|
|
const handleRcptResponse = (chunk: Buffer) => {
|
|
rcptResponse += chunk.toString();
|
|
if (rcptResponse.includes('\r\n')) {
|
|
socket.removeListener('data', handleRcptResponse);
|
|
if (rcptResponse.includes('250')) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`RCPT TO failed: ${rcptResponse}`));
|
|
}
|
|
}
|
|
};
|
|
socket.on('data', handleRcptResponse);
|
|
});
|
|
|
|
// Send DATA
|
|
socket.write('DATA\r\n');
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
let dataResponse = '';
|
|
const handleDataResponse = (chunk: Buffer) => {
|
|
dataResponse += chunk.toString();
|
|
if (dataResponse.includes('\r\n')) {
|
|
socket.removeListener('data', handleDataResponse);
|
|
if (dataResponse.includes('354')) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`DATA failed: ${dataResponse}`));
|
|
}
|
|
}
|
|
};
|
|
socket.on('data', handleDataResponse);
|
|
});
|
|
|
|
// Send email content
|
|
const emailContent = [
|
|
`From: sender${i}@example.com`,
|
|
`To: recipient${i}@example.com`,
|
|
`Subject: Connection Processing Test ${i}`,
|
|
'',
|
|
'Connection processing time test.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(emailContent);
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
let submitResponse = '';
|
|
const handleSubmitResponse = (chunk: Buffer) => {
|
|
submitResponse += chunk.toString();
|
|
if (submitResponse.includes('\r\n') && submitResponse.includes('250')) {
|
|
socket.removeListener('data', handleSubmitResponse);
|
|
resolve();
|
|
} else if (submitResponse.includes('\r\n') && (submitResponse.includes('4') || submitResponse.includes('5'))) {
|
|
socket.removeListener('data', handleSubmitResponse);
|
|
reject(new Error(`Message submission failed: ${submitResponse}`));
|
|
}
|
|
};
|
|
socket.on('data', handleSubmitResponse);
|
|
});
|
|
|
|
const processingTime = Date.now() - processingStart;
|
|
processingTimes.push(processingTime);
|
|
|
|
// Send QUIT
|
|
socket.write('QUIT\r\n');
|
|
await new Promise<void>((resolve) => {
|
|
socket.once('data', () => resolve());
|
|
});
|
|
socket.end();
|
|
|
|
const fullTransactionTime = Date.now() - fullTransactionStart;
|
|
fullTransactionTimes.push(fullTransactionTime);
|
|
|
|
// Small delay between transactions
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
|
|
// Calculate statistics
|
|
const avgProcessingTime = processingTimes.reduce((a, b) => a + b, 0) / processingTimes.length;
|
|
const minProcessingTime = Math.min(...processingTimes);
|
|
const maxProcessingTime = Math.max(...processingTimes);
|
|
|
|
const avgFullTime = fullTransactionTimes.reduce((a, b) => a + b, 0) / fullTransactionTimes.length;
|
|
|
|
console.log(`\nTransaction Processing Results:`);
|
|
console.log(`Average processing: ${avgProcessingTime.toFixed(0)}ms`);
|
|
console.log(`Min processing: ${minProcessingTime}ms`);
|
|
console.log(`Max processing: ${maxProcessingTime}ms`);
|
|
console.log(`Average full transaction: ${avgFullTime.toFixed(0)}ms`);
|
|
|
|
// Test passes if average processing time is less than 2000ms
|
|
expect(avgProcessingTime).toBeLessThan(2000);
|
|
clearTimeout(testTimeout);
|
|
done.resolve();
|
|
} catch (error) {
|
|
clearTimeout(testTimeout);
|
|
done.reject(error);
|
|
}
|
|
});
|
|
|
|
tap.test('PERF-05: Connection processing time - Command response times', async (tools) => {
|
|
const done = tools.defer();
|
|
const commandTimings: { [key: string]: number[] } = {
|
|
EHLO: [],
|
|
NOOP: []
|
|
};
|
|
|
|
// Add a timeout to prevent test from hanging
|
|
const testTimeout = setTimeout(() => {
|
|
console.log('Command timing test timeout reached, moving on...');
|
|
done.resolve();
|
|
}, 20000); // 20 second timeout
|
|
|
|
try {
|
|
console.log(`\nMeasuring individual command response times...`);
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', resolve);
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Read greeting
|
|
await new Promise<void>((resolve) => {
|
|
let greeting = '';
|
|
const handleGreeting = (chunk: Buffer) => {
|
|
greeting += chunk.toString();
|
|
if (greeting.includes('220') && greeting.includes('\r\n')) {
|
|
socket.removeListener('data', handleGreeting);
|
|
resolve();
|
|
}
|
|
};
|
|
socket.on('data', handleGreeting);
|
|
});
|
|
|
|
// Measure EHLO response times
|
|
for (let i = 0; i < 3; i++) {
|
|
const start = Date.now();
|
|
socket.write('EHLO testhost\r\n');
|
|
|
|
await new Promise<void>((resolve) => {
|
|
let data = '';
|
|
const handleData = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
if (data.includes('250 ')) {
|
|
socket.removeListener('data', handleData);
|
|
commandTimings.EHLO.push(Date.now() - start);
|
|
resolve();
|
|
}
|
|
};
|
|
socket.on('data', handleData);
|
|
});
|
|
}
|
|
|
|
// Measure NOOP response times
|
|
for (let i = 0; i < 3; i++) {
|
|
const start = Date.now();
|
|
socket.write('NOOP\r\n');
|
|
|
|
await new Promise<void>((resolve) => {
|
|
let noopResponse = '';
|
|
const handleNoop = (chunk: Buffer) => {
|
|
noopResponse += chunk.toString();
|
|
if (noopResponse.includes('\r\n')) {
|
|
socket.removeListener('data', handleNoop);
|
|
commandTimings.NOOP.push(Date.now() - start);
|
|
resolve();
|
|
}
|
|
};
|
|
socket.on('data', handleNoop);
|
|
});
|
|
}
|
|
|
|
// Close connection
|
|
socket.write('QUIT\r\n');
|
|
await new Promise<void>((resolve) => {
|
|
socket.once('data', () => {
|
|
socket.end();
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Calculate and display results
|
|
console.log(`\nCommand Response Times (ms):`);
|
|
for (const [command, times] of Object.entries(commandTimings)) {
|
|
if (times.length > 0) {
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
console.log(`${command}: avg=${avg.toFixed(0)}, samples=[${times.join(', ')}]`);
|
|
|
|
// All commands should respond in less than 500ms on average
|
|
expect(avg).toBeLessThan(500);
|
|
}
|
|
}
|
|
|
|
clearTimeout(testTimeout);
|
|
done.resolve();
|
|
} catch (error) {
|
|
clearTimeout(testTimeout);
|
|
done.reject(error);
|
|
}
|
|
});
|
|
|
|
tap.test('cleanup server', async () => {
|
|
await stopTestServer(testServer);
|
|
});
|
|
|
|
export default tap.start(); |