Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled

Full-stack web application for Telegram management
- Frontend: Vue 3 + Vben Admin
- Backend: NestJS
- Features: User management, group broadcast, statistics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
{
"name": "performance-tests",
"version": "1.0.0",
"description": "Performance benchmarking for Telegram Marketing Intelligence Agent System",
"type": "module",
"main": "index.js",
"scripts": {
"test": "node src/runner.js",
"test:api": "node src/tests/api-benchmark.js",
"test:load": "node src/tests/load-test.js",
"test:stress": "node src/tests/stress-test.js",
"test:database": "node src/tests/database-benchmark.js",
"test:message": "node src/tests/message-throughput.js",
"test:all": "npm run test:api && npm run test:load && npm run test:database && npm run test:message",
"report": "node src/report-generator.js"
},
"dependencies": {
"axios": "^1.6.2",
"autocannon": "^7.14.0",
"benchmark": "^2.1.4",
"chalk": "^5.3.0",
"cli-table3": "^0.6.3",
"dotenv": "^16.3.1",
"k6": "^0.47.0",
"mongodb": "^6.3.0",
"ora": "^7.0.1",
"socket.io-client": "^4.7.2"
},
"devDependencies": {
"eslint": "^8.56.0"
}
}

View File

@@ -0,0 +1,69 @@
#!/bin/bash
# Run Performance Tests for Telegram Marketing Intelligence Agent System
echo "🚀 Telegram Marketing Intelligence Agent - Performance Test Suite"
echo "================================================================"
echo ""
# Change to the performance-tests directory
cd "$(dirname "$0")"
# Check if dependencies are installed
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
echo ""
echo "Choose which tests to run:"
echo "1) All tests"
echo "2) API Benchmark only"
echo "3) Load Test only"
echo "4) Stress Test only"
echo "5) Database Benchmark only"
echo "6) Message Throughput only"
echo ""
read -p "Enter your choice (1-6): " choice
case $choice in
1)
echo ""
echo "Running all performance tests..."
node src/runner.js
;;
2)
echo ""
echo "Running API Benchmark..."
node src/tests/api-benchmark.js
;;
3)
echo ""
echo "Running Load Test..."
node src/tests/load-test.js
;;
4)
echo ""
echo "Running Stress Test..."
node src/tests/stress-test.js
;;
5)
echo ""
echo "Running Database Benchmark..."
node src/tests/database-benchmark.js
;;
6)
echo ""
echo "Running Message Throughput Test..."
node src/tests/message-throughput.js
;;
*)
echo "Invalid choice. Exiting."
exit 1
;;
esac
echo ""
echo "✅ Performance tests completed!"
echo "📊 Results saved in ./reports/ directory"

View File

@@ -0,0 +1,118 @@
import dotenv from 'dotenv';
dotenv.config();
export const config = {
// API Gateway
apiGateway: {
url: process.env.API_GATEWAY_URL || 'http://localhost:3000',
apiKey: process.env.API_KEY || 'test-api-key'
},
// Service URLs
services: {
orchestrator: process.env.ORCHESTRATOR_URL || 'http://localhost:3001',
claudeAgent: process.env.CLAUDE_AGENT_URL || 'http://localhost:3002',
gramjsAdapter: process.env.GRAMJS_ADAPTER_URL || 'http://localhost:3003',
analytics: process.env.ANALYTICS_URL || 'http://localhost:3005',
billing: process.env.BILLING_URL || 'http://localhost:3010'
},
// Database
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/marketing_agent'
},
// Test Configuration
test: {
// API Benchmark
api: {
duration: 30, // seconds
connections: 10,
pipelining: 1,
warmup: 5 // seconds
},
// Load Test
load: {
stages: [
{ duration: '30s', target: 10 }, // Ramp up to 10 users
{ duration: '1m', target: 50 }, // Stay at 50 users
{ duration: '30s', target: 100 }, // Ramp up to 100 users
{ duration: '2m', target: 100 }, // Stay at 100 users
{ duration: '30s', target: 0 } // Ramp down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests must complete below 500ms
http_req_failed: ['rate<0.1'], // Error rate must be below 10%
}
},
// Stress Test
stress: {
stages: [
{ duration: '30s', target: 100 }, // Ramp up to 100 users
{ duration: '30s', target: 200 }, // Ramp up to 200 users
{ duration: '30s', target: 500 }, // Ramp up to 500 users
{ duration: '1m', target: 1000 }, // Ramp up to 1000 users
{ duration: '30s', target: 0 } // Ramp down to 0 users
]
},
// Database Benchmark
database: {
operations: {
insert: 10000,
find: 50000,
update: 10000,
aggregate: 1000
},
batchSize: 100
},
// Message Throughput
message: {
totalMessages: 10000,
concurrentSenders: 10,
messageSize: 1000, // bytes
batchSize: 100
}
},
// Performance Thresholds
thresholds: {
api: {
responseTime: {
p50: 50, // 50th percentile
p75: 100, // 75th percentile
p90: 200, // 90th percentile
p95: 300, // 95th percentile
p99: 500 // 99th percentile
},
throughput: {
min: 1000 // requests per second
},
errorRate: {
max: 0.01 // 1%
}
},
database: {
operations: {
insert: 5000, // ops/sec
find: 10000, // ops/sec
update: 3000, // ops/sec
aggregate: 500 // ops/sec
}
},
message: {
throughput: 1000, // messages/sec
latency: 100 // ms
}
},
// Report Configuration
report: {
outputDir: './reports',
formats: ['json', 'html', 'csv']
}
};

View File

