@testsmith/perfornium 0.6.4 → 0.6.5

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 (190) hide show
  1. package/dist/cli/commands/distributed.js +2 -2
  2. package/dist/cli/commands/report.js +2 -2
  3. package/dist/cli/commands/run.js +2 -0
  4. package/dist/config/parser.js +2 -2
  5. package/dist/config/types/global-config.d.ts +82 -2
  6. package/dist/config/types/scenario-config.d.ts +2 -2
  7. package/dist/core/data/data-manager.d.ts +70 -0
  8. package/dist/core/data/data-manager.js +186 -0
  9. package/dist/core/data/data-provider.d.ts +85 -0
  10. package/dist/core/data/data-provider.js +468 -0
  11. package/dist/core/data/index.d.ts +8 -0
  12. package/dist/core/data/index.js +13 -0
  13. package/dist/core/execution/check-evaluator.d.ts +10 -0
  14. package/dist/core/execution/check-evaluator.js +79 -0
  15. package/dist/core/execution/data-extractor.d.ts +6 -0
  16. package/dist/core/execution/data-extractor.js +70 -0
  17. package/dist/core/execution/index.d.ts +3 -0
  18. package/dist/core/execution/index.js +9 -0
  19. package/dist/core/execution/json-payload-processor.d.ts +7 -0
  20. package/dist/core/execution/json-payload-processor.js +140 -0
  21. package/dist/core/factories/index.d.ts +2 -0
  22. package/dist/core/factories/index.js +7 -0
  23. package/dist/core/factories/output-handler-factory.d.ts +10 -0
  24. package/dist/core/factories/output-handler-factory.js +91 -0
  25. package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
  26. package/dist/core/factories/protocol-handler-factory.js +96 -0
  27. package/dist/core/index.d.ts +3 -2
  28. package/dist/core/index.js +8 -3
  29. package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
  30. package/dist/core/reporting/dashboard-reporter.js +127 -0
  31. package/dist/core/reporting/index.d.ts +1 -0
  32. package/dist/core/reporting/index.js +5 -0
  33. package/dist/core/step-executor.d.ts +6 -20
  34. package/dist/core/step-executor.js +72 -366
  35. package/dist/core/strategies/index.d.ts +2 -0
  36. package/dist/core/strategies/index.js +7 -0
  37. package/dist/core/strategies/scenario-selector.d.ts +13 -0
  38. package/dist/core/strategies/scenario-selector.js +37 -0
  39. package/dist/core/strategies/think-time-strategy.d.ts +15 -0
  40. package/dist/core/strategies/think-time-strategy.js +71 -0
  41. package/dist/core/test-runner.d.ts +4 -11
  42. package/dist/core/test-runner.js +105 -312
  43. package/dist/core/virtual-user.d.ts +7 -37
  44. package/dist/core/virtual-user.js +29 -269
  45. package/dist/dashboard/routes/api.d.ts +64 -0
  46. package/dist/dashboard/routes/api.js +569 -0
  47. package/dist/dashboard/routes/index.d.ts +2 -0
  48. package/dist/dashboard/routes/index.js +7 -0
  49. package/dist/dashboard/routes/static.d.ts +6 -0
  50. package/dist/dashboard/routes/static.js +76 -0
  51. package/dist/dashboard/server.d.ts +8 -84
  52. package/dist/dashboard/server.js +76 -2007
  53. package/dist/dashboard/services/file-scanner.d.ts +7 -0
  54. package/dist/dashboard/services/file-scanner.js +114 -0
  55. package/dist/dashboard/services/index.d.ts +5 -0
  56. package/dist/dashboard/services/index.js +13 -0
  57. package/dist/dashboard/services/influxdb-service.d.ts +41 -0
  58. package/dist/dashboard/services/influxdb-service.js +329 -0
  59. package/dist/dashboard/services/metrics-parser.d.ts +12 -0
  60. package/dist/dashboard/services/metrics-parser.js +209 -0
  61. package/dist/dashboard/services/results-manager.d.ts +17 -0
  62. package/dist/dashboard/services/results-manager.js +311 -0
  63. package/dist/dashboard/services/test-executor.d.ts +41 -0
  64. package/dist/dashboard/services/test-executor.js +250 -0
  65. package/dist/dashboard/services/workers-manager.d.ts +13 -0
  66. package/dist/dashboard/services/workers-manager.js +81 -0
  67. package/dist/dashboard/templates/index.html +122 -0
  68. package/dist/dashboard/templates/scripts/main.js +3280 -0
  69. package/dist/dashboard/templates/styles.css +402 -0
  70. package/dist/dashboard/types.d.ts +168 -0
  71. package/dist/dashboard/types.js +2 -0
  72. package/dist/distributed/result-aggregator.js +1 -3
  73. package/dist/metrics/batch/batch-processor.d.ts +27 -0
  74. package/dist/metrics/batch/batch-processor.js +85 -0
  75. package/dist/metrics/batch/index.d.ts +1 -0
  76. package/dist/metrics/batch/index.js +5 -0
  77. package/dist/metrics/collector.d.ts +46 -45
  78. package/dist/metrics/collector.js +179 -640
  79. package/dist/metrics/core/error-tracker.d.ts +9 -0
  80. package/dist/metrics/core/error-tracker.js +52 -0
  81. package/dist/metrics/core/index.d.ts +3 -0
  82. package/dist/metrics/core/index.js +9 -0
  83. package/dist/metrics/core/result-storage.d.ts +19 -0
  84. package/dist/metrics/core/result-storage.js +56 -0
  85. package/dist/metrics/core/statistics-engine.d.ts +27 -0
  86. package/dist/metrics/core/statistics-engine.js +91 -0
  87. package/dist/metrics/output/file-writer.d.ts +19 -0
  88. package/dist/metrics/output/file-writer.js +129 -0
  89. package/dist/metrics/output/index.d.ts +2 -0
  90. package/dist/metrics/output/index.js +10 -0
  91. package/dist/metrics/output/influxdb-writer.d.ts +89 -0
  92. package/dist/metrics/output/influxdb-writer.js +404 -0
  93. package/dist/metrics/realtime/dispatcher.d.ts +18 -0
  94. package/dist/metrics/realtime/dispatcher.js +45 -0
  95. package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
  96. package/dist/metrics/realtime/endpoints/graphite.js +61 -0
  97. package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
  98. package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
  99. package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
  100. package/dist/metrics/realtime/endpoints/webhook.js +22 -0
  101. package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
  102. package/dist/metrics/realtime/endpoints/websocket.js +25 -0
  103. package/dist/metrics/realtime/index.d.ts +5 -0
  104. package/dist/metrics/realtime/index.js +13 -0
  105. package/dist/metrics/reporting/index.d.ts +3 -0
  106. package/dist/metrics/reporting/index.js +9 -0
  107. package/dist/metrics/reporting/step-statistics.d.ts +6 -0
  108. package/dist/metrics/reporting/step-statistics.js +59 -0
  109. package/dist/metrics/reporting/summary-generator.d.ts +16 -0
  110. package/dist/metrics/reporting/summary-generator.js +46 -0
  111. package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
  112. package/dist/metrics/reporting/timeline-calculator.js +86 -0
  113. package/dist/metrics/types.d.ts +58 -0
  114. package/dist/outputs/csv.d.ts +2 -0
  115. package/dist/outputs/csv.js +21 -2
  116. package/dist/outputs/json.js +6 -2
  117. package/dist/protocols/rest/handler.d.ts +4 -53
  118. package/dist/protocols/rest/handler.js +73 -454
  119. package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
  120. package/dist/protocols/rest/request/auth-handler.js +30 -0
  121. package/dist/protocols/rest/request/body-processor.d.ts +11 -0
  122. package/dist/protocols/rest/request/body-processor.js +62 -0
  123. package/dist/protocols/rest/request/index.d.ts +2 -0
  124. package/dist/protocols/rest/request/index.js +7 -0
  125. package/dist/protocols/rest/response/checks.d.ts +6 -0
  126. package/dist/protocols/rest/response/checks.js +71 -0
  127. package/dist/protocols/rest/response/index.d.ts +2 -0
  128. package/dist/protocols/rest/response/index.js +7 -0
  129. package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
  130. package/dist/protocols/rest/response/size-calculator.js +64 -0
  131. package/dist/protocols/web/browser/highlight.d.ts +7 -0
  132. package/dist/protocols/web/browser/highlight.js +47 -0
  133. package/dist/protocols/web/browser/index.d.ts +4 -0
  134. package/dist/protocols/web/browser/index.js +11 -0
  135. package/dist/protocols/web/browser/manager.d.ts +20 -0
  136. package/dist/protocols/web/browser/manager.js +189 -0
  137. package/dist/protocols/web/browser/screenshot.d.ts +8 -0
  138. package/dist/protocols/web/browser/screenshot.js +69 -0
  139. package/dist/protocols/web/browser/storage.d.ts +5 -0
  140. package/dist/protocols/web/browser/storage.js +45 -0
  141. package/dist/protocols/web/commands/index.d.ts +5 -0
  142. package/dist/protocols/web/commands/index.js +11 -0
  143. package/dist/protocols/web/commands/interaction.d.ts +13 -0
  144. package/dist/protocols/web/commands/interaction.js +68 -0
  145. package/dist/protocols/web/commands/measurement.d.ts +16 -0
  146. package/dist/protocols/web/commands/measurement.js +33 -0
  147. package/dist/protocols/web/commands/navigation.d.ts +11 -0
  148. package/dist/protocols/web/commands/navigation.js +43 -0
  149. package/dist/protocols/web/commands/types.d.ts +12 -0
  150. package/dist/protocols/web/commands/types.js +2 -0
  151. package/dist/protocols/web/commands/verification.d.ts +11 -0
  152. package/dist/protocols/web/commands/verification.js +98 -0
  153. package/dist/protocols/web/handler.d.ts +19 -30
  154. package/dist/protocols/web/handler.js +160 -650
  155. package/dist/protocols/web/network/capture.d.ts +19 -0
  156. package/dist/protocols/web/network/capture.js +225 -0
  157. package/dist/protocols/web/network/filters.d.ts +5 -0
  158. package/dist/protocols/web/network/filters.js +49 -0
  159. package/dist/protocols/web/network/index.d.ts +4 -0
  160. package/dist/protocols/web/network/index.js +9 -0
  161. package/dist/protocols/web/network/types.d.ts +13 -0
  162. package/dist/protocols/web/network/types.js +2 -0
  163. package/dist/protocols/web/network/utils.d.ts +8 -0
  164. package/dist/protocols/web/network/utils.js +29 -0
  165. package/dist/reporting/chart-data/index.d.ts +5 -0
  166. package/dist/reporting/chart-data/index.js +13 -0
  167. package/dist/reporting/chart-data/network.d.ts +25 -0
  168. package/dist/reporting/chart-data/network.js +78 -0
  169. package/dist/reporting/chart-data/scenario.d.ts +37 -0
  170. package/dist/reporting/chart-data/scenario.js +76 -0
  171. package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
  172. package/dist/reporting/chart-data/step-statistics.js +94 -0
  173. package/dist/reporting/chart-data/throughput.d.ts +16 -0
  174. package/dist/reporting/chart-data/throughput.js +24 -0
  175. package/dist/reporting/chart-data/timeline.d.ts +17 -0
  176. package/dist/reporting/chart-data/timeline.js +46 -0
  177. package/dist/reporting/handlebars-helpers.d.ts +1 -0
  178. package/dist/reporting/handlebars-helpers.js +63 -0
  179. package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
  180. package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
  181. package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
  182. package/dist/utils/data-utils.d.ts +17 -0
  183. package/dist/utils/data-utils.js +129 -0
  184. package/dist/utils/template.js +2 -2
  185. package/package.json +5 -2
  186. package/dist/core/csv-data-provider.d.ts +0 -47
  187. package/dist/core/csv-data-provider.js +0 -265
  188. package/dist/reporting/generator.d.ts +0 -42
  189. package/dist/reporting/generator.js +0 -1217
  190. package/dist/reporting/templates/html.hbs +0 -2453
