@loadmill/executer 0.1.34 → 0.1.38
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/dist/errors.d.ts +5 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraction-combiner.d.ts +4 -2
- package/dist/extraction-combiner.js +96 -46
- package/dist/extraction-combiner.js.map +1 -1
- package/dist/mill-version.js +1 -1
- package/dist/post-script/ast-walker/index.js.map +1 -1
- package/dist/post-script/virtual-machine/vm2-virtual-machine.js +1 -1
- package/dist/post-script/virtual-machine/vm2-virtual-machine.js.map +1 -1
- package/dist/sampler.js +4 -3
- package/dist/sampler.js.map +1 -1
- package/dist/sequence.js +301 -134
- package/dist/sequence.js.map +1 -1
- package/dist/test-run-event-emitter.d.ts +11 -0
- package/dist/test-run-event-emitter.js +36 -0
- package/dist/test-run-event-emitter.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +29 -0
- package/dist/utils.js.map +1 -0
- package/dist/ws.d.ts +66 -0
- package/dist/ws.js +435 -0
- package/dist/ws.js.map +1 -0
- package/package.json +6 -6
- package/src/errors.ts +10 -0
- package/src/extraction-combiner.ts +15 -4
- package/src/mill-version.ts +1 -1
- package/src/post-script/ast-walker/index.ts +1 -1
- package/src/post-script/virtual-machine/vm2-virtual-machine.ts +1 -1
- package/src/sampler.ts +4 -3
- package/src/sequence.ts +141 -58
- package/src/single-runner.ts +3 -3
- package/src/test-run-event-emitter.ts +24 -0
- package/src/utils.ts +8 -0
- package/src/ws.ts +233 -0
- package/test/post-script-executor.spec.ts +24 -24
package/dist/ws.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (_) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.WSSequenceHandler = exports.WSRequest = void 0;
|
|
43
|
+
var ws_1 = __importDefault(require("ws")); // todo should be picked with other option (socketio or non in browser)
|
|
44
|
+
var promise_utils_1 = require("@loadmill/universal/dist/promise-utils");
|
|
45
|
+
var log_1 = __importDefault(require("@loadmill/universal/dist/log"));
|
|
46
|
+
var errors_1 = require("./errors");
|
|
47
|
+
var WSState;
|
|
48
|
+
(function (WSState) {
|
|
49
|
+
WSState[WSState["CONNECTING"] = 0] = "CONNECTING";
|
|
50
|
+
WSState[WSState["OPEN"] = 1] = "OPEN";
|
|
51
|
+
WSState[WSState["CLOSING"] = 2] = "CLOSING";
|
|
52
|
+
WSState[WSState["CLOSED"] = 3] = "CLOSED";
|
|
53
|
+
})(WSState || (WSState = {}));
|
|
54
|
+
var SWITCHING_PROTOCOLS = 'Switching Protocols';
|
|
55
|
+
var WS_CONNECTION_TIMEOUT_MS = 10000;
|
|
56
|
+
var WSRequest = /** @class */ (function () {
|
|
57
|
+
function WSRequest(wsRequestArgs, wsHandler, onError) {
|
|
58
|
+
var _this = this;
|
|
59
|
+
Object.defineProperty(this, "wsRequestArgs", {
|
|
60
|
+
enumerable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
value: wsRequestArgs
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(this, "wsHandler", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
writable: true,
|
|
69
|
+
value: wsHandler
|
|
70
|
+
});
|
|
71
|
+
Object.defineProperty(this, "onError", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
value: onError
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(this, "hasErrorOccured", {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
value: void 0
|
|
82
|
+
}); // need this otherwise we have race condition between verifyConnectedAndOpen and onError
|
|
83
|
+
Object.defineProperty(this, "ws", {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
writable: true,
|
|
87
|
+
value: void 0
|
|
88
|
+
});
|
|
89
|
+
Object.defineProperty(this, "url", {
|
|
90
|
+
enumerable: true,
|
|
91
|
+
configurable: true,
|
|
92
|
+
writable: true,
|
|
93
|
+
value: void 0
|
|
94
|
+
}); // need this for isExpectedStatus function
|
|
95
|
+
Object.defineProperty(this, "expectedStatus", {
|
|
96
|
+
enumerable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
writable: true,
|
|
99
|
+
value: void 0
|
|
100
|
+
}); // need this for isExpectedStatus function
|
|
101
|
+
/**
|
|
102
|
+
* Waiting for WS connection to go out of CONNECTING state
|
|
103
|
+
* @param socket The ws connection object
|
|
104
|
+
* @param timeout The timeout in milliseconds for the waiting procedure
|
|
105
|
+
* @returns WSState
|
|
106
|
+
*/
|
|
107
|
+
Object.defineProperty(this, "waitForConnectedState", {
|
|
108
|
+
enumerable: true,
|
|
109
|
+
configurable: true,
|
|
110
|
+
writable: true,
|
|
111
|
+
value: function (timeout) {
|
|
112
|
+
if (timeout === void 0) { timeout = WS_CONNECTION_TIMEOUT_MS; }
|
|
113
|
+
return __awaiter(_this, void 0, void 0, function () {
|
|
114
|
+
var WS_CONNECTION_INTERVAL_MS, maxIterations, i;
|
|
115
|
+
return __generator(this, function (_a) {
|
|
116
|
+
switch (_a.label) {
|
|
117
|
+
case 0:
|
|
118
|
+
WS_CONNECTION_INTERVAL_MS = 100;
|
|
119
|
+
if (!(this.ws.readyState !== ws_1.default.CONNECTING)) return [3 /*break*/, 1];
|
|
120
|
+
return [2 /*return*/, this.ws.readyState];
|
|
121
|
+
case 1:
|
|
122
|
+
maxIterations = timeout / WS_CONNECTION_INTERVAL_MS;
|
|
123
|
+
i = 0;
|
|
124
|
+
_a.label = 2;
|
|
125
|
+
case 2:
|
|
126
|
+
if (!(this.ws.readyState === ws_1.default.CONNECTING && i < maxIterations)) return [3 /*break*/, 4];
|
|
127
|
+
return [4 /*yield*/, promise_utils_1.delay(WS_CONNECTION_INTERVAL_MS)];
|
|
128
|
+
case 3:
|
|
129
|
+
_a.sent();
|
|
130
|
+
i++;
|
|
131
|
+
return [3 /*break*/, 2];
|
|
132
|
+
case 4: return [2 /*return*/, this.ws.readyState];
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
this.url = wsRequestArgs.url;
|
|
139
|
+
this.expectedStatus = wsRequestArgs.expectedStatus;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* This function is executed when we are ready to send the ws request
|
|
143
|
+
* @param cb This callback is being executed after we successfully connected to ws and sent a ws message if there was any
|
|
144
|
+
*/
|
|
145
|
+
Object.defineProperty(WSRequest.prototype, "ok", {
|
|
146
|
+
enumerable: false,
|
|
147
|
+
configurable: true,
|
|
148
|
+
writable: true,
|
|
149
|
+
value: function (cb) {
|
|
150
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
151
|
+
var response, existingWS;
|
|
152
|
+
return __generator(this, function (_a) {
|
|
153
|
+
switch (_a.label) {
|
|
154
|
+
case 0:
|
|
155
|
+
response = {
|
|
156
|
+
status: 101,
|
|
157
|
+
res: {
|
|
158
|
+
statusMessage: SWITCHING_PROTOCOLS,
|
|
159
|
+
},
|
|
160
|
+
header: {},
|
|
161
|
+
req: {},
|
|
162
|
+
};
|
|
163
|
+
existingWS = this.wsHandler.getConnection(this.wsRequestArgs.url);
|
|
164
|
+
log_1.default.debug("Connection state " + getConnectionState(existingWS));
|
|
165
|
+
if (!(!existingWS || (existingWS && existingWS.readyState !== WSState.OPEN))) return [3 /*break*/, 2];
|
|
166
|
+
return [4 /*yield*/, this.addConnection(response)];
|
|
167
|
+
case 1:
|
|
168
|
+
_a.sent();
|
|
169
|
+
return [3 /*break*/, 3];
|
|
170
|
+
case 2:
|
|
171
|
+
this.ws = existingWS;
|
|
172
|
+
log_1.default.debug('Reusing existing ws connection', this.ws.url);
|
|
173
|
+
_a.label = 3;
|
|
174
|
+
case 3:
|
|
175
|
+
try {
|
|
176
|
+
this.sendMessage();
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
log_1.default.error('Failed to send a ws message', e);
|
|
180
|
+
}
|
|
181
|
+
cb(response);
|
|
182
|
+
return [2 /*return*/, {}];
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
Object.defineProperty(WSRequest.prototype, "addConnection", {
|
|
189
|
+
enumerable: false,
|
|
190
|
+
configurable: true,
|
|
191
|
+
writable: true,
|
|
192
|
+
value: function (response) {
|
|
193
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
194
|
+
var _a;
|
|
195
|
+
return __generator(this, function (_b) {
|
|
196
|
+
switch (_b.label) {
|
|
197
|
+
case 0:
|
|
198
|
+
this.ws = new ws_1.default(this.wsRequestArgs.url, undefined, { headers: this.wsRequestArgs.headers });
|
|
199
|
+
this.addEventListeners(response);
|
|
200
|
+
_a = !this.hasErrorOccured;
|
|
201
|
+
if (!_a) return [3 /*break*/, 2];
|
|
202
|
+
return [4 /*yield*/, this.verifyOpen()];
|
|
203
|
+
case 1:
|
|
204
|
+
_a = (_b.sent());
|
|
205
|
+
_b.label = 2;
|
|
206
|
+
case 2:
|
|
207
|
+
_a;
|
|
208
|
+
this.wsHandler.storeConnection(this.wsRequestArgs.url, this.ws);
|
|
209
|
+
return [2 /*return*/];
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
Object.defineProperty(WSRequest.prototype, "addEventListeners", {
|
|
216
|
+
enumerable: false,
|
|
217
|
+
configurable: true,
|
|
218
|
+
writable: true,
|
|
219
|
+
value: function (response) {
|
|
220
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
221
|
+
return __generator(this, function (_a) {
|
|
222
|
+
this.addOnErrorListener();
|
|
223
|
+
this.addOnUpgradeListener(response);
|
|
224
|
+
this.addOnOpenListener();
|
|
225
|
+
this.addOnMessageListener();
|
|
226
|
+
return [2 /*return*/];
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
Object.defineProperty(WSRequest.prototype, "addOnErrorListener", {
|
|
232
|
+
enumerable: false,
|
|
233
|
+
configurable: true,
|
|
234
|
+
writable: true,
|
|
235
|
+
value: function () {
|
|
236
|
+
var _this = this;
|
|
237
|
+
this.ws.on('error', function (e) {
|
|
238
|
+
try {
|
|
239
|
+
log_1.default.debug('inside on error', e);
|
|
240
|
+
_this.onError(e);
|
|
241
|
+
_this.hasErrorOccured = true;
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
log_1.default.warn('Failed to handle a ws error', e);
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
log_1.default.debug('closing ws');
|
|
248
|
+
_this.ws.close();
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
log_1.default.warn('Failed to close a ws connection', e);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
Object.defineProperty(WSRequest.prototype, "addOnUpgradeListener", {
|
|
257
|
+
enumerable: false,
|
|
258
|
+
configurable: true,
|
|
259
|
+
writable: true,
|
|
260
|
+
value: function (response) {
|
|
261
|
+
this.ws.on('upgrade', function (event) {
|
|
262
|
+
try {
|
|
263
|
+
var statusCode = event.statusCode, statusMessage = event.statusMessage, headers = event.headers;
|
|
264
|
+
log_1.default.debug('inside upgrade event', { statusCode: statusCode, statusMessage: statusMessage, headers: headers });
|
|
265
|
+
response.status = statusCode || 101;
|
|
266
|
+
response.res.statusMessage = statusMessage || SWITCHING_PROTOCOLS;
|
|
267
|
+
response.header = headers;
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
log_1.default.debug('upgrade event err', e);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
Object.defineProperty(WSRequest.prototype, "addOnOpenListener", {
|
|
276
|
+
enumerable: false,
|
|
277
|
+
configurable: true,
|
|
278
|
+
writable: true,
|
|
279
|
+
value: function () {
|
|
280
|
+
this.ws.on('open', function () {
|
|
281
|
+
log_1.default.debug('inside on open, currently doing nothing here.');
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
Object.defineProperty(WSRequest.prototype, "addOnMessageListener", {
|
|
286
|
+
enumerable: false,
|
|
287
|
+
configurable: true,
|
|
288
|
+
writable: true,
|
|
289
|
+
value: function () {
|
|
290
|
+
var _this = this;
|
|
291
|
+
this.ws.on('message', function (message) {
|
|
292
|
+
try {
|
|
293
|
+
log_1.default.debug('got incoming message', message);
|
|
294
|
+
_this.wsHandler.addMessage(message);
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
log_1.default.debug('error getting message', e);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
Object.defineProperty(WSRequest.prototype, "verifyOpen", {
|
|
303
|
+
enumerable: false,
|
|
304
|
+
configurable: true,
|
|
305
|
+
writable: true,
|
|
306
|
+
value: function () {
|
|
307
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
308
|
+
var readyState;
|
|
309
|
+
return __generator(this, function (_a) {
|
|
310
|
+
switch (_a.label) {
|
|
311
|
+
case 0: return [4 /*yield*/, this.waitForConnectedState(this.wsRequestArgs.timeout)];
|
|
312
|
+
case 1:
|
|
313
|
+
readyState = _a.sent();
|
|
314
|
+
log_1.default.debug("Connection state " + WSState[readyState]);
|
|
315
|
+
if (readyState !== ws_1.default.OPEN && !this.hasErrorOccured) {
|
|
316
|
+
log_1.default.error('Could not connect to WebSocket address', { connectionState: WSState[readyState], url: this.ws.url });
|
|
317
|
+
throw new errors_1.RequestFailuresError('Could not connect to WebSocket address');
|
|
318
|
+
}
|
|
319
|
+
return [2 /*return*/];
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
Object.defineProperty(WSRequest.prototype, "sendMessage", {
|
|
326
|
+
enumerable: false,
|
|
327
|
+
configurable: true,
|
|
328
|
+
writable: true,
|
|
329
|
+
value: function () {
|
|
330
|
+
log_1.default.debug('about to send message', { readyState: WSState[this.ws.readyState] });
|
|
331
|
+
if (this.ws.readyState === WSState.OPEN && this.wsRequestArgs.message) {
|
|
332
|
+
log_1.default.debug('sending message', this.wsRequestArgs.message);
|
|
333
|
+
this.ws.send(this.wsRequestArgs.message);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// need this because the SequenceExecutor expectes a request obj with on func prop
|
|
338
|
+
Object.defineProperty(WSRequest.prototype, "on", {
|
|
339
|
+
enumerable: false,
|
|
340
|
+
configurable: true,
|
|
341
|
+
writable: true,
|
|
342
|
+
value: function (_eventName, _cb) {
|
|
343
|
+
//do nothing
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return WSRequest;
|
|
347
|
+
}());
|
|
348
|
+
exports.WSRequest = WSRequest;
|
|
349
|
+
var WSSequenceHandler = /** @class */ (function () {
|
|
350
|
+
function WSSequenceHandler() {
|
|
351
|
+
Object.defineProperty(this, "_connections", {
|
|
352
|
+
enumerable: true,
|
|
353
|
+
configurable: true,
|
|
354
|
+
writable: true,
|
|
355
|
+
value: void 0
|
|
356
|
+
});
|
|
357
|
+
Object.defineProperty(this, "messages", {
|
|
358
|
+
enumerable: true,
|
|
359
|
+
configurable: true,
|
|
360
|
+
writable: true,
|
|
361
|
+
value: void 0
|
|
362
|
+
});
|
|
363
|
+
this._connections = {};
|
|
364
|
+
this.clearMessages();
|
|
365
|
+
}
|
|
366
|
+
Object.defineProperty(WSSequenceHandler.prototype, "getConnection", {
|
|
367
|
+
enumerable: false,
|
|
368
|
+
configurable: true,
|
|
369
|
+
writable: true,
|
|
370
|
+
value: function (url) {
|
|
371
|
+
return this._connections[url];
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
Object.defineProperty(WSSequenceHandler.prototype, "storeConnection", {
|
|
375
|
+
enumerable: false,
|
|
376
|
+
configurable: true,
|
|
377
|
+
writable: true,
|
|
378
|
+
value: function (url, wsConnection) {
|
|
379
|
+
this._connections[url] = wsConnection;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
Object.defineProperty(WSSequenceHandler.prototype, "closeAllConnections", {
|
|
383
|
+
enumerable: false,
|
|
384
|
+
configurable: true,
|
|
385
|
+
writable: true,
|
|
386
|
+
value: function () {
|
|
387
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
388
|
+
var _i, _a, connection;
|
|
389
|
+
return __generator(this, function (_b) {
|
|
390
|
+
try {
|
|
391
|
+
log_1.default.debug('Closing all ws connections');
|
|
392
|
+
for (_i = 0, _a = Object.values(this._connections); _i < _a.length; _i++) {
|
|
393
|
+
connection = _a[_i];
|
|
394
|
+
connection.close();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
log_1.default.warn('Failed to close all connections', e, { connections: this._connections });
|
|
399
|
+
}
|
|
400
|
+
return [2 /*return*/];
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
Object.defineProperty(WSSequenceHandler.prototype, "getMessages", {
|
|
406
|
+
enumerable: false,
|
|
407
|
+
configurable: true,
|
|
408
|
+
writable: true,
|
|
409
|
+
value: function () {
|
|
410
|
+
return this.messages;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
Object.defineProperty(WSSequenceHandler.prototype, "addMessage", {
|
|
414
|
+
enumerable: false,
|
|
415
|
+
configurable: true,
|
|
416
|
+
writable: true,
|
|
417
|
+
value: function (message) {
|
|
418
|
+
this.messages.push(message);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
Object.defineProperty(WSSequenceHandler.prototype, "clearMessages", {
|
|
422
|
+
enumerable: false,
|
|
423
|
+
configurable: true,
|
|
424
|
+
writable: true,
|
|
425
|
+
value: function () {
|
|
426
|
+
this.messages = [];
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return WSSequenceHandler;
|
|
430
|
+
}());
|
|
431
|
+
exports.WSSequenceHandler = WSSequenceHandler;
|
|
432
|
+
function getConnectionState(existingWS) {
|
|
433
|
+
return existingWS ? WSState[existingWS.readyState] : null;
|
|
434
|
+
}
|
|
435
|
+
//# sourceMappingURL=ws.js.map
|
package/dist/ws.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmill/executer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "Loadmill executer library",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"ts-watch": "rimraf dist && tsc -w -p tsconfig.json",
|
|
7
7
|
"build": "rimraf dist && tsc -p tsconfig.json",
|
|
8
8
|
"check-types": "tsc -p tsconfig.json --noEmit",
|
|
9
9
|
"prepublish": "yarn build",
|
|
10
|
-
"unit-test": "mocha --require ts-node/register/transpile-only --ui tdd \"test/*.ts\""
|
|
10
|
+
"unit-test": "mocha --timeout 10000 --require ts-node/register/transpile-only --ui tdd \"test/*.ts\""
|
|
11
11
|
},
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=12.0.0"
|
|
14
14
|
},
|
|
15
15
|
"license": "Apache-2.0",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@loadmill/core": "0.3.
|
|
18
|
-
"@loadmill/universal": "0.3.
|
|
17
|
+
"@loadmill/core": "0.3.37",
|
|
18
|
+
"@loadmill/universal": "0.3.30",
|
|
19
19
|
"@types/estree": "^0.0.50",
|
|
20
20
|
"acorn": "^8.4.1",
|
|
21
21
|
"agentkeepalive": "^4.1.3",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"jsedn": "^0.4.1",
|
|
24
24
|
"lodash": "^4.17.21",
|
|
25
25
|
"randomstring": "^1.1.5",
|
|
26
|
-
"superagent": "^
|
|
26
|
+
"superagent": "^6.1.0",
|
|
27
27
|
"urijs": "^1.18.1",
|
|
28
|
-
"vm2": "^3.9.
|
|
28
|
+
"vm2": "^3.9.4"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Histogram } from './failures';
|
|
2
|
+
|
|
3
|
+
export class RequestFailuresError extends Error {
|
|
4
|
+
constructor(message: string, public histogram: Histogram = { [message]: 1 }) {
|
|
5
|
+
super(message);
|
|
6
|
+
|
|
7
|
+
// Workaround suggested in: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
|
8
|
+
Object.setPrototypeOf(this, RequestFailuresError.prototype);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -5,12 +5,14 @@ import {
|
|
|
5
5
|
CheerioExtractor,
|
|
6
6
|
JsonPathExtractor,
|
|
7
7
|
ExpressionExtractor,
|
|
8
|
-
ParametrizedExtractor
|
|
8
|
+
ParametrizedExtractor,
|
|
9
|
+
WsExtractor,
|
|
9
10
|
} from '@loadmill/core/dist/parameters/extractors';
|
|
10
11
|
import { Parameters } from '@loadmill/core/dist/parameters';
|
|
11
12
|
import { Extraction } from '@loadmill/core/dist/request';
|
|
12
13
|
import log from '@loadmill/universal/dist/log';
|
|
13
14
|
import ednParser from 'jsedn';
|
|
15
|
+
import { WSExtractionData } from '@loadmill/core/dist/parameters/extractors/ws-extractor';
|
|
14
16
|
|
|
15
17
|
export class ExtractionCombiner {
|
|
16
18
|
headerExtractor: HeaderExtractor;
|
|
@@ -19,19 +21,19 @@ export class ExtractionCombiner {
|
|
|
19
21
|
ednPathExtractor: JsonPathExtractor;
|
|
20
22
|
expressionExtractor: ExpressionExtractor;
|
|
21
23
|
|
|
22
|
-
constructor(private contextParameters: Parameters, private res: any) {
|
|
24
|
+
constructor(private contextParameters: Parameters, private res: any, private wsExtractionData: WSExtractionData) {
|
|
23
25
|
this.headerExtractor = new HeaderExtractor(res, this.contextParameters);
|
|
24
26
|
this.expressionExtractor = new ExpressionExtractor(this.contextParameters);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
combine(extraction: Extraction) {
|
|
29
|
+
async combine(extraction: Extraction) {
|
|
28
30
|
let query: string | string[], extractor!: ParametrizedExtractor;
|
|
29
31
|
|
|
30
32
|
if (typeof extraction === 'string') {
|
|
31
33
|
query = extraction;
|
|
32
34
|
extractor = this.expressionExtractor;
|
|
33
35
|
} else {
|
|
34
|
-
const { header, jQuery, jsonPath, edn, regex } = extraction;
|
|
36
|
+
const { header, jQuery, jsonPath, edn, regex, ws } = extraction;
|
|
35
37
|
|
|
36
38
|
if (header != null) {
|
|
37
39
|
query = header;
|
|
@@ -88,6 +90,15 @@ export class ExtractionCombiner {
|
|
|
88
90
|
|
|
89
91
|
query = regex;
|
|
90
92
|
}
|
|
93
|
+
|
|
94
|
+
if (ws != null) {
|
|
95
|
+
extractor = new WsExtractor(
|
|
96
|
+
this.wsExtractionData,
|
|
97
|
+
this.contextParameters
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
query = ws;
|
|
101
|
+
}
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
return () => extractor.extract(query);
|
package/src/mill-version.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Engine, VirtualMachine, VirtualMachineOptions } from './virtual-machine
|
|
|
5
5
|
|
|
6
6
|
export class VM2VirtualMachine implements VirtualMachine {
|
|
7
7
|
public engine: VM;
|
|
8
|
-
private DEFAULT_TIMOUT_MS = 1000;
|
|
8
|
+
private DEFAULT_TIMOUT_MS = Number(process.env.POSTSCRIPT_TIMEOUT_MS) || 1000;
|
|
9
9
|
|
|
10
10
|
create(options: VirtualMachineOptions): Engine {
|
|
11
11
|
const { timeout, globalObject, async, staticObject } = options;
|
package/src/sampler.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Failures, mergeFailures } from './failures';
|
|
|
7
7
|
import { PerRequestStats, setReqStats } from './request-stats';
|
|
8
8
|
import { Work } from './work';
|
|
9
9
|
import { messageCreators } from './message-creators';
|
|
10
|
+
import { DEFAULT_DURATION, DEFAULT_ITERATION_DELAY } from '@loadmill/core/dist/conf/defaults';
|
|
10
11
|
|
|
11
12
|
// 5 minutes:
|
|
12
13
|
const RES_KEEP_TIME = 5 * 60 * 1000;
|
|
@@ -34,7 +35,7 @@ export class Sampler {
|
|
|
34
35
|
startSampling = () => {
|
|
35
36
|
this.samplerMill.cancelWork(false);
|
|
36
37
|
|
|
37
|
-
promiseUtils.delay(this.work.duration).then(
|
|
38
|
+
promiseUtils.delay(this.work.duration || DEFAULT_DURATION).then(
|
|
38
39
|
() => {
|
|
39
40
|
if (!this.samplerMill.isStopped) {
|
|
40
41
|
log.debug('Time\'s up!');
|
|
@@ -45,7 +46,7 @@ export class Sampler {
|
|
|
45
46
|
);
|
|
46
47
|
|
|
47
48
|
const requests = this.work.requests;
|
|
48
|
-
const iterationDelay = this.work.iterationDelay;
|
|
49
|
+
const iterationDelay = this.work.iterationDelay || DEFAULT_ITERATION_DELAY;
|
|
49
50
|
|
|
50
51
|
const reportDelayy = reportDelay(iterationDelay, requests);
|
|
51
52
|
this.samplerMill.reportIntervalId = setInterval(
|
|
@@ -93,7 +94,7 @@ export class Sampler {
|
|
|
93
94
|
const skipCount = res.lastStartedIndex + 1;
|
|
94
95
|
await promiseUtils.delay(
|
|
95
96
|
minimalRemainingDuration(
|
|
96
|
-
this.work.iterationDelay,
|
|
97
|
+
this.work.iterationDelay || DEFAULT_ITERATION_DELAY,
|
|
97
98
|
this.work.requests,
|
|
98
99
|
skipCount
|
|
99
100
|
)
|