@@ -0,0 +1,238 @@
import chalk from 'chalk';
import { spawn } from 'child_process';
import ora from 'ora';
import fs from 'fs/promises';
import path from 'path';
import { config } from './config.js';
const tests = [
{
name: 'API Benchmark',
script: 'src/tests/api-benchmark.js',
description: 'Tests API endpoint performance and latency'
},
{
name: 'Load Test',
script: 'src/tests/load-test.js',
description: 'Simulates realistic user load patterns'
},
{
name: 'Stress Test',
script: 'src/tests/stress-test.js',
description: 'Tests system limits and breaking points'
},
{
name: 'Database Benchmark',
script: 'src/tests/database-benchmark.js',
description: 'Tests database operation performance'
},
{
name: 'Message Throughput',
script: 'src/tests/message-throughput.js',
description: 'Tests message sending capacity'
}
];
async function runTest(test) {
return new Promise((resolve, reject) => {
const spinner = ora(`Running ${test.name}...`).start();
const child = spawn('node', [test.script], {
stdio: 'pipe',
env: { ...process.env }
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => {
output += data.toString();
});
child.stderr.on('data', (data) => {
errorOutput += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
spinner.succeed(`${test.name} completed`);
resolve({ test, output, success: true });
} else {
spinner.fail(`${test.name} failed`);
resolve({ test, output, errorOutput, success: false });
}
});
child.on('error', (err) => {
spinner.fail(`${test.name} error`);
reject(err);
});
});
}
async function generateSummaryReport(results) {
const timestamp = new Date().toISOString();
const reportPath = path.join(config.report.outputDir, `performance-summary-${timestamp.replace(/:/g, '-')}.md`);
let report = `# Performance Test Summary\n\n`;
report += `Generated: ${timestamp}\n\n`;
report += `## Test Results\n\n`;
const passed = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
report += `- **Total Tests**: ${results.length}\n`;
report += `- **Passed**: ${passed}\n`;
report += `- **Failed**: ${failed}\n\n`;
report += `## Individual Test Results\n\n`;
for (const result of results) {
report += `### ${result.test.name}\n\n`;
report += `**Status**: ${result.success ? '✅ PASSED' : '❌ FAILED'}\n\n`;
report += `**Description**: ${result.test.description}\n\n`;
if (!result.success && result.errorOutput) {
report += `**Error Output**:\n\`\`\`\n${result.errorOutput}\n\`\`\`\n\n`;
}
// Extract key metrics from output
const metrics = extractMetrics(result.output);
if (metrics.length > 0) {
report += `**Key Metrics**:\n`;
metrics.forEach(metric => {
report += `- ${metric}\n`;
});
report += `\n`;
}
}
report += `## Recommendations\n\n`;
report += generateRecommendations(results);
await fs.mkdir(config.report.outputDir, { recursive: true });
await fs.writeFile(reportPath, report);
return reportPath;
}
function extractMetrics(output) {
const metrics = [];
const lines = output.split('\n');
for (const line of lines) {
if (line.includes('req/sec') ||
line.includes('msg/sec') ||
line.includes('ops/sec') ||
line.includes('latency') ||
line.includes('throughput')) {
metrics.push(line.trim());
}
}
return metrics;
}
function generateRecommendations(results) {
const recommendations = [];
// Analyze results and generate recommendations
for (const result of results) {
if (!result.success) {
recommendations.push(`- **${result.test.name}**: Test failed. Investigate system capacity and error logs.`);
continue;
}
const output = result.output.toLowerCase();
if (output.includes('threshold') && output.includes('exceed')) {
recommendations.push(`- **${result.test.name}**: Performance thresholds exceeded. Consider optimization or scaling.`);
}
if (output.includes('error rate') && output.includes('high')) {
recommendations.push(`- **${result.test.name}**: High error rate detected. Review error handling and system stability.`);
}
if (output.includes('degradation')) {
recommendations.push(`- **${result.test.name}**: Performance degradation detected. Analyze bottlenecks and optimize critical paths.`);
}
}
if (recommendations.length === 0) {
recommendations.push('- All tests passed within acceptable thresholds. Continue monitoring for performance regressions.');
}
return recommendations.join('\n');
}
async function main() {
console.log(chalk.bold.blue('🚀 Performance Test Suite'));
console.log('=' .repeat(60));
console.log(chalk.gray('This suite will run comprehensive performance tests'));
console.log(chalk.gray('for the Telegram Marketing Intelligence Agent System\n'));
const args = process.argv.slice(2);
let testsToRun = tests;
// Filter tests if specific ones are requested
if (args.length > 0) {
testsToRun = tests.filter(test =>
args.some(arg => test.name.toLowerCase().includes(arg.toLowerCase()))
);
if (testsToRun.length === 0) {
console.error(chalk.red('No matching tests found'));
process.exit(1);
}
}
console.log(chalk.yellow(`Running ${testsToRun.length} test(s):\n`));
testsToRun.forEach(test => {
console.log(`${test.name}: ${chalk.gray(test.description)}`);
});
console.log();
const results = [];
for (const test of testsToRun) {
try {
const result = await runTest(test);
results.push(result);
// Add delay between tests
if (testsToRun.indexOf(test) < testsToRun.length - 1) {
console.log(chalk.gray('\nWaiting before next test...\n'));
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error) {
console.error(chalk.red(`Error running ${test.name}:`), error);
results.push({ test, success: false, errorOutput: error.message });
}
}
// Generate summary report
console.log('\n' + chalk.bold.blue('Generating Summary Report...'));
const reportPath = await generateSummaryReport(results);
console.log(chalk.green(`✓ Summary report saved to: ${reportPath}`));
// Display final summary
console.log('\n' + chalk.bold('Final Summary'));
console.log('=' .repeat(60));
const passed = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`Total: ${results.length} | ` +
chalk.green(`Passed: ${passed}`) + ' | ' +
chalk.red(`Failed: ${failed}`)
);
if (failed > 0) {
console.log('\n' + chalk.red('❌ Some tests failed. Please review the results.'));
process.exit(1);
} else {
console.log('\n' + chalk.green('✅ All tests passed successfully!'));
}
}
main().catch(console.error);

View File

