@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,680 @@
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.StepExecutor = void 0;
37
+ const hooks_manager_1 = require("./hooks-manager");
38
+ const script_executor_1 = require("./script-executor");
39
+ const threshold_evaluator_1 = require("./threshold-evaluator");
40
+ const time_1 = require("../utils/time");
41
+ const template_1 = require("../utils/template");
42
+ const logger_1 = require("../utils/logger");
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ class StepExecutor {
46
+ constructor(handlers, testName = 'Load Test') {
47
+ this.templateProcessor = new template_1.TemplateProcessor();
48
+ this.handlers = handlers;
49
+ this.testName = testName;
50
+ // Register this instance with ScriptExecutor for step execution in hooks
51
+ script_executor_1.ScriptExecutor.setStepExecutor(this);
52
+ }
53
+ async executeStep(step, context, scenarioName) {
54
+ const startTime = Date.now();
55
+ const stepName = step.name || step.type;
56
+ // Create step hooks manager if step has hooks
57
+ let stepHooksManager;
58
+ if (step.hooks) {
59
+ stepHooksManager = new hooks_manager_1.StepHooksManager(this.testName, context.vu_id, scenarioName, stepName || 'rest', step.type || 'rest', step.hooks);
60
+ }
61
+ // Execute beforeStep hook
62
+ if (stepHooksManager) {
63
+ try {
64
+ const beforeStepResult = await stepHooksManager.executeBeforeStep(context.variables, context.extracted_data, context.csv_data);
65
+ // Merge any variables returned by beforeStep hook
66
+ if (beforeStepResult?.variables) {
67
+ Object.assign(context.variables, beforeStepResult.variables);
68
+ logger_1.logger.debug(`Hook VU${context.vu_id}: beforeStep hook set variables: ${Object.keys(beforeStepResult.variables).join(', ')}`);
69
+ }
70
+ }
71
+ catch (error) {
72
+ logger_1.logger.error(`VU ${context.vu_id} beforeStep hook failed:`, error);
73
+ }
74
+ }
75
+ let testResult;
76
+ try {
77
+ // Execute the actual step
78
+ testResult = await this.executeStepInternal(step, context, scenarioName, startTime);
79
+ }
80
+ catch (error) {
81
+ // Execute onStepError hook
82
+ if (stepHooksManager) {
83
+ try {
84
+ await stepHooksManager.executeOnStepError(error, context.variables, context.extracted_data, context.csv_data);
85
+ }
86
+ catch (hookError) {
87
+ logger_1.logger.error(`VU ${context.vu_id} onStepError hook failed:`, hookError);
88
+ }
89
+ }
90
+ // Create error result
91
+ const stepName = step.name || `${step.type}_${context.iteration}`;
92
+ const iteration = context.iteration || 1;
93
+ const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
94
+ testResult = {
95
+ id: `${context.vu_id}-${Date.now()}`,
96
+ vu_id: context.vu_id,
97
+ iteration: context.iteration,
98
+ scenario: scenarioName,
99
+ action: step.name || step.type || 'rest',
100
+ step_name: stepName,
101
+ thread_name: threadName,
102
+ timestamp: startTime,
103
+ duration: Date.now() - startTime,
104
+ success: false,
105
+ error: error.message,
106
+ shouldRecord: true
107
+ };
108
+ }
109
+ finally {
110
+ // Execute teardownStep hook (only if testResult was created)
111
+ if (stepHooksManager && testResult) {
112
+ try {
113
+ const teardownResult = await stepHooksManager.executeTeardownStep(context.variables, context.extracted_data, context.csv_data, testResult);
114
+ // Merge any variables returned by teardownStep hook
115
+ if (teardownResult?.variables) {
116
+ Object.assign(context.variables, teardownResult.variables);
117
+ }
118
+ }
119
+ catch (error) {
120
+ logger_1.logger.error(`VU ${context.vu_id} teardownStep hook failed:`, error);
121
+ }
122
+ }
123
+ }
124
+ // This should never happen, but TypeScript needs the guarantee
125
+ if (!testResult) {
126
+ const stepName = step.name || `${step.type}_${context.iteration}`;
127
+ const iteration = context.iteration || 1;
128
+ const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
129
+ testResult = {
130
+ id: `${context.vu_id}-${Date.now()}`,
131
+ vu_id: context.vu_id,
132
+ iteration: context.iteration,
133
+ scenario: scenarioName,
134
+ action: step.name || step.type || 'rest',
135
+ step_name: stepName,
136
+ thread_name: threadName,
137
+ timestamp: startTime,
138
+ duration: Date.now() - startTime,
139
+ success: false,
140
+ error: 'Unknown error occurred',
141
+ shouldRecord: true
142
+ };
143
+ }
144
+ // Evaluate thresholds if they are defined for this step
145
+ if (step.thresholds && step.thresholds.length > 0) {
146
+ try {
147
+ const evaluationResult = threshold_evaluator_1.ThresholdEvaluator.evaluate(step.thresholds, testResult, stepName);
148
+ if (!evaluationResult.passed) {
149
+ // Add threshold failures to test result
150
+ testResult.threshold_failures = evaluationResult.failures;
151
+ // Execute threshold actions (may throw errors for fail actions)
152
+ await threshold_evaluator_1.ThresholdEvaluator.executeThresholdActions(evaluationResult, stepName);
153
+ }
154
+ }
155
+ catch (error) {
156
+ logger_1.logger.error(`Threshold evaluation failed for step ${stepName}:`, error);
157
+ // If threshold action is to fail, we re-throw the error
158
+ if (error instanceof Error && error.message.includes('threshold violation')) {
159
+ throw error;
160
+ }
161
+ }
162
+ }
163
+ return testResult;
164
+ }
165
+ // Make this public so hooks can execute steps
166
+ async executeStepInternal(step, context, scenarioName, startTime) {
167
+ const processedStep = this.processTemplate(step, context);
168
+ // Check condition if specified
169
+ if (step.condition && !this.evaluateCondition(step.condition, context)) {
170
+ const stepName = step.name || `${step.type}_${context.iteration}`;
171
+ const iteration = context.iteration || 1;
172
+ const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
173
+ return {
174
+ id: `${context.vu_id}-${Date.now()}`,
175
+ vu_id: context.vu_id,
176
+ iteration: context.iteration,
177
+ scenario: scenarioName,
178
+ action: step.name || step.type || 'rest',
179
+ step_name: stepName,
180
+ thread_name: threadName,
181
+ timestamp: startTime,
182
+ duration: 0,
183
+ success: true,
184
+ custom_metrics: { skipped: true }
185
+ };
186
+ }
187
+ let result;
188
+ const stepType = step.type || 'rest';
189
+ switch (stepType) {
190
+ case 'rest':
191
+ result = await this.executeRESTStep(processedStep, context);
192
+ break;
193
+ case 'soap':
194
+ result = await this.executeSOAPStep(processedStep, context);
195
+ break;
196
+ case 'web':
197
+ result = await this.executeWebStep(processedStep, context);
198
+ break;
199
+ case 'custom':
200
+ result = await this.executeCustomStep(processedStep, context);
201
+ break;
202
+ case 'wait':
203
+ result = await this.executeWaitStep(processedStep);
204
+ break;
205
+ case 'script':
206
+ result = await this.executeScriptStep(processedStep, context);
207
+ break;
208
+ default:
209
+ throw new Error(`Unsupported step type: ${step.type}`);
210
+ }
211
+ // Use response_time from handler if available (e.g., web actions with action_time)
212
+ // Otherwise fall back to total elapsed time
213
+ const totalElapsed = Date.now() - startTime;
214
+ const duration = result.response_time !== undefined ? result.response_time : totalElapsed;
215
+ const stepName = step.name || `${step.type}_${context.iteration}`;
216
+ // Generate JMeter-style thread name: "iteration. step_name vu_id-iteration"
217
+ const iteration = context.iteration || 1;
218
+ const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
219
+ const testResult = {
220
+ id: `${context.vu_id}-${Date.now()}`,
221
+ vu_id: context.vu_id,
222
+ iteration: context.iteration,
223
+ scenario: scenarioName,
224
+ action: step.name || `${step.type}_action`,
225
+ step_name: stepName,
226
+ thread_name: threadName,
227
+ timestamp: startTime,
228
+ duration,
229
+ response_time: duration, // Add explicit response_time for reporting
230
+ success: result.success,
231
+ status: result.status,
232
+ status_text: result.status_text,
233
+ error: result.error,
234
+ error_code: result.error_code,
235
+ response_size: result.response_size,
236
+ request_url: result.request_url,
237
+ request_method: result.request_method,
238
+ request_headers: result.request_headers,
239
+ request_body: result.request_body,
240
+ response_headers: result.response_headers,
241
+ response_body: result.response_body,
242
+ custom_metrics: result.custom_metrics,
243
+ shouldRecord: result.shouldRecord !== undefined ? result.shouldRecord : this.shouldRecordStep(step, true),
244
+ // JMeter-style timing breakdown
245
+ sample_start: result.sample_start,
246
+ connect_time: result.connect_time,
247
+ latency: result.latency,
248
+ // JMeter-style size breakdown
249
+ sent_bytes: result.sent_bytes,
250
+ headers_size_sent: result.headers_size_sent,
251
+ body_size_sent: result.body_size_sent,
252
+ headers_size_received: result.headers_size_received,
253
+ body_size_received: result.body_size_received,
254
+ data_type: result.data_type,
255
+ };
256
+ // Run checks if configured
257
+ if ('checks' in step && step.checks) {
258
+ const checkResults = await this.runChecks(step.checks, result, context);
259
+ if (!checkResults.passed) {
260
+ testResult.success = false;
261
+ testResult.error = `Check failed: ${checkResults.errors.join(', ')}`;
262
+ }
263
+ }
264
+ // Extract data if configured
265
+ if ('extract' in step && step.extract) {
266
+ await this.extractData(step.extract, result, context);
267
+ }
268
+ return testResult;
269
+ }
270
+ shouldRecordStep(step, success) {
271
+ // Always record errors
272
+ if (!success)
273
+ return true;
274
+ // For web steps, only record meaningful performance measurements:
275
+ // - Verifications (verify_*) - time for elements/text to appear (measures app responsiveness)
276
+ // - Waits (wait_for_*) - time for conditions to be met
277
+ // - Performance measurements (measure_*, performance_audit)
278
+ // NOT recorded: goto, click, fill, press, select, hover, screenshot (navigation/interactions)
279
+ if (step.type === 'web' && step.action) {
280
+ const measurableCommands = [
281
+ 'verify_exists', 'verify_visible', 'verify_text', 'verify_contains', 'verify_not_exists',
282
+ 'wait_for_selector', 'wait_for_text',
283
+ 'measure_web_vitals', 'performance_audit'
284
+ ];
285
+ return measurableCommands.includes(step.action.command);
286
+ }
287
+ return true;
288
+ }
289
+ async executeRESTStep(step, context) {
290
+ const handler = this.handlers.get('rest');
291
+ if (!handler) {
292
+ throw new Error('REST handler not available');
293
+ }
294
+ // Handle jsonFile loading with optional overrides
295
+ const processedStep = this.processJsonFile(step, context);
296
+ return handler.execute(processedStep, context);
297
+ }
298
+ /**
299
+ * Load JSON payload from file and apply overrides
300
+ * Supports dot notation for nested paths in overrides
301
+ */
302
+ processJsonFile(step, context) {
303
+ if (!step.jsonFile) {
304
+ return step;
305
+ }
306
+ // Resolve file path relative to CWD
307
+ const filePath = path.resolve(process.cwd(), step.jsonFile);
308
+ if (!fs.existsSync(filePath)) {
309
+ throw new Error(`JSON payload file not found: ${step.jsonFile}`);
310
+ }
311
+ // Load and parse JSON file
312
+ let payload;
313
+ try {
314
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
315
+ payload = JSON.parse(fileContent);
316
+ }
317
+ catch (error) {
318
+ throw new Error(`Failed to parse JSON file ${step.jsonFile}: ${error.message}`);
319
+ }
320
+ // Apply overrides if specified
321
+ if (step.overrides) {
322
+ payload = this.applyOverrides(payload, step.overrides, context);
323
+ }
324
+ // Return new step with json property set (removing jsonFile and overrides)
325
+ const { jsonFile, overrides, ...restOfStep } = step;
326
+ return {
327
+ ...restOfStep,
328
+ json: payload
329
+ };
330
+ }
331
+ /**
332
+ * Apply overrides to a JSON object using dot notation for nested paths
333
+ * Override values are processed through the template processor
334
+ */
335
+ applyOverrides(obj, overrides, context) {
336
+ // Deep clone the object to avoid mutating the original
337
+ const result = JSON.parse(JSON.stringify(obj));
338
+ const contextData = {
339
+ __VU: context.vu_id,
340
+ __ITER: context.iteration,
341
+ vu_id: context.vu_id,
342
+ iteration: context.iteration,
343
+ variables: context.variables || {},
344
+ extracted_data: context.extracted_data || {},
345
+ ...context.variables,
346
+ ...context.extracted_data
347
+ };
348
+ for (const [path, value] of Object.entries(overrides)) {
349
+ // Process template expressions in override values
350
+ let processedValue = value;
351
+ if (typeof value === 'string') {
352
+ processedValue = this.templateProcessor.process(value, contextData);
353
+ // Try to parse as JSON if it looks like a number, boolean, or object
354
+ if (processedValue === 'true')
355
+ processedValue = true;
356
+ else if (processedValue === 'false')
357
+ processedValue = false;
358
+ else if (!isNaN(Number(processedValue)) && processedValue !== '') {
359
+ processedValue = Number(processedValue);
360
+ }
361
+ }
362
+ this.setNestedValue(result, path, processedValue);
363
+ }
364
+ return result;
365
+ }
366
+ /**
367
+ * Set a value at a nested path using dot notation
368
+ * Example: setNestedValue(obj, 'user.profile.name', 'John')
369
+ */
370
+ setNestedValue(obj, path, value) {
371
+ const keys = path.split('.');
372
+ let current = obj;
373
+ for (let i = 0; i < keys.length - 1; i++) {
374
+ const key = keys[i];
375
+ // Handle array indices like 'items[0]'
376
+ const arrayMatch = key.match(/^(.+)\[(\d+)]$/);
377
+ if (arrayMatch) {
378
+ const [, prop, index] = arrayMatch;
379
+ if (!current[prop])
380
+ current[prop] = [];
381
+ if (!current[prop][parseInt(index)])
382
+ current[prop][parseInt(index)] = {};
383
+ current = current[prop][parseInt(index)];
384
+ }
385
+ else {
386
+ if (!current[key] || typeof current[key] !== 'object') {
387
+ current[key] = {};
388
+ }
389
+ current = current[key];
390
+ }
391
+ }
392
+ const lastKey = keys[keys.length - 1];
393
+ // Handle array index in last key
394
+ const arrayMatch = lastKey.match(/^(.+)\[(\d+)]$/);
395
+ if (arrayMatch) {
396
+ const [, prop, index] = arrayMatch;
397
+ if (!current[prop])
398
+ current[prop] = [];
399
+ current[prop][parseInt(index)] = value;
400
+ }
401
+ else {
402
+ current[lastKey] = value;
403
+ }
404
+ }
405
+ async executeSOAPStep(step, context) {
406
+ const handler = this.handlers.get('soap');
407
+ if (!handler) {
408
+ throw new Error('SOAP handler not available');
409
+ }
410
+ return handler.execute(step, context);
411
+ }
412
+ async executeWebStep(step, context) {
413
+ const handler = this.handlers.get('web');
414
+ if (!handler) {
415
+ throw new Error('Web handler not available');
416
+ }
417
+ return handler.execute(step.action, context);
418
+ }
419
+ async executeCustomStep(step, context) {
420
+ const script = step.script;
421
+ const timeout = step.timeout || 30000;
422
+ try {
423
+ const result = await this.executeScript(script, context, timeout);
424
+ return {
425
+ success: true,
426
+ data: result,
427
+ custom_metrics: { script_executed: true }
428
+ };
429
+ }
430
+ catch (error) {
431
+ return {
432
+ success: false,
433
+ error: error.message
434
+ };
435
+ }
436
+ }
437
+ async executeWaitStep(step) {
438
+ const duration = (0, time_1.parseTime)(step.duration);
439
+ await (0, time_1.sleep)(duration);
440
+ return {
441
+ success: true,
442
+ data: { waited: duration },
443
+ custom_metrics: { wait_duration: duration }
444
+ };
445
+ }
446
+ async executeScriptStep(step, context) {
447
+ const { file, function: funcName, params, returns, timeout = 30000 } = step;
448
+ const path = require('path');
449
+ const fs = require('fs');
450
+ try {
451
+ // Resolve file path relative to current working directory
452
+ const filePath = path.resolve(process.cwd(), file);
453
+ // Load the module (supports both .ts and .js)
454
+ let module;
455
+ try {
456
+ if (filePath.endsWith('.ts')) {
457
+ // Transpile TypeScript on the fly using esbuild
458
+ const esbuild = require('esbuild');
459
+ const source = fs.readFileSync(filePath, 'utf-8');
460
+ const result = esbuild.transformSync(source, {
461
+ loader: 'ts',
462
+ format: 'cjs',
463
+ target: 'node18'
464
+ });
465
+ // Create a temporary module from the transpiled code
466
+ const Module = require('module');
467
+ const tempModule = new Module(filePath);
468
+ tempModule.filename = filePath;
469
+ tempModule.paths = Module._nodeModulePaths(path.dirname(filePath));
470
+ tempModule._compile(result.code, filePath);
471
+ module = tempModule.exports;
472
+ }
473
+ else {
474
+ // For JS files, clear require cache and load directly
475
+ delete require.cache[require.resolve(filePath)];
476
+ module = require(filePath);
477
+ }
478
+ }
479
+ catch (loadError) {
480
+ throw new Error(`Failed to load script file '${file}': ${loadError.message}`);
481
+ }
482
+ // Get the function from the module
483
+ const fn = module[funcName] || module.default?.[funcName];
484
+ if (typeof fn !== 'function') {
485
+ throw new Error(`Function '${funcName}' not found in '${file}'`);
486
+ }
487
+ // Build parameters with context available
488
+ const execParams = {
489
+ ...params,
490
+ __context: context,
491
+ __variables: context.variables,
492
+ __extracted_data: context.extracted_data,
493
+ __vu_id: context.vu_id,
494
+ __iteration: context.iteration
495
+ };
496
+ // Execute the function with timeout
497
+ const timeoutPromise = new Promise((_, reject) => {
498
+ setTimeout(() => reject(new Error(`Script execution timeout (${timeout}ms)`)), timeout);
499
+ });
500
+ const resultPromise = Promise.resolve(fn(execParams));
501
+ const result = await Promise.race([resultPromise, timeoutPromise]);
502
+ // Store return value if specified
503
+ if (returns && result !== undefined) {
504
+ context.extracted_data[returns] = result;
505
+ }
506
+ return {
507
+ success: true,
508
+ data: result,
509
+ custom_metrics: {
510
+ script_file: file,
511
+ script_function: funcName,
512
+ has_return_value: result !== undefined
513
+ }
514
+ };
515
+ }
516
+ catch (error) {
517
+ return {
518
+ success: false,
519
+ error: error.message,
520
+ custom_metrics: {
521
+ script_file: file,
522
+ script_function: funcName
523
+ }
524
+ };
525
+ }
526
+ }
527
+ async executeScript(script, context, timeout) {
528
+ const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
529
+ const fn = new AsyncFunction('context', 'require', script);
530
+ const timeoutPromise = new Promise((_, reject) => {
531
+ setTimeout(() => reject(new Error('Script execution timeout')), timeout);
532
+ });
533
+ return Promise.race([
534
+ fn(context, require),
535
+ timeoutPromise
536
+ ]);
537
+ }
538
+ evaluateCondition(condition, context) {
539
+ try {
540
+ const fn = new Function('context', `return ${condition}`);
541
+ return !!fn(context);
542
+ }
543
+ catch (error) {
544
+ logger_1.logger.warn(`Condition evaluation failed: ${condition}`, error);
545
+ return false;
546
+ }
547
+ }
548
+ processTemplate(step, context) {
549
+ const contextData = {
550
+ __VU: context.vu_id,
551
+ __ITER: context.iteration,
552
+ vu_id: context.vu_id,
553
+ iteration: context.iteration,
554
+ variables: context.variables || {},
555
+ extracted_data: context.extracted_data || {},
556
+ ...context.variables,
557
+ ...context.extracted_data
558
+ };
559
+ logger_1.logger.debug(`StepExecutor processing template for VU${context.vu_id} Iter${context.iteration}`);
560
+ logger_1.logger.debug(`Context data: ${JSON.stringify(contextData)}`);
561
+ const stepStr = JSON.stringify(step);
562
+ logger_1.logger.debug(`Original step JSON: ${stepStr}`);
563
+ const processed = this.templateProcessor.process(stepStr, contextData);
564
+ logger_1.logger.debug(`Raw processed result: ${processed}`);
565
+ logger_1.logger.debug(`Processed result type: ${typeof processed}`);
566
+ let processedStep;
567
+ try {
568
+ if (typeof processed === 'string') {
569
+ processedStep = JSON.parse(processed);
570
+ }
571
+ else {
572
+ processedStep = processed;
573
+ }
574
+ }
575
+ catch (error) {
576
+ logger_1.logger.error(`JSON parsing failed in StepExecutor`);
577
+ logger_1.logger.error(`Processed content (first 500 chars): ${processed.substring(0, 500)}`);
578
+ logger_1.logger.error(`Error: ${error}`);
579
+ throw new Error(`Failed to parse processed step JSON: ${error}`);
580
+ }
581
+ logger_1.logger.debug(`Successfully parsed step: ${JSON.stringify(processedStep)}`);
582
+ return processedStep;
583
+ }
584
+ async runChecks(checks, result, context) {
585
+ const errors = [];
586
+ for (const check of checks) {
587
+ try {
588
+ let passed = false;
589
+ switch (check.type) {
590
+ case 'status':
591
+ passed = result.status === check.value;
592
+ break;
593
+ case 'response_time':
594
+ const threshold = typeof check.value === 'string'
595
+ ? (0, time_1.parseTime)(check.value.replace(/[<>]/g, ''))
596
+ : check.value;
597
+ passed = (result.duration || 0) < threshold;
598
+ break;
599
+ case 'json_path':
600
+ const value = this.getJsonPath(result.data, check.value);
601
+ passed = value !== undefined && value !== null;
602
+ break;
603
+ case 'text_contains':
604
+ const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
605
+ passed = text.includes(check.value);
606
+ break;
607
+ case 'custom':
608
+ passed = await this.checkCustom(check.script, result, context);
609
+ break;
610
+ }
611
+ if (!passed) {
612
+ errors.push(check.description || `Check failed: ${check.type}`);
613
+ }
614
+ }
615
+ catch (error) {
616
+ errors.push(`Check error: ${error}`);
617
+ }
618
+ }
619
+ return { passed: errors.length === 0, errors };
620
+ }
621
+ async checkCustom(script, result, context) {
622
+ try {
623
+ const fn = new Function('result', 'context', `return ${script}`);
624
+ return !!fn(result, context);
625
+ }
626
+ catch (error) {
627
+ return false;
628
+ }
629
+ }
630
+ async extractData(extractors, result, context) {
631
+ for (const extractor of extractors) {
632
+ try {
633
+ let value;
634
+ switch (extractor.type) {
635
+ case 'json_path':
636
+ value = this.getJsonPath(result.data, extractor.expression);
637
+ break;
638
+ case 'regex':
639
+ const match = String(result.data).match(new RegExp(extractor.expression));
640
+ value = match ? (match[1] || match[0]) : null;
641
+ break;
642
+ case 'custom':
643
+ value = await this.extractCustom(extractor.script, result, context);
644
+ break;
645
+ }
646
+ if (value !== null && value !== undefined) {
647
+ context.extracted_data[extractor.name] = value;
648
+ }
649
+ else if (extractor.default !== undefined) {
650
+ context.extracted_data[extractor.name] = extractor.default;
651
+ }
652
+ }
653
+ catch (error) {
654
+ if (extractor.default !== undefined) {
655
+ context.extracted_data[extractor.name] = extractor.default;
656
+ }
657
+ }
658
+ }
659
+ }
660
+ async extractCustom(script, result, context) {
661
+ try {
662
+ const fn = new Function('result', 'context', `return ${script}`);
663
+ return fn(result, context);
664
+ }
665
+ catch (error) {
666
+ return null;
667
+ }
668
+ }
669
+ getJsonPath(obj, path) {
670
+ const keys = path.replace(/^\$\./, '').split('.');
671
+ return keys.reduce((current, key) => {
672
+ if (key.includes('[') && key.includes(']')) {
673
+ const [prop, index] = key.split(/[\[\]]/);
674
+ return current && current[prop] && current[prop][parseInt(index)];
675
+ }
676
+ return current && current[key];
677
+ }, typeof obj === 'string' ? JSON.parse(obj) : obj);
678
+ }
679
+ }
680
+ exports.StepExecutor = StepExecutor;