@testsmith/perfornium 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +14 -0
- package/dist/cli/commands/dashboard.d.ts +9 -0
- package/dist/cli/commands/dashboard.js +98 -0
- package/dist/cli/commands/run.d.ts +4 -0
- package/dist/cli/commands/run.js +143 -14
- package/dist/cli/commands/worker.d.ts +8 -0
- package/dist/cli/commands/worker.js +126 -0
- package/dist/config/types/global-config.d.ts +5 -0
- package/dist/config/types/load-config.d.ts +2 -1
- package/dist/core/test-runner.d.ts +5 -0
- package/dist/core/test-runner.js +124 -5
- package/dist/core/virtual-user.js +13 -3
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/server.d.ts +100 -0
- package/dist/dashboard/server.js +2173 -0
- package/dist/metrics/collector.d.ts +3 -0
- package/dist/metrics/collector.js +69 -13
- package/dist/protocols/rest/handler.d.ts +6 -0
- package/dist/protocols/rest/handler.js +29 -1
- package/dist/protocols/web/core-web-vitals.js +16 -6
- package/dist/protocols/web/handler.js +37 -2
- package/dist/workers/manager.d.ts +1 -0
- package/dist/workers/manager.js +31 -4
- package/dist/workers/worker.js +4 -1
- package/package.json +1 -1
package/dist/cli/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ const report_1 = require("./commands/report");
|
|
|
8
8
|
const worker_1 = require("./commands/worker");
|
|
9
9
|
const init_1 = require("./commands/init");
|
|
10
10
|
const mock_1 = require("./commands/mock");
|
|
11
|
+
const dashboard_1 = require("./commands/dashboard");
|
|
11
12
|
const native_recorder_1 = require("../recorder/native-recorder");
|
|
12
13
|
const distributed_1 = require("./commands/distributed");
|
|
13
14
|
// Add new import commands
|
|
@@ -31,6 +32,10 @@ program
|
|
|
31
32
|
.option('-v, --verbose', 'Enable verbose logging (info level)')
|
|
32
33
|
.option('-d, --debug', 'Enable debug logging (very detailed)')
|
|
33
34
|
.option('--max-users <number>', 'Maximum virtual users override')
|
|
35
|
+
.option('--vus <number>', 'Override virtual users count')
|
|
36
|
+
.option('--iterations <number>', 'Override iterations per VU')
|
|
37
|
+
.option('--duration <duration>', 'Override test duration (e.g., 30s, 1m, 5m)')
|
|
38
|
+
.option('--ramp-up <duration>', 'Override ramp-up time (e.g., 10s, 1m)')
|
|
34
39
|
.option('-g, --global <key=value>', 'Override global config (supports dot notation: browser.headless=false)', collectGlobals, [])
|
|
35
40
|
.action(run_1.runCommand);
|
|
36
41
|
// Helper function to collect --global options
|
|
@@ -189,4 +194,13 @@ program
|
|
|
189
194
|
.option('--host <host>', 'Host to bind to', 'localhost')
|
|
190
195
|
.option('-d, --delay <ms>', 'Add delay to all responses (ms)', '0')
|
|
191
196
|
.action(mock_1.mockCommand);
|
|
197
|
+
program
|
|
198
|
+
.command('dashboard')
|
|
199
|
+
.description('Start the dashboard web UI to view and compare test results')
|
|
200
|
+
.option('-p, --port <port>', 'Port to listen on', '3000')
|
|
201
|
+
.option('-r, --results <directory>', 'Results directory to scan', './results')
|
|
202
|
+
.option('-t, --tests <directory>', 'Tests directory (defaults to parent of results)')
|
|
203
|
+
.option('-w, --workers <file>', 'Workers configuration file (JSON) for distributed testing')
|
|
204
|
+
.option('--no-open', 'Do not open browser automatically')
|
|
205
|
+
.action(dashboard_1.dashboardCommand);
|
|
192
206
|
program.parse();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.dashboardCommand = dashboardCommand;
|
|
37
|
+
const dashboard_1 = require("../../dashboard");
|
|
38
|
+
const logger_1 = require("../../utils/logger");
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
async function dashboardCommand(options) {
|
|
42
|
+
const port = parseInt(options.port, 10);
|
|
43
|
+
const resultsDir = path.resolve(options.results);
|
|
44
|
+
// Default testsDir to parent of results directory (common structure: project/results, project/tests)
|
|
45
|
+
const testsDir = options.tests ? path.resolve(options.tests) : path.resolve(resultsDir, '..');
|
|
46
|
+
const workersFile = options.workers ? path.resolve(options.workers) : undefined;
|
|
47
|
+
console.log(`
|
|
48
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
49
|
+
║ Perfornium Dashboard ║
|
|
50
|
+
╠═══════════════════════════════════════════════════════════════╣
|
|
51
|
+
║ Results directory: ${resultsDir.padEnd(40)} ║
|
|
52
|
+
║ Tests directory: ${testsDir.padEnd(40)} ║
|
|
53
|
+
${workersFile ? `║ Workers file: ${workersFile.padEnd(40)} ║\n` : ''}║ Dashboard URL: http://localhost:${port.toString().padEnd(26)} ║
|
|
54
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
55
|
+
`);
|
|
56
|
+
const dashboard = new dashboard_1.DashboardServer({
|
|
57
|
+
port,
|
|
58
|
+
resultsDir,
|
|
59
|
+
testsDir,
|
|
60
|
+
workersFile
|
|
61
|
+
});
|
|
62
|
+
// Store dashboard instance globally for test runner integration
|
|
63
|
+
(0, dashboard_1.setDashboard)(dashboard);
|
|
64
|
+
await dashboard.start();
|
|
65
|
+
// Open browser if not disabled
|
|
66
|
+
if (options.open !== false) {
|
|
67
|
+
const url = `http://localhost:${port}`;
|
|
68
|
+
const platform = process.platform;
|
|
69
|
+
let command;
|
|
70
|
+
if (platform === 'darwin') {
|
|
71
|
+
command = `open "${url}"`;
|
|
72
|
+
}
|
|
73
|
+
else if (platform === 'win32') {
|
|
74
|
+
command = `start "${url}"`;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
command = `xdg-open "${url}"`;
|
|
78
|
+
}
|
|
79
|
+
(0, child_process_1.exec)(command, (error) => {
|
|
80
|
+
if (error) {
|
|
81
|
+
logger_1.logger.debug('Could not open browser automatically:', error.message);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
console.log('Press Ctrl+C to stop the dashboard\n');
|
|
86
|
+
// Keep running until interrupted
|
|
87
|
+
process.on('SIGINT', async () => {
|
|
88
|
+
console.log('\nShutting down dashboard...');
|
|
89
|
+
await dashboard.stop();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
92
|
+
process.on('SIGTERM', async () => {
|
|
93
|
+
await dashboard.stop();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
});
|
|
96
|
+
// Keep the process alive
|
|
97
|
+
await new Promise(() => { });
|
|
98
|
+
}
|
|
@@ -7,6 +7,10 @@ export interface RunOptions {
|
|
|
7
7
|
verbose?: boolean;
|
|
8
8
|
debug?: boolean;
|
|
9
9
|
maxUsers?: string;
|
|
10
|
+
vus?: string;
|
|
11
|
+
iterations?: string;
|
|
12
|
+
duration?: string;
|
|
13
|
+
rampUp?: string;
|
|
10
14
|
global?: string[];
|
|
11
15
|
}
|
|
12
16
|
export declare function runCommand(configPath: string, options: RunOptions): Promise<void>;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.runCommand = runCommand;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const http = __importStar(require("http"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
39
40
|
const parser_1 = require("../../config/parser");
|
|
40
41
|
const validator_1 = require("../../config/validator");
|
|
41
42
|
const test_runner_1 = require("../../core/test-runner");
|
|
@@ -104,15 +105,44 @@ async function runCommand(configPath, options) {
|
|
|
104
105
|
if (baseUrl.match(/^https?:\/\/localhost:3000/)) {
|
|
105
106
|
await startMockServer(3000);
|
|
106
107
|
}
|
|
107
|
-
// Apply
|
|
108
|
-
if (options.maxUsers) {
|
|
109
|
-
const maxUsers = parseInt(options.maxUsers);
|
|
108
|
+
// Apply load pattern overrides
|
|
109
|
+
if (options.maxUsers || options.vus || options.iterations || options.duration || options.rampUp) {
|
|
110
110
|
const { getPrimaryLoadPhase } = await Promise.resolve().then(() => __importStar(require('../../config/types/load-config')));
|
|
111
111
|
const primaryPhase = getPrimaryLoadPhase(config.load);
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
// Virtual users override
|
|
113
|
+
if (options.vus) {
|
|
114
|
+
const vus = parseInt(options.vus);
|
|
115
|
+
logger_1.logger.info(`Overriding virtual users to ${vus}`);
|
|
116
|
+
primaryPhase.virtual_users = vus;
|
|
117
|
+
if (primaryPhase.vus)
|
|
118
|
+
primaryPhase.vus = vus;
|
|
119
|
+
}
|
|
120
|
+
// Max users override (limit, not set)
|
|
121
|
+
if (options.maxUsers) {
|
|
122
|
+
const maxUsers = parseInt(options.maxUsers);
|
|
123
|
+
const currentUsers = primaryPhase.virtual_users || primaryPhase.vus;
|
|
124
|
+
if (currentUsers && currentUsers > maxUsers) {
|
|
125
|
+
logger_1.logger.warn(`Limiting virtual users from ${currentUsers} to ${maxUsers}`);
|
|
126
|
+
primaryPhase.virtual_users = maxUsers;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Iterations override
|
|
130
|
+
if (options.iterations) {
|
|
131
|
+
const iterations = parseInt(options.iterations);
|
|
132
|
+
logger_1.logger.info(`Overriding iterations to ${iterations}`);
|
|
133
|
+
primaryPhase.iterations = iterations;
|
|
134
|
+
}
|
|
135
|
+
// Duration override
|
|
136
|
+
if (options.duration) {
|
|
137
|
+
logger_1.logger.info(`Overriding duration to ${options.duration}`);
|
|
138
|
+
primaryPhase.duration = options.duration;
|
|
139
|
+
// Remove iterations if duration is set (they're mutually exclusive)
|
|
140
|
+
delete primaryPhase.iterations;
|
|
141
|
+
}
|
|
142
|
+
// Ramp-up override
|
|
143
|
+
if (options.rampUp) {
|
|
144
|
+
logger_1.logger.info(`Overriding ramp-up to ${options.rampUp}`);
|
|
145
|
+
primaryPhase.ramp_up = options.rampUp;
|
|
116
146
|
}
|
|
117
147
|
}
|
|
118
148
|
// Process templates with environment variables
|
|
@@ -138,12 +168,29 @@ async function runCommand(configPath, options) {
|
|
|
138
168
|
return;
|
|
139
169
|
}
|
|
140
170
|
// Override output directory if specified
|
|
141
|
-
if (options.output
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
if (options.output) {
|
|
172
|
+
// Ensure output directory exists
|
|
173
|
+
if (!fs.existsSync(options.output)) {
|
|
174
|
+
fs.mkdirSync(options.output, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').substring(0, 19);
|
|
177
|
+
const testName = processedConfig.name || path.basename(configPath, path.extname(configPath));
|
|
178
|
+
const defaultOutputFile = path.join(options.output, `${testName}-${timestamp}.json`);
|
|
179
|
+
if (processedConfig.outputs && processedConfig.outputs.length > 0) {
|
|
180
|
+
// Modify existing outputs to use the specified directory
|
|
181
|
+
processedConfig.outputs.forEach(output => {
|
|
182
|
+
if (output.file) {
|
|
183
|
+
output.file = path.join(options.output, path.basename(output.file));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// No outputs configured - add default JSON output
|
|
189
|
+
processedConfig.outputs = [{
|
|
190
|
+
type: 'json',
|
|
191
|
+
file: defaultOutputFile
|
|
192
|
+
}];
|
|
193
|
+
}
|
|
147
194
|
}
|
|
148
195
|
// Enable report generation if requested
|
|
149
196
|
if (options.report) {
|
|
@@ -160,12 +207,94 @@ async function runCommand(configPath, options) {
|
|
|
160
207
|
for (const address of workerAddresses) {
|
|
161
208
|
await manager.addWorker(address);
|
|
162
209
|
}
|
|
210
|
+
// Initialize metrics before distributing test
|
|
211
|
+
const metrics = manager.getAggregatedMetrics();
|
|
212
|
+
metrics.start();
|
|
213
|
+
// Track results as they come in
|
|
214
|
+
let resultCount = 0;
|
|
215
|
+
manager.on('result', () => {
|
|
216
|
+
resultCount++;
|
|
217
|
+
});
|
|
163
218
|
await manager.distributeTest(processedConfig);
|
|
219
|
+
logger_1.logger.info('📡 Test distributed to workers, starting progress reporting...');
|
|
220
|
+
// Start live progress reporting for dashboard
|
|
221
|
+
let lastReportedResultIndex = 0;
|
|
222
|
+
let isRunning = true;
|
|
223
|
+
// Output function for progress reporting
|
|
224
|
+
const outputProgress = () => {
|
|
225
|
+
if (!isRunning)
|
|
226
|
+
return;
|
|
227
|
+
const summary = metrics.getSummary();
|
|
228
|
+
const currentRequests = summary.total_requests || 0;
|
|
229
|
+
const rps = summary.requests_per_second || 0;
|
|
230
|
+
const p50 = summary.percentiles?.[50] || 0;
|
|
231
|
+
const p90 = summary.percentiles?.[90] || 0;
|
|
232
|
+
const p95 = summary.percentiles?.[95] || 0;
|
|
233
|
+
const p99 = summary.percentiles?.[99] || 0;
|
|
234
|
+
const successRate = summary.success_rate || 0;
|
|
235
|
+
// Output progress line for dashboard parsing
|
|
236
|
+
const progressLine = `[PROGRESS] VUs: ${manager.getWorkerCount()} | Requests: ${currentRequests} | Errors: ${summary.failed_requests || 0} | Avg RT: ${(summary.avg_response_time || 0).toFixed(0)}ms | RPS: ${rps.toFixed(1)} | P50: ${p50.toFixed(0)}ms | P90: ${p90.toFixed(0)}ms | P95: ${p95.toFixed(0)}ms | P99: ${p99.toFixed(0)}ms | Success: ${successRate.toFixed(1)}%`;
|
|
237
|
+
console.log(progressLine);
|
|
238
|
+
// Output step statistics if available
|
|
239
|
+
if (summary.step_statistics && summary.step_statistics.length > 0) {
|
|
240
|
+
const stepData = summary.step_statistics.map((s) => ({
|
|
241
|
+
n: s.step_name,
|
|
242
|
+
s: s.scenario,
|
|
243
|
+
r: s.total_requests,
|
|
244
|
+
e: s.failed_requests,
|
|
245
|
+
a: Math.round(s.avg_response_time),
|
|
246
|
+
p50: Math.round(s.percentiles?.[50] || 0),
|
|
247
|
+
p95: Math.round(s.percentiles?.[95] || 0),
|
|
248
|
+
p99: Math.round(s.percentiles?.[99] || 0),
|
|
249
|
+
sr: Math.round(s.success_rate * 10) / 10
|
|
250
|
+
}));
|
|
251
|
+
console.log(`[STEPS] ${JSON.stringify(stepData)}`);
|
|
252
|
+
}
|
|
253
|
+
// Output individual response times for charts
|
|
254
|
+
const allResults = metrics.getResults();
|
|
255
|
+
if (allResults.length > lastReportedResultIndex) {
|
|
256
|
+
const newResults = allResults.slice(lastReportedResultIndex, lastReportedResultIndex + 50);
|
|
257
|
+
const rtData = newResults.map((r) => ({
|
|
258
|
+
t: r.timestamp,
|
|
259
|
+
v: Math.round(r.duration),
|
|
260
|
+
s: r.success ? 1 : 0,
|
|
261
|
+
n: r.step_name || r.action || 'unknown' // Include step/request name for coloring
|
|
262
|
+
}));
|
|
263
|
+
if (rtData.length > 0) {
|
|
264
|
+
console.log(`[RT] ${JSON.stringify(rtData)}`);
|
|
265
|
+
}
|
|
266
|
+
lastReportedResultIndex = Math.min(allResults.length, lastReportedResultIndex + 50);
|
|
267
|
+
}
|
|
268
|
+
// Output top 10 errors if any
|
|
269
|
+
if (summary.error_details && summary.error_details.length > 0) {
|
|
270
|
+
const topErrors = summary.error_details.slice(0, 10).map((e) => ({
|
|
271
|
+
scenario: e.scenario,
|
|
272
|
+
action: e.action,
|
|
273
|
+
status: e.status,
|
|
274
|
+
error: e.error?.substring(0, 200), // Truncate long error messages
|
|
275
|
+
url: e.request_url,
|
|
276
|
+
count: e.count
|
|
277
|
+
}));
|
|
278
|
+
console.log(`[ERRORS] ${JSON.stringify(topErrors)}`);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
// Output initial progress immediately
|
|
282
|
+
outputProgress();
|
|
283
|
+
// Then continue outputting every 500ms
|
|
284
|
+
const progressInterval = setInterval(outputProgress, 500);
|
|
164
285
|
await manager.waitForCompletion();
|
|
165
|
-
|
|
286
|
+
logger_1.logger.info('✅ All workers completed, cleaning up...');
|
|
287
|
+
// Stop progress reporting
|
|
288
|
+
isRunning = false;
|
|
289
|
+
clearInterval(progressInterval);
|
|
166
290
|
const summary = metrics.getSummary();
|
|
167
291
|
logger_1.logger.success(`Distributed test completed: ${summary.success_rate.toFixed(2)}% success rate`);
|
|
292
|
+
logger_1.logger.info(`📊 Total requests: ${summary.total_requests}, Success rate: ${summary.success_rate.toFixed(1)}%`);
|
|
293
|
+
logger_1.logger.info('🧹 Starting cleanup...');
|
|
168
294
|
await manager.cleanup();
|
|
295
|
+
logger_1.logger.info('✅ Cleanup completed, exiting...');
|
|
296
|
+
// Force exit after distributed test cleanup to ensure process terminates
|
|
297
|
+
process.exit(0);
|
|
169
298
|
}
|
|
170
299
|
else {
|
|
171
300
|
const runner = new test_runner_1.TestRunner(processedConfig);
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
export declare class WorkerServer {
|
|
2
2
|
private server;
|
|
3
|
+
private wss;
|
|
3
4
|
private host;
|
|
4
5
|
private port;
|
|
5
6
|
private status;
|
|
6
7
|
private activeRunner;
|
|
7
8
|
private preparedConfig;
|
|
8
9
|
private completedRunner;
|
|
10
|
+
private wsClients;
|
|
9
11
|
constructor(host?: string, port?: number);
|
|
12
|
+
private setupWebSocketHandlers;
|
|
13
|
+
private handleWebSocketMessage;
|
|
14
|
+
private executeWsTest;
|
|
15
|
+
private stopWsTest;
|
|
16
|
+
private sendWsMessage;
|
|
17
|
+
private sendWsError;
|
|
10
18
|
private handleRequest;
|
|
11
19
|
private handleHealth;
|
|
12
20
|
private handleStatus;
|
|
@@ -38,12 +38,14 @@ exports.workerCommand = workerCommand;
|
|
|
38
38
|
const logger_1 = require("../../utils/logger");
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
40
|
const url_1 = require("url");
|
|
41
|
+
const ws_1 = require("ws");
|
|
41
42
|
const test_runner_1 = require("../../core/test-runner");
|
|
42
43
|
class WorkerServer {
|
|
43
44
|
constructor(host = 'localhost', port = 8080) {
|
|
44
45
|
this.activeRunner = null;
|
|
45
46
|
this.preparedConfig = null;
|
|
46
47
|
this.completedRunner = null;
|
|
48
|
+
this.wsClients = new Map();
|
|
47
49
|
this.host = host;
|
|
48
50
|
this.port = port;
|
|
49
51
|
this.status = {
|
|
@@ -57,6 +59,129 @@ class WorkerServer {
|
|
|
57
59
|
this.server = http.createServer((req, res) => {
|
|
58
60
|
this.handleRequest(req, res);
|
|
59
61
|
});
|
|
62
|
+
// WebSocket server for distributed testing
|
|
63
|
+
this.wss = new ws_1.WebSocketServer({
|
|
64
|
+
server: this.server,
|
|
65
|
+
path: '/perfornium'
|
|
66
|
+
});
|
|
67
|
+
this.setupWebSocketHandlers();
|
|
68
|
+
}
|
|
69
|
+
setupWebSocketHandlers() {
|
|
70
|
+
this.wss.on('connection', (ws, req) => {
|
|
71
|
+
const clientIP = req.socket.remoteAddress;
|
|
72
|
+
logger_1.logger.info(`🔌 WebSocket client connected from ${clientIP}`);
|
|
73
|
+
this.wsClients.set(ws, null);
|
|
74
|
+
ws.on('message', async (data) => {
|
|
75
|
+
try {
|
|
76
|
+
const message = JSON.parse(data.toString());
|
|
77
|
+
await this.handleWebSocketMessage(ws, message);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger_1.logger.error('❌ Error handling WebSocket message:', error);
|
|
81
|
+
this.sendWsError(ws, error.message);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
ws.on('close', () => {
|
|
85
|
+
logger_1.logger.info('👋 WebSocket client disconnected');
|
|
86
|
+
const runner = this.wsClients.get(ws);
|
|
87
|
+
if (runner) {
|
|
88
|
+
runner.stop().catch(err => logger_1.logger.error('❌ Error stopping runner on disconnect:', err));
|
|
89
|
+
}
|
|
90
|
+
this.wsClients.delete(ws);
|
|
91
|
+
});
|
|
92
|
+
ws.on('error', (error) => {
|
|
93
|
+
logger_1.logger.error('❌ WebSocket error:', error);
|
|
94
|
+
this.wsClients.delete(ws);
|
|
95
|
+
});
|
|
96
|
+
// Send heartbeat
|
|
97
|
+
this.sendWsMessage(ws, { type: 'heartbeat' });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async handleWebSocketMessage(ws, message) {
|
|
101
|
+
switch (message.type) {
|
|
102
|
+
case 'execute_test':
|
|
103
|
+
await this.executeWsTest(ws, message.config);
|
|
104
|
+
break;
|
|
105
|
+
case 'stop_test':
|
|
106
|
+
await this.stopWsTest(ws);
|
|
107
|
+
break;
|
|
108
|
+
case 'heartbeat_ack':
|
|
109
|
+
// Client is alive
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
logger_1.logger.warn(`⚠️ Unknown WebSocket message type: ${message.type}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async executeWsTest(ws, config) {
|
|
116
|
+
try {
|
|
117
|
+
// Stop any existing test for this connection
|
|
118
|
+
await this.stopWsTest(ws);
|
|
119
|
+
const runner = new test_runner_1.TestRunner(config);
|
|
120
|
+
this.wsClients.set(ws, runner);
|
|
121
|
+
logger_1.logger.info(`🚀 Starting test via WebSocket: ${config.name}`);
|
|
122
|
+
const metrics = runner.getMetrics();
|
|
123
|
+
// Listen for individual results
|
|
124
|
+
metrics.on('result', (result) => {
|
|
125
|
+
this.sendWsMessage(ws, {
|
|
126
|
+
type: 'test_result',
|
|
127
|
+
data: result
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// Send progress updates on batch events
|
|
131
|
+
metrics.on('batch', (batch) => {
|
|
132
|
+
this.sendWsMessage(ws, {
|
|
133
|
+
type: 'test_progress',
|
|
134
|
+
data: {
|
|
135
|
+
completed: batch.batch_number * batch.batch_size,
|
|
136
|
+
total: batch.batch_size
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// Log test start
|
|
141
|
+
this.sendWsMessage(ws, {
|
|
142
|
+
type: 'log',
|
|
143
|
+
message: `Starting test: ${config.name}`
|
|
144
|
+
});
|
|
145
|
+
// Start the test
|
|
146
|
+
runner.run().then(() => {
|
|
147
|
+
const summary = metrics.getSummary();
|
|
148
|
+
this.sendWsMessage(ws, {
|
|
149
|
+
type: 'test_completed',
|
|
150
|
+
summary: summary
|
|
151
|
+
});
|
|
152
|
+
this.wsClients.set(ws, null);
|
|
153
|
+
logger_1.logger.info(`✅ Test completed via WebSocket: ${config.name}`);
|
|
154
|
+
}).catch((error) => {
|
|
155
|
+
this.sendWsMessage(ws, {
|
|
156
|
+
type: 'test_error',
|
|
157
|
+
error: error.message
|
|
158
|
+
});
|
|
159
|
+
this.wsClients.set(ws, null);
|
|
160
|
+
logger_1.logger.error(`❌ Test failed via WebSocket: ${error.message}`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.sendWsMessage(ws, {
|
|
165
|
+
type: 'test_error',
|
|
166
|
+
error: error.message
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async stopWsTest(ws) {
|
|
171
|
+
const runner = this.wsClients.get(ws);
|
|
172
|
+
if (runner) {
|
|
173
|
+
await runner.stop();
|
|
174
|
+
this.wsClients.set(ws, null);
|
|
175
|
+
this.sendWsMessage(ws, { type: 'test_stopped' });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
sendWsMessage(ws, message) {
|
|
179
|
+
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
180
|
+
ws.send(JSON.stringify(message));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
sendWsError(ws, error) {
|
|
184
|
+
this.sendWsMessage(ws, { type: 'error', error });
|
|
60
185
|
}
|
|
61
186
|
async handleRequest(req, res) {
|
|
62
187
|
// Set CORS headers
|
|
@@ -261,6 +386,7 @@ class WorkerServer {
|
|
|
261
386
|
this.server.listen(this.port, this.host, () => {
|
|
262
387
|
logger_1.logger.info(`🚀 Worker server started on ${this.host}:${this.port}`);
|
|
263
388
|
logger_1.logger.info(`📋 Available endpoints:`);
|
|
389
|
+
logger_1.logger.info(` WS ws://${this.host}:${this.port}/perfornium (distributed testing)`);
|
|
264
390
|
logger_1.logger.info(` GET http://${this.host}:${this.port}/health`);
|
|
265
391
|
logger_1.logger.info(` GET http://${this.host}:${this.port}/status`);
|
|
266
392
|
logger_1.logger.info(` POST http://${this.host}:${this.port}/prepare`);
|
|
@@ -71,4 +71,9 @@ export interface DebugConfig {
|
|
|
71
71
|
max_response_body_size?: number;
|
|
72
72
|
capture_only_failures?: boolean;
|
|
73
73
|
log_level?: 'debug' | 'info' | 'warn' | 'error';
|
|
74
|
+
log_requests?: boolean;
|
|
75
|
+
log_responses?: boolean;
|
|
76
|
+
log_headers?: boolean;
|
|
77
|
+
log_body?: boolean;
|
|
78
|
+
log_timings?: boolean;
|
|
74
79
|
}
|
|
@@ -8,8 +8,13 @@ export declare class TestRunner {
|
|
|
8
8
|
private activeVUs;
|
|
9
9
|
private isRunning;
|
|
10
10
|
private startTime;
|
|
11
|
+
private testId;
|
|
12
|
+
private dashboardInterval;
|
|
11
13
|
constructor(config: TestConfiguration);
|
|
12
14
|
run(): Promise<void>;
|
|
15
|
+
private lastReportedResultIndex;
|
|
16
|
+
private startDashboardReporting;
|
|
17
|
+
private stopDashboardReporting;
|
|
13
18
|
private setupCSVBaseDirectory;
|
|
14
19
|
stop(): Promise<void>;
|
|
15
20
|
private initialize;
|