@@ -0,0 +1,173 @@
import autocannon from 'autocannon';
import chalk from 'chalk';
import ora from 'ora';
import { config } from '../config.js';
import { saveResults } from '../utils/results.js';
const endpoints = [
{
name: 'Health Check',
method: 'GET',
url: '/health'
},
{
name: 'Get Campaigns',
method: 'GET',
url: '/api/v1/orchestrator/campaigns',
headers: {
'Authorization': 'Bearer test-token',
'X-API-Key': config.apiGateway.apiKey
}
},
{
name: 'Create Campaign',
method: 'POST',
url: '/api/v1/orchestrator/campaigns',
headers: {
'Authorization': 'Bearer test-token',
'X-API-Key': config.apiGateway.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Performance Test Campaign',
description: 'Auto-generated campaign for performance testing',
targetAudience: {
groups: ['test-group-1'],
tags: ['performance-test']
},
message: {
content: 'This is a performance test message',
type: 'text'
},
schedule: {
type: 'immediate'
}
})
},
{
name: 'Get Analytics',
method: 'GET',
url: '/api/v1/analytics/campaigns/overview',
headers: {
'Authorization': 'Bearer test-token',
'X-API-Key': config.apiGateway.apiKey
}
},
{
name: 'Check Compliance',
method: 'POST',
url: '/api/v1/safety/check',
headers: {
'Authorization': 'Bearer test-token',
'X-API-Key': config.apiGateway.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: 'Test message for compliance check',
context: {
campaignId: 'test-campaign',
targetGroups: ['test-group']
}
})
}
];
async function runBenchmark(endpoint) {
const spinner = ora(`Testing ${endpoint.name}...`).start();
const instance = autocannon({
url: config.apiGateway.url + endpoint.url,
method: endpoint.method,
headers: endpoint.headers,
body: endpoint.body,
connections: config.test.api.connections,
pipelining: config.test.api.pipelining,
duration: config.test.api.duration,
warmup: config.test.api.warmup
});
return new Promise((resolve) => {
instance.on('done', (results) => {
spinner.succeed(`${endpoint.name} completed`);
resolve({
endpoint: endpoint.name,
results
});
});
});
}
async function analyzeResults(results) {
console.log('\n' + chalk.bold.blue('API Benchmark Results'));
console.log('=' .repeat(80));
const summary = [];
results.forEach(({ endpoint, results }) => {
const analysis = {
endpoint,
requests: results.requests.total,
throughput: results.throughput.mean,
latency: {
p50: results.latency.p50,
p90: results.latency.p90,
p95: results.latency.p95,
p99: results.latency.p99
},
errors: results.errors,
timeouts: results.timeouts
};
summary.push(analysis);
console.log(`\n${chalk.yellow(endpoint)}`);
console.log(` Requests: ${chalk.green(results.requests.total)}`);
console.log(` Throughput: ${chalk.green(results.throughput.mean.toFixed(2))} req/sec`);
console.log(` Latency:`);
console.log(` p50: ${chalk.cyan(results.latency.p50)} ms`);
console.log(` p90: ${chalk.cyan(results.latency.p90)} ms`);
console.log(` p95: ${chalk.cyan(results.latency.p95)} ms`);
console.log(` p99: ${chalk.cyan(results.latency.p99)} ms`);
// Check against thresholds
const thresholds = config.thresholds.api;
if (results.latency.p95 > thresholds.responseTime.p95) {
console.log(chalk.red(` ⚠️ P95 latency exceeds threshold (${thresholds.responseTime.p95}ms)`));
}
if (results.errors > 0) {
console.log(chalk.red(` ⚠️ ${results.errors} errors occurred`));
}
if (results.timeouts > 0) {
console.log(chalk.red(` ⚠️ ${results.timeouts} timeouts occurred`));
}
});
return summary;
}
async function main() {
console.log(chalk.bold.green('Starting API Benchmark Tests'));
console.log(`Testing against: ${config.apiGateway.url}`);
console.log(`Duration: ${config.test.api.duration}s per endpoint`);
console.log(`Connections: ${config.test.api.connections}`);
console.log(`Warmup: ${config.test.api.warmup}s\n`);
const results = [];
for (const endpoint of endpoints) {
const result = await runBenchmark(endpoint);
results.push(result);
}
const summary = await analyzeResults(results);
// Save results
await saveResults('api-benchmark', summary);
console.log('\n' + chalk.green('✓ API Benchmark completed'));
}
main().catch(console.error);

View File

