@artilleryio/int-core 2.27.0 → 2.28.0-2c19512
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/.turbo/turbo-build.log +4 -0
- package/dist/index.js +13 -0
- package/dist/lib/engine_http.js +817 -0
- package/dist/lib/engine_socketio.js +347 -0
- package/dist/lib/engine_ws.js +278 -0
- package/dist/lib/is-idle-phase.js +9 -0
- package/dist/lib/phases.js +213 -0
- package/dist/lib/readers.js +58 -0
- package/dist/lib/runner.js +463 -0
- package/dist/lib/ssms.js +555 -0
- package/dist/lib/update-global-object.js +75 -0
- package/dist/lib/weighted-pick.js +54 -0
- package/index.ts +14 -0
- package/lib/{engine_http.js → engine_http.ts} +28 -28
- package/lib/{engine_socketio.js → engine_socketio.ts} +13 -11
- package/lib/{engine_ws.js → engine_ws.ts} +16 -10
- package/lib/{is-idle-phase.js → is-idle-phase.ts} +1 -3
- package/lib/{phases.js → phases.ts} +12 -10
- package/lib/{readers.js → readers.ts} +2 -4
- package/lib/{runner.js → runner.ts} +84 -69
- package/lib/{ssms.js → ssms.ts} +18 -18
- package/{index.js → lib/update-global-object.ts} +9 -14
- package/lib/{weighted-pick.js → weighted-pick.ts} +7 -3
- package/package.json +10 -17
- package/tsconfig.build.json +9 -0
- package/types.d.ts +12 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
4
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
5
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
6
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
7
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return path;
|
|
11
|
+
};
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { engine_util as engineUtil } from '@artilleryio/int-commons';
|
|
16
|
+
import createDebug from 'debug';
|
|
17
|
+
import { EventEmitter } from 'eventemitter3';
|
|
18
|
+
import _ from 'lodash';
|
|
19
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
20
|
+
import HttpEngine from "./engine_http.js";
|
|
21
|
+
import SocketIoEngine from "./engine_socketio.js";
|
|
22
|
+
import WSEngine from "./engine_ws.js";
|
|
23
|
+
import createPhaser from "./phases.js";
|
|
24
|
+
import createReader from "./readers.js";
|
|
25
|
+
import { SSMS } from "./ssms.js";
|
|
26
|
+
import wl from "./weighted-pick.js";
|
|
27
|
+
const debug = createDebug('runner');
|
|
28
|
+
const debugPerf = createDebug('perf');
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const Engines = {
|
|
31
|
+
http: HttpEngine,
|
|
32
|
+
ws: WSEngine,
|
|
33
|
+
socketio: SocketIoEngine
|
|
34
|
+
};
|
|
35
|
+
const contextFuncs = {
|
|
36
|
+
$randomString,
|
|
37
|
+
$randomNumber
|
|
38
|
+
};
|
|
39
|
+
const runnerFuncs = {
|
|
40
|
+
handleScriptHook,
|
|
41
|
+
prepareScript,
|
|
42
|
+
loadProcessor
|
|
43
|
+
};
|
|
44
|
+
export { runner, contextFuncs, runnerFuncs };
|
|
45
|
+
async function loadEngines(script, ee, warnings = {
|
|
46
|
+
engines: {}
|
|
47
|
+
}) {
|
|
48
|
+
const engineSpecs = Object.assign({}, Engines, script.config.engines);
|
|
49
|
+
const loadedEngines = [];
|
|
50
|
+
for (const engineName of Object.keys(engineSpecs)) {
|
|
51
|
+
const moduleName = `artillery-engine-${engineName}`;
|
|
52
|
+
try {
|
|
53
|
+
let Engine;
|
|
54
|
+
if (typeof Engines[engineName] !== 'undefined') {
|
|
55
|
+
Engine = Engines[engineName];
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Resolve with CJS semantics (bare specifiers, directories via
|
|
59
|
+
// package.json "main"), load with import() - handles both CJS
|
|
60
|
+
// and ESM engines, including ESM with top-level await
|
|
61
|
+
const enginePath = require.resolve(moduleName);
|
|
62
|
+
const ns = await import(__rewriteRelativeImportExtension(pathToFileURL(enginePath).href));
|
|
63
|
+
Engine = ns.default ?? ns;
|
|
64
|
+
}
|
|
65
|
+
const engine = new Engine(script, ee, engineUtil);
|
|
66
|
+
engine.__name = engineName;
|
|
67
|
+
loadedEngines.push(engine);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.log('WARNING: engine %s specified but module %s could not be loaded', engineName, moduleName);
|
|
71
|
+
console.log(err.stack);
|
|
72
|
+
warnings.engines[engineName] = {
|
|
73
|
+
message: 'Could not load',
|
|
74
|
+
error: err
|
|
75
|
+
};
|
|
76
|
+
loadedEngines.push(undefined);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { loadedEngines, warnings };
|
|
80
|
+
}
|
|
81
|
+
async function loadProcessor(script, options) {
|
|
82
|
+
const absoluteScriptPath = path.resolve(process.cwd(), options.scriptPath);
|
|
83
|
+
if (script.config.processor) {
|
|
84
|
+
const processorPath = path.resolve(path.dirname(absoluteScriptPath), script.config.processor);
|
|
85
|
+
// Resolve with CJS semantics first (handles extensionless paths and
|
|
86
|
+
// directories); fall back to the path as-is
|
|
87
|
+
let resolvedPath = processorPath;
|
|
88
|
+
try {
|
|
89
|
+
resolvedPath = require.resolve(processorPath);
|
|
90
|
+
}
|
|
91
|
+
catch (_err) { }
|
|
92
|
+
// import() loads both CJS and ESM (including ESM with top-level
|
|
93
|
+
// await). Normalize the result into a plain mutable object: module
|
|
94
|
+
// namespace objects are frozen, and engines/plugins may attach
|
|
95
|
+
// properties to the processor object later (e.g. $rewriteMetricName)
|
|
96
|
+
const ns = await import(__rewriteRelativeImportExtension(pathToFileURL(resolvedPath).href));
|
|
97
|
+
script.config.processor = Object.assign({}, ns.default ?? {}, ns);
|
|
98
|
+
}
|
|
99
|
+
return script;
|
|
100
|
+
}
|
|
101
|
+
function prepareScript(script, payload) {
|
|
102
|
+
const runnableScript = _.cloneDeep(script);
|
|
103
|
+
_.each(runnableScript.config.phases, (phaseSpec) => {
|
|
104
|
+
phaseSpec.mode = phaseSpec.mode || runnableScript.config.mode;
|
|
105
|
+
});
|
|
106
|
+
if (payload) {
|
|
107
|
+
if (_.isArray(payload[0])) {
|
|
108
|
+
runnableScript.config.payload = [
|
|
109
|
+
{
|
|
110
|
+
fields: runnableScript.config.payload.fields,
|
|
111
|
+
reader: createReader(runnableScript.config.payload.order, runnableScript.config.payload),
|
|
112
|
+
data: payload
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
runnableScript.config.payload = payload;
|
|
118
|
+
_.each(runnableScript.config.payload, (el) => {
|
|
119
|
+
el.reader = createReader(el.order, el);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
runnableScript.config.payload = null;
|
|
125
|
+
}
|
|
126
|
+
// Flatten flows (can have nested arrays of request specs with YAML references):
|
|
127
|
+
_.each(runnableScript.scenarios, (scenarioSpec) => {
|
|
128
|
+
scenarioSpec.flow = _.flatten(scenarioSpec.flow);
|
|
129
|
+
});
|
|
130
|
+
return runnableScript;
|
|
131
|
+
}
|
|
132
|
+
async function runner(script, payload, options, callback) {
|
|
133
|
+
const opts = _.assign({
|
|
134
|
+
periodicStats: script.config.statsInterval || 30,
|
|
135
|
+
mode: script.config.mode || 'uniform'
|
|
136
|
+
}, options);
|
|
137
|
+
const metrics = new SSMS();
|
|
138
|
+
const warnings = {
|
|
139
|
+
engines: {}
|
|
140
|
+
};
|
|
141
|
+
const runnableScript = prepareScript(script, payload);
|
|
142
|
+
const ee = new EventEmitter();
|
|
143
|
+
//
|
|
144
|
+
// load engines:
|
|
145
|
+
//
|
|
146
|
+
const { loadedEngines: runnerEngines } = await loadEngines(runnableScript, ee, warnings);
|
|
147
|
+
for (const e of runnerEngines) {
|
|
148
|
+
if (e &&
|
|
149
|
+
typeof e.init === 'function' &&
|
|
150
|
+
e.init.constructor.name === 'AsyncFunction') {
|
|
151
|
+
await e.init();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const promise = new Promise((resolve, _reject) => {
|
|
155
|
+
ee.run = (contextVars) => {
|
|
156
|
+
const runState = {
|
|
157
|
+
pendingScenarios: 0,
|
|
158
|
+
// pendingRequests: 0,
|
|
159
|
+
compiledScenarios: null,
|
|
160
|
+
scenarioEvents: null,
|
|
161
|
+
picker: undefined,
|
|
162
|
+
engines: runnerEngines,
|
|
163
|
+
metrics: metrics
|
|
164
|
+
};
|
|
165
|
+
debug('run() with: %j', runnableScript);
|
|
166
|
+
run(runnableScript, ee, opts, runState, contextVars);
|
|
167
|
+
};
|
|
168
|
+
ee.stop = async (_done) => {
|
|
169
|
+
metrics.stop();
|
|
170
|
+
};
|
|
171
|
+
// FIXME: Warnings should be returned from this function instead along with
|
|
172
|
+
// the event emitter. That will be a breaking change.
|
|
173
|
+
ee.warnings = warnings;
|
|
174
|
+
resolve(ee);
|
|
175
|
+
});
|
|
176
|
+
if (callback && typeof callback === 'function') {
|
|
177
|
+
promise.then(callback.bind(null, null), callback);
|
|
178
|
+
}
|
|
179
|
+
return promise;
|
|
180
|
+
}
|
|
181
|
+
function run(script, ee, options, runState, contextVars) {
|
|
182
|
+
const metrics = runState.metrics;
|
|
183
|
+
const intermediates = [];
|
|
184
|
+
const phaser = createPhaser(script.config.phases);
|
|
185
|
+
let _scenarioContext;
|
|
186
|
+
phaser.on('arrival', (spec) => {
|
|
187
|
+
if (runState.pendingScenarios >= spec.maxVusers) {
|
|
188
|
+
metrics.counter('vusers.skipped', 1);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
_scenarioContext = runScenario(script, metrics, runState, contextVars, options);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
phaser.on('phaseStarted', (spec) => {
|
|
195
|
+
ee.emit('phaseStarted', spec);
|
|
196
|
+
});
|
|
197
|
+
phaser.on('phaseCompleted', (spec) => {
|
|
198
|
+
ee.emit('phaseCompleted', spec);
|
|
199
|
+
});
|
|
200
|
+
phaser.on('done', () => {
|
|
201
|
+
debug('All phases launched');
|
|
202
|
+
const doneYet = setInterval(function checkIfDone() {
|
|
203
|
+
if (runState.pendingScenarios === 0) {
|
|
204
|
+
clearInterval(doneYet);
|
|
205
|
+
metrics.aggregate(true);
|
|
206
|
+
const totals = SSMS.pack(intermediates);
|
|
207
|
+
ee.emit('done', totals);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
debug('Pending scenarios: %s', runState.pendingScenarios);
|
|
211
|
+
}
|
|
212
|
+
}, 1000);
|
|
213
|
+
});
|
|
214
|
+
metrics.on('metricData', (_ts, periodData) => {
|
|
215
|
+
const cloned = SSMS.deserializeMetrics(SSMS.serializeMetrics(periodData));
|
|
216
|
+
intermediates.push(periodData);
|
|
217
|
+
ee.emit('stats', cloned);
|
|
218
|
+
});
|
|
219
|
+
phaser.run();
|
|
220
|
+
}
|
|
221
|
+
function runScenario(script, metrics, runState, contextVars, options) {
|
|
222
|
+
const start = process.hrtime();
|
|
223
|
+
//
|
|
224
|
+
// Compile scenarios if needed
|
|
225
|
+
//
|
|
226
|
+
if (!runState.compiledScenarios) {
|
|
227
|
+
_.each(script.scenarios, (scenario) => {
|
|
228
|
+
if (typeof scenario.weight === 'undefined') {
|
|
229
|
+
scenario.weight = 1;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
debug(`scenario ${scenario.name} weight = ${scenario.weight}`);
|
|
233
|
+
const variableValues = Object.assign(datafileVariables(script), inlineVariables(script), { $processEnvironment: process.env });
|
|
234
|
+
const w = engineUtil.template(scenario.weight, {
|
|
235
|
+
vars: variableValues
|
|
236
|
+
});
|
|
237
|
+
scenario.weight = Number.isNaN(parseInt(w, 10)) ? 0 : parseInt(w, 10);
|
|
238
|
+
debug(`scenario ${scenario.name} weight has been set to ${scenario.weight}`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
runState.picker = wl(script.scenarios);
|
|
242
|
+
runState.scenarioEvents = new EventEmitter();
|
|
243
|
+
runState.scenarioEvents.on('counter', (name, value) => {
|
|
244
|
+
metrics.counter(name, value);
|
|
245
|
+
});
|
|
246
|
+
// TODO: Deprecate
|
|
247
|
+
runState.scenarioEvents.on('customStat', (stat) => {
|
|
248
|
+
metrics.summary(stat.stat, stat.value);
|
|
249
|
+
});
|
|
250
|
+
runState.scenarioEvents.on('summary', (name, value) => {
|
|
251
|
+
metrics.summary(name, value);
|
|
252
|
+
});
|
|
253
|
+
runState.scenarioEvents.on('histogram', (name, value) => {
|
|
254
|
+
metrics.summary(name, value);
|
|
255
|
+
});
|
|
256
|
+
runState.scenarioEvents.on('rate', (name) => {
|
|
257
|
+
metrics.rate(name);
|
|
258
|
+
});
|
|
259
|
+
runState.scenarioEvents.on('started', () => {
|
|
260
|
+
runState.pendingScenarios++;
|
|
261
|
+
});
|
|
262
|
+
// TODO: Take an object so that it can have code, description etc
|
|
263
|
+
runState.scenarioEvents.on('error', (errCode) => {
|
|
264
|
+
metrics.counter(`errors.${errCode}`, 1);
|
|
265
|
+
});
|
|
266
|
+
runState.compiledScenarios = _.map(script.scenarios, (scenarioSpec, scenarioIndex) => {
|
|
267
|
+
const name = scenarioSpec.engine || script.config.engine || 'http';
|
|
268
|
+
const engine = runState.engines.find((e) => e.__name === name);
|
|
269
|
+
if (typeof engine === 'undefined') {
|
|
270
|
+
const scenarioNameOrIndex = scenarioSpec.name || scenarioIndex;
|
|
271
|
+
throw new Error(`Failed to run scenario "${scenarioNameOrIndex}": unknown engine "${name}". Did you forget to include it in "config.engines.${name}"?`);
|
|
272
|
+
}
|
|
273
|
+
return engine.createScenario(scenarioSpec, runState.scenarioEvents);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
//default to weighted picked scenario
|
|
277
|
+
let i = runState.picker()[0];
|
|
278
|
+
if (options.scenarioName) {
|
|
279
|
+
let foundIndex;
|
|
280
|
+
const foundScenario = script.scenarios.filter((scenario, index) => {
|
|
281
|
+
const hasScenarioByRegex = new RegExp(options.scenarioName).test(scenario.name);
|
|
282
|
+
const hasScenarioByName = scenario.name === options.scenarioName;
|
|
283
|
+
const hasScenario = hasScenarioByName || hasScenarioByRegex;
|
|
284
|
+
if (hasScenario) {
|
|
285
|
+
foundIndex = index;
|
|
286
|
+
}
|
|
287
|
+
return hasScenario;
|
|
288
|
+
});
|
|
289
|
+
if (foundScenario?.length === 0) {
|
|
290
|
+
throw new Error(`Scenario ${options.scenarioName} not found in script. Make sure your chosen scenario matches the one in your script exactly.`);
|
|
291
|
+
}
|
|
292
|
+
else if (foundScenario.length > 1) {
|
|
293
|
+
throw new Error(`Multiple scenarios for ${options.scenarioName} found in script. Make sure you give unique names to your scenarios in your script.`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
debug(`Scenario ${options.scenarioName} found in script. running it!`);
|
|
297
|
+
i = foundIndex;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
debug('picking scenario %s (%s) weight = %s', i, script.scenarios[i].name, script.scenarios[i].weight);
|
|
301
|
+
metrics.counter(`vusers.created_by_name.${script.scenarios[i].name || i}`, 1);
|
|
302
|
+
metrics.counter('vusers.created', 1);
|
|
303
|
+
const scenarioStartedAt = process.hrtime();
|
|
304
|
+
const scenarioContext = createContext(script, contextVars, {
|
|
305
|
+
scenario: script.scenarios[i]
|
|
306
|
+
});
|
|
307
|
+
const finish = process.hrtime(start);
|
|
308
|
+
const runScenarioDelta = finish[0] * 1e9 + finish[1];
|
|
309
|
+
debugPerf('runScenarioDelta: %s', Math.round((runScenarioDelta / 1e6) * 100) / 100);
|
|
310
|
+
runState.compiledScenarios[i](scenarioContext, (err, _context) => {
|
|
311
|
+
runState.pendingScenarios--;
|
|
312
|
+
if (err) {
|
|
313
|
+
debug(err);
|
|
314
|
+
metrics.counter('vusers.failed', 1);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
metrics.counter('vusers.failed', 0);
|
|
318
|
+
metrics.counter('vusers.completed', 1);
|
|
319
|
+
const scenarioFinishedAt = process.hrtime(scenarioStartedAt);
|
|
320
|
+
const delta = scenarioFinishedAt[0] * 1e9 + scenarioFinishedAt[1];
|
|
321
|
+
metrics.summary('vusers.session_length', delta / 1e6);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
return scenarioContext;
|
|
325
|
+
}
|
|
326
|
+
function datafileVariables(script) {
|
|
327
|
+
const result = {};
|
|
328
|
+
if (script.config.payload) {
|
|
329
|
+
_.each(script.config.payload, (el) => {
|
|
330
|
+
if (!el.loadAll) {
|
|
331
|
+
// Load individual fields from the CSV into VU context variables
|
|
332
|
+
// If data = [] (i.e. the CSV file is empty, or only has headers and
|
|
333
|
+
// skipHeaders = true), then row could = undefined
|
|
334
|
+
const row = el.reader(el.data) || [];
|
|
335
|
+
_.each(el.fields, (fieldName, j) => {
|
|
336
|
+
result[fieldName] = row[j];
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
if (typeof el.name !== 'undefined') {
|
|
341
|
+
// Make the entire CSV available
|
|
342
|
+
result[el.name] = el.reader(el.data);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log('WARNING: loadAll is set to true but no name is provided for the CSV data');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
function inlineVariables(script) {
|
|
353
|
+
const result = {};
|
|
354
|
+
if (script.config.variables) {
|
|
355
|
+
_.each(script.config.variables, (v, k) => {
|
|
356
|
+
let val;
|
|
357
|
+
if (_.isArray(v)) {
|
|
358
|
+
val = _.sample(v);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
val = v;
|
|
362
|
+
}
|
|
363
|
+
result[k] = val;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Create initial context for a scenario.
|
|
370
|
+
*/
|
|
371
|
+
function createContext(script, contextVars, additionalProperties = {}) {
|
|
372
|
+
//allow for additional properties to be passed in, but not override vars and funcs
|
|
373
|
+
const additionalPropertiesWithoutOverride = _.omit(additionalProperties, [
|
|
374
|
+
'vars',
|
|
375
|
+
'funcs'
|
|
376
|
+
]);
|
|
377
|
+
const INITIAL_CONTEXT = {
|
|
378
|
+
vars: Object.assign({
|
|
379
|
+
target: script.config.target,
|
|
380
|
+
$environment: script._environment,
|
|
381
|
+
$processEnvironment: process.env, // TODO: deprecate
|
|
382
|
+
$env: process.env,
|
|
383
|
+
$testId: global.artillery.testRunId
|
|
384
|
+
}, contextVars || {}),
|
|
385
|
+
funcs: {
|
|
386
|
+
$randomNumber: $randomNumber,
|
|
387
|
+
$randomString: $randomString,
|
|
388
|
+
$template: (input) => engineUtil.template(input, { vars: result.vars })
|
|
389
|
+
},
|
|
390
|
+
...additionalPropertiesWithoutOverride
|
|
391
|
+
};
|
|
392
|
+
if (script._configPath) {
|
|
393
|
+
INITIAL_CONTEXT.vars.$dirname = path.dirname(script._configPath);
|
|
394
|
+
}
|
|
395
|
+
if (script._scriptPath) {
|
|
396
|
+
INITIAL_CONTEXT.vars.$scenarioFile = script._scriptPath;
|
|
397
|
+
}
|
|
398
|
+
const result = INITIAL_CONTEXT;
|
|
399
|
+
// variables from payloads:
|
|
400
|
+
const variableValues1 = datafileVariables(script);
|
|
401
|
+
Object.assign(result.vars, variableValues1);
|
|
402
|
+
// inline variables:
|
|
403
|
+
const variableValues2 = inlineVariables(script);
|
|
404
|
+
Object.assign(result.vars, variableValues2);
|
|
405
|
+
result._uid = uuidv4();
|
|
406
|
+
result.vars.$uuid = result._uid;
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
//
|
|
410
|
+
// Generator functions for template strings:
|
|
411
|
+
//
|
|
412
|
+
function $randomNumber(min, max) {
|
|
413
|
+
return _.random(min, max);
|
|
414
|
+
}
|
|
415
|
+
function $randomString(length = 10) {
|
|
416
|
+
let s = '';
|
|
417
|
+
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
418
|
+
const alphabetLength = alphabet.length;
|
|
419
|
+
while (s.length < length) {
|
|
420
|
+
s += alphabet.charAt((Math.random() * alphabetLength) | 0);
|
|
421
|
+
}
|
|
422
|
+
return s;
|
|
423
|
+
}
|
|
424
|
+
async function handleScriptHook(hook, script, hookEvents, contextVars = {}) {
|
|
425
|
+
if (!script[hook]) {
|
|
426
|
+
return {};
|
|
427
|
+
}
|
|
428
|
+
const { loadedEngines: engines } = await loadEngines(script, hookEvents);
|
|
429
|
+
for (const e of engines) {
|
|
430
|
+
if (e &&
|
|
431
|
+
typeof e.init === 'function' &&
|
|
432
|
+
e.init.constructor.name === 'AsyncFunction') {
|
|
433
|
+
await e.init();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const ee = new EventEmitter();
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
ee.on('request', () => {
|
|
439
|
+
hookEvents.emit(`${hook}TestRequest`);
|
|
440
|
+
});
|
|
441
|
+
ee.on('error', (error) => {
|
|
442
|
+
hookEvents.emit(`${hook}TestError`, error);
|
|
443
|
+
});
|
|
444
|
+
const name = script[hook].engine || 'http';
|
|
445
|
+
const engine = engines.find((e) => e.__name === name);
|
|
446
|
+
if (typeof engine === 'undefined') {
|
|
447
|
+
throw new Error(`Failed to run ${hook} hook: unknown engine "${name}". Did you forget to include it in "config.engines.${name}"?`);
|
|
448
|
+
}
|
|
449
|
+
const hookScenario = engine.createScenario(script[hook], ee);
|
|
450
|
+
const hookContext = createContext(script, contextVars, {
|
|
451
|
+
scenario: script[hook]
|
|
452
|
+
});
|
|
453
|
+
hookScenario(hookContext, (err, context) => {
|
|
454
|
+
if (err) {
|
|
455
|
+
debug(err);
|
|
456
|
+
return reject(err);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
return resolve(context.vars);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
}
|