@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,34 @@
1
+ import { TestConfiguration } from '../config';
2
+ import { MetricsCollector } from '../metrics/collector';
3
+ export declare class TestRunner {
4
+ private config;
5
+ private metrics;
6
+ private handlers;
7
+ private outputs;
8
+ private activeVUs;
9
+ private isRunning;
10
+ private startTime;
11
+ constructor(config: TestConfiguration);
12
+ run(): Promise<void>;
13
+ private setupCSVBaseDirectory;
14
+ stop(): Promise<void>;
15
+ private initialize;
16
+ private initializeProtocolHandlers;
17
+ private getRequiredProtocols;
18
+ private findWSDLUrl;
19
+ private initializeOutputs;
20
+ /**
21
+ * Process template variables in file paths and automatically add timestamp if not present
22
+ */
23
+ private processTemplateFilePath;
24
+ private executeLoadPattern;
25
+ private getLoadPatternForPhase;
26
+ private createVUFactory;
27
+ private waitForVUsToComplete;
28
+ private cleanup;
29
+ private finalize;
30
+ private logSummary;
31
+ private logErrorDetails;
32
+ private generateReport;
33
+ getMetrics(): MetricsCollector;
34
+ }
@@ -0,0 +1,465 @@
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.TestRunner = void 0;
37
+ const collector_1 = require("../metrics/collector");
38
+ const virtual_user_1 = require("./virtual-user");
39
+ const handler_1 = require("../protocols/rest/handler");
40
+ const handler_2 = require("../protocols/soap/handler");
41
+ const handler_3 = require("../protocols/web/handler");
42
+ const basic_1 = require("../load-patterns/basic");
43
+ const stepping_1 = require("../load-patterns/stepping");
44
+ const arrivals_1 = require("../load-patterns/arrivals");
45
+ const csv_1 = require("../outputs/csv");
46
+ const json_1 = require("../outputs/json");
47
+ const influxdb_1 = require("../outputs/influxdb");
48
+ const graphite_1 = require("../outputs/graphite");
49
+ const webhook_1 = require("../outputs/webhook");
50
+ const logger_1 = require("../utils/logger");
51
+ const time_1 = require("../utils/time");
52
+ const csv_data_provider_1 = require("./csv-data-provider");
53
+ const file_manager_1 = require("../utils/file-manager");
54
+ class TestRunner {
55
+ constructor(config) {
56
+ this.handlers = new Map();
57
+ this.outputs = [];
58
+ this.activeVUs = [];
59
+ this.isRunning = false;
60
+ this.startTime = 0;
61
+ this.config = config;
62
+ this.metrics = new collector_1.MetricsCollector();
63
+ // Set log level based on debug config
64
+ if (config.debug?.log_level || config.global?.debug?.log_level) {
65
+ const logLevel = config.debug?.log_level || config.global?.debug?.log_level;
66
+ switch (logLevel) {
67
+ case 'debug':
68
+ logger_1.logger.setLevel(logger_1.LogLevel.DEBUG);
69
+ break;
70
+ case 'info':
71
+ logger_1.logger.setLevel(logger_1.LogLevel.INFO);
72
+ break;
73
+ case 'warn':
74
+ logger_1.logger.setLevel(logger_1.LogLevel.WARN);
75
+ break;
76
+ case 'error':
77
+ logger_1.logger.setLevel(logger_1.LogLevel.ERROR);
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ async run() {
83
+ logger_1.logger.info(`๐Ÿš€ Starting test: ${this.config.name}`);
84
+ this.isRunning = true;
85
+ this.startTime = Date.now();
86
+ try {
87
+ await this.initialize();
88
+ // NO CSV termination callback setup needed anymore
89
+ await this.executeLoadPattern();
90
+ await this.finalize();
91
+ const duration = Date.now() - this.startTime;
92
+ logger_1.logger.success(`โœ… Test completed successfully in ${(duration / 1000).toFixed(1)}s`);
93
+ }
94
+ catch (error) {
95
+ logger_1.logger.error('โŒ Test failed:', error);
96
+ throw error;
97
+ }
98
+ finally {
99
+ this.isRunning = false;
100
+ }
101
+ }
102
+ setupCSVBaseDirectory() {
103
+ const hasCSVScenarios = this.config.scenarios?.some((s) => s.csv_data);
104
+ if (hasCSVScenarios) {
105
+ const baseDir = process.cwd();
106
+ csv_data_provider_1.CSVDataProvider.setBaseDir(baseDir);
107
+ // Check if any scenario has cycleOnExhaustion: false
108
+ const hasStopOnExhaustion = this.config.scenarios?.some((s) => s.csv_data && s.csv_data.cycleOnExhaustion === false);
109
+ if (hasStopOnExhaustion) {
110
+ logger_1.logger.info('โน๏ธ CSV exhaustion handling enabled - individual VUs will stop when CSV data is exhausted');
111
+ }
112
+ logger_1.logger.debug(`Set CSV base directory: ${baseDir}`);
113
+ }
114
+ }
115
+ // FIXED: Added stop method that was missing
116
+ async stop() {
117
+ logger_1.logger.info('โน๏ธ Stopping test...');
118
+ this.isRunning = false;
119
+ // Stop all active VUs
120
+ this.activeVUs.forEach(vu => vu.stop());
121
+ // Wait for VUs to finish current operations
122
+ await this.waitForVUsToComplete(10000); // 10 second timeout
123
+ // Cleanup handlers
124
+ await this.cleanup();
125
+ }
126
+ async initialize() {
127
+ logger_1.logger.debug('๐Ÿ”ง Initializing test runner...');
128
+ // Initialize protocol handlers
129
+ await this.initializeProtocolHandlers();
130
+ // Initialize outputs
131
+ await this.initializeOutputs();
132
+ // Setup metrics collection
133
+ this.metrics.start();
134
+ this.metrics.on('result', (result) => {
135
+ this.outputs.forEach(output => {
136
+ if (output && typeof output.writeResult === 'function') {
137
+ output.writeResult(result).catch(err => logger_1.logger.warn('โš ๏ธ Output write failed:', err));
138
+ }
139
+ });
140
+ });
141
+ logger_1.logger.debug('โœ… Test runner initialized');
142
+ }
143
+ async initializeProtocolHandlers() {
144
+ // Check which protocols are needed based on scenarios
145
+ const protocolsNeeded = this.getRequiredProtocols();
146
+ const debugConfig = this.config.debug || this.config.global?.debug;
147
+ // Initialize REST handler if needed
148
+ if (protocolsNeeded.has('rest')) {
149
+ const handler = new handler_1.RESTHandler(this.config.global?.base_url, this.config.global?.headers || {}, this.config.global?.timeout, debugConfig // Pass debug config to handler
150
+ );
151
+ this.handlers.set('rest', handler);
152
+ logger_1.logger.debug('๐ŸŒ REST handler initialized');
153
+ }
154
+ // Initialize SOAP handler if needed
155
+ if (protocolsNeeded.has('soap')) {
156
+ const wsdlUrl = this.findWSDLUrl();
157
+ if (wsdlUrl) {
158
+ const handler = new handler_2.SOAPHandler(wsdlUrl);
159
+ await handler.initialize();
160
+ this.handlers.set('soap', handler);
161
+ logger_1.logger.debug('๐Ÿงผ SOAP handler initialized');
162
+ }
163
+ }
164
+ // Initialize Web handler if needed
165
+ if (protocolsNeeded.has('web')) {
166
+ const browserConfig = this.config.global?.web || this.config.global?.browser || {};
167
+ const webConfig = {
168
+ type: browserConfig.type || 'chromium',
169
+ headless: browserConfig.headless ?? true,
170
+ base_url: browserConfig.base_url || this.config.global?.base_url,
171
+ viewport: browserConfig.viewport,
172
+ slow_mo: browserConfig.slow_mo,
173
+ highlight: browserConfig.highlight,
174
+ clear_storage: browserConfig.clear_storage
175
+ };
176
+ const handler = new handler_3.WebHandler(webConfig);
177
+ await handler.initialize();
178
+ this.handlers.set('web', handler);
179
+ logger_1.logger.debug('๐ŸŒ Web handler initialized');
180
+ }
181
+ }
182
+ getRequiredProtocols() {
183
+ const protocols = new Set();
184
+ for (const scenario of this.config.scenarios) {
185
+ for (const step of scenario.steps) {
186
+ protocols.add(step.type || 'rest');
187
+ }
188
+ }
189
+ return protocols;
190
+ }
191
+ findWSDLUrl() {
192
+ // First check global config
193
+ if (this.config.global?.wsdl_url) {
194
+ return this.config.global.wsdl_url;
195
+ }
196
+ // Fallback: check individual steps (for backward compatibility)
197
+ for (const scenario of this.config.scenarios) {
198
+ for (const step of scenario.steps) {
199
+ if (step.type === 'soap' && 'wsdl' in step && step.wsdl) {
200
+ return step.wsdl;
201
+ }
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+ async initializeOutputs() {
207
+ if (!this.config.outputs)
208
+ return;
209
+ for (const outputConfig of this.config.outputs) {
210
+ // Skip disabled outputs
211
+ if (outputConfig.enabled === false) {
212
+ continue;
213
+ }
214
+ let output;
215
+ try {
216
+ // Process timestamp templates in file paths
217
+ const processedFilePath = this.processTemplateFilePath(outputConfig.file);
218
+ switch (outputConfig.type) {
219
+ case 'csv':
220
+ output = new csv_1.CSVOutput(processedFilePath);
221
+ break;
222
+ case 'json':
223
+ output = new json_1.JSONOutput(processedFilePath);
224
+ break;
225
+ case 'influxdb':
226
+ output = new influxdb_1.InfluxDBOutput(outputConfig.url, outputConfig.database, outputConfig.tags);
227
+ break;
228
+ case 'graphite':
229
+ const [host, port] = (outputConfig.url || 'localhost:2003').split(':');
230
+ output = new graphite_1.GraphiteOutput(host, parseInt(port || '2003'), 'perfornium');
231
+ break;
232
+ case 'webhook':
233
+ output = new webhook_1.WebhookOutput(outputConfig.url, outputConfig.headers || {}, 'json', outputConfig.template);
234
+ break;
235
+ default:
236
+ logger_1.logger.warn(`โš ๏ธ Unsupported output type: ${outputConfig.type}`);
237
+ continue;
238
+ }
239
+ await output.initialize();
240
+ this.outputs.push(output);
241
+ logger_1.logger.debug(`๐Ÿ“Š ${outputConfig.type} output initialized`);
242
+ }
243
+ catch (error) {
244
+ logger_1.logger.warn(`โš ๏ธ Failed to initialize ${outputConfig.type} output:`, error);
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * Process template variables in file paths and automatically add timestamp if not present
250
+ */
251
+ processTemplateFilePath(filePath) {
252
+ if (!filePath) {
253
+ return `results/${this.config.name}-{{timestamp}}.csv`;
254
+ }
255
+ // If no timestamp placeholder exists, automatically add one before the extension
256
+ let processedPath = filePath;
257
+ if (!filePath.includes('{{timestamp}}')) {
258
+ const lastDot = filePath.lastIndexOf('.');
259
+ if (lastDot > 0) {
260
+ const name = filePath.substring(0, lastDot);
261
+ const ext = filePath.substring(lastDot);
262
+ processedPath = `${name}-{{timestamp}}${ext}`;
263
+ }
264
+ else {
265
+ processedPath = `${filePath}-{{timestamp}}`;
266
+ }
267
+ }
268
+ // Use FileManager to process timestamp templates
269
+ return file_manager_1.FileManager.processFilePath(processedPath);
270
+ }
271
+ async executeLoadPattern() {
272
+ // Setup base directory for CSV files BEFORE creating VUs
273
+ this.setupCSVBaseDirectory();
274
+ const vuFactory = this.createVUFactory();
275
+ // Support both single load config and array of phases
276
+ const loadPhases = Array.isArray(this.config.load) ? this.config.load : [this.config.load];
277
+ logger_1.logger.info(`๐Ÿ“ˆ Executing ${loadPhases.length} load phase(s)`);
278
+ // Execute each load phase sequentially
279
+ for (let i = 0; i < loadPhases.length; i++) {
280
+ const phase = loadPhases[i];
281
+ const phaseName = phase.name || `Phase ${i + 1}`;
282
+ logger_1.logger.info(`\n๐Ÿ”„ Starting ${phaseName}: ${phase.pattern} pattern`);
283
+ if (phase.virtual_users || phase.vus) {
284
+ logger_1.logger.info(` Users: ${phase.virtual_users || phase.vus}, Duration: ${phase.duration}`);
285
+ }
286
+ const pattern = this.getLoadPatternForPhase(phase);
287
+ await pattern.execute(phase, vuFactory);
288
+ // Wait for phase to complete
289
+ if (phase.duration) {
290
+ await this.waitForVUsToComplete();
291
+ }
292
+ else {
293
+ // For "run once" mode, clear activeVUs
294
+ this.activeVUs.forEach(vu => vu.stop());
295
+ this.activeVUs.length = 0;
296
+ }
297
+ logger_1.logger.success(`โœ… Completed ${phaseName}`);
298
+ // Small gap between phases
299
+ if (i < loadPhases.length - 1) {
300
+ logger_1.logger.info('โธ๏ธ Pausing 2s between phases...');
301
+ await new Promise(resolve => setTimeout(resolve, 2000));
302
+ }
303
+ }
304
+ logger_1.logger.success(`\n๐ŸŽ‰ All ${loadPhases.length} load phase(s) completed`);
305
+ }
306
+ getLoadPatternForPhase(phase) {
307
+ let pattern;
308
+ switch (phase.pattern) {
309
+ case 'basic':
310
+ pattern = new basic_1.BasicPattern();
311
+ if ('setTestRunningChecker' in pattern) {
312
+ pattern.setTestRunningChecker(() => this.isRunning);
313
+ }
314
+ break;
315
+ case 'stepping':
316
+ pattern = new stepping_1.SteppingPattern();
317
+ break;
318
+ case 'arrivals':
319
+ pattern = new arrivals_1.ArrivalsPattern();
320
+ break;
321
+ default:
322
+ throw new Error(`Unsupported load pattern: ${phase.pattern}`);
323
+ }
324
+ return pattern;
325
+ }
326
+ createVUFactory() {
327
+ return {
328
+ create: async (id) => {
329
+ // Pass global think time to VU
330
+ const globalThinkTime = this.config.global?.think_time;
331
+ // Build global CSV options if configured
332
+ let globalCSV;
333
+ if (this.config.global?.csv_data) {
334
+ globalCSV = {
335
+ config: this.config.global.csv_data,
336
+ mode: this.config.global.csv_mode || 'next'
337
+ };
338
+ logger_1.logger.debug(`VU ${id}: Global CSV configured from test config`);
339
+ }
340
+ const vu = new virtual_user_1.VirtualUser(id, this.metrics, this.handlers, this.config.name, undefined, // vuHooks
341
+ globalThinkTime, globalCSV);
342
+ // CRITICAL: Must await the CSV initialization
343
+ logger_1.logger.debug(`Initializing VU ${id} with scenarios (including CSV)...`);
344
+ await vu.setScenarios(this.config.scenarios);
345
+ logger_1.logger.debug(`VU ${id} fully initialized`);
346
+ this.activeVUs.push(vu);
347
+ return vu;
348
+ },
349
+ getMetrics: () => this.metrics
350
+ };
351
+ }
352
+ async waitForVUsToComplete(timeoutMs = 60000) {
353
+ const startTime = Date.now();
354
+ while (this.activeVUs.length > 0 && this.isRunning) {
355
+ const elapsed = Date.now() - startTime;
356
+ if (elapsed > timeoutMs) {
357
+ logger_1.logger.warn(`โš ๏ธ Timeout waiting for ${this.activeVUs.length} VUs to complete`);
358
+ // Force stop remaining VUs
359
+ this.activeVUs.forEach(vu => vu.stop());
360
+ break;
361
+ }
362
+ await (0, time_1.sleep)(1000);
363
+ if (this.activeVUs.length > 0 && elapsed % 5000 === 0) { // Log every 5 seconds
364
+ logger_1.logger.debug(`โณ Waiting for ${this.activeVUs.length} VUs to complete...`);
365
+ }
366
+ }
367
+ logger_1.logger.debug('โœ… All VUs completed');
368
+ }
369
+ async cleanup() {
370
+ logger_1.logger.debug('๐Ÿงน Cleaning up handlers...');
371
+ // Cleanup protocol handlers
372
+ for (const [name, handler] of this.handlers) {
373
+ try {
374
+ if (handler.cleanup) {
375
+ await handler.cleanup();
376
+ }
377
+ }
378
+ catch (error) {
379
+ logger_1.logger.warn(`โš ๏ธ Error cleaning up ${name} handler:`, error);
380
+ }
381
+ }
382
+ }
383
+ async finalize() {
384
+ logger_1.logger.debug('๐Ÿ Finalizing test...');
385
+ // Cleanup handlers
386
+ await this.cleanup();
387
+ // Generate summary and write to outputs
388
+ const summary = this.metrics.getSummary();
389
+ this.logSummary(summary);
390
+ this.logErrorDetails(summary);
391
+ // Write summary to outputs
392
+ for (const output of this.outputs) {
393
+ try {
394
+ await output.writeSummary(summary);
395
+ await output.finalize();
396
+ }
397
+ catch (error) {
398
+ logger_1.logger.warn('โš ๏ธ Output finalization failed:', error);
399
+ }
400
+ }
401
+ // Generate HTML report if configured
402
+ if (this.config.report?.generate) {
403
+ await this.generateReport(summary);
404
+ }
405
+ }
406
+ logSummary(summary) {
407
+ logger_1.logger.info('๐Ÿ“Š Test Summary:');
408
+ logger_1.logger.info(` Total Requests: ${summary.total_requests}`);
409
+ logger_1.logger.info(` Success Rate: ${summary.success_rate.toFixed(2)}%`);
410
+ logger_1.logger.info(` Avg Response Time: ${summary.avg_response_time.toFixed(2)}ms`);
411
+ logger_1.logger.info(` Requests/sec: ${summary.requests_per_second.toFixed(2)}`);
412
+ logger_1.logger.info(` Duration: ${(summary.total_duration / 1000).toFixed(1)}s`);
413
+ }
414
+ logErrorDetails(summary) {
415
+ if (summary.failed_requests > 0) {
416
+ logger_1.logger.warn(`โŒ ${summary.failed_requests} requests failed`);
417
+ // Log status code distribution
418
+ logger_1.logger.info('๐Ÿ“Š Status Code Distribution:');
419
+ Object.entries(summary.status_distribution)
420
+ .sort(([a], [b]) => parseInt(a) - parseInt(b))
421
+ .forEach(([status, count]) => {
422
+ const statusNum = parseInt(status);
423
+ const isError = statusNum >= 400;
424
+ const emoji = isError ? 'โŒ' : 'โœ…';
425
+ logger_1.logger.info(` ${emoji} ${status}: ${count} requests`);
426
+ });
427
+ // Log top errors
428
+ if (summary.error_details && summary.error_details.length > 0) {
429
+ logger_1.logger.warn('๐Ÿ” Top Error Details:');
430
+ summary.error_details.slice(0, 5).forEach((error, index) => {
431
+ logger_1.logger.warn(` ${index + 1}. ${error.error} (${error.count}x)`);
432
+ if (error.status) {
433
+ logger_1.logger.warn(` Status: ${error.status}`);
434
+ }
435
+ if (error.request_url) {
436
+ logger_1.logger.warn(` URL: ${error.request_url}`);
437
+ }
438
+ if (error.response_body && error.response_body.length < 200) {
439
+ logger_1.logger.warn(` Response: ${error.response_body}`);
440
+ }
441
+ });
442
+ }
443
+ }
444
+ }
445
+ async generateReport(summary) {
446
+ try {
447
+ const { EnhancedHTMLReportGenerator } = await Promise.resolve().then(() => __importStar(require('../reporting/enhanced-html-generator')));
448
+ const generator = new EnhancedHTMLReportGenerator();
449
+ // Process report path with automatic timestamp
450
+ const reportPath = this.processTemplateFilePath(this.config.report.output);
451
+ await generator.generate({
452
+ testName: this.config.name,
453
+ summary,
454
+ results: this.metrics.getResults()
455
+ }, reportPath);
456
+ }
457
+ catch (error) {
458
+ logger_1.logger.error('โŒ Report generation failed:', error);
459
+ }
460
+ }
461
+ getMetrics() {
462
+ return this.metrics;
463
+ }
464
+ }
465
+ exports.TestRunner = TestRunner;
@@ -0,0 +1,43 @@
1
+ import { ThresholdConfig } from '../config';
2
+ import { TestResult } from '../metrics/types';
3
+ export interface ThresholdEvaluationResult {
4
+ passed: boolean;
5
+ failures: ThresholdFailure[];
6
+ }
7
+ export interface ThresholdFailure {
8
+ threshold: ThresholdConfig;
9
+ actualValue: number | string;
10
+ expectedValue: number | string;
11
+ message: string;
12
+ severity: 'warning' | 'error' | 'critical';
13
+ }
14
+ export declare class ThresholdEvaluator {
15
+ /**
16
+ * Evaluate all thresholds against a test result
17
+ */
18
+ static evaluate(thresholds: ThresholdConfig[], result: TestResult, stepName?: string): ThresholdEvaluationResult;
19
+ /**
20
+ * Evaluate a single threshold
21
+ */
22
+ private static evaluateThreshold;
23
+ /**
24
+ * Extract the actual metric value from test result
25
+ */
26
+ private static extractMetricValue;
27
+ /**
28
+ * Parse expected value, handling units like "1s", "500ms", etc.
29
+ */
30
+ private static parseExpectedValue;
31
+ /**
32
+ * Compare actual vs expected values using the specified operator
33
+ */
34
+ private static compareValues;
35
+ /**
36
+ * Get human-readable operator symbol
37
+ */
38
+ private static getOperatorSymbol;
39
+ /**
40
+ * Execute threshold action based on severity and configuration
41
+ */
42
+ static executeThresholdActions(evaluationResult: ThresholdEvaluationResult, stepName?: string): Promise<void>;
43
+ }