@artilleryio/int-commons 1.0.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/engine_util.js +672 -0
- package/index.js +4 -0
- package/jitter.js +34 -0
- package/package.json +14 -0
package/engine_util.js
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
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
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const async = require('async');
|
|
8
|
+
const debug = require('debug')('engine_util');
|
|
9
|
+
const deepForEach = require('deep-for-each');
|
|
10
|
+
const espree = require('espree');
|
|
11
|
+
const L = require('lodash');
|
|
12
|
+
const vm = require('vm');
|
|
13
|
+
const A = require('async');
|
|
14
|
+
const { JSONPath: jsonpath } = require('jsonpath-plus');
|
|
15
|
+
const cheerio = require('cheerio');
|
|
16
|
+
const jitter = require('./jitter').jitter;
|
|
17
|
+
|
|
18
|
+
let xmlCapture;
|
|
19
|
+
try {
|
|
20
|
+
xmlCapture = require('artillery-xml-capture');
|
|
21
|
+
} catch (e) {
|
|
22
|
+
xmlCapture = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
createThink: createThink,
|
|
27
|
+
createLoopWithCount: createLoopWithCount,
|
|
28
|
+
createParallel: createParallel,
|
|
29
|
+
isProbableEnough: isProbableEnough,
|
|
30
|
+
template: template,
|
|
31
|
+
captureOrMatch,
|
|
32
|
+
evil: evil,
|
|
33
|
+
ensurePropertyIsAList: ensurePropertyIsAList,
|
|
34
|
+
_renderVariables: renderVariables
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function createThink(requestSpec, opts) {
|
|
38
|
+
opts = opts || {};
|
|
39
|
+
|
|
40
|
+
let thinkspec = requestSpec.think;
|
|
41
|
+
|
|
42
|
+
let f = function think(context, callback) {
|
|
43
|
+
let thinktime = parseFloat(template(thinkspec, context)) * 1000;
|
|
44
|
+
if (requestSpec.jitter || opts.jitter) {
|
|
45
|
+
thinktime = jitter(`${thinktime}:${requestSpec.jitter || opts.jitter}`);
|
|
46
|
+
}
|
|
47
|
+
debug(
|
|
48
|
+
'think %s, %s, %s -> %s',
|
|
49
|
+
requestSpec.think,
|
|
50
|
+
requestSpec.jitter,
|
|
51
|
+
opts.jitter,
|
|
52
|
+
thinktime
|
|
53
|
+
);
|
|
54
|
+
setTimeout(function () {
|
|
55
|
+
callback(null, context);
|
|
56
|
+
}, thinktime);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return f;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// "count" can be an integer (negative or positive) or a string defining a range
|
|
63
|
+
// like "1-15"
|
|
64
|
+
function createLoopWithCount(count, steps, opts) {
|
|
65
|
+
return function aLoop(context, callback) {
|
|
66
|
+
let count2 = count;
|
|
67
|
+
if (typeof count === 'string') {
|
|
68
|
+
count2 = template(count, context);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let from = parseLoopCount(count2).from;
|
|
72
|
+
let to = parseLoopCount(count2).to;
|
|
73
|
+
|
|
74
|
+
let i = from;
|
|
75
|
+
let newContext = context;
|
|
76
|
+
let loopIndexVar = (opts && opts.loopValue) || '$loopCount';
|
|
77
|
+
let loopElementVar = (opts && opts.loopElement) || '$loopElement';
|
|
78
|
+
// Should we stop early because the value of "over" is not an array
|
|
79
|
+
let abortEarly = false;
|
|
80
|
+
|
|
81
|
+
let overValues = null;
|
|
82
|
+
let loopValue = i; // default to the current iteration of the loop, ie same as $loopCount
|
|
83
|
+
if (typeof opts.overValues !== 'undefined') {
|
|
84
|
+
if (opts.overValues && typeof opts.overValues === 'object') {
|
|
85
|
+
overValues = opts.overValues;
|
|
86
|
+
loopValue = overValues[i];
|
|
87
|
+
} else if (opts.overValues && typeof opts.overValues === 'string') {
|
|
88
|
+
overValues = L.get(context.vars, opts.overValues);
|
|
89
|
+
if (L.isArray(overValues)) {
|
|
90
|
+
loopValue = overValues[i];
|
|
91
|
+
} else {
|
|
92
|
+
abortEarly = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
newContext.vars[loopElementVar] = loopValue;
|
|
98
|
+
newContext.vars[loopIndexVar] = i;
|
|
99
|
+
|
|
100
|
+
let shouldContinue = true;
|
|
101
|
+
|
|
102
|
+
A.whilst(
|
|
103
|
+
function test() {
|
|
104
|
+
if (abortEarly) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (opts.whileTrue) {
|
|
108
|
+
return shouldContinue;
|
|
109
|
+
}
|
|
110
|
+
if (overValues !== null) {
|
|
111
|
+
return i !== overValues.length;
|
|
112
|
+
} else {
|
|
113
|
+
return i < to || to === -1;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
function repeated(cb) {
|
|
117
|
+
let zero = function (cb2) {
|
|
118
|
+
return cb2(null, newContext);
|
|
119
|
+
};
|
|
120
|
+
let steps2 = L.flatten([zero, steps]);
|
|
121
|
+
|
|
122
|
+
A.waterfall(steps2, function (err, context2) {
|
|
123
|
+
if (err) {
|
|
124
|
+
return cb(err, context2);
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
newContext = context2;
|
|
128
|
+
|
|
129
|
+
newContext.vars[loopIndexVar]++;
|
|
130
|
+
if (overValues !== null) {
|
|
131
|
+
newContext.vars[loopElementVar] = overValues[i];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (opts.whileTrue) {
|
|
135
|
+
opts.whileTrue(context2, function done(b) {
|
|
136
|
+
shouldContinue = b;
|
|
137
|
+
return cb(err, context2);
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
return cb(err, context2);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
function (err, finalContext) {
|
|
145
|
+
if (typeof finalContext === 'undefined') {
|
|
146
|
+
// this happens if test() returns false immediately, e.g. with
|
|
147
|
+
// nested loops where one of the inner loops goes over an
|
|
148
|
+
// empty array
|
|
149
|
+
return callback(err, newContext);
|
|
150
|
+
}
|
|
151
|
+
return callback(err, finalContext);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createParallel(steps, opts) {
|
|
158
|
+
let limit = (opts && opts.limitValue) || 100;
|
|
159
|
+
|
|
160
|
+
return function aParallel(context, callback) {
|
|
161
|
+
let newContext = context;
|
|
162
|
+
let newCallback = callback;
|
|
163
|
+
|
|
164
|
+
// Remap the steps array to pass the context into each step.
|
|
165
|
+
let newSteps = L.map(steps, function (step) {
|
|
166
|
+
return function (callback) {
|
|
167
|
+
step(newContext, callback);
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Run each of the steps in parallel.
|
|
172
|
+
A.parallelLimit(newSteps, limit, function (err, finalContext) {
|
|
173
|
+
// We don't need to do anything with the array of contexts returned from each step at the moment.
|
|
174
|
+
return newCallback(err, newContext);
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isProbableEnough(obj) {
|
|
180
|
+
if (typeof obj.probability === 'undefined') {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let probability = Number(obj.probability) || 0;
|
|
185
|
+
if (probability > 100) {
|
|
186
|
+
probability = 100;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let r = L.random(100);
|
|
190
|
+
return r < probability;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function template(o, context, inPlace) {
|
|
194
|
+
let result;
|
|
195
|
+
|
|
196
|
+
if (typeof o === 'undefined') {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (o && (o.constructor === Object || o.constructor === Array)) {
|
|
201
|
+
if (!inPlace) {
|
|
202
|
+
result = L.cloneDeep(o);
|
|
203
|
+
} else {
|
|
204
|
+
result = o;
|
|
205
|
+
}
|
|
206
|
+
templateObjectOrArray(result, context);
|
|
207
|
+
} else if (typeof o === 'string') {
|
|
208
|
+
if (!/{{/.test(o)) {
|
|
209
|
+
return o;
|
|
210
|
+
}
|
|
211
|
+
const funcCallRegex =
|
|
212
|
+
/{{\s*(\$[A-Za-z0-9_]+\s*\(\s*[A-Za-z0-9_,\s]*\s*\))\s*}}/;
|
|
213
|
+
let match = o.match(funcCallRegex);
|
|
214
|
+
if (match) {
|
|
215
|
+
// This looks like it could be a function call:
|
|
216
|
+
const syntax = espree.parse(match[1]);
|
|
217
|
+
// TODO: Use a proper schema for what we expect here
|
|
218
|
+
if (
|
|
219
|
+
syntax.body &&
|
|
220
|
+
syntax.body.length === 1 &&
|
|
221
|
+
syntax.body[0].type === 'ExpressionStatement'
|
|
222
|
+
) {
|
|
223
|
+
let funcName = syntax.body[0].expression.callee.name;
|
|
224
|
+
let args = L.map(syntax.body[0].expression.arguments, function (arg) {
|
|
225
|
+
return arg.value;
|
|
226
|
+
});
|
|
227
|
+
if (funcName in context.funcs) {
|
|
228
|
+
return template(
|
|
229
|
+
o.replace(funcCallRegex, context.funcs[funcName].apply(null, args)),
|
|
230
|
+
context
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
if (!o.match(/{{/)) {
|
|
236
|
+
return o;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
result = renderVariables(o, context.vars);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
return o;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Mutates the object in place
|
|
249
|
+
function templateObjectOrArray(o, context) {
|
|
250
|
+
deepForEach(o, (value, key, subj, path) => {
|
|
251
|
+
const newPath = template(path, context, true);
|
|
252
|
+
|
|
253
|
+
let newValue;
|
|
254
|
+
if (value && value.constructor !== Object && value.constructor !== Array) {
|
|
255
|
+
newValue = template(value, context, true);
|
|
256
|
+
} else {
|
|
257
|
+
newValue = value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
debug(
|
|
261
|
+
`path = ${path} ; value = ${JSON.stringify(
|
|
262
|
+
value
|
|
263
|
+
)} (${typeof value}) ; (subj type: ${
|
|
264
|
+
subj.length ? 'list' : 'hash'
|
|
265
|
+
}) ; newValue = ${JSON.stringify(newValue)} ; newPath = ${newPath}`
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// If path has changed, we need to unset the original path and
|
|
269
|
+
// explicitly walk down the new subtree from this path:
|
|
270
|
+
if (path !== newPath) {
|
|
271
|
+
L.unset(o, path);
|
|
272
|
+
newValue = template(value, context, true);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (newPath.endsWith(key)) {
|
|
276
|
+
const keyIndex = newPath.lastIndexOf(key);
|
|
277
|
+
const prefix = newPath.substr(0, keyIndex - 1);
|
|
278
|
+
L.set(o, `${prefix}["${key}"]`, newValue);
|
|
279
|
+
} else {
|
|
280
|
+
L.set(o, newPath, newValue);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function renderVariables(str, vars) {
|
|
286
|
+
const RX = /{{{?[\s$\w\.\[\]\'\"-]+}}}?/g;
|
|
287
|
+
let rxmatch;
|
|
288
|
+
let result = str.substring(0, str.length);
|
|
289
|
+
|
|
290
|
+
// Special case for handling integer/boolean/object substitution:
|
|
291
|
+
//
|
|
292
|
+
// Does the template string contain one variable and nothing else?
|
|
293
|
+
// e.g.: "{{ myvar }" or "{{ myvar }", but NOT " {{ myvar }"
|
|
294
|
+
// If so, we treat it as a special case.
|
|
295
|
+
const matches = str.match(RX);
|
|
296
|
+
if (matches && matches.length === 1) {
|
|
297
|
+
if (matches[0] === str) {
|
|
298
|
+
// there's nothing else in the template but the variable
|
|
299
|
+
const varName = str.replace(/{/g, '').replace(/}/g, '').trim();
|
|
300
|
+
return sanitiseValue(L.get(vars, varName));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
while (result.search(RX) > -1) {
|
|
305
|
+
let templateStr = result.match(RX)[0];
|
|
306
|
+
const varName = templateStr.replace(/{/g, '').replace(/}/g, '').trim();
|
|
307
|
+
|
|
308
|
+
let varValue = L.get(vars, varName);
|
|
309
|
+
|
|
310
|
+
if (typeof varValue === 'object') {
|
|
311
|
+
varValue = JSON.stringify(varValue);
|
|
312
|
+
}
|
|
313
|
+
result = result.replace(templateStr, varValue);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Presume code is valid JS code (i.e. that it has been checked elsewhere)
|
|
320
|
+
function evil(sandbox, code) {
|
|
321
|
+
let context = vm.createContext(sandbox);
|
|
322
|
+
let script = new vm.Script(code);
|
|
323
|
+
try {
|
|
324
|
+
return script.runInContext(context);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseLoopCount(countSpec) {
|
|
331
|
+
let from = 0;
|
|
332
|
+
let to = 0;
|
|
333
|
+
|
|
334
|
+
if (typeof countSpec === 'number') {
|
|
335
|
+
from = 0;
|
|
336
|
+
to = countSpec;
|
|
337
|
+
} else if (typeof countSpec === 'string') {
|
|
338
|
+
if (isNaN(Number(countSpec))) {
|
|
339
|
+
if (/\d\-\d/.test(countSpec)) {
|
|
340
|
+
from = Number(countSpec.split('-')[0]);
|
|
341
|
+
to = Number(countSpec.split('-')[1]);
|
|
342
|
+
} else {
|
|
343
|
+
to = 0;
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
to = Number(countSpec);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
to = 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { from: from, to: to };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isCaptureFailed(v, defaultStrict) {
|
|
356
|
+
const noValue =
|
|
357
|
+
typeof v.value === 'undefined' ||
|
|
358
|
+
v.value === '' ||
|
|
359
|
+
typeof v.error !== 'undefined';
|
|
360
|
+
|
|
361
|
+
if (!noValue) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return !(
|
|
366
|
+
(typeof defaultStrict === 'undefined' && v.strict === false) ||
|
|
367
|
+
(defaultStrict === true && v.strict === false) ||
|
|
368
|
+
(defaultStrict === false && typeof v.strict === 'undefined') ||
|
|
369
|
+
(defaultStrict === false && v.strict === false)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Helper function to wrap an object's property in a list if it's
|
|
374
|
+
// defined, or set it to an empty list if not.
|
|
375
|
+
function ensurePropertyIsAList(obj, prop) {
|
|
376
|
+
if (Array.isArray(obj[prop])) {
|
|
377
|
+
return obj;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
obj[prop] = [].concat(typeof obj[prop] === 'undefined' ? [] : obj[prop]);
|
|
381
|
+
return obj;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function captureOrMatch(params, response, context, done) {
|
|
385
|
+
if (
|
|
386
|
+
(!params.capture || params.capture.length === 0) &&
|
|
387
|
+
(!params.match || params.match.length === 0)
|
|
388
|
+
) {
|
|
389
|
+
return done(null, null);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let result = {
|
|
393
|
+
captures: {},
|
|
394
|
+
matches: {},
|
|
395
|
+
failedCaptures: false
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Objects updated in place the first time this runs:
|
|
399
|
+
ensurePropertyIsAList(params, 'capture');
|
|
400
|
+
ensurePropertyIsAList(params, 'match');
|
|
401
|
+
|
|
402
|
+
let specs = params.capture.concat(params.match);
|
|
403
|
+
|
|
404
|
+
async.eachSeries(
|
|
405
|
+
specs,
|
|
406
|
+
function (spec, next) {
|
|
407
|
+
let parsedSpec = parseSpec(spec, response);
|
|
408
|
+
let parser = parsedSpec.parser;
|
|
409
|
+
let extractor = parsedSpec.extractor;
|
|
410
|
+
let expr = parsedSpec.expr;
|
|
411
|
+
|
|
412
|
+
// are we looking at body or headers:
|
|
413
|
+
var content = response.body;
|
|
414
|
+
if (spec.header) {
|
|
415
|
+
content = response.headers;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
parser(content, function (err, doc) {
|
|
419
|
+
if (err) {
|
|
420
|
+
if (spec.as) {
|
|
421
|
+
result.captures[spec.as] = {
|
|
422
|
+
error: err,
|
|
423
|
+
strict: spec.strict
|
|
424
|
+
};
|
|
425
|
+
result.captures[spec.as].failed = isCaptureFailed(
|
|
426
|
+
result.captures[spec.as],
|
|
427
|
+
context._defaultStrictCapture
|
|
428
|
+
);
|
|
429
|
+
} else {
|
|
430
|
+
result.matches[spec.expr] = {
|
|
431
|
+
error: err,
|
|
432
|
+
strict: spec.strict
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return next(null);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let extractedValue = extractor(doc, template(expr, context), spec);
|
|
439
|
+
|
|
440
|
+
if (spec.value !== undefined) {
|
|
441
|
+
// this is a match spec
|
|
442
|
+
let expected = template(spec.value, context);
|
|
443
|
+
debug(
|
|
444
|
+
'match: %s, expected: %s, got: %s',
|
|
445
|
+
expr,
|
|
446
|
+
expected,
|
|
447
|
+
extractedValue
|
|
448
|
+
);
|
|
449
|
+
if (extractedValue !== expected) {
|
|
450
|
+
result.matches[expr] = {
|
|
451
|
+
success: false,
|
|
452
|
+
expected: expected,
|
|
453
|
+
got: extractedValue,
|
|
454
|
+
expression: expr,
|
|
455
|
+
strict: spec.strict
|
|
456
|
+
};
|
|
457
|
+
} else {
|
|
458
|
+
result.matches.expr = {
|
|
459
|
+
success: true,
|
|
460
|
+
expected: expected,
|
|
461
|
+
expression: expr
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return next(null);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (spec.as) {
|
|
468
|
+
// this is a capture
|
|
469
|
+
debug('capture: %s = %s', spec.as, extractedValue);
|
|
470
|
+
result.captures[spec.as] = {
|
|
471
|
+
value: extractedValue,
|
|
472
|
+
strict: spec.strict
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
result.captures[spec.as].failed = isCaptureFailed(
|
|
476
|
+
result.captures[spec.as],
|
|
477
|
+
context._defaultStrictCapture
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return next(null);
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
function (err) {
|
|
485
|
+
if (err) {
|
|
486
|
+
return done(err, null);
|
|
487
|
+
} else {
|
|
488
|
+
return done(null, result);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function parseSpec(spec, response) {
|
|
495
|
+
let parser;
|
|
496
|
+
let extractor;
|
|
497
|
+
let expr;
|
|
498
|
+
|
|
499
|
+
if (spec.json) {
|
|
500
|
+
parser = parseJSON;
|
|
501
|
+
extractor = extractJSONPath;
|
|
502
|
+
expr = spec.json;
|
|
503
|
+
} else if (xmlCapture && spec.xpath) {
|
|
504
|
+
parser = xmlCapture.parseXML;
|
|
505
|
+
extractor = xmlCapture.extractXPath;
|
|
506
|
+
expr = spec.xpath;
|
|
507
|
+
} else if (spec.regexp) {
|
|
508
|
+
parser = dummyParser;
|
|
509
|
+
extractor = extractRegExp;
|
|
510
|
+
expr = spec.regexp;
|
|
511
|
+
} else if (spec.header) {
|
|
512
|
+
parser = dummyParser;
|
|
513
|
+
extractor = extractHeader;
|
|
514
|
+
expr = spec.header;
|
|
515
|
+
} else if (spec.selector) {
|
|
516
|
+
parser = dummyParser;
|
|
517
|
+
extractor = extractCheerio;
|
|
518
|
+
expr = spec.selector;
|
|
519
|
+
} else {
|
|
520
|
+
if (isJSON(response)) {
|
|
521
|
+
parser = parseJSON;
|
|
522
|
+
extractor = extractJSONPath;
|
|
523
|
+
expr = spec.json;
|
|
524
|
+
} else if (xmlCapture && isXML(response)) {
|
|
525
|
+
parser = xmlCapture.parseXML;
|
|
526
|
+
extractor = xmlCapture.extractXPath;
|
|
527
|
+
expr = spec.xpath;
|
|
528
|
+
} else {
|
|
529
|
+
// We really don't know what to do here.
|
|
530
|
+
parser = dummyParser;
|
|
531
|
+
extractor = dummyExtractor;
|
|
532
|
+
expr = '';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return { parser: parser, extractor: extractor, expr: expr };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/*
|
|
540
|
+
* Wrap JSON.parse in a callback
|
|
541
|
+
*/
|
|
542
|
+
function parseJSON(body, callback) {
|
|
543
|
+
let r = null;
|
|
544
|
+
let err = null;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
if (typeof body === 'string') {
|
|
548
|
+
r = JSON.parse(body);
|
|
549
|
+
} else {
|
|
550
|
+
r = body;
|
|
551
|
+
}
|
|
552
|
+
} catch (e) {
|
|
553
|
+
err = e;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return callback(err, r);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function dummyParser(body, callback) {
|
|
560
|
+
return callback(null, body);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// doc is a JSON object
|
|
564
|
+
function extractJSONPath(doc, expr) {
|
|
565
|
+
// typeof null is 'object' hence the explicit check here
|
|
566
|
+
if (typeof doc !== 'object' || doc === null) {
|
|
567
|
+
return '';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let results;
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
results = jsonpath(expr, doc);
|
|
574
|
+
} catch (queryErr) {
|
|
575
|
+
debug(queryErr);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!results) {
|
|
579
|
+
return '';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (results.length > 1) {
|
|
583
|
+
return results[randomInt(0, results.length - 1)];
|
|
584
|
+
} else {
|
|
585
|
+
return results[0];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// doc is a string or an object (body parsed by Request when headers indicate JSON)
|
|
590
|
+
function extractRegExp(doc, expr, opts) {
|
|
591
|
+
let group = opts.group;
|
|
592
|
+
let flags = opts.flags;
|
|
593
|
+
let str;
|
|
594
|
+
if (typeof doc === 'string') {
|
|
595
|
+
str = doc;
|
|
596
|
+
} else {
|
|
597
|
+
str = JSON.stringify(doc); // FIXME: not the same string as the one we got from the server
|
|
598
|
+
}
|
|
599
|
+
let rx;
|
|
600
|
+
if (flags) {
|
|
601
|
+
rx = new RegExp(expr, flags);
|
|
602
|
+
} else {
|
|
603
|
+
rx = new RegExp(expr);
|
|
604
|
+
}
|
|
605
|
+
let match = rx.exec(str);
|
|
606
|
+
if (!match) {
|
|
607
|
+
return '';
|
|
608
|
+
}
|
|
609
|
+
if (group && match[group]) {
|
|
610
|
+
return match[group];
|
|
611
|
+
} else if (match[0]) {
|
|
612
|
+
return match[0];
|
|
613
|
+
} else {
|
|
614
|
+
return '';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function extractHeader(headers, headerName) {
|
|
619
|
+
return headers[headerName] || '';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function extractCheerio(doc, expr, opts) {
|
|
623
|
+
let $ = cheerio.load(doc);
|
|
624
|
+
let els = $(expr);
|
|
625
|
+
let i = 0;
|
|
626
|
+
if (typeof opts.index !== 'undefined') {
|
|
627
|
+
if (opts.index === 'random') {
|
|
628
|
+
i = Math.ceil(Math.random() * els.get().length - 1);
|
|
629
|
+
} else if (opts.index === 'last') {
|
|
630
|
+
i = els.get().length() - 1;
|
|
631
|
+
} else if (typeof Number(opts.index) === 'number') {
|
|
632
|
+
i = Number(opts.index);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return els.slice(i, i + 1).attr(opts.attr);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function dummyExtractor() {
|
|
639
|
+
return '';
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/*
|
|
643
|
+
* Given a response object determine if it's JSON
|
|
644
|
+
*/
|
|
645
|
+
function isJSON(res) {
|
|
646
|
+
debug('isJSON: content-type = %s', res.headers['content-type']);
|
|
647
|
+
return (
|
|
648
|
+
res.headers['content-type'] &&
|
|
649
|
+
/^application\/json/.test(res.headers['content-type'])
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/*
|
|
654
|
+
* Given a response object determine if it's some kind of XML
|
|
655
|
+
*/
|
|
656
|
+
function isXML(res) {
|
|
657
|
+
return (
|
|
658
|
+
res.headers['content-type'] &&
|
|
659
|
+
(/^[a-zA-Z]+\/xml/.test(res.headers['content-type']) ||
|
|
660
|
+
/^[a-zA-Z]+\/[a-zA-Z]+\+xml/.test(res.headers['content-type']))
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function randomInt(low, high) {
|
|
665
|
+
return Math.floor(Math.random() * (high - low + 1) + low);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function sanitiseValue(value) {
|
|
669
|
+
if (value === 0 || value === false || value === null || value === undefined)
|
|
670
|
+
return value;
|
|
671
|
+
return value ? value : '';
|
|
672
|
+
}
|
package/index.js
ADDED
package/jitter.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
jitter: jitter
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function jitter(sApprox) {
|
|
12
|
+
if (!sApprox) {
|
|
13
|
+
return sApprox;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof sApprox !== 'string') {
|
|
17
|
+
return sApprox;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (sApprox.indexOf(':') < 0) {
|
|
21
|
+
return sApprox;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let inputs = sApprox.split(':');
|
|
25
|
+
let nb = parseInt(inputs[0], 10);
|
|
26
|
+
let approxPercent = parseInt(inputs[1], 10);
|
|
27
|
+
|
|
28
|
+
let approx = approxPercent;
|
|
29
|
+
if (inputs[1].indexOf('%') >= 0) {
|
|
30
|
+
approx = (nb * approxPercent) / 100;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Math.max(0, nb - approx + Math.random() * 2 * approx);
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@artilleryio/int-commons",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./index.js",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"async": "^2.6.4",
|
|
7
|
+
"cheerio": "^1.0.0-rc.10",
|
|
8
|
+
"debug": "^4.3.1",
|
|
9
|
+
"deep-for-each": "^3.0.0",
|
|
10
|
+
"espree": "^9.4.1",
|
|
11
|
+
"jsonpath-plus": "^7.2.0",
|
|
12
|
+
"lodash": "^4.17.19"
|
|
13
|
+
}
|
|
14
|
+
}
|