@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
@@ -1,37 +1,4 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.StepExecutor = void 0;
37
4
  const hooks_manager_1 = require("./hooks-manager");
@@ -41,11 +8,16 @@ const rendezvous_1 = require("./rendezvous");
41
8
  const time_1 = require("../utils/time");
42
9
  const template_1 = require("../utils/template");
43
10
  const logger_1 = require("../utils/logger");
44
- const fs = __importStar(require("fs"));
45
- const path = __importStar(require("path"));
11
+ const check_evaluator_1 = require("./execution/check-evaluator");
12
+ const data_extractor_1 = require("./execution/data-extractor");
13
+ const json_payload_processor_1 = require("./execution/json-payload-processor");
46
14
  class StepExecutor {
47
15
  constructor(handlers, testName = 'Load Test') {
48
16
  this.templateProcessor = new template_1.TemplateProcessor();
17
+ // Extracted modules
18
+ this.checkEvaluator = new check_evaluator_1.CheckEvaluator();
19
+ this.dataExtractor = new data_extractor_1.DataExtractor();
20
+ this.jsonPayloadProcessor = new json_payload_processor_1.JSONPayloadProcessor();
49
21
  this.handlers = handlers;
50
22
  this.testName = testName;
51
23
  // Register this instance with ScriptExecutor for step execution in hooks
@@ -63,7 +35,6 @@ class StepExecutor {
63
35
  if (stepHooksManager) {
64
36
  try {
65
37
  const beforeStepResult = await stepHooksManager.executeBeforeStep(context.variables, context.extracted_data, context.csv_data);
66
- // Merge any variables returned by beforeStep hook
67
38
  if (beforeStepResult?.variables) {
68
39
  Object.assign(context.variables, beforeStepResult.variables);
69
40
  logger_1.logger.debug(`Hook VU${context.vu_id}: beforeStep hook set variables: ${Object.keys(beforeStepResult.variables).join(', ')}`);
@@ -75,11 +46,9 @@ class StepExecutor {
75
46
  }
76
47
  let testResult;
77
48
  try {
78
- // Execute the actual step
79
49
  testResult = await this.executeStepInternal(step, context, scenarioName, startTime);
80
50
  }
81
51
  catch (error) {
82
- // Execute onStepError hook
83
52
  if (stepHooksManager) {
84
53
  try {
85
54
  await stepHooksManager.executeOnStepError(error, context.variables, context.extracted_data, context.csv_data);
@@ -88,31 +57,12 @@ class StepExecutor {
88
57
  logger_1.logger.error(`VU ${context.vu_id} onStepError hook failed:`, hookError);
89
58
  }
90
59
  }
91
- // Create error result
92
- const stepName = step.name || `${step.type}_${context.iteration}`;
93
- const iteration = context.iteration || 1;
94
- const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
95
- testResult = {
96
- id: `${context.vu_id}-${Date.now()}`,
97
- vu_id: context.vu_id,
98
- iteration: context.iteration,
99
- scenario: scenarioName,
100
- action: step.name || step.type || 'rest',
101
- step_name: stepName,
102
- thread_name: threadName,
103
- timestamp: startTime,
104
- duration: Date.now() - startTime,
105
- success: false,
106
- error: error.message,
107
- shouldRecord: true
108
- };
60
+ testResult = this.createErrorResult(step, context, scenarioName, startTime, error);
109
61
  }
110
62
  finally {
111
- // Execute teardownStep hook (only if testResult was created)
112
63
  if (stepHooksManager && testResult) {
113
64
  try {
114
65
  const teardownResult = await stepHooksManager.executeTeardownStep(context.variables, context.extracted_data, context.csv_data, testResult);
115
- // Merge any variables returned by teardownStep hook
116
66
  if (teardownResult?.variables) {
117
67
  Object.assign(context.variables, teardownResult.variables);
118
68
  }
@@ -122,40 +72,20 @@ class StepExecutor {
122
72
  }
123
73
  }
124
74
  }
125
- // This should never happen, but TypeScript needs the guarantee
126
75
  if (!testResult) {
127
- const stepName = step.name || `${step.type}_${context.iteration}`;
128
- const iteration = context.iteration || 1;
129
- const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
130
- testResult = {
131
- id: `${context.vu_id}-${Date.now()}`,
132
- vu_id: context.vu_id,
133
- iteration: context.iteration,
134
- scenario: scenarioName,
135
- action: step.name || step.type || 'rest',
136
- step_name: stepName,
137
- thread_name: threadName,
138
- timestamp: startTime,
139
- duration: Date.now() - startTime,
140
- success: false,
141
- error: 'Unknown error occurred',
142
- shouldRecord: true
143
- };
76
+ testResult = this.createErrorResult(step, context, scenarioName, startTime, new Error('Unknown error occurred'));
144
77
  }
145
- // Evaluate thresholds if they are defined for this step
78
+ // Evaluate thresholds
146
79
  if (step.thresholds && step.thresholds.length > 0) {
147
80
  try {
148
81
  const evaluationResult = threshold_evaluator_1.ThresholdEvaluator.evaluate(step.thresholds, testResult, stepName);
149
82
  if (!evaluationResult.passed) {
150
- // Add threshold failures to test result
151
83
  testResult.threshold_failures = evaluationResult.failures;
152
- // Execute threshold actions (may throw errors for fail actions)
153
84
  await threshold_evaluator_1.ThresholdEvaluator.executeThresholdActions(evaluationResult, stepName);
154
85
  }
155
86
  }
156
87
  catch (error) {
157
88
  logger_1.logger.error(`Threshold evaluation failed for step ${stepName}:`, error);
158
- // If threshold action is to fail, we re-throw the error
159
89
  if (error instanceof Error && error.message.includes('threshold violation')) {
160
90
  throw error;
161
91
  }
@@ -163,27 +93,30 @@ class StepExecutor {
163
93
  }
164
94
  return testResult;
165
95
  }
166
- // Make this public so hooks can execute steps
96
+ createErrorResult(step, context, scenarioName, startTime, error) {
97
+ const stepName = step.name || `${step.type}_${context.iteration}`;
98
+ const iteration = context.iteration || 1;
99
+ const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
100
+ return {
101
+ id: `${context.vu_id}-${Date.now()}`,
102
+ vu_id: context.vu_id,
103
+ iteration: context.iteration,
104
+ scenario: scenarioName,
105
+ action: step.name || step.type || 'rest',
106
+ step_name: stepName,
107
+ thread_name: threadName,
108
+ timestamp: startTime,
109
+ duration: Date.now() - startTime,
110
+ success: false,
111
+ error: error.message,
112
+ shouldRecord: true
113
+ };
114
+ }
167
115
  async executeStepInternal(step, context, scenarioName, startTime) {
168
116
  const processedStep = this.processTemplate(step, context);
169
117
  // Check condition if specified
170
118
  if (step.condition && !this.evaluateCondition(step.condition, context)) {
171
- const stepName = step.name || `${step.type}_${context.iteration}`;
172
- const iteration = context.iteration || 1;
173
- const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
174
- return {
175
- id: `${context.vu_id}-${Date.now()}`,
176
- vu_id: context.vu_id,
177
- iteration: context.iteration,
178
- scenario: scenarioName,
179
- action: step.name || step.type || 'rest',
180
- step_name: stepName,
181
- thread_name: threadName,
182
- timestamp: startTime,
183
- duration: 0,
184
- success: true,
185
- custom_metrics: { skipped: true }
186
- };
119
+ return this.createSkippedResult(step, context, scenarioName, startTime);
187
120
  }
188
121
  let result;
189
122
  const stepType = step.type || 'rest';
@@ -213,15 +146,46 @@ class StepExecutor {
213
146
  default:
214
147
  throw new Error(`Unsupported step type: ${step.type}`);
215
148
  }
216
- // Use response_time from handler if available (e.g., web actions with action_time)
217
- // Otherwise fall back to total elapsed time
149
+ const testResult = this.buildTestResult(step, context, scenarioName, startTime, result);
150
+ // Run checks if configured
151
+ if ('checks' in step && step.checks) {
152
+ const checkResults = await this.checkEvaluator.runChecks(step.checks, result, context);
153
+ if (!checkResults.passed) {
154
+ testResult.success = false;
155
+ testResult.error = checkResults.errors.join('; ');
156
+ }
157
+ }
158
+ // Extract data if configured
159
+ if ('extract' in step && step.extract) {
160
+ await this.dataExtractor.extractData(step.extract, result, context);
161
+ }
162
+ return testResult;
163
+ }
164
+ createSkippedResult(step, context, scenarioName, startTime) {
165
+ const stepName = step.name || `${step.type}_${context.iteration}`;
166
+ const iteration = context.iteration || 1;
167
+ const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
168
+ return {
169
+ id: `${context.vu_id}-${Date.now()}`,
170
+ vu_id: context.vu_id,
171
+ iteration: context.iteration,
172
+ scenario: scenarioName,
173
+ action: step.name || step.type || 'rest',
174
+ step_name: stepName,
175
+ thread_name: threadName,
176
+ timestamp: startTime,
177
+ duration: 0,
178
+ success: true,
179
+ custom_metrics: { skipped: true }
180
+ };
181
+ }
182
+ buildTestResult(step, context, scenarioName, startTime, result) {
218
183
  const totalElapsed = Date.now() - startTime;
219
184
  const duration = result.response_time !== undefined ? result.response_time : totalElapsed;
220
185
  const stepName = step.name || `${step.type}_${context.iteration}`;
221
- // Generate JMeter-style thread name: "iteration. step_name vu_id-iteration"
222
186
  const iteration = context.iteration || 1;
223
187
  const threadName = `${iteration}. ${stepName} ${context.vu_id}-${iteration}`;
224
- const testResult = {
188
+ return {
225
189
  id: `${context.vu_id}-${Date.now()}`,
226
190
  vu_id: context.vu_id,
227
191
  iteration: context.iteration,
@@ -231,7 +195,7 @@ class StepExecutor {
231
195
  thread_name: threadName,
232
196
  timestamp: startTime,
233
197
  duration,
234
- response_time: duration, // Add explicit response_time for reporting
198
+ response_time: duration,
235
199
  success: result.success,
236
200
  status: result.status,
237
201
  status_text: result.status_text,
@@ -246,11 +210,9 @@ class StepExecutor {
246
210
  response_body: result.response_body,
247
211
  custom_metrics: result.custom_metrics,
248
212
  shouldRecord: result.shouldRecord !== undefined ? result.shouldRecord : this.shouldRecordStep(step, true),
249
- // JMeter-style timing breakdown
250
213
  sample_start: result.sample_start,
251
214
  connect_time: result.connect_time,
252
215
  latency: result.latency,
253
- // JMeter-style size breakdown
254
216
  sent_bytes: result.sent_bytes,
255
217
  headers_size_sent: result.headers_size_sent,
256
218
  body_size_sent: result.body_size_sent,
@@ -258,29 +220,10 @@ class StepExecutor {
258
220
  body_size_received: result.body_size_received,
259
221
  data_type: result.data_type,
260
222
  };
261
- // Run checks if configured
262
- if ('checks' in step && step.checks) {
263
- const checkResults = await this.runChecks(step.checks, result, context);
264
- if (!checkResults.passed) {
265
- testResult.success = false;
266
- testResult.error = `Check failed: ${checkResults.errors.join(', ')}`;
267
- }
268
- }
269
- // Extract data if configured
270
- if ('extract' in step && step.extract) {
271
- await this.extractData(step.extract, result, context);
272
- }
273
- return testResult;
274
223
  }
275
224
  shouldRecordStep(step, success) {
276
- // Always record errors
277
225
  if (!success)
278
226
  return true;
279
- // For web steps, only record meaningful performance measurements:
280
- // - Verifications (verify_*) - time for elements/text to appear (measures app responsiveness)
281
- // - Waits (wait_for_*) - time for conditions to be met
282
- // - Performance measurements (measure_*, performance_audit)
283
- // NOT recorded: goto, click, fill, press, select, hover, screenshot (navigation/interactions)
284
227
  if (step.type === 'web' && step.action) {
285
228
  const measurableCommands = [
286
229
  'verify_exists', 'verify_visible', 'verify_text', 'verify_contains', 'verify_not_exists',
@@ -296,117 +239,9 @@ class StepExecutor {
296
239
  if (!handler) {
297
240
  throw new Error('REST handler not available');
298
241
  }
299
- // Handle jsonFile loading with optional overrides
300
- const processedStep = this.processJsonFile(step, context);
242
+ const processedStep = this.jsonPayloadProcessor.processJsonFile(step, context);
301
243
  return handler.execute(processedStep, context);
302
244
  }
303
- /**
304
- * Load JSON payload from file and apply overrides
305
- * Supports dot notation for nested paths in overrides
306
- */
307
- processJsonFile(step, context) {
308
- if (!step.jsonFile) {
309
- return step;
310
- }
311
- // Resolve file path relative to CWD
312
- const filePath = path.resolve(process.cwd(), step.jsonFile);
313
- if (!fs.existsSync(filePath)) {
314
- throw new Error(`JSON payload file not found: ${step.jsonFile}`);
315
- }
316
- // Load and parse JSON file
317
- let payload;
318
- try {
319
- const fileContent = fs.readFileSync(filePath, 'utf-8');
320
- payload = JSON.parse(fileContent);
321
- }
322
- catch (error) {
323
- throw new Error(`Failed to parse JSON file ${step.jsonFile}: ${error.message}`);
324
- }
325
- // Apply overrides if specified
326
- if (step.overrides) {
327
- payload = this.applyOverrides(payload, step.overrides, context);
328
- }
329
- // Return new step with json property set (removing jsonFile and overrides)
330
- const { jsonFile, overrides, ...restOfStep } = step;
331
- return {
332
- ...restOfStep,
333
- json: payload
334
- };
335
- }
336
- /**
337
- * Apply overrides to a JSON object using dot notation for nested paths
338
- * Override values are processed through the template processor
339
- */
340
- applyOverrides(obj, overrides, context) {
341
- // Deep clone the object to avoid mutating the original
342
- const result = JSON.parse(JSON.stringify(obj));
343
- const contextData = {
344
- __VU: context.vu_id,
345
- __ITER: context.iteration,
346
- vu_id: context.vu_id,
347
- iteration: context.iteration,
348
- variables: context.variables || {},
349
- extracted_data: context.extracted_data || {},
350
- ...context.variables,
351
- ...context.extracted_data
352
- };
353
- for (const [path, value] of Object.entries(overrides)) {
354
- // Process template expressions in override values
355
- let processedValue = value;
356
- if (typeof value === 'string') {
357
- processedValue = this.templateProcessor.process(value, contextData);
358
- // Try to parse as JSON if it looks like a number, boolean, or object
359
- if (processedValue === 'true')
360
- processedValue = true;
361
- else if (processedValue === 'false')
362
- processedValue = false;
363
- else if (!isNaN(Number(processedValue)) && processedValue !== '') {
364
- processedValue = Number(processedValue);
365
- }
366
- }
367
- this.setNestedValue(result, path, processedValue);
368
- }
369
- return result;
370
- }
371
- /**
372
- * Set a value at a nested path using dot notation
373
- * Example: setNestedValue(obj, 'user.profile.name', 'John')
374
- */
375
- setNestedValue(obj, path, value) {
376
- const keys = path.split('.');
377
- let current = obj;
378
- for (let i = 0; i < keys.length - 1; i++) {
379
- const key = keys[i];
380
- // Handle array indices like 'items[0]'
381
- const arrayMatch = key.match(/^(.+)\[(\d+)]$/);
382
- if (arrayMatch) {
383
- const [, prop, index] = arrayMatch;
384
- if (!current[prop])
385
- current[prop] = [];
386
- if (!current[prop][parseInt(index)])
387
- current[prop][parseInt(index)] = {};
388
- current = current[prop][parseInt(index)];
389
- }
390
- else {
391
- if (!current[key] || typeof current[key] !== 'object') {
392
- current[key] = {};
393
- }
394
- current = current[key];
395
- }
396
- }
397
- const lastKey = keys[keys.length - 1];
398
- // Handle array index in last key
399
- const arrayMatch = lastKey.match(/^(.+)\[(\d+)]$/);
400
- if (arrayMatch) {
401
- const [, prop, index] = arrayMatch;
402
- if (!current[prop])
403
- current[prop] = [];
404
- current[prop][parseInt(index)] = value;
405
- }
406
- else {
407
- current[lastKey] = value;
408
- }
409
- }
410
245
  async executeSOAPStep(step, context) {
411
246
  const handler = this.handlers.get('soap');
412
247
  if (!handler) {
@@ -450,8 +285,7 @@ class StepExecutor {
450
285
  }
451
286
  async executeRendezvousStep(step, context) {
452
287
  const rendezvousManager = rendezvous_1.RendezvousManager.getInstance();
453
- // Parse timeout - can be number (ms) or string ("30s", "1m")
454
- let timeoutMs = 30000; // Default 30 seconds
288
+ let timeoutMs = 30000;
455
289
  if (step.timeout !== undefined) {
456
290
  if (typeof step.timeout === 'number') {
457
291
  timeoutMs = step.timeout;
@@ -482,7 +316,7 @@ class StepExecutor {
482
316
  rendezvous_reason: result.reason,
483
317
  rendezvous_vu_count: result.vuCount
484
318
  },
485
- shouldRecord: true // Always record rendezvous timing
319
+ shouldRecord: true
486
320
  };
487
321
  }
488
322
  catch (error) {
@@ -497,18 +331,15 @@ class StepExecutor {
497
331
  }
498
332
  }
499
333
  async executeScriptStep(step, context) {
500
- logger_1.logger.info(`📜 Executing script step: file=${step.file}, function=${step.function}`);
334
+ logger_1.logger.info(`Executing script step: file=${step.file}, function=${step.function}`);
501
335
  const { file, function: funcName, params, returns, timeout = 30000 } = step;
502
336
  const path = require('path');
503
337
  const fs = require('fs');
504
338
  try {
505
- // Resolve file path relative to current working directory
506
339
  const filePath = path.resolve(process.cwd(), file);
507
- // Load the module (supports both .ts and .js)
508
340
  let module;
509
341
  try {
510
342
  if (filePath.endsWith('.ts')) {
511
- // Transpile TypeScript on the fly using esbuild
512
343
  const esbuild = require('esbuild');
513
344
  const source = fs.readFileSync(filePath, 'utf-8');
514
345
  const result = esbuild.transformSync(source, {
@@ -516,11 +347,9 @@ class StepExecutor {
516
347
  format: 'cjs',
517
348
  target: 'node18'
518
349
  });
519
- // Create a temporary module from the transpiled code
520
350
  const Module = require('module');
521
351
  const tempModule = new Module(filePath);
522
352
  tempModule.filename = filePath;
523
- // Include both script dir and cwd node_modules for dependency resolution
524
353
  const scriptPaths = Module._nodeModulePaths(path.dirname(filePath));
525
354
  const cwdPaths = Module._nodeModulePaths(process.cwd());
526
355
  tempModule.paths = [...new Set([...scriptPaths, ...cwdPaths])];
@@ -528,7 +357,6 @@ class StepExecutor {
528
357
  module = tempModule.exports;
529
358
  }
530
359
  else {
531
- // For JS files, clear require cache and load directly
532
360
  delete require.cache[require.resolve(filePath)];
533
361
  module = require(filePath);
534
362
  }
@@ -536,12 +364,10 @@ class StepExecutor {
536
364
  catch (loadError) {
537
365
  throw new Error(`Failed to load script file '${file}': ${loadError.message}`);
538
366
  }
539
- // Get the function from the module
540
367
  const fn = module[funcName] || module.default?.[funcName];
541
368
  if (typeof fn !== 'function') {
542
369
  throw new Error(`Function '${funcName}' not found in '${file}'`);
543
370
  }
544
- // Build parameters with context available
545
371
  const execParams = {
546
372
  ...params,
547
373
  __context: context,
@@ -550,14 +376,12 @@ class StepExecutor {
550
376
  __vu_id: context.vu_id,
551
377
  __iteration: context.iteration
552
378
  };
553
- // Execute the function with timeout
554
379
  const timeoutPromise = new Promise((_, reject) => {
555
380
  setTimeout(() => reject(new Error(`Script execution timeout (${timeout}ms)`)), timeout);
556
381
  });
557
382
  const resultPromise = Promise.resolve(fn(execParams));
558
383
  const result = await Promise.race([resultPromise, timeoutPromise]);
559
384
  logger_1.logger.debug(`Script ${funcName} returned: ${JSON.stringify(result)}`);
560
- // Store return value if specified
561
385
  if (returns && result !== undefined) {
562
386
  context.extracted_data[returns] = result;
563
387
  }
@@ -616,138 +440,20 @@ class StepExecutor {
616
440
  };
617
441
  logger_1.logger.debug(`StepExecutor processing template for VU${context.vu_id} Iter${context.iteration}`);
618
442
  logger_1.logger.debug(`Extracted data keys: ${Object.keys(context.extracted_data || {}).join(', ') || '(none)'}`);
619
- logger_1.logger.debug(`Context data keys at top level: ${Object.keys(contextData).join(', ')}`);
620
443
  const stepStr = JSON.stringify(step);
621
- logger_1.logger.debug(`Original step JSON: ${stepStr}`);
622
444
  const processed = this.templateProcessor.process(stepStr, contextData);
623
- logger_1.logger.debug(`Raw processed result: ${processed}`);
624
- logger_1.logger.debug(`Processed result type: ${typeof processed}`);
625
- let processedStep;
626
445
  try {
627
446
  if (typeof processed === 'string') {
628
- processedStep = JSON.parse(processed);
447
+ return JSON.parse(processed);
629
448
  }
630
449
  else {
631
- processedStep = processed;
450
+ return processed;
632
451
  }
633
452
  }
634
453
  catch (error) {
635
454
  logger_1.logger.error(`JSON parsing failed in StepExecutor`);
636
- logger_1.logger.error(`Processed content (first 500 chars): ${processed.substring(0, 500)}`);
637
- logger_1.logger.error(`Error: ${error}`);
638
455
  throw new Error(`Failed to parse processed step JSON: ${error}`);
639
456
  }
640
- logger_1.logger.debug(`Successfully parsed step: ${JSON.stringify(processedStep)}`);
641
- return processedStep;
642
- }
643
- async runChecks(checks, result, context) {
644
- const errors = [];
645
- for (const check of checks) {
646
- try {
647
- let passed = false;
648
- switch (check.type) {
649
- case 'status':
650
- passed = result.status === check.value;
651
- break;
652
- case 'response_time':
653
- const threshold = typeof check.value === 'string'
654
- ? (0, time_1.parseTime)(check.value.replace(/[<>]/g, ''))
655
- : check.value;
656
- passed = (result.duration || 0) < threshold;
657
- break;
658
- case 'json_path':
659
- const value = this.getJsonPath(result.data, check.value);
660
- passed = value !== undefined && value !== null;
661
- break;
662
- case 'text_contains':
663
- const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
664
- passed = text.includes(check.value);
665
- break;
666
- case 'custom':
667
- passed = await this.checkCustom(check.script, result, context);
668
- break;
669
- }
670
- if (!passed) {
671
- errors.push(check.description || `Check failed: ${check.type}`);
672
- }
673
- }
674
- catch (error) {
675
- errors.push(`Check error: ${error}`);
676
- }
677
- }
678
- return { passed: errors.length === 0, errors };
679
- }
680
- async checkCustom(script, result, context) {
681
- try {
682
- const fn = new Function('result', 'context', `return ${script}`);
683
- return !!fn(result, context);
684
- }
685
- catch (error) {
686
- return false;
687
- }
688
- }
689
- async extractData(extractors, result, context) {
690
- for (const extractor of extractors) {
691
- try {
692
- let value;
693
- // Normalize type: accept both "jsonpath" and "json_path"
694
- const extractType = (extractor.type || 'jsonpath').toLowerCase().replace('_', '');
695
- // Normalize expression: accept both "path" and "expression"
696
- const expression = extractor.expression || extractor.path;
697
- switch (extractType) {
698
- case 'jsonpath':
699
- value = this.getJsonPath(result.data, expression);
700
- break;
701
- case 'regex':
702
- const match = String(result.data).match(new RegExp(expression));
703
- value = match ? (match[1] || match[0]) : null;
704
- break;
705
- case 'header':
706
- value = result.headers?.[expression.toLowerCase()];
707
- break;
708
- case 'custom':
709
- value = await this.extractCustom(extractor.script, result, context);
710
- break;
711
- default:
712
- // Default to jsonpath if type not recognized but path/expression provided
713
- if (expression) {
714
- value = this.getJsonPath(result.data, expression);
715
- }
716
- }
717
- if (value !== null && value !== undefined) {
718
- context.extracted_data[extractor.name] = value;
719
- logger_1.logger.debug(`Extracted ${extractor.name} = ${JSON.stringify(value)}`);
720
- }
721
- else if (extractor.default !== undefined) {
722
- context.extracted_data[extractor.name] = extractor.default;
723
- }
724
- }
725
- catch (error) {
726
- logger_1.logger.debug(`Extraction failed for ${extractor.name}: ${error}`);
727
- if (extractor.default !== undefined) {
728
- context.extracted_data[extractor.name] = extractor.default;
729
- }
730
- }
731
- }
732
- }
733
- async extractCustom(script, result, context) {
734
- try {
735
- const fn = new Function('result', 'context', `return ${script}`);
736
- return fn(result, context);
737
- }
738
- catch (error) {
739
- return null;
740
- }
741
- }
742
- getJsonPath(obj, path) {
743
- const keys = path.replace(/^\$\./, '').split('.');
744
- return keys.reduce((current, key) => {
745
- if (key.includes('[') && key.includes(']')) {
746
- const [prop, index] = key.split(/[\[\]]/);
747
- return current && current[prop] && current[prop][parseInt(index)];
748
- }
749
- return current && current[key];
750
- }, typeof obj === 'string' ? JSON.parse(obj) : obj);
751
457
  }
752
458
  }
753
459
  exports.StepExecutor = StepExecutor;
@@ -0,0 +1,2 @@
1
+ export { ThinkTimeStrategy } from './think-time-strategy';
2
+ export { ScenarioSelector } from './scenario-selector';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScenarioSelector = exports.ThinkTimeStrategy = void 0;
4
+ var think_time_strategy_1 = require("./think-time-strategy");
5
+ Object.defineProperty(exports, "ThinkTimeStrategy", { enumerable: true, get: function () { return think_time_strategy_1.ThinkTimeStrategy; } });
6
+ var scenario_selector_1 = require("./scenario-selector");
7
+ Object.defineProperty(exports, "ScenarioSelector", { enumerable: true, get: function () { return scenario_selector_1.ScenarioSelector; } });
@@ -0,0 +1,13 @@
1
+ import { Scenario } from '../../config/types/hooks';
2
+ export declare class ScenarioSelector {
3
+ /**
4
+ * Select scenarios based on weights using proportional distribution.
5
+ *
6
+ * Weights determine the probability of a scenario being selected:
7
+ * - scenario1 (weight: 50) + scenario2 (weight: 25) + scenario3 (weight: 25) = 100
8
+ * - 50% of VUs will run scenario1, 25% scenario2, 25% scenario3
9
+ *
10
+ * If weights don't sum to 100, they are normalized proportionally.
11
+ */
12
+ selectScenarios(scenarios: Scenario[]): Scenario[];
13
+ }