@testsmith/perfornium 0.1.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/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimestampHelper = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Enhanced timestamp utility for file naming and templating
|
|
6
|
+
*/
|
|
7
|
+
class TimestampHelper {
|
|
8
|
+
/**
|
|
9
|
+
* Generate various timestamp formats
|
|
10
|
+
*/
|
|
11
|
+
static getTimestamp(format = 'unix') {
|
|
12
|
+
const now = new Date();
|
|
13
|
+
switch (format) {
|
|
14
|
+
case 'unix':
|
|
15
|
+
return Date.now().toString();
|
|
16
|
+
case 'iso':
|
|
17
|
+
return now.toISOString();
|
|
18
|
+
case 'readable':
|
|
19
|
+
return now.toLocaleString().replace(/[/\s:]/g, '-');
|
|
20
|
+
case 'file': {
|
|
21
|
+
// Safe for filenames: YYYYMMDD-HHMMSS-mmm
|
|
22
|
+
const year = now.getFullYear();
|
|
23
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
24
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
25
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
26
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
27
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
28
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
29
|
+
return `${year}${month}${day}-${hour}${minute}${second}-${ms}`;
|
|
30
|
+
}
|
|
31
|
+
default:
|
|
32
|
+
return Date.now().toString();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create filename with timestamp ensuring directory exists
|
|
37
|
+
*/
|
|
38
|
+
static createTimestampedPath(template, format = 'file') {
|
|
39
|
+
const timestamp = this.getTimestamp(format);
|
|
40
|
+
return template.replace(/\{\{timestamp\}\}/g, timestamp);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generate filename ensuring uniqueness
|
|
44
|
+
*/
|
|
45
|
+
static generateUniqueFilename(baseTemplate) {
|
|
46
|
+
const timestamp = this.getTimestamp('file');
|
|
47
|
+
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
|
|
48
|
+
return baseTemplate
|
|
49
|
+
.replace(/\{\{timestamp\}\}/g, timestamp)
|
|
50
|
+
.replace(/\{\{unique\}\}/g, `${timestamp}-${random}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.TimestampHelper = TimestampHelper;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { TestConfiguration } from '../config/types';
|
|
3
|
+
import { MetricsCollector } from '../metrics/collector';
|
|
4
|
+
export declare class WorkerManager extends EventEmitter {
|
|
5
|
+
private workers;
|
|
6
|
+
private aggregatedMetrics;
|
|
7
|
+
addWorker(address: string): Promise<void>;
|
|
8
|
+
distributeTest(config: TestConfiguration): Promise<void>;
|
|
9
|
+
waitForCompletion(): Promise<void>;
|
|
10
|
+
getAggregatedMetrics(): MetricsCollector;
|
|
11
|
+
cleanup(): Promise<void>;
|
|
12
|
+
getWorkerCount(): number;
|
|
13
|
+
getWorkerStatuses(): Array<{
|
|
14
|
+
address: string;
|
|
15
|
+
connected: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
private removeWorker;
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkerManager = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const collector_1 = require("../metrics/collector");
|
|
6
|
+
const worker_1 = require("./worker");
|
|
7
|
+
const logger_1 = require("../utils/logger");
|
|
8
|
+
class WorkerManager extends events_1.EventEmitter {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.workers = [];
|
|
12
|
+
this.aggregatedMetrics = new collector_1.MetricsCollector();
|
|
13
|
+
}
|
|
14
|
+
async addWorker(address) {
|
|
15
|
+
try {
|
|
16
|
+
const worker = new worker_1.WorkerNode(address);
|
|
17
|
+
await worker.connect();
|
|
18
|
+
worker.on('result', (result) => {
|
|
19
|
+
this.aggregatedMetrics.recordResult(result);
|
|
20
|
+
this.emit('result', result);
|
|
21
|
+
});
|
|
22
|
+
worker.on('error', (error) => {
|
|
23
|
+
logger_1.logger.error(`❌ Worker ${address} error:`, error);
|
|
24
|
+
this.emit('worker-error', { worker: address, error });
|
|
25
|
+
});
|
|
26
|
+
worker.on('disconnected', () => {
|
|
27
|
+
logger_1.logger.warn(`⚠️ Worker ${address} disconnected`);
|
|
28
|
+
this.removeWorker(worker);
|
|
29
|
+
});
|
|
30
|
+
this.workers.push(worker);
|
|
31
|
+
logger_1.logger.info(`✅ Worker added: ${address}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger_1.logger.error(`❌ Failed to add worker ${address}:`, error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async distributeTest(config) {
|
|
39
|
+
if (this.workers.length === 0) {
|
|
40
|
+
throw new Error('No workers available for distributed testing');
|
|
41
|
+
}
|
|
42
|
+
const { getPrimaryLoadPhase } = require('../config/types/load-config');
|
|
43
|
+
const primaryPhase = getPrimaryLoadPhase(config.load);
|
|
44
|
+
const totalVUs = primaryPhase.virtual_users || primaryPhase.vus || 1;
|
|
45
|
+
const vusPerWorker = Math.ceil(totalVUs / this.workers.length);
|
|
46
|
+
logger_1.logger.info(`🔄 Distributing ${totalVUs} VUs across ${this.workers.length} workers`);
|
|
47
|
+
const promises = this.workers.map(async (worker, index) => {
|
|
48
|
+
const workerVUs = Math.min(vusPerWorker, totalVUs - (index * vusPerWorker));
|
|
49
|
+
if (workerVUs <= 0)
|
|
50
|
+
return;
|
|
51
|
+
const workerConfig = {
|
|
52
|
+
...config,
|
|
53
|
+
name: `${config.name} - Worker ${index + 1}`,
|
|
54
|
+
load: {
|
|
55
|
+
...config.load,
|
|
56
|
+
virtual_users: workerVUs
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
logger_1.logger.debug(`🎯 Assigning ${workerVUs} VUs to worker ${worker.getAddress()}`);
|
|
60
|
+
return worker.executeTest(workerConfig);
|
|
61
|
+
});
|
|
62
|
+
await Promise.all(promises);
|
|
63
|
+
}
|
|
64
|
+
async waitForCompletion() {
|
|
65
|
+
logger_1.logger.info('⏳ Waiting for all workers to complete...');
|
|
66
|
+
const promises = this.workers.map(worker => worker.waitForCompletion());
|
|
67
|
+
await Promise.all(promises);
|
|
68
|
+
logger_1.logger.info('✅ All workers completed');
|
|
69
|
+
}
|
|
70
|
+
getAggregatedMetrics() {
|
|
71
|
+
return this.aggregatedMetrics;
|
|
72
|
+
}
|
|
73
|
+
async cleanup() {
|
|
74
|
+
logger_1.logger.info('🧹 Cleaning up workers...');
|
|
75
|
+
const promises = this.workers.map(worker => worker.disconnect());
|
|
76
|
+
await Promise.all(promises);
|
|
77
|
+
this.workers = [];
|
|
78
|
+
}
|
|
79
|
+
getWorkerCount() {
|
|
80
|
+
return this.workers.length;
|
|
81
|
+
}
|
|
82
|
+
getWorkerStatuses() {
|
|
83
|
+
return this.workers.map(worker => ({
|
|
84
|
+
address: worker.getAddress(),
|
|
85
|
+
connected: worker.isConnected()
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
removeWorker(worker) {
|
|
89
|
+
const index = this.workers.indexOf(worker);
|
|
90
|
+
if (index > -1) {
|
|
91
|
+
this.workers.splice(index, 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.WorkerManager = WorkerManager;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class WorkerServer {
|
|
2
|
+
private server;
|
|
3
|
+
private wss;
|
|
4
|
+
private port;
|
|
5
|
+
private host;
|
|
6
|
+
private activeRunners;
|
|
7
|
+
constructor(port?: number, host?: string);
|
|
8
|
+
private setupWebSocketHandlers;
|
|
9
|
+
private handleMessage;
|
|
10
|
+
private executeTest;
|
|
11
|
+
private stopTest;
|
|
12
|
+
private sendMessage;
|
|
13
|
+
private sendError;
|
|
14
|
+
private sendHeartbeat;
|
|
15
|
+
private cleanup;
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
getActiveConnections(): number;
|
|
19
|
+
getActiveTests(): number;
|
|
20
|
+
getStatus(): any;
|
|
21
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
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.WorkerServer = void 0;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const WebSocket = __importStar(require("ws"));
|
|
39
|
+
const test_runner_1 = require("../core/test-runner");
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
class WorkerServer {
|
|
42
|
+
constructor(port = 8080, host = 'localhost') {
|
|
43
|
+
this.activeRunners = new Map();
|
|
44
|
+
this.port = port;
|
|
45
|
+
this.host = host;
|
|
46
|
+
this.server = http.createServer();
|
|
47
|
+
this.wss = new WebSocket.Server({
|
|
48
|
+
server: this.server,
|
|
49
|
+
path: '/perfornium'
|
|
50
|
+
});
|
|
51
|
+
this.setupWebSocketHandlers();
|
|
52
|
+
}
|
|
53
|
+
setupWebSocketHandlers() {
|
|
54
|
+
this.wss.on('connection', (ws, req) => {
|
|
55
|
+
const clientIP = req.socket.remoteAddress;
|
|
56
|
+
logger_1.logger.info(`👤 Worker client connected from ${clientIP}`);
|
|
57
|
+
ws.on('message', async (data) => {
|
|
58
|
+
try {
|
|
59
|
+
const message = JSON.parse(data.toString());
|
|
60
|
+
await this.handleMessage(ws, message);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger_1.logger.error('❌ Error handling message:', error);
|
|
64
|
+
this.sendError(ws, 'Invalid message format');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
ws.on('close', () => {
|
|
68
|
+
logger_1.logger.info('👋 Worker client disconnected');
|
|
69
|
+
this.cleanup(ws);
|
|
70
|
+
});
|
|
71
|
+
ws.on('error', (error) => {
|
|
72
|
+
logger_1.logger.error('❌ WebSocket error:', error);
|
|
73
|
+
this.cleanup(ws);
|
|
74
|
+
});
|
|
75
|
+
// Send initial heartbeat
|
|
76
|
+
this.sendHeartbeat(ws);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async handleMessage(ws, message) {
|
|
80
|
+
switch (message.type) {
|
|
81
|
+
case 'execute_test':
|
|
82
|
+
await this.executeTest(ws, message.config);
|
|
83
|
+
break;
|
|
84
|
+
case 'stop_test':
|
|
85
|
+
await this.stopTest(ws);
|
|
86
|
+
break;
|
|
87
|
+
case 'heartbeat_ack':
|
|
88
|
+
// Client is alive, do nothing
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
logger_1.logger.warn(`⚠️ Unknown message type: ${message.type}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async executeTest(ws, config) {
|
|
95
|
+
try {
|
|
96
|
+
// Stop any existing test for this connection
|
|
97
|
+
await this.stopTest(ws);
|
|
98
|
+
logger_1.logger.info(`🚀 Starting test: ${config.name}`);
|
|
99
|
+
const runner = new test_runner_1.TestRunner(config);
|
|
100
|
+
this.activeRunners.set(ws, runner);
|
|
101
|
+
// Forward results to coordinator
|
|
102
|
+
runner.getMetrics().on('result', (result) => {
|
|
103
|
+
this.sendMessage(ws, {
|
|
104
|
+
type: 'test_result',
|
|
105
|
+
data: result
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// Execute test
|
|
109
|
+
await runner.run();
|
|
110
|
+
// Test completed
|
|
111
|
+
const summary = runner.getMetrics().getSummary();
|
|
112
|
+
this.sendMessage(ws, {
|
|
113
|
+
type: 'test_completed',
|
|
114
|
+
summary: summary
|
|
115
|
+
});
|
|
116
|
+
logger_1.logger.info(`✅ Test completed: ${config.name}`);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
logger_1.logger.error('❌ Test execution failed:', error);
|
|
120
|
+
this.sendError(ws, error.message);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
this.activeRunners.delete(ws);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async stopTest(ws) {
|
|
127
|
+
const runner = this.activeRunners.get(ws);
|
|
128
|
+
if (runner) {
|
|
129
|
+
logger_1.logger.info('⏹️ Stopping test...');
|
|
130
|
+
await runner.stop();
|
|
131
|
+
this.activeRunners.delete(ws);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
sendMessage(ws, message) {
|
|
135
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
136
|
+
ws.send(JSON.stringify(message));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
sendError(ws, error) {
|
|
140
|
+
this.sendMessage(ws, {
|
|
141
|
+
type: 'test_error',
|
|
142
|
+
error: error,
|
|
143
|
+
timestamp: Date.now()
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
sendHeartbeat(ws) {
|
|
147
|
+
const interval = setInterval(() => {
|
|
148
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
149
|
+
this.sendMessage(ws, {
|
|
150
|
+
type: 'heartbeat',
|
|
151
|
+
timestamp: Date.now()
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
clearInterval(interval);
|
|
156
|
+
}
|
|
157
|
+
}, 30000); // 30 seconds
|
|
158
|
+
}
|
|
159
|
+
cleanup(ws) {
|
|
160
|
+
const runner = this.activeRunners.get(ws);
|
|
161
|
+
if (runner) {
|
|
162
|
+
runner.stop().catch(err => logger_1.logger.error('❌ Error stopping runner during cleanup:', err));
|
|
163
|
+
this.activeRunners.delete(ws);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async start() {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
this.server.listen(this.port, this.host, () => {
|
|
169
|
+
logger_1.logger.info(`🚀 Worker server started on ${this.host}:${this.port}`);
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async stop() {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
// Stop all active tests
|
|
177
|
+
for (const [, runner] of this.activeRunners) {
|
|
178
|
+
runner.stop().catch(err => logger_1.logger.error('❌ Error stopping runner:', err));
|
|
179
|
+
}
|
|
180
|
+
this.wss.close(() => {
|
|
181
|
+
this.server.close(() => {
|
|
182
|
+
logger_1.logger.info('👋 Worker server stopped');
|
|
183
|
+
resolve();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
getActiveConnections() {
|
|
189
|
+
return this.wss.clients.size;
|
|
190
|
+
}
|
|
191
|
+
getActiveTests() {
|
|
192
|
+
return this.activeRunners.size;
|
|
193
|
+
}
|
|
194
|
+
getStatus() {
|
|
195
|
+
return {
|
|
196
|
+
host: this.host,
|
|
197
|
+
port: this.port,
|
|
198
|
+
active_connections: this.getActiveConnections(),
|
|
199
|
+
active_tests: this.getActiveTests(),
|
|
200
|
+
uptime: process.uptime(),
|
|
201
|
+
memory_usage: process.memoryUsage()
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.WorkerServer = WorkerServer;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { TestConfiguration } from '../config/types';
|
|
3
|
+
export declare class WorkerNode extends EventEmitter {
|
|
4
|
+
private address;
|
|
5
|
+
private ws?;
|
|
6
|
+
private isRunning;
|
|
7
|
+
private connectionRetries;
|
|
8
|
+
private maxRetries;
|
|
9
|
+
constructor(address: string);
|
|
10
|
+
connect(): Promise<void>;
|
|
11
|
+
executeTest(config: TestConfiguration): Promise<void>;
|
|
12
|
+
private handleMessage;
|
|
13
|
+
private sendHeartbeat;
|
|
14
|
+
waitForCompletion(): Promise<void>;
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
disconnect(): Promise<void>;
|
|
17
|
+
getAddress(): string;
|
|
18
|
+
isConnected(): boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WorkerNode = void 0;
|
|
7
|
+
const events_1 = require("events");
|
|
8
|
+
const ws_1 = __importDefault(require("ws"));
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
class WorkerNode extends events_1.EventEmitter {
|
|
11
|
+
constructor(address) {
|
|
12
|
+
super();
|
|
13
|
+
this.isRunning = false;
|
|
14
|
+
this.connectionRetries = 0;
|
|
15
|
+
this.maxRetries = 3;
|
|
16
|
+
this.address = address;
|
|
17
|
+
}
|
|
18
|
+
async connect() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const wsUrl = `ws://${this.address}/perfornium`;
|
|
21
|
+
logger_1.logger.debug(`🔌 Connecting to worker: ${wsUrl}`);
|
|
22
|
+
this.ws = new ws_1.default(wsUrl);
|
|
23
|
+
const timeout = setTimeout(() => {
|
|
24
|
+
reject(new Error(`Connection timeout to worker ${this.address}`));
|
|
25
|
+
}, 10000);
|
|
26
|
+
this.ws.on('open', () => {
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
this.connectionRetries = 0;
|
|
29
|
+
logger_1.logger.debug(`✅ Connected to worker: ${this.address}`);
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
this.ws.on('error', (error) => {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
logger_1.logger.error(`❌ Connection error to worker ${this.address}:`, error);
|
|
35
|
+
reject(error);
|
|
36
|
+
});
|
|
37
|
+
this.ws.on('message', (data) => {
|
|
38
|
+
try {
|
|
39
|
+
const message = JSON.parse(data.toString());
|
|
40
|
+
this.handleMessage(message);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger_1.logger.error('❌ Invalid message from worker:', error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
this.ws.on('close', (code, reason) => {
|
|
47
|
+
logger_1.logger.debug(`🔌 Worker ${this.address} disconnected: ${code} ${reason}`);
|
|
48
|
+
this.emit('disconnected');
|
|
49
|
+
// Attempt reconnection if unexpected disconnect
|
|
50
|
+
if (this.isRunning && this.connectionRetries < this.maxRetries) {
|
|
51
|
+
this.connectionRetries++;
|
|
52
|
+
logger_1.logger.info(`🔄 Attempting to reconnect to worker ${this.address} (${this.connectionRetries}/${this.maxRetries})`);
|
|
53
|
+
setTimeout(() => this.connect(), 5000);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async executeTest(config) {
|
|
59
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
60
|
+
throw new Error(`Worker ${this.address} is not connected`);
|
|
61
|
+
}
|
|
62
|
+
this.isRunning = true;
|
|
63
|
+
const message = {
|
|
64
|
+
type: 'execute_test',
|
|
65
|
+
config: config,
|
|
66
|
+
timestamp: Date.now()
|
|
67
|
+
};
|
|
68
|
+
logger_1.logger.debug(`🚀 Starting test on worker ${this.address}`);
|
|
69
|
+
this.ws.send(JSON.stringify(message));
|
|
70
|
+
}
|
|
71
|
+
handleMessage(message) {
|
|
72
|
+
switch (message.type) {
|
|
73
|
+
case 'test_result':
|
|
74
|
+
this.emit('result', message.data);
|
|
75
|
+
break;
|
|
76
|
+
case 'test_progress':
|
|
77
|
+
logger_1.logger.debug(`📊 Worker ${this.address} progress: ${message.data.completed}/${message.data.total}`);
|
|
78
|
+
this.emit('progress', message.data);
|
|
79
|
+
break;
|
|
80
|
+
case 'test_completed':
|
|
81
|
+
this.isRunning = false;
|
|
82
|
+
logger_1.logger.info(`✅ Worker ${this.address} completed test`);
|
|
83
|
+
this.emit('completed', message.summary);
|
|
84
|
+
break;
|
|
85
|
+
case 'test_error':
|
|
86
|
+
this.isRunning = false;
|
|
87
|
+
logger_1.logger.error(`❌ Worker ${this.address} test error: ${message.error}`);
|
|
88
|
+
this.emit('error', new Error(message.error));
|
|
89
|
+
break;
|
|
90
|
+
case 'heartbeat':
|
|
91
|
+
this.sendHeartbeat();
|
|
92
|
+
break;
|
|
93
|
+
case 'log':
|
|
94
|
+
logger_1.logger.debug(`📝 Worker ${this.address}: ${message.message}`);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
logger_1.logger.warn(`⚠️ Unknown message type from worker ${this.address}: ${message.type}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
sendHeartbeat() {
|
|
101
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
102
|
+
this.ws.send(JSON.stringify({
|
|
103
|
+
type: 'heartbeat_ack',
|
|
104
|
+
timestamp: Date.now()
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async waitForCompletion() {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
if (!this.isRunning) {
|
|
111
|
+
resolve();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const timeout = setTimeout(() => {
|
|
115
|
+
reject(new Error(`Worker ${this.address} completion timeout`));
|
|
116
|
+
}, 300000); // 5 minute timeout
|
|
117
|
+
this.once('completed', () => {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
this.once('error', (error) => {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async stop() {
|
|
128
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
129
|
+
this.ws.send(JSON.stringify({ type: 'stop_test' }));
|
|
130
|
+
}
|
|
131
|
+
this.isRunning = false;
|
|
132
|
+
}
|
|
133
|
+
async disconnect() {
|
|
134
|
+
this.isRunning = false;
|
|
135
|
+
if (this.ws) {
|
|
136
|
+
this.ws.close();
|
|
137
|
+
this.ws = undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
getAddress() {
|
|
141
|
+
return this.address;
|
|
142
|
+
}
|
|
143
|
+
isConnected() {
|
|
144
|
+
return this.ws?.readyState === ws_1.default.OPEN;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.WorkerNode = WorkerNode;
|
package/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@testsmith/perfornium",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Flexible performance testing framework for REST, SOAP, and web applications",
|
|
5
|
+
"author": "TestSmith",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/testsmith-io/perfornium.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/testsmith-io/perfornium#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/testsmith-io/perfornium/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"perfornium": "dist/cli/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./dsl": {
|
|
26
|
+
"types": "./dist/dsl/index.d.ts",
|
|
27
|
+
"default": "./dist/dsl/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc && npm run copy-templates",
|
|
39
|
+
"copy-templates": "mkdir -p dist/reporting/templates && cp -r src/reporting/templates/* dist/reporting/templates/ 2>/dev/null || true",
|
|
40
|
+
"lint": "eslint src/",
|
|
41
|
+
"lint:fix": "eslint src/ --fix",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"test:run": "vitest run",
|
|
44
|
+
"test:coverage": "vitest --coverage",
|
|
45
|
+
"test:ui": "vitest --ui",
|
|
46
|
+
"benchmark": "npm run build && node --expose-gc dist/benchmarks/performance-tests.js",
|
|
47
|
+
"prepare": "npm run build",
|
|
48
|
+
"dev": "tsc --watch",
|
|
49
|
+
"dev:link": "npm run build && npm link",
|
|
50
|
+
"dev:test": "npm run build && node dist/cli/cli.js",
|
|
51
|
+
"report:web-vitals": "node generate-web-vitals-report.js"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"performance",
|
|
55
|
+
"testing",
|
|
56
|
+
"load-testing",
|
|
57
|
+
"rest",
|
|
58
|
+
"soap",
|
|
59
|
+
"web",
|
|
60
|
+
"playwright"
|
|
61
|
+
],
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@faker-js/faker": "^9.9.0",
|
|
64
|
+
"@types/papaparse": "^5.3.16",
|
|
65
|
+
"@xmldom/xmldom": "^0.8.11",
|
|
66
|
+
"axios": "^1.6.0",
|
|
67
|
+
"chalk": "^5.3.0",
|
|
68
|
+
"commander": "^11.0.0",
|
|
69
|
+
"csv-parse": "^5.5.0",
|
|
70
|
+
"csv-writer": "^1.6.0",
|
|
71
|
+
"esbuild": "^0.27.1",
|
|
72
|
+
"handlebars": "^4.7.0",
|
|
73
|
+
"influx": "^5.10.0",
|
|
74
|
+
"inquirer": "^12.9.4",
|
|
75
|
+
"js-yaml": "^4.1.0",
|
|
76
|
+
"ora": "^8.2.0",
|
|
77
|
+
"papaparse": "^5.5.3",
|
|
78
|
+
"playwright": "^1.40.0",
|
|
79
|
+
"soap": "^1.0.0",
|
|
80
|
+
"ws": "^8.14.0",
|
|
81
|
+
"yaml": "^2.3.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@eslint/js": "^9.39.2",
|
|
85
|
+
"@types/handlebars": "^4.1.0",
|
|
86
|
+
"@types/js-yaml": "^4.0.9",
|
|
87
|
+
"@types/node": "^20.0.0",
|
|
88
|
+
"@types/ws": "^8.18.1",
|
|
89
|
+
"@types/xmldom": "^0.1.34",
|
|
90
|
+
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
|
91
|
+
"@typescript-eslint/parser": "^8.49.0",
|
|
92
|
+
"@vitest/coverage-v8": "^1.0.0",
|
|
93
|
+
"@vitest/ui": "^1.0.0",
|
|
94
|
+
"eslint": "^9.39.2",
|
|
95
|
+
"typescript": "^5.2.0",
|
|
96
|
+
"typescript-eslint": "^8.49.0",
|
|
97
|
+
"vitest": "^1.0.0"
|
|
98
|
+
},
|
|
99
|
+
"engines": {
|
|
100
|
+
"node": ">=18.0.0"
|
|
101
|
+
}
|
|
102
|
+
}
|