@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.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. 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
+ }