@@ -36,23 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.TestRunner = void 0;
37
37
  const collector_1 = require("../metrics/collector");
38
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");
39
+ const data_1 = require("./data");
42
40
  const basic_1 = require("../load-patterns/basic");
43
41
  const stepping_1 = require("../load-patterns/stepping");
44
42
  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
43
  const logger_1 = require("../utils/logger");
51
44
  const time_1 = require("../utils/time");
52
- const csv_data_provider_1 = require("./csv-data-provider");
53
45
  const rendezvous_1 = require("./rendezvous");
54
46
  const file_manager_1 = require("../utils/file-manager");
55
- const dashboard_1 = require("../dashboard");
47
+ const protocol_handler_factory_1 = require("./factories/protocol-handler-factory");
48
+ const output_handler_factory_1 = require("./factories/output-handler-factory");
49
+ const dashboard_reporter_1 = require("./reporting/dashboard-reporter");
56
50
  class TestRunner {
57
51
  constructor(config) {
58
52
  this.handlers = new Map();
@@ -61,10 +55,32 @@ class TestRunner {
61
55
  this.isRunning = false;
62
56
  this.startTime = 0;
63
57
  this.testId = '';
64
- this.dashboardInterval = null;
65
- this.lastReportedResultIndex = 0;
58
+ this.dashboardReporter = null;
66
59
  this.config = config;
67
- this.metrics = new collector_1.MetricsCollector();
60
+ // Build realtime config including InfluxDB settings
61
+ const realtimeConfig = {
62
+ enabled: true,
63
+ batch_size: 10,
64
+ incremental_files: {
65
+ enabled: true
66
+ }
67
+ };
68
+ // Add InfluxDB config if enabled in global config
69
+ if (config.global?.influxdb?.enabled) {
70
+ realtimeConfig.influxdb = {
71
+ enabled: true,
72
+ url: config.global.influxdb.url,
73
+ token: config.global.influxdb.token,
74
+ org: config.global.influxdb.org,
75
+ bucket: config.global.influxdb.bucket,
76
+ batch_size: config.global.influxdb.batch_size,
77
+ flush_interval: config.global.influxdb.flush_interval
78
+ };
79
+ }
80
+ this.metrics = new collector_1.MetricsCollector(realtimeConfig);
81
+ // Initialize factories (pass metrics collector for network call recording)
82
+ this.protocolHandlerFactory = new protocol_handler_factory_1.ProtocolHandlerFactory(config, this.metrics);
83
+ this.outputHandlerFactory = new output_handler_factory_1.OutputHandlerFactory(config.name);
68
84
  // Set log level based on debug config
69
85
  if (config.debug?.log_level || config.global?.debug?.log_level) {
70
86
  const logLevel = config.debug?.log_level || config.global?.debug?.log_level;
@@ -85,24 +101,23 @@ class TestRunner {
85
101
  }
86
102
  }
87
103
  async run() {
88
- logger_1.logger.info(`🚀 Starting test: ${this.config.name}`);
104
+ logger_1.logger.info(`Starting test: ${this.config.name}`);
89
105
  this.isRunning = true;
90
106
  this.startTime = Date.now();
91
107
  this.testId = `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
92
108
  // Reset rendezvous manager for this test run
93
109
  rendezvous_1.RendezvousManager.getInstance().reset();
94
- // Start dashboard reporting if dashboard is running
110
+ // Start dashboard reporting
95
111
  this.startDashboardReporting();
96
112
  try {
97
113
  await this.initialize();
98
- // NO CSV termination callback setup needed anymore
99
114
  await this.executeLoadPattern();
100
115
  await this.finalize();
101
116
  const duration = Date.now() - this.startTime;
102
- logger_1.logger.success(`✅ Test completed successfully in ${(duration / 1000).toFixed(1)}s`);
117
+ logger_1.logger.success(`Test completed successfully in ${(duration / 1000).toFixed(1)}s`);
103
118
  }
104
119
  catch (error) {
105
- logger_1.logger.error('Test failed:', error);
120
+ logger_1.logger.error('Test failed:', error);
106
121
  throw error;
107
122
  }
108
123
  finally {
@@ -111,282 +126,65 @@ class TestRunner {
111
126
  }
112
127
  }
113
128
  startDashboardReporting() {
114
- const dashboard = (0, dashboard_1.getDashboard)();
115
- // If running standalone (no dashboard singleton), output progress to stdout for dashboard parsing
116
- const outputProgress = !dashboard && process.env.PERFORNIUM_PROGRESS !== '0';
117
- if (dashboard) {
118
- // Report initial state to in-process dashboard
119
- dashboard.reportLiveUpdate(this.testId, {
120
- id: this.testId,
121
- name: this.config.name,
122
- startTime: new Date(),
123
- status: 'running',
124
- metrics: {
125
- requests: 0,
126
- errors: 0,
127
- avgResponseTime: 0,
128
- currentVUs: 0
129
- }
130
- });
131
- }
132
- // Track last request count to detect activity
133
- let lastRequestCount = 0;
134
- // Report updates every 500ms
135
- this.dashboardInterval = setInterval(() => {
136
- if (!this.isRunning)
137
- return;
138
- const summary = this.metrics.getSummary();
139
- const currentVUs = this.activeVUs.filter(vu => vu.isRunning()).length;
140
- const currentRequests = summary.total_requests || 0;
141
- // Skip reporting if VUs are 0 and no new requests (test is winding down)
142
- const hasActivity = currentVUs > 0 || currentRequests > lastRequestCount;
143
- lastRequestCount = currentRequests;
144
- if (dashboard) {
145
- dashboard.reportLiveUpdate(this.testId, {
146
- metrics: {
147
- requests: currentRequests,
148
- errors: summary.failed_requests || 0,
149
- avgResponseTime: summary.avg_response_time || 0,
150
- currentVUs
151
- }
152
- });
153
- }
154
- // Output machine-readable progress for dashboard parsing (only when there's activity)
155
- if (outputProgress && hasActivity) {
156
- const rps = summary.requests_per_second || 0;
157
- const p50 = summary.percentiles?.[50] || 0;
158
- const p90 = summary.percentiles?.[90] || 0;
159
- const p95 = summary.percentiles?.[95] || 0;
160
- const p99 = summary.percentiles?.[99] || 0;
161
- const successRate = summary.success_rate || 0;
162
- // Main progress line with percentiles
163
- console.log(`[PROGRESS] VUs: ${currentVUs} | Requests: ${currentRequests} | Errors: ${summary.failed_requests || 0} | Avg RT: ${(summary.avg_response_time || 0).toFixed(0)}ms | RPS: ${rps.toFixed(1)} | P50: ${p50.toFixed(0)}ms | P90: ${p90.toFixed(0)}ms | P95: ${p95.toFixed(0)}ms | P99: ${p99.toFixed(0)}ms | Success: ${successRate.toFixed(1)}%`);
164
- // Output step statistics if available
165
- if (summary.step_statistics && summary.step_statistics.length > 0) {
166
- const stepData = summary.step_statistics.map(s => ({
167
- n: s.step_name,
168
- s: s.scenario,
169
- r: s.total_requests,
170
- e: s.failed_requests,
171
- a: Math.round(s.avg_response_time),
172
- p50: Math.round(s.percentiles?.[50] || 0),
173
- p95: Math.round(s.percentiles?.[95] || 0),
174
- p99: Math.round(s.percentiles?.[99] || 0),
175
- sr: Math.round(s.success_rate * 10) / 10
176
- }));
177
- console.log(`[STEPS] ${JSON.stringify(stepData)}`);
178
- }
179
- // Output individual response times (last 50 new results)
180
- const allResults = this.metrics.getResults();
181
- if (allResults.length > this.lastReportedResultIndex) {
182
- const newResults = allResults.slice(this.lastReportedResultIndex, this.lastReportedResultIndex + 50);
183
- const rtData = newResults.map(r => ({
184
- t: r.timestamp,
185
- v: Math.round(r.duration),
186
- s: r.success ? 1 : 0,
187
- n: r.step_name || r.action || 'unknown' // Include step/request name for coloring
188
- }));
189
- if (rtData.length > 0) {
190
- console.log(`[RT] ${JSON.stringify(rtData)}`);
191
- }
192
- this.lastReportedResultIndex = Math.min(allResults.length, this.lastReportedResultIndex + 50);
193
- }
194
- // Output top 10 errors if any
195
- if (summary.error_details && summary.error_details.length > 0) {
196
- const topErrors = summary.error_details.slice(0, 10).map((e) => ({
197
- scenario: e.scenario,
198
- action: e.action,
199
- status: e.status,
200
- error: e.error?.substring(0, 200),
201
- url: e.request_url,
202
- count: e.count
203
- }));
204
- console.log(`[ERRORS] ${JSON.stringify(topErrors)}`);
205
- }
206
- }
207
- }, 500);
129
+ this.dashboardReporter = new dashboard_reporter_1.DashboardReporter({
130
+ testId: this.testId,
131
+ testName: this.config.name
132
+ });
133
+ this.dashboardReporter.start(this.metrics, () => this.activeVUs, () => this.isRunning);
208
134
  }
209
135
  stopDashboardReporting() {
210
- if (this.dashboardInterval) {
211
- clearInterval(this.dashboardInterval);
212
- this.dashboardInterval = null;
213
- }
214
- const dashboard = (0, dashboard_1.getDashboard)();
215
- if (dashboard) {
216
- dashboard.reportTestComplete(this.testId);
136
+ if (this.dashboardReporter) {
137
+ this.dashboardReporter.stop();
138
+ this.dashboardReporter = null;
217
139
  }
218
140
  }
219
141
  setupCSVBaseDirectory() {
220
142
  const hasCSVScenarios = this.config.scenarios?.some((s) => s.csv_data);
221
143
  if (hasCSVScenarios) {
222
144
  const baseDir = process.cwd();
223
- csv_data_provider_1.CSVDataProvider.setBaseDir(baseDir);
224
- // Check if any scenario has cycleOnExhaustion: false
145
+ data_1.DataProvider.setBaseDir(baseDir);
225
146
  const hasStopOnExhaustion = this.config.scenarios?.some((s) => s.csv_data && s.csv_data.cycleOnExhaustion === false);
226
147
  if (hasStopOnExhaustion) {
227
- logger_1.logger.info('⏹️ CSV exhaustion handling enabled - individual VUs will stop when CSV data is exhausted');
148
+ logger_1.logger.info('CSV exhaustion handling enabled - individual VUs will stop when CSV data is exhausted');
228
149
  }
229
150
  logger_1.logger.debug(`Set CSV base directory: ${baseDir}`);
230
151
  }
231
152
  }
232
- // FIXED: Added stop method that was missing
233
153
  async stop() {
234
- logger_1.logger.info('⏹️ Stopping test...');
154
+ logger_1.logger.info('Stopping test...');
235
155
  this.isRunning = false;
236
156
  // Stop rendezvous manager - releases any waiting VUs
237
157
  rendezvous_1.RendezvousManager.getInstance().stop();
238
158
  // Stop all active VUs
239
159
  this.activeVUs.forEach(vu => vu.stop());
240
160
  // Wait for VUs to finish current operations
241
- await this.waitForVUsToComplete(10000); // 10 second timeout
161
+ await this.waitForVUsToComplete(10000);
162
+ // Finalize metrics (stops batch processor timers)
163
+ await this.metrics.finalize();
242
164
  // Cleanup handlers
243
- await this.cleanup();
165
+ await protocol_handler_factory_1.ProtocolHandlerFactory.cleanupHandlers(this.handlers);
166
+ // Generate summary and finalize outputs (ensures results files are properly closed)
167
+ const summary = this.metrics.getSummary();
168
+ await output_handler_factory_1.OutputHandlerFactory.finalizeOutputs(this.outputs, summary);
244
169
  }
245
170
  async initialize() {
246
- logger_1.logger.debug('🔧 Initializing test runner...');
247
- // Initialize protocol handlers
248
- await this.initializeProtocolHandlers();
249
- // Initialize outputs
250
- await this.initializeOutputs();
251
- // Setup metrics collection
252
- this.metrics.start();
171
+ logger_1.logger.debug('Initializing test runner...');
172
+ // Initialize protocol handlers using factory
173
+ this.handlers = await this.protocolHandlerFactory.createHandlers();
174
+ // Initialize outputs using factory
175
+ this.outputs = await this.outputHandlerFactory.createOutputs(this.config.outputs);
176
+ // Initialize metrics collector (connects to InfluxDB if configured)
177
+ await this.metrics.initialize();
178
+ // Setup metrics collection with test name for InfluxDB tagging
179
+ this.metrics.start(this.config.name);
253
180
  this.metrics.on('result', (result) => {
254
181
  this.outputs.forEach(output => {
255
182
  if (output && typeof output.writeResult === 'function') {
256
- output.writeResult(result).catch(err => logger_1.logger.warn('⚠️ Output write failed:', err));
183
+ output.writeResult(result).catch(err => logger_1.logger.warn('Output write failed:', err));
257
184
  }
258
185
  });
259
186
  });
260
- logger_1.logger.debug('Test runner initialized');
261
- }
262
- async initializeProtocolHandlers() {
263
- // Check which protocols are needed based on scenarios
264
- const protocolsNeeded = this.getRequiredProtocols();
265
- const debugConfig = this.config.debug || this.config.global?.debug;
266
- // Initialize REST handler if needed
267
- if (protocolsNeeded.has('rest')) {
268
- 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
269
- );
270
- this.handlers.set('rest', handler);
271
- logger_1.logger.debug('🌐 REST handler initialized');
272
- }
273
- // Initialize SOAP handler if needed
274
- if (protocolsNeeded.has('soap')) {
275
- const wsdlUrl = this.findWSDLUrl();
276
- if (wsdlUrl) {
277
- const handler = new handler_2.SOAPHandler(wsdlUrl);
278
- await handler.initialize();
279
- this.handlers.set('soap', handler);
280
- logger_1.logger.debug('🧼 SOAP handler initialized');
281
- }
282
- }
283
- // Initialize Web handler if needed
284
- if (protocolsNeeded.has('web')) {
285
- const browserConfig = this.config.global?.web || this.config.global?.browser || {};
286
- const webConfig = {
287
- type: browserConfig.type || 'chromium',
288
- headless: browserConfig.headless ?? true,
289
- base_url: browserConfig.base_url || this.config.global?.base_url,
290
- viewport: browserConfig.viewport,
291
- slow_mo: browserConfig.slow_mo,
292
- highlight: browserConfig.highlight,
293
- clear_storage: browserConfig.clear_storage,
294
- screenshot_on_failure: browserConfig.screenshot_on_failure
295
- };
296
- const handler = new handler_3.WebHandler(webConfig);
297
- await handler.initialize();
298
- this.handlers.set('web', handler);
299
- logger_1.logger.debug('🌐 Web handler initialized');
300
- }
301
- }
302
- getRequiredProtocols() {
303
- const protocols = new Set();
304
- for (const scenario of this.config.scenarios) {
305
- for (const step of scenario.steps) {
306
- protocols.add(step.type || 'rest');
307
- }
308
- }
309
- return protocols;
310
- }
311
- findWSDLUrl() {
312
- // First check global config
313
- if (this.config.global?.wsdl_url) {
314
- return this.config.global.wsdl_url;
315
- }
316
- // Fallback: check individual steps (for backward compatibility)
317
- for (const scenario of this.config.scenarios) {
318
- for (const step of scenario.steps) {
319
- if (step.type === 'soap' && 'wsdl' in step && step.wsdl) {
320
- return step.wsdl;
321
- }
322
- }
323
- }
324
- return null;
325
- }
326
- async initializeOutputs() {
327
- if (!this.config.outputs)
328
- return;
329
- for (const outputConfig of this.config.outputs) {
330
- // Skip disabled outputs
331
- if (outputConfig.enabled === false) {
332
- continue;
333
- }
334
- let output;
335
- try {
336
- // Process timestamp templates in file paths
337
- const processedFilePath = this.processTemplateFilePath(outputConfig.file);
338
- switch (outputConfig.type) {
339
- case 'csv':
340
- output = new csv_1.CSVOutput(processedFilePath);
341
- break;
342
- case 'json':
343
- output = new json_1.JSONOutput(processedFilePath);
344
- break;
345
- case 'influxdb':
346
- output = new influxdb_1.InfluxDBOutput(outputConfig.url, outputConfig.database, outputConfig.tags);
347
- break;
348
- case 'graphite':
349
- const [host, port] = (outputConfig.url || 'localhost:2003').split(':');
350
- output = new graphite_1.GraphiteOutput(host, parseInt(port || '2003'), 'perfornium');
351
- break;
352
- case 'webhook':
353
- output = new webhook_1.WebhookOutput(outputConfig.url, outputConfig.headers || {}, 'json', outputConfig.template);
354
- break;
355
- default:
356
- logger_1.logger.warn(`⚠️ Unsupported output type: ${outputConfig.type}`);
357
- continue;
358
- }
359
- await output.initialize();
360
- this.outputs.push(output);
361
- logger_1.logger.debug(`📊 ${outputConfig.type} output initialized`);
362
- }
363
- catch (error) {
364
- logger_1.logger.warn(`⚠️ Failed to initialize ${outputConfig.type} output:`, error);
365
- }
366
- }
367
- }
368
- /**
369
- * Process template variables in file paths and automatically add timestamp if not present
370
- */
371
- processTemplateFilePath(filePath) {
372
- if (!filePath) {
373
- return `results/${this.config.name}-{{timestamp}}.csv`;
374
- }
375
- // If no timestamp placeholder exists, automatically add one before the extension
376
- let processedPath = filePath;
377
- if (!filePath.includes('{{timestamp}}')) {
378
- const lastDot = filePath.lastIndexOf('.');
379
- if (lastDot > 0) {
380
- const name = filePath.substring(0, lastDot);
381
- const ext = filePath.substring(lastDot);
382
- processedPath = `${name}-{{timestamp}}${ext}`;
383
- }
384
- else {
385
- processedPath = `${filePath}-{{timestamp}}`;
386
- }
387
- }
388
- // Use FileManager to process timestamp templates
389
- return file_manager_1.FileManager.processFilePath(processedPath);
187
+ logger_1.logger.debug('Test runner initialized');
390
188
  }
391
189
  async executeLoadPattern() {
392
190
  // Setup base directory for CSV files BEFORE creating VUs
@@ -394,12 +192,12 @@ class TestRunner {
394
192
  const vuFactory = this.createVUFactory();
395
193
  // Support both single load config and array of phases
396
194
  const loadPhases = Array.isArray(this.config.load) ? this.config.load : [this.config.load];
397
- logger_1.logger.info(`📈 Executing ${loadPhases.length} load phase(s)`);
195
+ logger_1.logger.info(`Executing ${loadPhases.length} load phase(s)`);
398
196
  // Execute each load phase sequentially
399
197
  for (let i = 0; i < loadPhases.length; i++) {
400
198
  const phase = loadPhases[i];
401
199
  const phaseName = phase.name || `Phase ${i + 1}`;
402
- logger_1.logger.info(`\n🔄 Starting ${phaseName}: ${phase.pattern} pattern`);
200
+ logger_1.logger.info(`\nStarting ${phaseName}: ${phase.pattern} pattern`);
403
201
  if (phase.virtual_users || phase.vus) {
404
202
  logger_1.logger.info(` Users: ${phase.virtual_users || phase.vus}, Duration: ${phase.duration}`);
405
203
  }
@@ -414,14 +212,14 @@ class TestRunner {
414
212
  this.activeVUs.forEach(vu => vu.stop());
415
213
  this.activeVUs.length = 0;
416
214
  }
417
- logger_1.logger.success(`✅ Completed ${phaseName}`);
215
+ logger_1.logger.success(`Completed ${phaseName}`);
418
216
  // Small gap between phases
419
217
  if (i < loadPhases.length - 1) {
420
- logger_1.logger.info('⏸️ Pausing 2s between phases...');
218
+ logger_1.logger.info('Pausing 2s between phases...');
421
219
  await new Promise(resolve => setTimeout(resolve, 2000));
422
220
  }
423
221
  }
424
- logger_1.logger.success(`\n🎉 All ${loadPhases.length} load phase(s) completed`);
222
+ logger_1.logger.success(`\nAll ${loadPhases.length} load phase(s) completed`);
425
223
  }
426
224
  getLoadPatternForPhase(phase) {
427
225
  let pattern;
@@ -471,65 +269,42 @@ class TestRunner {
471
269
  }
472
270
  async waitForVUsToComplete(timeoutMs = 60000) {
473
271
  const startTime = Date.now();
474
- // Filter to only running VUs
475
272
  const getRunningVUs = () => this.activeVUs.filter(vu => vu.isRunning());
476
273
  while (getRunningVUs().length > 0 && this.isRunning) {
477
274
  const elapsed = Date.now() - startTime;
478
275
  const runningCount = getRunningVUs().length;
479
276
  if (elapsed > timeoutMs) {
480
- logger_1.logger.warn(`⚠️ Timeout waiting for ${runningCount} VUs to complete`);
481
- // Force stop remaining VUs
277
+ logger_1.logger.warn(`Timeout waiting for ${runningCount} VUs to complete`);
482
278
  this.activeVUs.forEach(vu => vu.stop());
483
279
  break;
484
280
  }
485
- await (0, time_1.sleep)(100); // Check more frequently
486
- if (runningCount > 0 && elapsed % 5000 === 0) { // Log every 5 seconds
487
- logger_1.logger.debug(`⏳ Waiting for ${runningCount} VUs to complete...`);
281
+ await (0, time_1.sleep)(100);
282
+ if (runningCount > 0 && elapsed % 5000 === 0) {
283
+ logger_1.logger.debug(`Waiting for ${runningCount} VUs to complete...`);
488
284
  }
489
285
  }
490
- // Clear the activeVUs array
491
286
  this.activeVUs.length = 0;
492
- logger_1.logger.debug('All VUs completed');
493
- }
494
- async cleanup() {
495
- logger_1.logger.debug('🧹 Cleaning up handlers...');
496
- // Cleanup protocol handlers
497
- for (const [name, handler] of this.handlers) {
498
- try {
499
- if (handler.cleanup) {
500
- await handler.cleanup();
501
- }
502
- }
503
- catch (error) {
504
- logger_1.logger.warn(`⚠️ Error cleaning up ${name} handler:`, error);
505
- }
506
- }
287
+ logger_1.logger.debug('All VUs completed');
507
288
  }
508
289
  async finalize() {
509
- logger_1.logger.debug('🏁 Finalizing test...');
290
+ logger_1.logger.debug('Finalizing test...');
291
+ // Finalize metrics (stops batch processor timers)
292
+ await this.metrics.finalize();
510
293
  // Cleanup handlers
511
- await this.cleanup();
294
+ await protocol_handler_factory_1.ProtocolHandlerFactory.cleanupHandlers(this.handlers);
512
295
  // Generate summary and write to outputs
513
296
  const summary = this.metrics.getSummary();
514
297
  this.logSummary(summary);
515
298
  this.logErrorDetails(summary);
516
- // Write summary to outputs
517
- for (const output of this.outputs) {
518
- try {
519
- await output.writeSummary(summary);
520
- await output.finalize();
521
- }
522
- catch (error) {
523
- logger_1.logger.warn('⚠️ Output finalization failed:', error);
524
- }
525
- }
299
+ // Finalize outputs
300
+ await output_handler_factory_1.OutputHandlerFactory.finalizeOutputs(this.outputs, summary);
526
301
  // Generate HTML report if configured
527
302
  if (this.config.report?.generate) {
528
303
  await this.generateReport(summary);
529
304
  }
530
305
  }
531
306
  logSummary(summary) {
532
- logger_1.logger.info('📊 Test Summary:');
307
+ logger_1.logger.info('Test Summary:');
533
308
  logger_1.logger.info(` Total Requests: ${summary.total_requests}`);
534
309
  logger_1.logger.info(` Success Rate: ${summary.success_rate.toFixed(2)}%`);
535
310
  logger_1.logger.info(` Avg Response Time: ${summary.avg_response_time.toFixed(2)}ms`);
@@ -538,20 +313,20 @@ class TestRunner {
538
313
  }
539
314
  logErrorDetails(summary) {
540
315
  if (summary.failed_requests > 0) {
541
- logger_1.logger.warn(`❌ ${summary.failed_requests} requests failed`);
316
+ logger_1.logger.warn(`${summary.failed_requests} requests failed`);
542
317
  // Log status code distribution
543
- logger_1.logger.info('📊 Status Code Distribution:');
318
+ logger_1.logger.info('Status Code Distribution:');
544
319
  Object.entries(summary.status_distribution)
545
320
  .sort(([a], [b]) => parseInt(a) - parseInt(b))
546
321
  .forEach(([status, count]) => {
547
322
  const statusNum = parseInt(status);
548
323
  const isError = statusNum >= 400;
549
- const emoji = isError ? '' : '';
550
- logger_1.logger.info(` ${emoji} ${status}: ${count} requests`);
324
+ const indicator = isError ? 'ERROR' : 'OK';
325
+ logger_1.logger.info(` ${indicator} ${status}: ${count} requests`);
551
326
  });
552
327
  // Log top errors
553
328
  if (summary.error_details && summary.error_details.length > 0) {
554
- logger_1.logger.warn('🔍 Top Error Details:');
329
+ logger_1.logger.warn('Top Error Details:');
555
330
  summary.error_details.slice(0, 5).forEach((error, index) => {
556
331
  logger_1.logger.warn(` ${index + 1}. ${error.error} (${error.count}x)`);
557
332
  if (error.status) {
@@ -569,8 +344,8 @@ class TestRunner {
569
344
  }
570
345
  async generateReport(summary) {
571
346
  try {
572
- const { EnhancedHTMLReportGenerator } = await Promise.resolve().then(() => __importStar(require('../reporting/enhanced-html-generator')));
573
- const generator = new EnhancedHTMLReportGenerator();
347
+ const { HTMLReportGenerator } = await Promise.resolve().then(() => __importStar(require('../reporting/html-generator')));
348
+ const generator = new HTMLReportGenerator();
574
349
  // Process report path with automatic timestamp
575
350
  const reportPath = this.processTemplateFilePath(this.config.report.output);
576
351
  await generator.generate({
@@ -580,8 +355,26 @@ class TestRunner {
580
355
  }, reportPath);
581
356
  }
582
357
  catch (error) {
583
- logger_1.logger.error('Report generation failed:', error);
358
+ logger_1.logger.error('Report generation failed:', error);
359
+ }
360
+ }
361
+ processTemplateFilePath(filePath) {
362
+ if (!filePath) {
363
+ return `results/${this.config.name}-{{timestamp}}.html`;
364
+ }
365
+ let processedPath = filePath;
366
+ if (!filePath.includes('{{timestamp}}')) {
367
+ const lastDot = filePath.lastIndexOf('.');
368
+ if (lastDot > 0) {
369
+ const name = filePath.substring(0, lastDot);
370
+ const ext = filePath.substring(lastDot);
371
+ processedPath = `${name}-{{timestamp}}${ext}`;
372
+ }
373
+ else {
374
+ processedPath = `${filePath}-{{timestamp}}`;
375
+ }
584
376
  }
377
+ return file_manager_1.FileManager.processFilePath(processedPath);
585
378
  }
586
379
  getMetrics() {
587
380
  return this.metrics;
@@ -1,11 +1,9 @@
1
1
  import { Scenario, VUHooks } from '../config/types/hooks';
2
2
  import { MetricsCollector } from '../metrics/collector';
3
3
  import { ProtocolHandler } from '../protocols/base';
4
- import { GlobalCSVConfig } from '../config';
5
- export interface GlobalCSVOptions {
6
- config: GlobalCSVConfig;
7
- mode?: 'next' | 'unique' | 'random';
8
- }
4
+ import { DataOptions } from './data';
5
+ export { DataOptions as GlobalCSVOptions } from './data';
6
+ export type { DataRow as CSVDataRow } from './data';
9
7
  export declare class VirtualUser {
10
8
  private id;
11
9
  private context;
@@ -13,47 +11,19 @@ export declare class VirtualUser {
13
11
  private stepExecutor;
14
12
  private isActive;
15
13
  private scenarios;
16
- private csvProviders;
17
14
  private vuHooksManager;
15
+ private dataManager;
16
+ private thinkTimeStrategy;
17
+ private scenarioSelector;
18
18
  private testName;
19
19
  private handlers;
20
- private globalThinkTime?;
21
- private globalCSVProvider?;
22
- private globalCSVMode?;
23
- constructor(id: number, metrics: MetricsCollector, handlers: Map<string, ProtocolHandler>, testName?: string, vuHooks?: VUHooks, globalThinkTime?: string | number, globalCSV?: GlobalCSVOptions);
20
+ constructor(id: number, metrics: MetricsCollector, handlers: Map<string, ProtocolHandler>, testName?: string, vuHooks?: VUHooks, globalThinkTime?: string | number, globalCSV?: DataOptions);
24
21
  setScenarios(scenarios: Scenario[]): Promise<void>;
25
- /**
26
- * Initialize CSV providers only for scenarios that need them
27
- */
28
- private initializeCSVProvidersIfNeeded;
29
- /**
30
- * Load global CSV data and merge into context variables
31
- * Called once at the start of each VU execution cycle
32
- */
33
- private loadGlobalCSVData;
34
22
  executeScenarios(): Promise<void>;
35
23
  executeScenario(scenario: Scenario): Promise<void>;
36
- private loadCSVDataIfNeeded;
37
- private loadCSVDataForScenario;
38
- /**
39
- * Select scenarios based on weights using proportional distribution.
40
- *
41
- * Weights determine the probability of a scenario being selected:
42
- * - scenario1 (weight: 50) + scenario2 (weight: 25) + scenario3 (weight: 25) = 100
43
- * - 50% of VUs will run scenario1, 25% scenario2, 25% scenario3
44
- *
45
- * If weights don't sum to 100, they are normalized proportionally.
46
- */
47
- private selectScenarios;
48
24
  private executeSetup;
49
25
  private executeTeardown;
50
26
  private executeScript;
51
- /**
52
- * Get effective think time using hierarchical override:
53
- * Step think_time > Scenario think_time > Global think_time
54
- */
55
- private getEffectiveThinkTime;
56
- private applyThinkTime;
57
27
  stop(): Promise<void>;
58
28
  getId(): number;
59
29
  isRunning(): boolean;