@@ -0,0 +1,395 @@
import { MongoClient } from 'mongodb';
import Benchmark from 'benchmark';
import chalk from 'chalk';
import ora from 'ora';
import { config } from '../config.js';
import { saveResults } from '../utils/results.js';
let client;
let db;
let collections = {};
const testData = {
campaigns: [],
messages: [],
users: [],
analytics: []
};
// Generate test data
function generateTestData() {
const spinner = ora('Generating test data...').start();
// Generate campaigns
for (let i = 0; i < 1000; i++) {
testData.campaigns.push({
name: `Campaign ${i}`,
description: `Description for campaign ${i}`,
tenantId: `tenant-${i % 10}`,
targetAudience: {
groups: [`group-${i % 20}`, `group-${(i + 1) % 20}`],
tags: [`tag-${i % 50}`, `tag-${(i + 1) % 50}`],
filters: {
location: `city-${i % 100}`,
age: { min: 18, max: 65 }
}
},
message: {
content: `Message content ${i}`.repeat(10),
type: 'text',
attachments: []
},
schedule: {
type: i % 2 === 0 ? 'immediate' : 'scheduled',
scheduledAt: new Date(Date.now() + i * 3600000)
},
status: ['draft', 'active', 'completed', 'paused'][i % 4],
metrics: {
sent: Math.floor(Math.random() * 10000),
delivered: Math.floor(Math.random() * 9000),
read: Math.floor(Math.random() * 5000),
clicked: Math.floor(Math.random() * 1000)
},
createdAt: new Date(Date.now() - i * 86400000),
updatedAt: new Date()
});
}
// Generate messages
for (let i = 0; i < 10000; i++) {
testData.messages.push({
campaignId: testData.campaigns[i % 1000]._id,
tenantId: `tenant-${i % 10}`,
recipient: {
userId: `user-${i}`,
chatId: `chat-${i}`,
username: `user${i}`
},
content: `Message ${i}`,
status: ['pending', 'sent', 'delivered', 'failed'][i % 4],
attempts: Math.floor(Math.random() * 3),
sentAt: new Date(),
deliveredAt: i % 4 >= 2 ? new Date() : null,
error: i % 4 === 3 ? 'Network error' : null
});
}
// Generate users
for (let i = 0; i < 5000; i++) {
testData.users.push({
userId: `user-${i}`,
tenantId: `tenant-${i % 10}`,
username: `user${i}`,
firstName: `First${i}`,
lastName: `Last${i}`,
groups: [`group-${i % 20}`, `group-${(i + 1) % 20}`],
tags: [`tag-${i % 50}`, `tag-${(i + 1) % 50}`],
metadata: {
location: `city-${i % 100}`,
age: 18 + (i % 47),
interests: ['tech', 'business', 'marketing', 'sales'][i % 4]
},
stats: {
messagesReceived: Math.floor(Math.random() * 100),
messagesRead: Math.floor(Math.random() * 80),
lastActive: new Date()
},
createdAt: new Date(Date.now() - i * 86400000)
});
}
// Generate analytics events
for (let i = 0; i < 50000; i++) {
testData.analytics.push({
eventType: ['message_sent', 'message_delivered', 'message_read', 'link_clicked'][i % 4],
tenantId: `tenant-${i % 10}`,
campaignId: `campaign-${i % 1000}`,
userId: `user-${i % 5000}`,
messageId: `message-${i % 10000}`,
timestamp: new Date(Date.now() - Math.random() * 86400000 * 30),
metadata: {
deviceType: ['mobile', 'desktop', 'tablet'][i % 3],
os: ['iOS', 'Android', 'Windows', 'MacOS'][i % 4],
browser: ['Chrome', 'Safari', 'Firefox', 'Edge'][i % 4]
}
});
}
spinner.succeed('Test data generated');
}
async function setupDatabase() {
const spinner = ora('Setting up database connection...').start();
try {
client = new MongoClient(config.mongodb.uri);
await client.connect();
db = client.db('performance_test');
// Create collections
collections.campaigns = db.collection('campaigns');
collections.messages = db.collection('messages');
collections.users = db.collection('users');
collections.analytics = db.collection('analytics');
// Create indexes
await collections.campaigns.createIndex({ tenantId: 1, createdAt: -1 });
await collections.campaigns.createIndex({ 'targetAudience.groups': 1 });
await collections.campaigns.createIndex({ status: 1 });
await collections.messages.createIndex({ tenantId: 1, campaignId: 1 });
await collections.messages.createIndex({ status: 1, sentAt: -1 });
await collections.users.createIndex({ tenantId: 1, userId: 1 });
await collections.users.createIndex({ groups: 1 });
await collections.users.createIndex({ tags: 1 });
await collections.analytics.createIndex({ tenantId: 1, timestamp: -1 });
await collections.analytics.createIndex({ eventType: 1, campaignId: 1 });
spinner.succeed('Database setup completed');
} catch (error) {
spinner.fail('Database setup failed');
throw error;
}
}
async function cleanupDatabase() {
const spinner = ora('Cleaning up database...').start();
try {
await db.dropDatabase();
await client.close();
spinner.succeed('Database cleanup completed');
} catch (error) {
spinner.fail('Database cleanup failed');
throw error;
}
}
function createBenchmarkSuite() {
const suite = new Benchmark.Suite('Database Operations');
// Insert benchmarks
suite.add('Insert Single Campaign', {
defer: true,
fn: async function(deferred) {
const campaign = { ...testData.campaigns[0], _id: new Date().getTime() };
await collections.campaigns.insertOne(campaign);
deferred.resolve();
}
});
suite.add('Bulk Insert Messages', {
defer: true,
fn: async function(deferred) {
const messages = testData.messages.slice(0, 100).map((m, i) => ({
...m,
_id: new Date().getTime() + i
}));
await collections.messages.insertMany(messages);
deferred.resolve();
}
});
// Find benchmarks
suite.add('Find Campaigns by Tenant', {
defer: true,
fn: async function(deferred) {
await collections.campaigns.find({
tenantId: 'tenant-1'
}).limit(100).toArray();
deferred.resolve();
}
});
suite.add('Find Messages with Status', {
defer: true,
fn: async function(deferred) {
await collections.messages.find({
status: 'sent',
sentAt: { $gte: new Date(Date.now() - 86400000) }
}).limit(100).toArray();
deferred.resolve();
}
});
suite.add('Find Users by Groups', {
defer: true,
fn: async function(deferred) {
await collections.users.find({
groups: { $in: ['group-1', 'group-2'] }
}).limit(100).toArray();
deferred.resolve();
}
});
// Update benchmarks
suite.add('Update Campaign Status', {
defer: true,
fn: async function(deferred) {
await collections.campaigns.updateOne(
{ _id: testData.campaigns[0]._id },
{ $set: { status: 'active', updatedAt: new Date() } }
);
deferred.resolve();
}
});
suite.add('Bulk Update Message Status', {
defer: true,
fn: async function(deferred) {
await collections.messages.updateMany(
{ status: 'pending', attempts: { $lt: 3 } },
{ $set: { status: 'sent', sentAt: new Date() } }
);
deferred.resolve();
}
});
// Aggregation benchmarks
suite.add('Campaign Analytics Aggregation', {
defer: true,
fn: async function(deferred) {
await collections.analytics.aggregate([
{ $match: {
campaignId: 'campaign-1',
timestamp: { $gte: new Date(Date.now() - 86400000 * 7) }
}},
{ $group: {
_id: '$eventType',
count: { $sum: 1 }
}},
{ $sort: { count: -1 } }
]).toArray();
deferred.resolve();
}
});
suite.add('User Engagement Aggregation', {
defer: true,
fn: async function(deferred) {
await collections.analytics.aggregate([
{ $match: {
eventType: { $in: ['message_read', 'link_clicked'] }
}},
{ $group: {
_id: '$userId',
engagementCount: { $sum: 1 },
lastEngagement: { $max: '$timestamp' }
}},
{ $sort: { engagementCount: -1 } },
{ $limit: 100 }
]).toArray();
deferred.resolve();
}
});
// Complex query benchmarks
suite.add('Complex Campaign Query', {
defer: true,
fn: async function(deferred) {
await collections.campaigns.find({
tenantId: 'tenant-1',
status: { $in: ['active', 'scheduled'] },
'targetAudience.groups': { $in: ['group-1', 'group-2'] },
'schedule.scheduledAt': { $lte: new Date() },
'metrics.sent': { $gte: 100 }
}).sort({ createdAt: -1 }).limit(50).toArray();
deferred.resolve();
}
});
return suite;
}
async function runBenchmarks() {
console.log('\n' + chalk.bold.blue('Database Benchmark'));
console.log('=' .repeat(80));
const results = [];
// Populate initial data
const spinner = ora('Populating test data...').start();
await collections.campaigns.insertMany(testData.campaigns);
await collections.messages.insertMany(testData.messages.slice(0, 1000));
await collections.users.insertMany(testData.users);
await collections.analytics.insertMany(testData.analytics.slice(0, 5000));
spinner.succeed('Test data populated');
const suite = createBenchmarkSuite();
suite.on('cycle', function(event) {
const benchmark = event.target;
console.log(chalk.yellow(benchmark.name));
console.log(` ${chalk.green(benchmark.hz.toFixed(2))} ops/sec`);
console.log(` ${chalk.cyan((1000 / benchmark.hz).toFixed(2))} ms/op`);
results.push({
operation: benchmark.name,
opsPerSec: benchmark.hz,
msPerOp: 1000 / benchmark.hz,
samples: benchmark.stats.sample.length
});
});
suite.on('complete', function() {
console.log('\n' + chalk.green('✓ Database benchmarks completed'));
});
// Run benchmarks
await new Promise((resolve) => {
suite.on('complete', resolve);
suite.run({ async: true });
});
return results;
}
async function main() {
try {
generateTestData();
await setupDatabase();
const results = await runBenchmarks();
// Check against thresholds
console.log('\n' + chalk.bold('Performance Analysis'));
console.log('=' .repeat(80));
const thresholds = config.thresholds.database.operations;
let allPassed = true;
results.forEach(result => {
let threshold;
if (result.operation.includes('Insert')) threshold = thresholds.insert;
else if (result.operation.includes('Find')) threshold = thresholds.find;
else if (result.operation.includes('Update')) threshold = thresholds.update;
else if (result.operation.includes('Aggregation')) threshold = thresholds.aggregate;
if (threshold && result.opsPerSec < threshold) {
console.log(chalk.red(`${result.operation}: ${result.opsPerSec.toFixed(2)} ops/sec (threshold: ${threshold})`));
allPassed = false;
} else {
console.log(chalk.green(`${result.operation}: ${result.opsPerSec.toFixed(2)} ops/sec`));
}
});
if (allPassed) {
console.log('\n' + chalk.green('✓ All database operations meet performance thresholds'));
} else {
console.log('\n' + chalk.red('✗ Some database operations are below performance thresholds'));
}
// Save results
await saveResults('database-benchmark', results);
await cleanupDatabase();
} catch (error) {
console.error(chalk.red('Error:'), error);
if (client) await client.close();
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,151 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
import { config } from '../config.js';
// Custom metrics
const errorRate = new Rate('errors');
const campaignCreationTime = new Trend('campaign_creation_time');
const messageDeliveryTime = new Trend('message_delivery_time');
const analyticsQueryTime = new Trend('analytics_query_time');
// Test configuration
export const options = {
stages: config.test.load.stages,
thresholds: config.test.load.thresholds
};
// Setup function - runs once before the test
export function setup() {
// Login and get auth token
const loginRes = http.post(`${config.apiGateway.url}/api/v1/auth/login`, {
email: 'test@example.com',
password: 'testpass123'
});
const authToken = loginRes.json('token');
return { authToken };
}
// Main test function - runs for each virtual user
export default function(data) {
const headers = {
'Authorization': `Bearer ${data.authToken}`,
'X-API-Key': config.apiGateway.apiKey,
'Content-Type': 'application/json'
};
// Scenario 1: Create a campaign
const campaignPayload = {
name: `Load Test Campaign ${__VU}-${__ITER}`,
description: 'Campaign created during load testing',
targetAudience: {
groups: [`group-${__VU % 10}`],
tags: ['load-test', `vu-${__VU}`]
},
message: {
content: `This is message ${__ITER} from VU ${__VU}`,
type: 'text'
},
schedule: {
type: 'immediate'
}
};
const createCampaignRes = http.post(
`${config.apiGateway.url}/api/v1/orchestrator/campaigns`,
JSON.stringify(campaignPayload),
{ headers, tags: { name: 'CreateCampaign' } }
);
check(createCampaignRes, {
'campaign created': (r) => r.status === 201,
'has campaign ID': (r) => r.json('campaign.id') !== undefined
});
errorRate.add(createCampaignRes.status !== 201);
campaignCreationTime.add(createCampaignRes.timings.duration);
sleep(1);
// Scenario 2: Send a message
if (createCampaignRes.status === 201) {
const campaignId = createCampaignRes.json('campaign.id');
const sendMessageRes = http.post(
`${config.apiGateway.url}/api/v1/orchestrator/campaigns/${campaignId}/execute`,
null,
{ headers, tags: { name: 'SendMessage' } }
);
check(sendMessageRes, {
'message sent': (r) => r.status === 200
});
errorRate.add(sendMessageRes.status !== 200);
messageDeliveryTime.add(sendMessageRes.timings.duration);
}
sleep(2);
// Scenario 3: Query analytics
const analyticsRes = http.get(
`${config.apiGateway.url}/api/v1/analytics/campaigns/overview?period=daily`,
{ headers, tags: { name: 'GetAnalytics' } }
);
check(analyticsRes, {
'analytics retrieved': (r) => r.status === 200,
'has data': (r) => r.json('data') !== undefined
});
errorRate.add(analyticsRes.status !== 200);
analyticsQueryTime.add(analyticsRes.timings.duration);
sleep(1);
// Scenario 4: List campaigns
const listCampaignsRes = http.get(
`${config.apiGateway.url}/api/v1/orchestrator/campaigns?limit=10`,
{ headers, tags: { name: 'ListCampaigns' } }
);
check(listCampaignsRes, {
'campaigns listed': (r) => r.status === 200,
'has campaigns array': (r) => Array.isArray(r.json('campaigns'))
});
errorRate.add(listCampaignsRes.status !== 200);
sleep(2);
// Scenario 5: Check compliance
const compliancePayload = {
content: `Test message ${__ITER} for compliance check`,
context: {
campaignId: 'test-campaign',
targetGroups: ['test-group']
}
};
const complianceRes = http.post(
`${config.apiGateway.url}/api/v1/safety/check`,
JSON.stringify(compliancePayload),
{ headers, tags: { name: 'CheckCompliance' } }
);
check(complianceRes, {
'compliance checked': (r) => r.status === 200,
'has approval status': (r) => r.json('approved') !== undefined
});
errorRate.add(complianceRes.status !== 200);
sleep(1);
}
// Teardown function - runs once after the test
export function teardown(data) {
// Clean up test data if needed
console.log('Load test completed');
}

View File

@@ -0,0 +1,371 @@
import axios from 'axios';
import { io } from 'socket.io-client';
import chalk from 'chalk';
import ora from 'ora';
import { config } from '../config.js';
import { saveResults } from '../utils/results.js';
class MessageThroughputTest {
constructor() {
this.results = {
totalMessages: 0,
successfulMessages: 0,
failedMessages: 0,
startTime: null,
endTime: null,
latencies: [],
throughputSamples: [],
errors: []
};
this.sockets = [];
this.messageQueue = [];
this.running = false;
}
async setup() {
const spinner = ora('Setting up test environment...').start();
try {
// Login and get auth token
const loginRes = await axios.post(`${config.apiGateway.url}/api/v1/auth/login`, {
email: 'test@example.com',
password: 'testpass123'
});
this.authToken = loginRes.data.token;
// Create test campaign
const campaignRes = await axios.post(
`${config.apiGateway.url}/api/v1/orchestrator/campaigns`,
{
name: 'Message Throughput Test Campaign',
description: 'Campaign for throughput testing',
targetAudience: {
groups: ['throughput-test-group'],
tags: ['performance-test']
},
message: {
content: 'Throughput test message',
type: 'text'
},
schedule: {
type: 'immediate'
}
},
{
headers: {
'Authorization': `Bearer ${this.authToken}`,
'X-API-Key': config.apiGateway.apiKey
}
}
);
this.campaignId = campaignRes.data.campaign.id;
// Connect WebSocket clients for real-time monitoring
for (let i = 0; i < config.test.message.concurrentSenders; i++) {
const socket = io(config.apiGateway.url, {
auth: {
token: this.authToken
}
});
socket.on('message:sent', (data) => {
this.onMessageSent(data);
});
socket.on('message:delivered', (data) => {
this.onMessageDelivered(data);
});
socket.on('message:failed', (data) => {
this.onMessageFailed(data);
});
this.sockets.push(socket);
}
spinner.succeed('Test environment setup completed');
} catch (error) {
spinner.fail('Setup failed');
throw error;
}
}
async generateMessages() {
const totalMessages = config.test.message.totalMessages;
const messageSize = config.test.message.messageSize;
for (let i = 0; i < totalMessages; i++) {
this.messageQueue.push({
id: `msg-${i}`,
recipient: {
userId: `user-${i % 1000}`,
chatId: `chat-${i % 100}`,
username: `user${i % 1000}`
},
content: `Performance test message ${i} `.padEnd(messageSize, 'X'),
timestamp: Date.now()
});
}
}
async runTest() {
console.log('\n' + chalk.bold.green('Starting Message Throughput Test'));
console.log(`Total messages: ${config.test.message.totalMessages}`);
console.log(`Concurrent senders: ${config.test.message.concurrentSenders}`);
console.log(`Message size: ${config.test.message.messageSize} bytes\n`);
this.results.startTime = Date.now();
this.running = true;
// Start throughput monitoring
this.monitorThroughput();
// Process messages in batches
const batchSize = config.test.message.batchSize;
const concurrentSenders = config.test.message.concurrentSenders;
const progressBar = ora('Sending messages...').start();
while (this.messageQueue.length > 0) {
const batch = this.messageQueue.splice(0, batchSize * concurrentSenders);
const promises = [];
// Distribute batch across concurrent senders
for (let i = 0; i < concurrentSenders && i < batch.length; i++) {
const senderBatch = batch.filter((_, index) => index % concurrentSenders === i);
promises.push(this.sendBatch(senderBatch));
}
await Promise.all(promises);
const progress = ((this.results.totalMessages / config.test.message.totalMessages) * 100).toFixed(1);
progressBar.text = `Sending messages... ${progress}% (${this.results.totalMessages}/${config.test.message.totalMessages})`;
}
progressBar.succeed('All messages sent');
// Wait for all messages to be processed
console.log(chalk.yellow('Waiting for message delivery confirmation...'));
await this.waitForCompletion();
this.running = false;
this.results.endTime = Date.now();
}
async sendBatch(messages) {
const promises = messages.map(async (message) => {
const startTime = Date.now();
try {
const response = await axios.post(
`${config.apiGateway.url}/api/v1/gramjs-adapter/messages/send`,
{
campaignId: this.campaignId,
recipients: [message.recipient],
message: {
content: message.content,
type: 'text'
}
},
{
headers: {
'Authorization': `Bearer ${this.authToken}`,
'X-API-Key': config.apiGateway.apiKey
}
}
);
const latency = Date.now() - startTime;
this.results.latencies.push(latency);
this.results.totalMessages++;
if (response.data.success) {
this.results.successfulMessages++;
} else {
this.results.failedMessages++;
this.results.errors.push({
message: message.id,
error: response.data.error
});
}
} catch (error) {
const latency = Date.now() - startTime;
this.results.latencies.push(latency);
this.results.totalMessages++;
this.results.failedMessages++;
this.results.errors.push({
message: message.id,
error: error.message
});
}
});
await Promise.all(promises);
}
monitorThroughput() {
let lastCount = 0;
const interval = setInterval(() => {
if (!this.running) {
clearInterval(interval);
return;
}
const currentCount = this.results.successfulMessages;
const throughput = currentCount - lastCount;
this.results.throughputSamples.push({
timestamp: Date.now(),
throughput
});
lastCount = currentCount;
}, 1000); // Sample every second
}
async waitForCompletion() {
const timeout = 60000; // 1 minute timeout
const startWait = Date.now();
while (Date.now() - startWait < timeout) {
const pending = this.results.totalMessages -
(this.results.successfulMessages + this.results.failedMessages);
if (pending === 0) break;
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
onMessageSent(data) {
// Real-time event handling
}
onMessageDelivered(data) {
// Real-time event handling
}
onMessageFailed(data) {
// Real-time event handling
}
analyzeResults() {
console.log('\n' + chalk.bold.cyan('Message Throughput Test Results'));
console.log('=' .repeat(80));
const duration = (this.results.endTime - this.results.startTime) / 1000;
const averageThroughput = this.results.successfulMessages / duration;
// Calculate latency percentiles
const sortedLatencies = [...this.results.latencies].sort((a, b) => a - b);
const p50 = sortedLatencies[Math.floor(sortedLatencies.length * 0.5)];
const p90 = sortedLatencies[Math.floor(sortedLatencies.length * 0.9)];
const p95 = sortedLatencies[Math.floor(sortedLatencies.length * 0.95)];
const p99 = sortedLatencies[Math.floor(sortedLatencies.length * 0.99)];
// Calculate throughput statistics
const throughputValues = this.results.throughputSamples.map(s => s.throughput);
const maxThroughput = Math.max(...throughputValues);
const minThroughput = Math.min(...throughputValues);
console.log(`Duration: ${chalk.yellow(duration.toFixed(2))} seconds`);
console.log(`Total messages: ${chalk.green(this.results.totalMessages)}`);
console.log(`Successful: ${chalk.green(this.results.successfulMessages)}`);
console.log(`Failed: ${this.results.failedMessages > 0 ? chalk.red(this.results.failedMessages) : chalk.green(this.results.failedMessages)}`);
console.log(`Success rate: ${chalk.cyan(((this.results.successfulMessages / this.results.totalMessages) * 100).toFixed(2))}%`);
console.log('\nThroughput:');
console.log(` Average: ${chalk.green(averageThroughput.toFixed(2))} msg/sec`);
console.log(` Maximum: ${chalk.green(maxThroughput)} msg/sec`);
console.log(` Minimum: ${chalk.yellow(minThroughput)} msg/sec`);
console.log('\nLatency:');
console.log(` p50: ${chalk.cyan(p50)} ms`);
console.log(` p90: ${chalk.cyan(p90)} ms`);
console.log(` p95: ${chalk.cyan(p95)} ms`);
console.log(` p99: ${chalk.cyan(p99)} ms`);
// Check against thresholds
const thresholds = config.thresholds.message;
if (averageThroughput < thresholds.throughput) {
console.log(chalk.red(`\n⚠️ Average throughput (${averageThroughput.toFixed(2)} msg/sec) is below threshold (${thresholds.throughput} msg/sec)`));
} else {
console.log(chalk.green(`\n✓ Average throughput meets threshold`));
}
if (p95 > thresholds.latency) {
console.log(chalk.red(`⚠️ P95 latency (${p95}ms) exceeds threshold (${thresholds.latency}ms)`));
} else {
console.log(chalk.green(`✓ P95 latency meets threshold`));
}
return {
duration,
totalMessages: this.results.totalMessages,
successfulMessages: this.results.successfulMessages,
failedMessages: this.results.failedMessages,
successRate: (this.results.successfulMessages / this.results.totalMessages) * 100,
throughput: {
average: averageThroughput,
max: maxThroughput,
min: minThroughput
},
latency: {
p50,
p90,
p95,
p99
}
};
}
async cleanup() {
const spinner = ora('Cleaning up...').start();
// Close WebSocket connections
this.sockets.forEach(socket => socket.close());
// Delete test campaign
try {
await axios.delete(
`${config.apiGateway.url}/api/v1/orchestrator/campaigns/${this.campaignId}`,
{
headers: {
'Authorization': `Bearer ${this.authToken}`,
'X-API-Key': config.apiGateway.apiKey
}
}
);
} catch (error) {
// Ignore cleanup errors
}
spinner.succeed('Cleanup completed');
}
}
async function main() {
const test = new MessageThroughputTest();
try {
await test.setup();
await test.generateMessages();
await test.runTest();
const results = test.analyzeResults();
await saveResults('message-throughput', results);
console.log('\n' + chalk.green('✓ Message throughput test completed'));
} catch (error) {
console.error(chalk.red('Error:'), error);
process.exit(1);
} finally {
await test.cleanup();
}
}
main();

View File

@@ -0,0 +1,262 @@
import autocannon from 'autocannon';
import chalk from 'chalk';
import ora from 'ora';
import { config } from '../config.js';
import { saveResults } from '../utils/results.js';
const stressScenarios = [
{
name: 'Burst Traffic',
description: 'Sudden spike in traffic',
connections: 1000,
duration: 30,
amount: 50000, // total requests
workers: 4
},
{
name: 'Sustained High Load',
description: 'Continuous high traffic',
connections: 500,
duration: 120, // 2 minutes
workers: 4
},
{
name: 'Connection Saturation',
description: 'Maximum concurrent connections',
connections: 2000,
duration: 60,
workers: 8
},
{
name: 'Large Payload Stress',
description: 'Large message payloads',
connections: 100,
duration: 60,
workers: 2,
bodySize: 1024 * 1024 // 1MB payload
}
];
function generateLargePayload(size) {
return {
name: 'Stress Test Campaign',
description: 'A'.repeat(size / 2),
targetAudience: {
groups: Array(100).fill('group'),
tags: Array(1000).fill('tag')
},
message: {
content: 'B'.repeat(size / 2),
type: 'text'
}
};
}
async function runStressScenario(scenario) {
const spinner = ora(`Running ${scenario.name}...`).start();
const options = {
url: `${config.apiGateway.url}/api/v1/orchestrator/campaigns`,
method: 'POST',
headers: {
'Authorization': 'Bearer test-token',
'X-API-Key': config.apiGateway.apiKey,
'Content-Type': 'application/json'
},
connections: scenario.connections,
duration: scenario.duration,
workers: scenario.workers
};
if (scenario.amount) {
options.amount = scenario.amount;
}
if (scenario.bodySize) {
options.body = JSON.stringify(generateLargePayload(scenario.bodySize));
} else {
options.body = JSON.stringify({
name: 'Stress Test Campaign',
description: 'Auto-generated campaign for stress testing',
targetAudience: {
groups: ['stress-test-group'],
tags: ['stress-test']
},
message: {
content: 'This is a stress test message',
type: 'text'
}
});
}
const instance = autocannon(options);
return new Promise((resolve) => {
const metrics = {
scenario: scenario.name,
description: scenario.description,
startTime: Date.now(),
systemMetrics: []
};
// Collect system metrics during test
const metricsInterval = setInterval(() => {
metrics.systemMetrics.push({
timestamp: Date.now(),
memory: process.memoryUsage(),
cpu: process.cpuUsage()
});
}, 1000);
instance.on('done', (results) => {
clearInterval(metricsInterval);
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.results = results;
spinner.succeed(`${scenario.name} completed`);
resolve(metrics);
});
instance.on('error', (err) => {
clearInterval(metricsInterval);
spinner.fail(`${scenario.name} failed: ${err.message}`);
metrics.error = err.message;
resolve(metrics);
});
});
}
async function analyzeStressResults(results) {
console.log('\n' + chalk.bold.red('Stress Test Results'));
console.log('=' .repeat(80));
const summary = [];
results.forEach((result) => {
const { scenario, description, results: data, error } = result;
console.log(`\n${chalk.yellow(scenario)}`);
console.log(` ${chalk.gray(description)}`);
if (error) {
console.log(chalk.red(` ❌ Test failed: ${error}`));
summary.push({ scenario, status: 'failed', error });
return;
}
const analysis = {
scenario,
status: 'completed',
requests: data.requests.total,
throughput: data.throughput.mean,
latency: {
p50: data.latency.p50,
p90: data.latency.p90,
p95: data.latency.p95,
p99: data.latency.p99,
max: data.latency.max
},
errors: data.errors,
timeouts: data.timeouts,
connections: data.connections,
systemLoad: calculateSystemLoad(result.systemMetrics)
};
summary.push(analysis);
console.log(` Requests: ${chalk.green(data.requests.total)}`);
console.log(` Throughput: ${chalk.green(data.throughput.mean.toFixed(2))} req/sec`);
console.log(` Errors: ${data.errors > 0 ? chalk.red(data.errors) : chalk.green(data.errors)}`);
console.log(` Timeouts: ${data.timeouts > 0 ? chalk.red(data.timeouts) : chalk.green(data.timeouts)}`);
console.log(` Latency:`);
console.log(` p50: ${chalk.cyan(data.latency.p50)} ms`);
console.log(` p90: ${chalk.cyan(data.latency.p90)} ms`);
console.log(` p95: ${chalk.cyan(data.latency.p95)} ms`);
console.log(` p99: ${chalk.cyan(data.latency.p99)} ms`);
console.log(` max: ${chalk.cyan(data.latency.max)} ms`);
// Performance degradation analysis
if (data.latency.p95 > 1000) {
console.log(chalk.red(` ⚠️ Severe performance degradation detected`));
} else if (data.latency.p95 > 500) {
console.log(chalk.yellow(` ⚠️ Moderate performance degradation detected`));
}
const errorRate = (data.errors / data.requests.total) * 100;
if (errorRate > 5) {
console.log(chalk.red(` ⚠️ High error rate: ${errorRate.toFixed(2)}%`));
}
});
return summary;
}
function calculateSystemLoad(metrics) {
if (!metrics || metrics.length === 0) return null;
const lastMetric = metrics[metrics.length - 1];
const firstMetric = metrics[0];
return {
memoryUsage: {
heapUsed: lastMetric.memory.heapUsed,
heapTotal: lastMetric.memory.heapTotal,
external: lastMetric.memory.external,
rss: lastMetric.memory.rss
},
cpuUsage: {
user: lastMetric.cpu.user - firstMetric.cpu.user,
system: lastMetric.cpu.system - firstMetric.cpu.system
}
};
}
async function main() {
console.log(chalk.bold.red('Starting Stress Tests'));
console.log(chalk.yellow('⚠️ Warning: These tests will put significant load on the system'));
console.log(`Testing against: ${config.apiGateway.url}\n`);
const results = [];
for (const scenario of stressScenarios) {
const result = await runStressScenario(scenario);
results.push(result);
// Cool down period between scenarios
console.log(chalk.gray(' Cooling down...'));
await new Promise(resolve => setTimeout(resolve, 10000));
}
const summary = await analyzeStressResults(results);
// Save results
await saveResults('stress-test', summary);
// Overall system assessment
console.log('\n' + chalk.bold('System Stress Assessment'));
console.log('=' .repeat(80));
const failedScenarios = summary.filter(s => s.status === 'failed');
const highErrorScenarios = summary.filter(s =>
s.status === 'completed' && s.errors > 0 && (s.errors / s.requests) > 0.05
);
if (failedScenarios.length === 0 && highErrorScenarios.length === 0) {
console.log(chalk.green('✓ System handled all stress scenarios successfully'));
} else {
console.log(chalk.red(`✗ System showed signs of stress in ${failedScenarios.length + highErrorScenarios.length} scenarios`));
if (failedScenarios.length > 0) {
console.log(chalk.red(` - ${failedScenarios.length} scenarios failed completely`));
}
if (highErrorScenarios.length > 0) {
console.log(chalk.yellow(` - ${highErrorScenarios.length} scenarios had high error rates`));
}
}
console.log('\n' + chalk.green('✓ Stress tests completed'));
}
main().catch(console.error);

View File

@@ -0,0 +1,247 @@
import fs from 'fs/promises';
import path from 'path';
import { config } from '../config.js';
export async function saveResults(testName, results) {
const timestamp = new Date().toISOString().replace(/:/g, '-');
const outputDir = config.report.outputDir;
// Ensure output directory exists
await fs.mkdir(outputDir, { recursive: true });
// Save JSON format
if (config.report.formats.includes('json')) {
const jsonPath = path.join(outputDir, `${testName}-${timestamp}.json`);
await fs.writeFile(jsonPath, JSON.stringify(results, null, 2));
}
// Save CSV format
if (config.report.formats.includes('csv')) {
const csvPath = path.join(outputDir, `${testName}-${timestamp}.csv`);
const csv = convertToCSV(results);
await fs.writeFile(csvPath, csv);
}
// Save HTML format
if (config.report.formats.includes('html')) {
const htmlPath = path.join(outputDir, `${testName}-${timestamp}.html`);
const html = generateHTMLReport(testName, results);
await fs.writeFile(htmlPath, html);
}
}
function convertToCSV(results) {
if (Array.isArray(results)) {
if (results.length === 0) return '';
const headers = Object.keys(results[0]).join(',');
const rows = results.map(row =>
Object.values(row).map(value =>
typeof value === 'object' ? JSON.stringify(value) : value
).join(',')
);
return [headers, ...rows].join('\n');
} else {
// Flatten object for CSV
const flattened = flattenObject(results);
const headers = Object.keys(flattened).join(',');
const values = Object.values(flattened).join(',');
return [headers, values].join('\n');
}
}
function flattenObject(obj, prefix = '') {
const flattened = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(flattened, flattenObject(value, newKey));
} else {
flattened[newKey] = value;
}
}
return flattened;
}
function generateHTMLReport(testName, results) {
const timestamp = new Date().toLocaleString();
return `<!DOCTYPE html>
<html>
<head>
<title>${testName} Performance Report</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.metadata {
color: #666;
font-size: 14px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #007bff;
color: white;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.metric {
display: inline-block;
margin: 10px;
padding: 15px;
background-color: #f0f0f0;
border-radius: 5px;
}
.metric-label {
color: #666;
font-size: 12px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: #333;
}
.success {
color: #28a745;
}
.warning {
color: #ffc107;
}
.error {
color: #dc3545;
}
.chart {
margin: 20px 0;
}
pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>${testName} Performance Report</h1>
<div class="metadata">
Generated: ${timestamp}
</div>
${generateHTMLContent(results)}
</div>
</body>
</html>`;
}
function generateHTMLContent(results) {
if (Array.isArray(results)) {
// Generate table for array results
if (results.length === 0) return '<p>No results</p>';
const headers = Object.keys(results[0]);
const tableHTML = `
<table>
<thead>
<tr>
${headers.map(h => `<th>${h}</th>`).join('')}
</tr>
</thead>
<tbody>
${results.map(row => `
<tr>
${headers.map(h => `<td>${formatValue(row[h])}</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
`;
return tableHTML;
} else {
// Generate metrics display for object results
return generateMetricsHTML(results);
}
}
function generateMetricsHTML(results, prefix = '') {
let html = '';
for (const [key, value] of Object.entries(results)) {
const displayKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
html += `<h3>${displayKey}</h3>`;
html += generateMetricsHTML(value);
} else if (Array.isArray(value)) {
html += `<h3>${displayKey}</h3>`;
html += `<pre>${JSON.stringify(value, null, 2)}</pre>`;
} else {
html += `
<div class="metric">
<div class="metric-label">${displayKey}</div>
<div class="metric-value ${getValueClass(key, value)}">${formatValue(value)}</div>
</div>
`;
}
}
return html;
}
function formatValue(value) {
if (typeof value === 'number') {
if (value % 1 === 0) {
return value.toLocaleString();
} else {
return value.toFixed(2);
}
} else if (typeof value === 'object') {
return JSON.stringify(value);
} else {
return value;
}
}
function getValueClass(key, value) {
key = key.toLowerCase();
if (key.includes('error') || key.includes('fail')) {
return value > 0 ? 'error' : 'success';
} else if (key.includes('success') || key.includes('throughput')) {
return 'success';
} else if (key.includes('latency') || key.includes('duration')) {
return value > 1000 ? 'warning' : 'success';
}
return '';
}