@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,488 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VirtualUser = void 0;
4
+ const step_executor_1 = require("./step-executor");
5
+ const csv_data_provider_1 = require("./csv-data-provider");
6
+ const hooks_manager_1 = require("./hooks-manager");
7
+ const time_1 = require("../utils/time");
8
+ const logger_1 = require("../utils/logger");
9
+ class VirtualUser {
10
+ constructor(id, metrics, handlers, testName = 'Load Test', vuHooks, globalThinkTime, globalCSV) {
11
+ this.isActive = true;
12
+ this.scenarios = [];
13
+ this.csvProviders = new Map();
14
+ logger_1.logger.debug(`VirtualUser ${id} created`);
15
+ this.id = id;
16
+ this.metrics = metrics;
17
+ this.handlers = handlers;
18
+ this.testName = testName;
19
+ this.globalThinkTime = globalThinkTime; // Store global think time
20
+ this.stepExecutor = new step_executor_1.StepExecutor(handlers, testName); // Pass testName to StepExecutor
21
+ this.vuHooksManager = new hooks_manager_1.VUHooksManager(testName, id, vuHooks);
22
+ // Initialize global CSV if configured
23
+ if (globalCSV?.config) {
24
+ this.globalCSVMode = globalCSV.mode || 'next';
25
+ this.globalCSVProvider = csv_data_provider_1.CSVDataProvider.getInstance(globalCSV.config);
26
+ logger_1.logger.debug(`VU${id}: Global CSV configured (mode: ${this.globalCSVMode})`);
27
+ }
28
+ this.context = {
29
+ vu_id: id,
30
+ iteration: 0,
31
+ variables: {},
32
+ extracted_data: {}
33
+ // csv_data and global_csv_data will be added only when needed
34
+ };
35
+ }
36
+ // FIXED: Now async to support CSV initialization
37
+ async setScenarios(scenarios) {
38
+ logger_1.logger.debug(`VU${this.id}: setScenarios called with ${scenarios.length} scenarios`);
39
+ this.scenarios = scenarios;
40
+ // Debug: Let's see what the scenarios look like
41
+ for (const scenario of scenarios) {
42
+ logger_1.logger.debug(`VU${this.id}: Scenario "${scenario.name}" config: ${JSON.stringify(scenario, null, 2)}`);
43
+ }
44
+ // Initialize CSV providers only if needed
45
+ await this.initializeCSVProvidersIfNeeded();
46
+ logger_1.logger.debug(`VU${this.id}: setScenarios completed`);
47
+ }
48
+ /**
49
+ * Initialize CSV providers only for scenarios that need them
50
+ */
51
+ async initializeCSVProvidersIfNeeded() {
52
+ const csvScenarios = this.scenarios.filter(s => s.csv_data);
53
+ if (csvScenarios.length === 0) {
54
+ // No CSV scenarios - skip initialization entirely
55
+ logger_1.logger.debug(`VU${this.id}: No CSV scenarios found, skipping CSV initialization`);
56
+ return;
57
+ }
58
+ logger_1.logger.debug(`VU${this.id}: Found ${csvScenarios.length} scenarios with CSV data`);
59
+ for (const scenario of csvScenarios) {
60
+ const csvScenario = scenario; // Cast to access CSV properties
61
+ logger_1.logger.debug(`VU${this.id}: Processing CSV for scenario "${scenario.name}": ${JSON.stringify(csvScenario.csv_data)}`);
62
+ if (csvScenario.csv_data) {
63
+ try {
64
+ const provider = csv_data_provider_1.CSVDataProvider.getInstance(csvScenario.csv_data);
65
+ await provider.loadData();
66
+ this.csvProviders.set(scenario.name, provider);
67
+ logger_1.logger.debug(`VU${this.id}: Initialized CSV provider for scenario "${scenario.name}"`);
68
+ }
69
+ catch (error) {
70
+ logger_1.logger.warn(`VU${this.id}: Failed to initialize CSV for scenario "${scenario.name}":`, error);
71
+ // Don't fail the entire VU - just log the warning and continue
72
+ }
73
+ }
74
+ }
75
+ logger_1.logger.debug(`VU${this.id}: CSV initialization completed. Providers: ${this.csvProviders.size}`);
76
+ }
77
+ /**
78
+ * Load global CSV data and merge into context variables
79
+ * Called once at the start of each VU execution cycle
80
+ */
81
+ async loadGlobalCSVData() {
82
+ if (!this.globalCSVProvider) {
83
+ return;
84
+ }
85
+ try {
86
+ await this.globalCSVProvider.loadData();
87
+ let csvData = null;
88
+ switch (this.globalCSVMode) {
89
+ case 'unique':
90
+ csvData = await this.globalCSVProvider.getUniqueRow(this.id);
91
+ break;
92
+ case 'random':
93
+ csvData = await this.globalCSVProvider.getRandomRow(this.id);
94
+ break;
95
+ case 'next':
96
+ default:
97
+ csvData = await this.globalCSVProvider.getNextRow(this.id);
98
+ break;
99
+ }
100
+ if (csvData) {
101
+ this.context.global_csv_data = csvData;
102
+ // Merge global CSV data into variables (can be overridden by scenario CSV)
103
+ logger_1.logger.debug(`VU${this.id}: Adding global CSV columns to variables: ${Object.keys(csvData).join(', ')}`);
104
+ for (const [key, value] of Object.entries(csvData)) {
105
+ this.context.variables[key] = value;
106
+ logger_1.logger.debug(`VU${this.id}: Set global CSV variable: ${key} = ${value}`);
107
+ }
108
+ logger_1.logger.debug(`VU ${this.id}: Loaded global CSV data: ${Object.keys(csvData).join(', ')}`);
109
+ }
110
+ else {
111
+ logger_1.logger.debug(`VU${this.id}: Global CSV data exhausted - stopping VU`);
112
+ delete this.context.global_csv_data;
113
+ await this.stop();
114
+ throw new Error(`VU${this.id} terminated due to global CSV data exhaustion`);
115
+ }
116
+ }
117
+ catch (error) {
118
+ if (error instanceof Error && error.message.includes('terminated due to global CSV')) {
119
+ throw error;
120
+ }
121
+ logger_1.logger.warn(`๐Ÿ“Š VU ${this.id}: Failed to load global CSV data:`, error);
122
+ delete this.context.global_csv_data;
123
+ }
124
+ }
125
+ // This method executes scenarios once (called repeatedly by load patterns)
126
+ async executeScenarios() {
127
+ if (!this.isActive || this.scenarios.length === 0) {
128
+ return;
129
+ }
130
+ // Load global CSV data first (available to all scenarios)
131
+ try {
132
+ await this.loadGlobalCSVData();
133
+ }
134
+ catch (error) {
135
+ if (error instanceof Error && error.message.includes('terminated due to global CSV')) {
136
+ logger_1.logger.warn(`๐Ÿ“Š VU ${this.id}: Stopping due to global CSV exhaustion`);
137
+ return;
138
+ }
139
+ // Log but continue if global CSV fails for other reasons
140
+ logger_1.logger.warn(`๐Ÿ“Š VU ${this.id}: Global CSV loading failed, continuing without:`, error);
141
+ }
142
+ // Execute beforeVU hook
143
+ try {
144
+ const beforeVUResult = await this.vuHooksManager.executeBeforeVU(this.context.variables, this.context.extracted_data);
145
+ // Merge any variables returned by beforeVU hook
146
+ if (beforeVUResult?.variables) {
147
+ Object.assign(this.context.variables, beforeVUResult.variables);
148
+ logger_1.logger.debug(`VU${this.id}: beforeVU hook set variables: ${Object.keys(beforeVUResult.variables).join(', ')}`);
149
+ }
150
+ }
151
+ catch (error) {
152
+ logger_1.logger.error(`โŒ VU ${this.id} beforeVU hook failed:`, error);
153
+ // Continue execution even if beforeVU fails
154
+ }
155
+ try {
156
+ const selectedScenarios = this.selectScenarios(this.scenarios);
157
+ for (const scenario of selectedScenarios) {
158
+ if (!this.isActive)
159
+ break;
160
+ try {
161
+ await this.executeScenario(scenario);
162
+ }
163
+ catch (error) {
164
+ logger_1.logger.error(`โŒ VU ${this.id} failed executing scenario ${scenario.name}:`, error);
165
+ this.metrics.recordError(this.id, scenario.name, 'scenario', error);
166
+ }
167
+ }
168
+ }
169
+ finally {
170
+ // Execute teardownVU hook
171
+ try {
172
+ await this.vuHooksManager.executeTeardownVU(this.context.variables, this.context.extracted_data);
173
+ }
174
+ catch (error) {
175
+ logger_1.logger.error(`โŒ VU ${this.id} teardownVU hook failed:`, error);
176
+ }
177
+ }
178
+ }
179
+ async executeScenario(scenario) {
180
+ // Handle both number and LoopConfig for backwards compatibility
181
+ const loops = typeof scenario.loop === 'number' ? scenario.loop : (scenario.loop?.count || 1);
182
+ logger_1.logger.debug(`VU ${this.id} executing scenario: ${scenario.name} (${loops} loops)`);
183
+ logger_1.logger.debug(`Scenario variables: ${JSON.stringify(scenario.variables)}`);
184
+ // Process scenario variables and store in context
185
+ if (scenario.variables) {
186
+ logger_1.logger.debug(`Processing ${Object.keys(scenario.variables).length} scenario variables`);
187
+ for (const [key, value] of Object.entries(scenario.variables)) {
188
+ this.context.variables[key] = value;
189
+ logger_1.logger.debug(`Set variable: ${key} = ${value}`);
190
+ }
191
+ }
192
+ // Load CSV data if this scenario uses it (completely optional)
193
+ logger_1.logger.debug(`About to load CSV data if needed...`);
194
+ await this.loadCSVDataIfNeeded(scenario);
195
+ logger_1.logger.debug(`CSV data loading completed`);
196
+ logger_1.logger.debug(`Context variables after CSV setup: ${JSON.stringify(this.context.variables)}`);
197
+ // Create scenario hooks manager
198
+ const scenarioHooksManager = new hooks_manager_1.ScenarioHooksManager(this.testName, this.id, scenario.name, scenario.hooks);
199
+ // Execute beforeScenario hook
200
+ try {
201
+ const beforeScenarioResult = await scenarioHooksManager.executeBeforeScenario(this.context.variables, this.context.extracted_data, this.context.csv_data);
202
+ // CRITICAL FIX: The hook manager should have updated the objects by reference
203
+ // But let's also merge any returned variables to be safe
204
+ if (beforeScenarioResult?.variables) {
205
+ Object.assign(this.context.variables, beforeScenarioResult.variables);
206
+ logger_1.logger.debug(`VU${this.id}: beforeScenario hook merged additional variables: ${Object.keys(beforeScenarioResult.variables).join(', ')}`);
207
+ }
208
+ // Debug: Show what's in context after beforeScenario
209
+ logger_1.logger.debug(`VU${this.id}: Variables after beforeScenario: ${Object.keys(this.context.variables).join(', ')}`);
210
+ logger_1.logger.debug(`VU${this.id}: Extracted data after beforeScenario: ${Object.keys(this.context.extracted_data).join(', ')}`);
211
+ logger_1.logger.debug(`VU${this.id}: Extracted data values: ${JSON.stringify(this.context.extracted_data)}`);
212
+ }
213
+ catch (error) {
214
+ logger_1.logger.error(`โŒ VU ${this.id} beforeScenario hook failed:`, error);
215
+ // You might want to return here if authentication is critical
216
+ if (scenario.hooks?.beforeScenario?.continueOnError === false) {
217
+ throw error;
218
+ }
219
+ }
220
+ // Legacy support: Run setup if configured (deprecated - use hooks.beforeScenario)
221
+ if (scenario.setup && !scenario.hooks?.beforeScenario) {
222
+ await this.executeSetup(scenario.setup);
223
+ }
224
+ try {
225
+ for (let iteration = 0; iteration < loops; iteration++) {
226
+ if (!this.isActive)
227
+ break;
228
+ this.context.iteration = iteration;
229
+ logger_1.logger.debug(`๐Ÿ” VU ${this.id} scenario ${scenario.name} iteration ${iteration + 1}/${loops}`);
230
+ // Execute beforeLoop hook
231
+ try {
232
+ const beforeLoopResult = await scenarioHooksManager.executeBeforeLoop(iteration, this.context.variables, this.context.extracted_data, this.context.csv_data);
233
+ if (beforeLoopResult?.variables) {
234
+ Object.assign(this.context.variables, beforeLoopResult.variables);
235
+ }
236
+ }
237
+ catch (error) {
238
+ logger_1.logger.error(`โŒ VU ${this.id} beforeLoop hook failed:`, error);
239
+ }
240
+ // For unique CSV mode, get new CSV data each iteration
241
+ const csvScenario = scenario;
242
+ if (csvScenario.csv_mode === 'unique' && iteration > 0 && this.csvProviders.has(scenario.name)) {
243
+ await this.loadCSVDataIfNeeded(scenario);
244
+ }
245
+ try {
246
+ // Execute all steps in sequence
247
+ for (let stepIndex = 0; stepIndex < scenario.steps.length; stepIndex++) {
248
+ if (!this.isActive)
249
+ break;
250
+ const step = scenario.steps[stepIndex];
251
+ // Debug: Show context before each step
252
+ logger_1.logger.debug(`VU${this.id}: About to execute step "${step.name || step.type}"`);
253
+ logger_1.logger.debug(`VU${this.id}: Available variables: ${Object.keys(this.context.variables).join(', ')}`);
254
+ logger_1.logger.debug(`VU${this.id}: Available extracted_data: ${Object.keys(this.context.extracted_data).join(', ')}`);
255
+ try {
256
+ const result = await this.stepExecutor.executeStep(step, this.context, scenario.name);
257
+ if (result.shouldRecord) {
258
+ this.metrics.recordResult(result);
259
+ }
260
+ // Apply hierarchical think time: step > scenario > global
261
+ const effectiveThinkTime = this.getEffectiveThinkTime(step, scenario);
262
+ if (effectiveThinkTime !== undefined) {
263
+ await this.applyThinkTime(effectiveThinkTime);
264
+ }
265
+ }
266
+ catch (error) {
267
+ logger_1.logger.error(`โŒ VU ${this.id} step failed:`, error);
268
+ this.metrics.recordError(this.id, scenario.name, step.name || step.type || 'rest', error);
269
+ }
270
+ }
271
+ // Execute afterLoop hook
272
+ try {
273
+ await scenarioHooksManager.executeAfterLoop(iteration, this.context.variables, this.context.extracted_data, this.context.csv_data);
274
+ }
275
+ catch (error) {
276
+ logger_1.logger.error(`โŒ VU ${this.id} afterLoop hook failed:`, error);
277
+ }
278
+ }
279
+ catch (error) {
280
+ // Execute afterLoop hook even on error
281
+ try {
282
+ await scenarioHooksManager.executeAfterLoop(iteration, this.context.variables, this.context.extracted_data, this.context.csv_data);
283
+ }
284
+ catch (hookError) {
285
+ logger_1.logger.error(`โŒ VU ${this.id} afterLoop hook failed during error handling:`, hookError);
286
+ }
287
+ throw error;
288
+ }
289
+ // Think time between iterations (except after last iteration)
290
+ if (iteration < loops - 1) {
291
+ await this.applyThinkTime(scenario.think_time);
292
+ }
293
+ }
294
+ }
295
+ finally {
296
+ // Execute teardownScenario hook
297
+ try {
298
+ await scenarioHooksManager.executeTeardownScenario(this.context.variables, this.context.extracted_data, this.context.csv_data);
299
+ }
300
+ catch (error) {
301
+ logger_1.logger.error(`โŒ VU ${this.id} teardownScenario hook failed:`, error);
302
+ }
303
+ // Legacy support: Run teardown if configured (deprecated - use hooks.teardownScenario)
304
+ if (scenario.teardown && !scenario.hooks?.teardownScenario) {
305
+ await this.executeTeardown(scenario.teardown);
306
+ }
307
+ }
308
+ }
309
+ // ... rest of your existing methods remain exactly the same
310
+ async loadCSVDataIfNeeded(scenario) {
311
+ const csvScenario = scenario;
312
+ logger_1.logger.debug(`VU${this.id}: Checking CSV need for scenario "${scenario.name}"`);
313
+ logger_1.logger.debug(`VU${this.id}: Has csv_data config: ${!!csvScenario.csv_data}`);
314
+ logger_1.logger.debug(`VU${this.id}: Has CSV provider: ${this.csvProviders.has(scenario.name)}`);
315
+ if (!csvScenario.csv_data || !this.csvProviders.has(scenario.name)) {
316
+ logger_1.logger.debug(`VU${this.id}: No CSV data needed for scenario "${scenario.name}"`);
317
+ delete this.context.csv_data;
318
+ return;
319
+ }
320
+ try {
321
+ logger_1.logger.debug(`VU${this.id}: Loading CSV data for scenario "${scenario.name}"...`);
322
+ const csvData = await this.loadCSVDataForScenario(csvScenario);
323
+ if (csvData) {
324
+ this.context.csv_data = csvData;
325
+ logger_1.logger.debug(`VU${this.id}: Adding CSV columns to variables: ${Object.keys(csvData).join(', ')}`);
326
+ for (const [key, value] of Object.entries(csvData)) {
327
+ if (!(key in this.context.variables)) {
328
+ this.context.variables[key] = value;
329
+ logger_1.logger.debug(`VU${this.id}: Added CSV variable: ${key} = ${value}`);
330
+ }
331
+ else {
332
+ logger_1.logger.debug(`VU${this.id}: Skipped CSV variable ${key} (already in variables)`);
333
+ }
334
+ }
335
+ logger_1.logger.debug(`VU ${this.id}: Loaded CSV data for scenario "${scenario.name}": ${Object.keys(csvData).join(', ')}`);
336
+ }
337
+ else {
338
+ logger_1.logger.debug(`VU${this.id}: No CSV data available - terminating this VU`);
339
+ delete this.context.csv_data;
340
+ this.stop();
341
+ throw new Error(`VU${this.id} terminated due to CSV data exhaustion in scenario "${scenario.name}"`);
342
+ }
343
+ }
344
+ catch (error) {
345
+ if (error instanceof Error && error.message.includes('terminated due to CSV data exhaustion')) {
346
+ throw error;
347
+ }
348
+ logger_1.logger.warn(`VU ${this.id}: Failed to load CSV data for scenario "${scenario.name}":`, error);
349
+ delete this.context.csv_data;
350
+ logger_1.logger.debug(`VU${this.id}: Continuing with fallback variables after CSV error`);
351
+ }
352
+ }
353
+ async loadCSVDataForScenario(scenario) {
354
+ const provider = this.csvProviders.get(scenario.name);
355
+ if (!provider) {
356
+ return null;
357
+ }
358
+ const mode = scenario.csv_mode || 'next';
359
+ switch (mode) {
360
+ case 'unique':
361
+ return await provider.getUniqueRow(this.id);
362
+ case 'random':
363
+ return await provider.getRandomRow();
364
+ case 'next':
365
+ default:
366
+ return await provider.getNextRow(this.id);
367
+ }
368
+ }
369
+ selectScenarios(scenarios) {
370
+ const selected = [];
371
+ for (const scenario of scenarios) {
372
+ const weight = scenario.weight || 100;
373
+ const random = Math.random() * 100;
374
+ if (random < weight) {
375
+ selected.push(scenario);
376
+ }
377
+ }
378
+ // Ensure at least one scenario is selected
379
+ if (selected.length === 0 && scenarios.length > 0) {
380
+ selected.push(scenarios[0]);
381
+ }
382
+ return selected;
383
+ }
384
+ async executeSetup(setupScript) {
385
+ try {
386
+ await this.executeScript(setupScript, 'setup');
387
+ }
388
+ catch (error) {
389
+ logger_1.logger.warn(`โš ๏ธ VU ${this.id} setup script failed:`, error);
390
+ }
391
+ }
392
+ async executeTeardown(teardownScript) {
393
+ try {
394
+ await this.executeScript(teardownScript, 'teardown');
395
+ }
396
+ catch (error) {
397
+ logger_1.logger.warn(`โš ๏ธ VU ${this.id} teardown script failed:`, error);
398
+ }
399
+ }
400
+ async executeScript(script, type) {
401
+ const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
402
+ const fn = new AsyncFunction('context', 'require', 'console', script);
403
+ const timeout = 30000; // 30 seconds timeout
404
+ const timeoutPromise = new Promise((_, reject) => {
405
+ setTimeout(() => reject(new Error(`${type} script timeout`)), timeout);
406
+ });
407
+ return Promise.race([
408
+ fn(this.context, require, console),
409
+ timeoutPromise
410
+ ]);
411
+ }
412
+ /**
413
+ * Get effective think time using hierarchical override:
414
+ * Step think_time > Scenario think_time > Global think_time
415
+ */
416
+ getEffectiveThinkTime(step, scenario) {
417
+ // Step level has highest priority
418
+ if (step.think_time !== undefined) {
419
+ return step.think_time;
420
+ }
421
+ // Scenario level is next
422
+ if (scenario.think_time !== undefined) {
423
+ return scenario.think_time;
424
+ }
425
+ // Global level is fallback
426
+ return this.globalThinkTime;
427
+ }
428
+ async applyThinkTime(thinkTime) {
429
+ if (!thinkTime) {
430
+ // No think time specified at any level - skip
431
+ return;
432
+ }
433
+ if (typeof thinkTime === 'number') {
434
+ logger_1.logger.debug(`Applying thinktime: ${thinkTime} seconds`);
435
+ await (0, time_1.sleep)(thinkTime * 1000);
436
+ return;
437
+ }
438
+ const rangeMatch = thinkTime.match(/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)([sm])?$/);
439
+ if (rangeMatch) {
440
+ const [, minStr, maxStr, unit] = rangeMatch;
441
+ let min = parseFloat(minStr);
442
+ let max = parseFloat(maxStr);
443
+ if (unit === 's' || !unit) {
444
+ min *= 1000;
445
+ max *= 1000;
446
+ }
447
+ const thinkTimeMs = (0, time_1.randomBetween)(min, max);
448
+ await (0, time_1.sleep)(thinkTimeMs);
449
+ }
450
+ else {
451
+ try {
452
+ const thinkTimeMs = (0, time_1.parseTime)(thinkTime);
453
+ await (0, time_1.sleep)(thinkTimeMs);
454
+ }
455
+ catch (error) {
456
+ logger_1.logger.warn(`โš ๏ธ Invalid think time format: ${thinkTime}`);
457
+ await (0, time_1.sleep)((0, time_1.randomBetween)(1000, 3000));
458
+ }
459
+ }
460
+ }
461
+ // stop(): void {
462
+ // this.isActive = false;
463
+ // logger.debug(`โน๏ธ VU ${this.id} stopped`);
464
+ // }
465
+ async stop() {
466
+ this.isActive = false;
467
+ logger_1.logger.debug(`โน๏ธ VU ${this.id} stopping...`);
468
+ // Clean up browser resources if WebHandler exists
469
+ const webHandler = this.handlers.get('web');
470
+ if (webHandler && typeof webHandler.cleanupVU === 'function') {
471
+ try {
472
+ await webHandler.cleanupVU(this.id);
473
+ logger_1.logger.debug(`๐Ÿงน VU ${this.id}: Browser cleanup completed`);
474
+ }
475
+ catch (error) {
476
+ logger_1.logger.warn(`โš ๏ธ VU ${this.id}: Error during browser cleanup:`, error);
477
+ }
478
+ }
479
+ logger_1.logger.debug(`โน๏ธ VU ${this.id} stopped`);
480
+ }
481
+ getId() {
482
+ return this.id;
483
+ }
484
+ isRunning() {
485
+ return this.isActive;
486
+ }
487
+ }
488
+ exports.VirtualUser = VirtualUser;
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'events';
2
+ import { TestConfiguration } from '../config';
3
+ import { RemoteWorker, RemoteWorkerConfig } from './remote-worker';
4
+ export interface DistributedTestConfig {
5
+ workers: RemoteWorkerConfig[];
6
+ strategy: 'even' | 'capacity_based' | 'round_robin' | 'geographic';
7
+ sync_start: boolean;
8
+ heartbeat_interval: number;
9
+ timeout: number;
10
+ retry_failed: boolean;
11
+ }
12
+ export interface WorkAssignment {
13
+ worker: RemoteWorker;
14
+ config: TestConfiguration;
15
+ virtualUsers: number;
16
+ }
17
+ export declare class DistributedCoordinator extends EventEmitter {
18
+ private workers;
19
+ private config;
20
+ private testConfig;
21
+ private loadDistributor;
22
+ private resultAggregator;
23
+ private healthMonitor;
24
+ private isRunning;
25
+ constructor(config: DistributedTestConfig);
26
+ initialize(): Promise<void>;
27
+ executeTest(testConfig: TestConfiguration): Promise<void>;
28
+ private synchronizedStart;
29
+ private rollingStart;
30
+ private waitForCompletion;
31
+ stop(): Promise<void>;
32
+ cleanup(): Promise<void>;
33
+ getAggregatedResults(): any;
34
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DistributedCoordinator = void 0;
4
+ const events_1 = require("events");
5
+ const remote_worker_1 = require("./remote-worker"); // Fixed import
6
+ const load_distributor_1 = require("./load-distributor");
7
+ const result_aggregator_1 = require("./result-aggregator");
8
+ const health_monitor_1 = require("./health-monitor");
9
+ const logger_1 = require("../utils/logger");
10
+ class DistributedCoordinator extends events_1.EventEmitter {
11
+ constructor(config) {
12
+ super();
13
+ this.workers = [];
14
+ this.testConfig = null;
15
+ this.isRunning = false;
16
+ this.config = config;
17
+ this.loadDistributor = new load_distributor_1.LoadDistributor();
18
+ this.resultAggregator = new result_aggregator_1.ResultAggregator();
19
+ this.healthMonitor = new health_monitor_1.HealthMonitor();
20
+ }
21
+ async initialize() {
22
+ logger_1.logger.info(`๐ŸŒ Initializing distributed test with ${this.config.workers.length} workers`);
23
+ for (const workerConfig of this.config.workers) {
24
+ try {
25
+ const worker = new remote_worker_1.RemoteWorker(workerConfig);
26
+ worker.on('result', (result) => {
27
+ this.resultAggregator.addResult(result);
28
+ this.emit('result', result);
29
+ });
30
+ worker.on('error', (error) => {
31
+ logger_1.logger.error(`โŒ Worker ${worker.getAddress()} error:`, error);
32
+ this.emit('worker-error', { worker: worker.getAddress(), error });
33
+ });
34
+ worker.on('status', (status) => {
35
+ this.healthMonitor.updateWorkerStatus(worker.getAddress(), status);
36
+ });
37
+ await worker.connect();
38
+ this.workers.push(worker);
39
+ logger_1.logger.info(`โœ… Connected to worker: ${worker.getAddress()}`);
40
+ }
41
+ catch (error) {
42
+ logger_1.logger.error(`โŒ Failed to connect to worker ${workerConfig.host}:${workerConfig.port}:`, error);
43
+ if (!this.config.retry_failed) {
44
+ throw error;
45
+ }
46
+ }
47
+ }
48
+ if (this.workers.length === 0) {
49
+ throw new Error('No workers available for distributed testing');
50
+ }
51
+ this.healthMonitor.start(this.workers, this.config.heartbeat_interval);
52
+ logger_1.logger.info(`๐ŸŽฏ ${this.workers.length} workers ready for distributed testing`);
53
+ }
54
+ async executeTest(testConfig) {
55
+ this.testConfig = testConfig;
56
+ this.isRunning = true;
57
+ try {
58
+ const workAssignments = this.loadDistributor.distribute(testConfig, this.workers, this.config.strategy);
59
+ logger_1.logger.info(`๐Ÿ“Š Load distribution:`);
60
+ workAssignments.forEach(assignment => {
61
+ logger_1.logger.info(` ${assignment.worker.getAddress()}: ${assignment.virtualUsers} VUs`);
62
+ });
63
+ this.resultAggregator.start();
64
+ if (this.config.sync_start) {
65
+ await this.synchronizedStart(workAssignments);
66
+ }
67
+ else {
68
+ await this.rollingStart(workAssignments);
69
+ }
70
+ await this.waitForCompletion();
71
+ }
72
+ finally {
73
+ this.isRunning = false;
74
+ }
75
+ }
76
+ async synchronizedStart(assignments) {
77
+ logger_1.logger.info('๐Ÿš€ Starting synchronized distributed test...');
78
+ const preparations = assignments.map(async (assignment) => {
79
+ await assignment.worker.prepareTest(assignment.config);
80
+ });
81
+ await Promise.all(preparations);
82
+ logger_1.logger.info('โœ… All workers prepared');
83
+ const startTime = Date.now() + 5000;
84
+ const starts = assignments.map(async (assignment) => {
85
+ await assignment.worker.startTest(startTime);
86
+ });
87
+ await Promise.all(starts);
88
+ logger_1.logger.info('๐ŸŽฏ All workers started');
89
+ }
90
+ async rollingStart(assignments) {
91
+ logger_1.logger.info('๐Ÿ”„ Starting rolling distributed test...');
92
+ for (const assignment of assignments) {
93
+ try {
94
+ await assignment.worker.executeTest(assignment.config);
95
+ logger_1.logger.info(`โœ… Started worker ${assignment.worker.getAddress()}`);
96
+ }
97
+ catch (error) {
98
+ logger_1.logger.error(`โŒ Failed to start worker ${assignment.worker.getAddress()}:`, error);
99
+ }
100
+ }
101
+ }
102
+ async waitForCompletion() {
103
+ logger_1.logger.info('โณ Waiting for all workers to complete...');
104
+ const completionPromises = this.workers.map(worker => worker.waitForCompletion().catch((error) => {
105
+ logger_1.logger.error(`โŒ Worker ${worker.getAddress()} completion error:`, error);
106
+ return null;
107
+ }));
108
+ await Promise.all(completionPromises);
109
+ // Collect results from all workers
110
+ logger_1.logger.info('๐Ÿ“Š Collecting results from workers...');
111
+ for (const worker of this.workers) {
112
+ try {
113
+ const workerResults = await worker.getResults();
114
+ if (workerResults) {
115
+ // Add worker results to aggregator
116
+ if (workerResults.results) {
117
+ workerResults.results.forEach((result) => {
118
+ this.resultAggregator.addResult(result, worker.getAddress());
119
+ });
120
+ }
121
+ // Add VU ramp-up events from worker's summary
122
+ if (workerResults.summary?.vu_ramp_up && Array.isArray(workerResults.summary.vu_ramp_up)) {
123
+ this.resultAggregator.addVURampUpEvents(workerResults.summary.vu_ramp_up, worker.getAddress());
124
+ logger_1.logger.debug(`๐Ÿ“Š Collected ${workerResults.summary.vu_ramp_up.length} VU events from ${worker.getAddress()}`);
125
+ }
126
+ }
127
+ }
128
+ catch (error) {
129
+ logger_1.logger.warn(`โš ๏ธ Failed to get results from worker ${worker.getAddress()}:`, error);
130
+ }
131
+ }
132
+ this.resultAggregator.stop();
133
+ logger_1.logger.info('โœ… All workers completed');
134
+ }
135
+ async stop() {
136
+ logger_1.logger.info('โน๏ธ Stopping distributed test...');
137
+ this.isRunning = false;
138
+ const stopPromises = this.workers.map(worker => worker.stop().catch((error) => {
139
+ logger_1.logger.warn(`โš ๏ธ Error stopping worker ${worker.getAddress()}:`, error);
140
+ }));
141
+ await Promise.all(stopPromises);
142
+ logger_1.logger.info('๐Ÿ›‘ All workers stopped');
143
+ }
144
+ async cleanup() {
145
+ logger_1.logger.info('๐Ÿงน Cleaning up distributed test...');
146
+ this.healthMonitor.stop();
147
+ const cleanupPromises = this.workers.map(worker => worker.disconnect().catch((error) => {
148
+ logger_1.logger.warn(`โš ๏ธ Error cleaning up worker ${worker.getAddress()}:`, error);
149
+ }));
150
+ await Promise.all(cleanupPromises);
151
+ this.workers = [];
152
+ logger_1.logger.info('โœ… Cleanup completed');
153
+ }
154
+ getAggregatedResults() {
155
+ return this.resultAggregator.getAggregatedResults();
156
+ }
157
+ }
158
+ exports.DistributedCoordinator = DistributedCoordinator;