@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.
- package/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- 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;
|