@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,18 @@
1
+ import { RemoteWorker } from './remote-worker';
2
+ export interface WorkerHealth {
3
+ address: string;
4
+ lastHeartbeat: number;
5
+ isHealthy: boolean;
6
+ responseTime: number;
7
+ errorCount: number;
8
+ status: 'connected' | 'disconnected' | 'unhealthy' | 'timeout';
9
+ }
10
+ export declare class HealthMonitor {
11
+ private workerHealth;
12
+ private monitorInterval?;
13
+ private isMonitoring;
14
+ start(workers: RemoteWorker[], intervalMs?: number): void;
15
+ stop(): void;
16
+ updateWorkerStatus(address: string, status: any): void;
17
+ private checkWorkerHealth;
18
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HealthMonitor = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ class HealthMonitor {
6
+ constructor() {
7
+ this.workerHealth = new Map();
8
+ this.isMonitoring = false;
9
+ }
10
+ start(workers, intervalMs = 30000) {
11
+ this.isMonitoring = true;
12
+ // Initialize health tracking
13
+ workers.forEach(worker => {
14
+ this.workerHealth.set(worker.getAddress(), {
15
+ address: worker.getAddress(),
16
+ lastHeartbeat: Date.now(),
17
+ isHealthy: true,
18
+ responseTime: 0,
19
+ errorCount: 0,
20
+ status: 'connected'
21
+ });
22
+ });
23
+ // Start monitoring loop
24
+ this.monitorInterval = setInterval(() => {
25
+ this.checkWorkerHealth();
26
+ }, intervalMs);
27
+ logger_1.logger.debug(`💓 Health monitoring started (${intervalMs}ms interval)`);
28
+ }
29
+ stop() {
30
+ this.isMonitoring = false;
31
+ if (this.monitorInterval) {
32
+ clearInterval(this.monitorInterval);
33
+ this.monitorInterval = undefined;
34
+ }
35
+ logger_1.logger.debug('💓 Health monitoring stopped');
36
+ }
37
+ updateWorkerStatus(address, status) {
38
+ const health = this.workerHealth.get(address);
39
+ if (health) {
40
+ health.lastHeartbeat = Date.now();
41
+ health.responseTime = status.responseTime || 0;
42
+ health.status = 'connected';
43
+ if (status.error) {
44
+ health.errorCount++;
45
+ }
46
+ this.workerHealth.set(address, health);
47
+ }
48
+ }
49
+ checkWorkerHealth() {
50
+ const now = Date.now();
51
+ const timeoutMs = 60000; // 1 minute timeout
52
+ for (const [address, health] of this.workerHealth) {
53
+ const timeSinceHeartbeat = now - health.lastHeartbeat;
54
+ if (timeSinceHeartbeat > timeoutMs) {
55
+ health.isHealthy = false;
56
+ health.status = 'timeout';
57
+ logger_1.logger.warn(`⚠️ Worker ${address} health check timeout`);
58
+ }
59
+ else if (health.errorCount > 10) {
60
+ health.isHealthy = false;
61
+ health.status = 'unhealthy';
62
+ logger_1.logger.warn(`⚠️ Worker ${address} has too many errors`);
63
+ }
64
+ else {
65
+ health.isHealthy = true;
66
+ health.status = 'connected';
67
+ }
68
+ this.workerHealth.set(address, health);
69
+ }
70
+ }
71
+ }
72
+ exports.HealthMonitor = HealthMonitor;
@@ -0,0 +1,17 @@
1
+ import { TestConfiguration } from '../config';
2
+ import { RemoteWorker } from './remote-worker';
3
+ export type DistributionStrategy = 'even' | 'capacity_based' | 'round_robin' | 'geographic';
4
+ export interface WorkerAssignment {
5
+ worker: RemoteWorker;
6
+ config: TestConfiguration;
7
+ virtualUsers: number;
8
+ startDelay?: number;
9
+ }
10
+ export declare class LoadDistributor {
11
+ distribute(testConfig: TestConfiguration, workers: RemoteWorker[], strategy: DistributionStrategy): WorkerAssignment[];
12
+ private evenDistribution;
13
+ private capacityBasedDistribution;
14
+ private roundRobinDistribution;
15
+ private geographicDistribution;
16
+ private createWorkerConfig;
17
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoadDistributor = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ class LoadDistributor {
6
+ distribute(testConfig, workers, strategy) {
7
+ const { getPrimaryLoadPhase } = require('../config/types/load-config');
8
+ const primaryPhase = getPrimaryLoadPhase(testConfig.load);
9
+ const totalVUs = primaryPhase.virtual_users || primaryPhase.vus || 1;
10
+ logger_1.logger.info(`📊 Distributing ${totalVUs} VUs using ${strategy} strategy`);
11
+ switch (strategy) {
12
+ case 'even':
13
+ return this.evenDistribution(testConfig, workers, totalVUs);
14
+ case 'capacity_based':
15
+ return this.capacityBasedDistribution(testConfig, workers, totalVUs);
16
+ case 'round_robin':
17
+ return this.roundRobinDistribution(testConfig, workers, totalVUs);
18
+ case 'geographic':
19
+ return this.geographicDistribution(testConfig, workers, totalVUs);
20
+ default:
21
+ throw new Error(`Unknown distribution strategy: ${strategy}`);
22
+ }
23
+ }
24
+ evenDistribution(testConfig, workers, totalVUs) {
25
+ const vusPerWorker = Math.floor(totalVUs / workers.length);
26
+ const remainder = totalVUs % workers.length;
27
+ return workers.map((worker, index) => {
28
+ const virtualUsers = vusPerWorker + (index < remainder ? 1 : 0);
29
+ return {
30
+ worker,
31
+ virtualUsers,
32
+ config: this.createWorkerConfig(testConfig, virtualUsers, index)
33
+ };
34
+ });
35
+ }
36
+ capacityBasedDistribution(testConfig, workers, totalVUs) {
37
+ const totalCapacity = workers.reduce((sum, worker) => sum + worker.getCapacity(), 0);
38
+ if (totalCapacity === 0) {
39
+ throw new Error('No worker capacity available');
40
+ }
41
+ return workers.map((worker, index) => {
42
+ const workerRatio = worker.getCapacity() / totalCapacity;
43
+ const virtualUsers = Math.round(totalVUs * workerRatio);
44
+ return {
45
+ worker,
46
+ virtualUsers,
47
+ config: this.createWorkerConfig(testConfig, virtualUsers, index)
48
+ };
49
+ });
50
+ }
51
+ roundRobinDistribution(testConfig, workers, totalVUs) {
52
+ const assignments = workers.map((worker, index) => ({
53
+ worker,
54
+ virtualUsers: 0,
55
+ config: this.createWorkerConfig(testConfig, 0, index)
56
+ }));
57
+ for (let vu = 0; vu < totalVUs; vu++) {
58
+ const workerIndex = vu % workers.length;
59
+ assignments[workerIndex].virtualUsers++;
60
+ }
61
+ assignments.forEach((assignment, index) => {
62
+ assignment.config = this.createWorkerConfig(testConfig, assignment.virtualUsers, index);
63
+ });
64
+ return assignments;
65
+ }
66
+ geographicDistribution(testConfig, workers, totalVUs) {
67
+ const regionGroups = new Map();
68
+ workers.forEach(worker => {
69
+ const region = worker.getRegion();
70
+ if (!regionGroups.has(region)) {
71
+ regionGroups.set(region, []);
72
+ }
73
+ regionGroups.get(region).push(worker);
74
+ });
75
+ const vusPerRegion = Math.floor(totalVUs / regionGroups.size);
76
+ const regionRemainder = totalVUs % regionGroups.size;
77
+ const assignments = [];
78
+ let regionIndex = 0;
79
+ for (const [region, regionWorkers] of regionGroups) {
80
+ const regionVUs = vusPerRegion + (regionIndex < regionRemainder ? 1 : 0);
81
+ const regionAssignments = this.evenDistribution(testConfig, regionWorkers, regionVUs);
82
+ assignments.push(...regionAssignments);
83
+ regionIndex++;
84
+ }
85
+ return assignments;
86
+ }
87
+ createWorkerConfig(baseConfig, virtualUsers, workerIndex) {
88
+ // Fixed: Ensure report config has required fields
89
+ const reportConfig = baseConfig.report ? {
90
+ ...baseConfig.report,
91
+ generate: false, // Workers shouldn't generate reports
92
+ output: baseConfig.report.output || 'worker-report.html' // Provide default
93
+ } : undefined;
94
+ return {
95
+ ...baseConfig,
96
+ name: `${baseConfig.name} - Worker ${workerIndex + 1}`,
97
+ load: {
98
+ ...baseConfig.load,
99
+ virtual_users: virtualUsers
100
+ },
101
+ outputs: baseConfig.outputs?.filter(output => output.type !== 'webhook'),
102
+ report: reportConfig
103
+ };
104
+ }
105
+ }
106
+ exports.LoadDistributor = LoadDistributor;
@@ -0,0 +1,37 @@
1
+ import { EventEmitter } from 'events';
2
+ import { TestConfiguration } from '../config/types';
3
+ export interface RemoteWorkerConfig {
4
+ host: string;
5
+ port: number;
6
+ capacity: number;
7
+ region: string;
8
+ }
9
+ export interface WorkerStatus {
10
+ connected: boolean;
11
+ running: boolean;
12
+ virtualUsers: number;
13
+ requestsPerSecond: number;
14
+ responseTime: number;
15
+ errorRate: number;
16
+ activeRunner?: string;
17
+ }
18
+ export declare class RemoteWorker extends EventEmitter {
19
+ private config;
20
+ private connected;
21
+ private status;
22
+ constructor(config: RemoteWorkerConfig);
23
+ connect(): Promise<void>;
24
+ disconnect(): Promise<void>;
25
+ prepareTest(testConfig: TestConfiguration): Promise<void>;
26
+ startTest(startTime?: number): Promise<void>;
27
+ executeTest(testConfig: TestConfiguration): Promise<void>;
28
+ stop(): Promise<void>;
29
+ waitForCompletion(): Promise<void>;
30
+ getWorkerStatus(): Promise<WorkerStatus>;
31
+ private sendHealthCheck;
32
+ private sendRequest;
33
+ getAddress(): string;
34
+ getCapacity(): number;
35
+ getRegion(): string;
36
+ getResults(): Promise<any>;
37
+ }
@@ -0,0 +1,241 @@
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.RemoteWorker = void 0;
37
+ const events_1 = require("events");
38
+ const logger_1 = require("../utils/logger");
39
+ const http = __importStar(require("http"));
40
+ class RemoteWorker extends events_1.EventEmitter {
41
+ constructor(config) {
42
+ super();
43
+ this.connected = false;
44
+ this.config = config;
45
+ this.status = {
46
+ connected: false,
47
+ running: false,
48
+ virtualUsers: 0,
49
+ requestsPerSecond: 0,
50
+ responseTime: 0,
51
+ errorRate: 0
52
+ };
53
+ }
54
+ async connect() {
55
+ try {
56
+ // Implement connection logic to worker
57
+ await this.sendHealthCheck();
58
+ this.connected = true;
59
+ this.status.connected = true;
60
+ logger_1.logger.debug(`🔗 Connected to worker ${this.getAddress()}`);
61
+ }
62
+ catch (error) {
63
+ this.connected = false;
64
+ this.status.connected = false;
65
+ throw new Error(`Failed to connect to worker ${this.getAddress()}: ${error}`);
66
+ }
67
+ }
68
+ async disconnect() {
69
+ this.connected = false;
70
+ this.status.connected = false;
71
+ logger_1.logger.debug(`🔌 Disconnected from worker ${this.getAddress()}`);
72
+ }
73
+ async prepareTest(testConfig) {
74
+ if (!this.connected) {
75
+ throw new Error(`Worker ${this.getAddress()} not connected`);
76
+ }
77
+ try {
78
+ await this.sendRequest('/prepare', {
79
+ method: 'POST',
80
+ data: testConfig
81
+ });
82
+ logger_1.logger.debug(`📋 Test prepared on worker ${this.getAddress()}`);
83
+ }
84
+ catch (error) {
85
+ throw new Error(`Failed to prepare test on worker ${this.getAddress()}: ${error}`);
86
+ }
87
+ }
88
+ async startTest(startTime) {
89
+ if (!this.connected) {
90
+ throw new Error(`Worker ${this.getAddress()} not connected`);
91
+ }
92
+ try {
93
+ await this.sendRequest('/start', {
94
+ method: 'POST',
95
+ data: { startTime }
96
+ });
97
+ this.status.running = true;
98
+ logger_1.logger.debug(`🚀 Test started on worker ${this.getAddress()}`);
99
+ }
100
+ catch (error) {
101
+ throw new Error(`Failed to start test on worker ${this.getAddress()}: ${error}`);
102
+ }
103
+ }
104
+ async executeTest(testConfig) {
105
+ await this.prepareTest(testConfig);
106
+ await this.startTest();
107
+ }
108
+ async stop() {
109
+ if (!this.connected) {
110
+ return;
111
+ }
112
+ try {
113
+ await this.sendRequest('/stop', { method: 'POST' });
114
+ this.status.running = false;
115
+ logger_1.logger.debug(`⏹️ Test stopped on worker ${this.getAddress()}`);
116
+ }
117
+ catch (error) {
118
+ logger_1.logger.warn(`Failed to stop test on worker ${this.getAddress()}: ${error}`);
119
+ }
120
+ }
121
+ async waitForCompletion() {
122
+ if (!this.status.running) {
123
+ return;
124
+ }
125
+ return new Promise((resolve, reject) => {
126
+ const checkCompletion = async () => {
127
+ try {
128
+ const status = await this.getWorkerStatus();
129
+ logger_1.logger.debug(`🔍 Worker ${this.getAddress()} status: running=${status.running}, activeRunner=${status.activeRunner}`);
130
+ // Check if worker is no longer running AND has no active runner
131
+ if (!status.running && (!status.activeRunner || status.activeRunner === 'idle')) {
132
+ this.status.running = false;
133
+ logger_1.logger.debug(`✅ Worker ${this.getAddress()} completed`);
134
+ resolve();
135
+ }
136
+ else {
137
+ // Continue checking
138
+ setTimeout(checkCompletion, 2000); // Check every 2 seconds
139
+ }
140
+ }
141
+ catch (error) {
142
+ logger_1.logger.warn(`⚠️ Error checking worker ${this.getAddress()} status:`, error);
143
+ // On connection error, assume worker completed
144
+ this.status.running = false;
145
+ resolve();
146
+ }
147
+ };
148
+ checkCompletion();
149
+ });
150
+ }
151
+ async getWorkerStatus() {
152
+ if (!this.connected) {
153
+ return this.status;
154
+ }
155
+ try {
156
+ const response = await this.sendRequest('/status', { method: 'GET' });
157
+ this.status = { ...this.status, ...response };
158
+ this.emit('status', this.status);
159
+ return this.status;
160
+ }
161
+ catch (error) {
162
+ this.emit('error', error);
163
+ throw error;
164
+ }
165
+ }
166
+ async sendHealthCheck() {
167
+ const response = await this.sendRequest('/health', { method: 'GET' });
168
+ if (response.status !== 'healthy') {
169
+ throw new Error(`Worker health check failed: ${JSON.stringify(response)}`);
170
+ }
171
+ }
172
+ async sendRequest(path, options) {
173
+ return new Promise((resolve, reject) => {
174
+ const postData = options.data ? JSON.stringify(options.data) : undefined;
175
+ const requestOptions = {
176
+ hostname: this.config.host,
177
+ port: this.config.port,
178
+ path,
179
+ method: options.method,
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ ...(postData && { 'Content-Length': Buffer.byteLength(postData) })
183
+ },
184
+ timeout: 30000
185
+ };
186
+ const req = http.request(requestOptions, (res) => {
187
+ let data = '';
188
+ res.on('data', (chunk) => {
189
+ data += chunk;
190
+ });
191
+ res.on('end', () => {
192
+ try {
193
+ const response = data ? JSON.parse(data) : {};
194
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
195
+ resolve(response);
196
+ }
197
+ else {
198
+ reject(new Error(`HTTP ${res.statusCode}: ${response.message || 'Request failed'}`));
199
+ }
200
+ }
201
+ catch (error) {
202
+ reject(new Error(`Invalid JSON response: ${error}`));
203
+ }
204
+ });
205
+ });
206
+ req.on('error', (error) => {
207
+ reject(error);
208
+ });
209
+ req.on('timeout', () => {
210
+ req.destroy();
211
+ reject(new Error('Request timeout'));
212
+ });
213
+ if (postData) {
214
+ req.write(postData);
215
+ }
216
+ req.end();
217
+ });
218
+ }
219
+ getAddress() {
220
+ return `${this.config.host}:${this.config.port}`;
221
+ }
222
+ getCapacity() {
223
+ return this.config.capacity;
224
+ }
225
+ getRegion() {
226
+ return this.config.region;
227
+ }
228
+ async getResults() {
229
+ if (!this.connected) {
230
+ throw new Error(`Worker ${this.getAddress()} not connected`);
231
+ }
232
+ try {
233
+ return await this.sendRequest('/results', { method: 'GET' });
234
+ }
235
+ catch (error) {
236
+ this.emit('error', error);
237
+ throw error;
238
+ }
239
+ }
240
+ }
241
+ exports.RemoteWorker = RemoteWorker;
@@ -0,0 +1,43 @@
1
+ import { TestResult, VUStartEvent } from '../metrics/types';
2
+ export interface AggregatedResults {
3
+ summary: {
4
+ total_requests: number;
5
+ success_rate: number;
6
+ avg_response_time: number;
7
+ requests_per_second: number;
8
+ total_errors: number;
9
+ start_time: number;
10
+ end_time: number;
11
+ duration: number;
12
+ total_duration: number;
13
+ total_virtual_users: number;
14
+ peak_virtual_users: number;
15
+ successful_requests: number;
16
+ failed_requests: number;
17
+ vu_ramp_up: VUStartEvent[];
18
+ };
19
+ results: TestResult[];
20
+ workers: {
21
+ [workerAddress: string]: {
22
+ requests: number;
23
+ errors: number;
24
+ avg_response_time: number;
25
+ requests_per_second: number;
26
+ };
27
+ };
28
+ }
29
+ export declare class ResultAggregator {
30
+ private results;
31
+ private workerResults;
32
+ private vuRampUpEvents;
33
+ private workerVUEvents;
34
+ private isAggregating;
35
+ private startTime;
36
+ private endTime;
37
+ start(): void;
38
+ stop(): void;
39
+ addResult(result: TestResult, workerAddress?: string): void;
40
+ addVURampUpEvents(events: VUStartEvent[], workerAddress: string): void;
41
+ getAggregatedResults(): AggregatedResults;
42
+ clear(): void;
43
+ }
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResultAggregator = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ class ResultAggregator {
6
+ constructor() {
7
+ this.results = [];
8
+ this.workerResults = new Map();
9
+ this.vuRampUpEvents = [];
10
+ this.workerVUEvents = new Map();
11
+ this.isAggregating = false;
12
+ this.startTime = 0;
13
+ this.endTime = 0;
14
+ }
15
+ start() {
16
+ this.isAggregating = true;
17
+ this.startTime = Date.now();
18
+ this.results = [];
19
+ this.workerResults.clear();
20
+ this.vuRampUpEvents = [];
21
+ this.workerVUEvents.clear();
22
+ logger_1.logger.debug('📊 Result aggregation started');
23
+ }
24
+ stop() {
25
+ this.isAggregating = false;
26
+ this.endTime = Date.now();
27
+ logger_1.logger.debug('📊 Result aggregation stopped');
28
+ }
29
+ addResult(result, workerAddress) {
30
+ if (!this.isAggregating) {
31
+ return;
32
+ }
33
+ this.results.push(result);
34
+ if (workerAddress) {
35
+ if (!this.workerResults.has(workerAddress)) {
36
+ this.workerResults.set(workerAddress, []);
37
+ }
38
+ this.workerResults.get(workerAddress).push(result);
39
+ }
40
+ }
41
+ addVURampUpEvents(events, workerAddress) {
42
+ if (!this.isAggregating || !events || events.length === 0) {
43
+ return;
44
+ }
45
+ // Store per-worker VU events (with worker-unique VU IDs)
46
+ if (!this.workerVUEvents.has(workerAddress)) {
47
+ this.workerVUEvents.set(workerAddress, []);
48
+ }
49
+ // Create globally unique VU IDs by prefixing with worker index
50
+ const workerIndex = Array.from(this.workerVUEvents.keys()).indexOf(workerAddress);
51
+ const workerPrefix = workerIndex >= 0 ? workerIndex : this.workerVUEvents.size;
52
+ const maxVUsPerWorker = 10000; // Allows up to 10000 VUs per worker
53
+ events.forEach(event => {
54
+ // Create a globally unique VU ID: workerPrefix * maxVUsPerWorker + original vu_id
55
+ const globalVUId = (workerPrefix * maxVUsPerWorker) + event.vu_id;
56
+ const globalEvent = {
57
+ ...event,
58
+ vu_id: globalVUId
59
+ };
60
+ this.vuRampUpEvents.push(globalEvent);
61
+ this.workerVUEvents.get(workerAddress).push(event); // Keep original for per-worker tracking
62
+ });
63
+ logger_1.logger.debug(`📊 Added ${events.length} VU ramp-up events from ${workerAddress}`);
64
+ }
65
+ getAggregatedResults() {
66
+ const totalRequests = this.results.length;
67
+ const successfulRequests = this.results.filter(r => r.success).length;
68
+ const failedRequests = totalRequests - successfulRequests;
69
+ const successRate = totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0;
70
+ const responseTimes = this.results.map(r => r.duration || r.response_size || 0);
71
+ const avgResponseTime = responseTimes.length > 0
72
+ ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
73
+ : 0;
74
+ const duration = this.endTime - this.startTime;
75
+ const requestsPerSecond = duration > 0 ? (totalRequests / duration) * 1000 : 0;
76
+ // Aggregate worker-specific results
77
+ const workerStats = {};
78
+ for (const [workerAddress, workerResults] of this.workerResults) {
79
+ const workerRequests = workerResults.length;
80
+ const workerSuccessful = workerResults.filter(r => r.success).length;
81
+ const workerErrors = workerRequests - workerSuccessful;
82
+ const workerResponseTimes = workerResults.map(r => r.duration || r.response_size || 0);
83
+ const workerAvgResponseTime = workerResponseTimes.length > 0
84
+ ? workerResponseTimes.reduce((sum, time) => sum + time, 0) / workerResponseTimes.length
85
+ : 0;
86
+ const workerRequestsPerSecond = duration > 0 ? (workerRequests / duration) * 1000 : 0;
87
+ workerStats[workerAddress] = {
88
+ requests: workerRequests,
89
+ errors: workerErrors,
90
+ avg_response_time: workerAvgResponseTime,
91
+ requests_per_second: workerRequestsPerSecond
92
+ };
93
+ }
94
+ // Calculate total virtual users across all workers
95
+ // Prefer VU ramp-up events if available (most accurate)
96
+ let totalVUs = 0;
97
+ if (this.vuRampUpEvents.length > 0) {
98
+ // Count from actual VU start events (already made globally unique)
99
+ totalVUs = this.vuRampUpEvents.length;
100
+ }
101
+ else {
102
+ // Fallback: Each worker has its own VU IDs (1, 2, 3...), so count per worker and sum
103
+ for (const [, workerResults] of this.workerResults) {
104
+ const uniqueVUsPerWorker = new Set(workerResults.map(r => r.vu_id)).size;
105
+ totalVUs += uniqueVUsPerWorker;
106
+ }
107
+ // Fallback if no worker info available
108
+ if (totalVUs === 0) {
109
+ totalVUs = new Set(this.results.map(r => r.vu_id)).size;
110
+ }
111
+ }
112
+ // Duration in seconds for report display
113
+ const totalDurationSeconds = duration / 1000;
114
+ // Sort VU ramp-up events by start time for proper chart rendering
115
+ const sortedVURampUp = [...this.vuRampUpEvents].sort((a, b) => a.start_time - b.start_time);
116
+ return {
117
+ summary: {
118
+ total_requests: totalRequests,
119
+ success_rate: successRate,
120
+ avg_response_time: avgResponseTime,
121
+ requests_per_second: requestsPerSecond,
122
+ total_errors: failedRequests,
123
+ start_time: this.startTime,
124
+ end_time: this.endTime,
125
+ duration,
126
+ total_duration: totalDurationSeconds,
127
+ total_virtual_users: totalVUs,
128
+ peak_virtual_users: totalVUs,
129
+ successful_requests: successfulRequests,
130
+ failed_requests: failedRequests,
131
+ vu_ramp_up: sortedVURampUp
132
+ },
133
+ results: this.results,
134
+ workers: workerStats
135
+ };
136
+ }
137
+ clear() {
138
+ this.results = [];
139
+ this.workerResults.clear();
140
+ this.vuRampUpEvents = [];
141
+ this.workerVUEvents.clear();
142
+ this.startTime = 0;
143
+ this.endTime = 0;
144
+ }
145
+ }
146
+ exports.ResultAggregator = ResultAggregator;
@@ -0,0 +1,3 @@
1
+ export { TestBuilder, ScenarioBuilder, LoadBuilder, test, load, faker, testData } from './test-builder';
2
+ export type { ScenarioContext } from './test-builder';
3
+ export type { TestConfiguration, Scenario, Step } from '../config/types';