loadtest 7.0.0 → 7.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 CHANGED
@@ -28,8 +28,9 @@ Versions 6 and later should be used at least with Node.js v16 or later:
28
28
 
29
29
  * Node.js v16 or later: ^6.0.0
30
30
  * Node.js v10 or later: ^5.0.0
31
- * Node.js v8 or later: 4.x.y.
32
- * Node.js v6 or earlier: ^3.1.0.
31
+ * Node.js v8 or later: 4.x.y
32
+ * Node.js v6 or earlier: ^3.1.0
33
+ * ES5 support (no `let`, `const` or arrow functions): ^2.0.0.
33
34
 
34
35
  ## Usage
35
36
 
@@ -557,15 +558,14 @@ const server = await startServer({port: 8000})
557
558
  await server.close()
558
559
  ```
559
560
 
560
- The following options are available:
561
+ The following options are available,
562
+ see [doc/api.md](doc/api.md) for details.
563
+
561
564
  * `port`: optional port to use for the server, default 7357.
562
565
  * `delay`: milliseconds to wait before answering each request.
563
566
  * `error`: HTTP status code to return, default 200 (no error).
564
567
  * `percent`: return error only for the given % of requests.
565
- * `logger(request, response)`
566
-
567
- A function to be called as `logger(request, response)` after every request served by the test server.
568
- Where `request` and `response` are the usual HTTP objects.
568
+ * `logger(request, response)`: function to call after every request.
569
569
 
570
570
  ### Configuration file
571
571
 
@@ -611,14 +611,8 @@ For more information about the actual configuration file name, read the [confino
611
611
 
612
612
  ### Complete Example
613
613
 
614
- The file `test/integration.js` shows a complete example, which is also a full integration test:
615
- it starts the server, send 1000 requests, waits for the callback and closes down the server.
616
-
617
- ## Versioning
618
-
619
- Version 3.x uses ES2015 (ES6) features,
620
- such as `const` or `let` and arrow functions.
621
- For ES5 support please use versions 2.x.
614
+ The file `test/integration.js` contains complete examples, which are also a full integration test suite:
615
+ they start the server with different options, send requests, waits for finalization and close down the server.
622
616
 
623
617
  ## Licensed under The MIT License
624
618
 
package/lib/baseClient.js CHANGED
@@ -3,9 +3,9 @@ import {addUserAgent} from './headers.js'
3
3
 
4
4
 
5
5
  export class BaseClient {
6
- constructor(operation, params) {
7
- this.operation = operation;
8
- this.params = params;
6
+ constructor(loadTest, options) {
7
+ this.loadTest = loadTest;
8
+ this.options = options;
9
9
  this.generateMessage = undefined;
10
10
  }
11
11
 
@@ -22,44 +22,44 @@ export class BaseClient {
22
22
  errorCode = '-1';
23
23
  }
24
24
  }
25
- this.operation.latency.end(id, errorCode);
25
+ this.loadTest.latency.end(id, errorCode);
26
26
  let callback;
27
- if (!this.params.requestsPerSecond) {
27
+ if (!this.options.requestsPerSecond) {
28
28
  callback = () => this.makeRequest();
29
29
  }
30
- this.operation.callback(error, result, callback);
30
+ this.loadTest.finishRequest(error, result, callback);
31
31
  };
32
32
  }
33
33
 
34
34
  /**
35
- * Init options and message to send.
35
+ * Init params and message to send.
36
36
  */
37
37
  init() {
38
- this.options = urlLib.parse(this.params.url);
39
- this.options.headers = {};
40
- if (this.params.headers) {
41
- this.options.headers = this.params.headers;
38
+ this.params = urlLib.parse(this.options.url);
39
+ this.params.headers = {};
40
+ if (this.options.headers) {
41
+ this.params.headers = this.options.headers;
42
42
  }
43
- if (this.params.cert && this.params.key) {
44
- this.options.cert = this.params.cert;
45
- this.options.key = this.params.key;
43
+ if (this.options.cert && this.options.key) {
44
+ this.params.cert = this.options.cert;
45
+ this.params.key = this.options.key;
46
46
  }
47
- this.options.agent = false;
48
- if (this.params.body) {
49
- if (typeof this.params.body == 'string') {
50
- this.generateMessage = () => this.params.body;
51
- } else if (typeof this.params.body == 'object') {
52
- this.generateMessage = () => this.params.body;
53
- } else if (typeof this.params.body == 'function') {
54
- this.generateMessage = this.params.body;
47
+ this.params.agent = false;
48
+ if (this.options.body) {
49
+ if (typeof this.options.body == 'string') {
50
+ this.generateMessage = () => this.options.body;
51
+ } else if (typeof this.options.body == 'object') {
52
+ this.generateMessage = () => this.options.body;
53
+ } else if (typeof this.options.body == 'function') {
54
+ this.generateMessage = this.options.body;
55
55
  } else {
56
- console.error('Unrecognized body: %s', typeof this.params.body);
56
+ console.error('Unrecognized body: %s', typeof this.options.body);
57
57
  }
58
- this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
58
+ this.params.headers['Content-Type'] = this.options.contentType || 'text/plain';
59
59
  }
60
- addUserAgent(this.options.headers);
61
- if (this.params.secureProtocol) {
62
- this.options.secureProtocol = this.params.secureProtocol;
60
+ addUserAgent(this.params.headers);
61
+ if (this.options.secureProtocol) {
62
+ this.params.secureProtocol = this.options.secureProtocol;
63
63
  }
64
64
  }
65
65
  }
package/lib/httpClient.js CHANGED
@@ -15,94 +15,94 @@ let uniqueIndex = 1
15
15
  * Create a new HTTP client.
16
16
  * Seem parameters below.
17
17
  */
18
- export function create(operation, params) {
19
- return new HttpClient(operation, params);
18
+ export function create(loadTest, options) {
19
+ return new HttpClient(loadTest, options);
20
20
  }
21
21
 
22
22
  /**
23
- * A client for an HTTP connection.
24
- * Operation is an object which has these attributes:
25
- * - latency: a variable to measure latency.
26
- * - running: if the operation is running or not.
27
- * Params is an object with the same options as exports.loadTest.
23
+ * A client for an HTTP connection. Constructor:
24
+ * - `loadTest`: an object with the following attributes:
25
+ * - latency: a variable to measure latency.
26
+ * - running: if the loadTest is running or not.
27
+ * - `options`: same options as exports.loadTest.
28
28
  */
29
29
  class HttpClient {
30
- constructor(operation, params) {
31
- this.operation = operation
32
- this.params = params
30
+ constructor(loadTest, options) {
31
+ this.loadTest = loadTest
32
+ this.options = options
33
33
  this.stopped = false
34
34
  this.init();
35
35
  }
36
36
 
37
37
  /**
38
- * Init options and message to send.
38
+ * Init and message to send.
39
39
  */
40
40
  init() {
41
- this.options = urlLib.parse(this.params.url);
42
- this.options.headers = this.params.headers || {}
43
- if (this.params.cert && this.params.key) {
44
- this.options.cert = this.params.cert;
45
- this.options.key = this.params.key;
41
+ this.params = urlLib.parse(this.options.url);
42
+ this.params.headers = this.options.headers || {}
43
+ if (this.options.cert && this.options.key) {
44
+ this.params.cert = this.options.cert;
45
+ this.params.key = this.options.key;
46
46
  }
47
- this.options.agent = false;
48
- if (this.params.requestsPerSecond) {
47
+ this.params.agent = false;
48
+ if (this.options.requestsPerSecond) {
49
49
  // rps for each client is total / concurrency (# of clients)
50
- this.options.requestsPerSecond = this.params.requestsPerSecond / this.params.concurrency
50
+ this.params.requestsPerSecond = this.options.requestsPerSecond / this.options.concurrency
51
51
  }
52
- if (this.params.agentKeepAlive) {
53
- const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
52
+ if (this.options.agentKeepAlive) {
53
+ const KeepAlive = (this.params.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
54
54
  let maxSockets = 10;
55
- if (this.options.requestsPerSecond) {
56
- maxSockets += Math.floor(this.options.requestsPerSecond);
55
+ if (this.params.requestsPerSecond) {
56
+ maxSockets += Math.floor(this.params.requestsPerSecond);
57
57
  }
58
- this.options.agent = new KeepAlive({
58
+ this.params.agent = new KeepAlive({
59
59
  maxSockets: maxSockets,
60
60
  maxKeepAliveRequests: 0, // max requests per keepalive socket, default is 0, no limit
61
61
  maxKeepAliveTime: 30000 // keepalive for 30 seconds
62
62
  });
63
63
  }
64
- if (this.params.method) {
65
- this.options.method = this.params.method;
64
+ if (this.options.method) {
65
+ this.params.method = this.options.method;
66
66
  }
67
- if (this.params.body) {
68
- if (typeof this.params.body == 'string') {
69
- this.generateMessage = () => this.params.body;
70
- } else if (typeof this.params.body == 'object') {
71
- if (this.params.contentType === 'application/x-www-form-urlencoded') {
72
- this.params.body = querystring.stringify(this.params.body);
67
+ if (this.options.body) {
68
+ if (typeof this.options.body == 'string') {
69
+ this.generateMessage = () => this.options.body;
70
+ } else if (typeof this.options.body == 'object') {
71
+ if (this.options.contentType === 'application/x-www-form-urlencoded') {
72
+ this.options.body = querystring.stringify(this.options.body);
73
73
  }
74
- this.generateMessage = () => this.params.body;
75
- } else if (typeof this.params.body == 'function') {
76
- this.generateMessage = this.params.body;
74
+ this.generateMessage = () => this.options.body;
75
+ } else if (typeof this.options.body == 'function') {
76
+ this.generateMessage = this.options.body;
77
77
  } else {
78
- throw new Error(`Unrecognized body: ${typeof this.params.body}`);
78
+ throw new Error(`Unrecognized body: ${typeof this.options.body}`);
79
79
  }
80
- this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
80
+ this.params.headers['Content-Type'] = this.options.contentType || 'text/plain';
81
81
  }
82
- if (this.params.cookies) {
83
- if (Array.isArray(this.params.cookies)) {
84
- this.options.headers.Cookie = this.params.cookies.join('; ');
85
- } else if (typeof this.params.cookies == 'string') {
86
- this.options.headers.Cookie = this.params.cookies;
82
+ if (this.options.cookies) {
83
+ if (Array.isArray(this.options.cookies)) {
84
+ this.params.headers.Cookie = this.options.cookies.join('; ');
85
+ } else if (typeof this.options.cookies == 'string') {
86
+ this.params.headers.Cookie = this.options.cookies;
87
87
  } else {
88
- throw new Error(`Invalid cookies ${JSON.stringify(this.params.cookies)}, please use an array or a string`);
88
+ throw new Error(`Invalid cookies ${JSON.stringify(this.options.cookies)}, please use an array or a string`);
89
89
  }
90
90
  }
91
- addUserAgent(this.options.headers);
92
- if (this.params.secureProtocol) {
93
- this.options.secureProtocol = this.params.secureProtocol;
91
+ addUserAgent(this.params.headers);
92
+ if (this.options.secureProtocol) {
93
+ this.params.secureProtocol = this.options.secureProtocol;
94
94
  }
95
95
  // adding proxy configuration
96
- if (this.params.proxy) {
97
- const proxy = this.params.proxy;
96
+ if (this.options.proxy) {
97
+ const proxy = this.options.proxy;
98
98
  const agent = new HttpsProxyAgent(proxy);
99
- this.options.agent = agent;
99
+ this.params.agent = agent;
100
100
  }
101
- if (this.params.indexParam) {
102
- this.options.indexParamFinder = new RegExp(this.params.indexParam, 'g');
101
+ if (this.options.indexParam) {
102
+ this.params.indexParamFinder = new RegExp(this.options.indexParam, 'g');
103
103
  }
104
104
  // Disable certificate checking
105
- if (this.params.insecure === true) {
105
+ if (this.options.insecure === true) {
106
106
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
107
107
  }
108
108
  }
@@ -116,10 +116,10 @@ class HttpClient {
116
116
  // so sometimes they are stopped before they have even started
117
117
  return
118
118
  }
119
- if (!this.options.requestsPerSecond) {
119
+ if (!this.params.requestsPerSecond) {
120
120
  return this.makeRequest();
121
121
  }
122
- const interval = 1000 / this.options.requestsPerSecond;
122
+ const interval = 1000 / this.params.requestsPerSecond;
123
123
  this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
124
124
  }
125
125
 
@@ -137,159 +137,147 @@ class HttpClient {
137
137
  * Make a single request to the server.
138
138
  */
139
139
  makeRequest() {
140
- if (!this.operation.running) {
140
+ if (!this.loadTest.running) {
141
141
  return;
142
142
  }
143
- if (this.operation.options.maxRequests && this.operation.requests >= this.operation.options.maxRequests) return
144
- this.operation.requests += 1;
143
+ if (this.loadTest.options.maxRequests && this.loadTest.requests >= this.loadTest.options.maxRequests) return
144
+ this.loadTest.requests += 1;
145
145
 
146
- const id = this.operation.latency.start();
147
- const options = {...this.options, headers: {...this.options.headers}}
148
- const requestFinished = this.getRequestFinisher(id);
149
- this.customizeIndex(options)
150
- const request = this.getRequest(id, options, requestFinished)
151
- if (this.params.timeout) {
152
- const timeout = parseInt(this.params.timeout);
146
+ const id = this.loadTest.latency.start();
147
+ const params = {...this.params, headers: {...this.params.headers}}
148
+ this.customizeIndex(params)
149
+ const request = this.getRequest(id, params)
150
+ if (this.options.timeout) {
151
+ const timeout = parseInt(this.options.timeout);
153
152
  if (!timeout) {
154
- console.error('Invalid timeout %s', this.params.timeout);
153
+ console.error(`Invalid timeout ${this.options.timeout}`);
155
154
  }
156
155
  request.setTimeout(timeout, () => {
157
- requestFinished('Connection timed out');
156
+ this.finishRequest(id, 'Connection timed out');
158
157
  });
159
158
  }
160
- const message = this.getMessage(id, options)
159
+ const message = this.getMessage(id, params)
161
160
  if (message) {
162
161
  request.write(message);
163
- options.headers['Content-Length'] = Buffer.byteLength(message);
162
+ params.headers['Content-Length'] = Buffer.byteLength(message);
164
163
  }
165
164
  request.on('error', error => {
166
- requestFinished('Connection error: ' + error.message);
165
+ this.finishRequest(id, `Connection error: ${error.message}`);
167
166
  });
168
167
  request.end();
169
168
  }
170
169
 
171
- customizeIndex(options) {
172
- if (!this.options.indexParamFinder) {
170
+ customizeIndex(params) {
171
+ if (!this.params.indexParamFinder) {
173
172
  return
174
173
  }
175
- options.customIndex = this.getCustomIndex()
176
- options.path = this.options.path.replace(this.options.indexParamFinder, options.customIndex);
174
+ params.customIndex = this.getCustomIndex()
175
+ params.path = this.params.path.replace(this.params.indexParamFinder, params.customIndex);
177
176
  }
178
177
 
179
178
  getCustomIndex() {
180
- if (this.options.indexParamCallback instanceof Function) {
181
- return this.options.indexParamCallback();
179
+ if (this.params.indexParamCallback instanceof Function) {
180
+ return this.params.indexParamCallback();
182
181
  }
183
182
  const customIndex = uniqueIndex
184
183
  uniqueIndex += 1
185
184
  return customIndex
186
185
  }
187
186
 
188
- getLib() {
189
- if (this.options.protocol == 'https:') {
190
- return https;
191
- }
192
- if (this.options.protocol == 'ws:') {
193
- return websocket;
194
- }
195
- return http;
196
- }
197
-
198
- getMessage(id, options) {
187
+ getMessage(id, params) {
199
188
  if (!this.generateMessage) {
200
189
  return
201
190
  }
202
191
  const candidate = this.generateMessage(id);
203
192
  const message = typeof candidate === 'object' ? JSON.stringify(candidate) : candidate
204
- if (this.options.indexParamFinder) {
205
- return message.replace(this.options.indexParamFinder, options.customIndex);
193
+ if (this.params.indexParamFinder) {
194
+ return message.replace(this.params.indexParamFinder, params.customIndex);
206
195
  }
207
196
  return message
208
197
  }
209
198
 
210
- getRequest(id, options, requestFinished) {
199
+ getRequest(id, params) {
211
200
  const lib = this.getLib()
212
- if (typeof this.params.requestGenerator == 'function') {
213
- const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
214
- return this.params.requestGenerator(this.params, options, lib.request, connect);
201
+ if (typeof this.options.requestGenerator == 'function') {
202
+ return this.options.requestGenerator(this.options, params, lib.request, connection => this.connect(connection, id))
215
203
  }
216
- return lib.request(options, this.getConnect(id, requestFinished, this.params.contentInspector));
204
+ return lib.request(params, connection => this.connect(connection, id))
217
205
  }
218
206
 
219
- /**
220
- * Get a function that finishes one request and goes for the next.
221
- */
222
- getRequestFinisher(id) {
223
- return (error, result) => {
224
- let errorCode = null;
225
- if (error) {
226
- if (result) {
227
- errorCode = result.statusCode;
228
- if (result.customErrorCode !== undefined) {
229
- errorCode = errorCode + ":" + result.customErrorCode
230
- }
231
- } else {
232
- errorCode = '-1';
207
+ getLib() {
208
+ if (this.params.protocol == 'https:') {
209
+ return https;
210
+ }
211
+ if (this.params.protocol == 'ws:') {
212
+ return websocket;
213
+ }
214
+ return http;
215
+ }
216
+
217
+ finishRequest(id, error, result) {
218
+ let errorCode = null;
219
+ if (error) {
220
+ if (result) {
221
+ errorCode = result.statusCode;
222
+ if (result.customErrorCode !== undefined) {
223
+ errorCode = errorCode + ":" + result.customErrorCode
233
224
  }
225
+ } else {
226
+ errorCode = '-1';
234
227
  }
228
+ }
229
+ const elapsed = this.loadTest.latency.end(id, errorCode);
230
+ if (elapsed < 0) {
231
+ // not found or not running
232
+ return;
233
+ }
234
+ if (result) {
235
+ result.requestElapsed = elapsed;
236
+ }
237
+ this.loadTest.finishRequest(error, result, () => this.makeRequest());
238
+ }
235
239
 
236
- const elapsed = this.operation.latency.end(id, errorCode);
237
- if (elapsed < 0) {
238
- // not found or not running
239
- return;
240
+ connect(connection, id) {
241
+ const bodyBuffers = []
242
+ connection.on('data', chunk => {
243
+ bodyBuffers.push(chunk)
244
+ });
245
+ connection.on('error', error => {
246
+ this.finishRequest(id, `Connection ${id} failed: ${error}`);
247
+ });
248
+ connection.on('end', () => {
249
+ const body = Buffer.concat(bodyBuffers).toString()
250
+ const result = this.createResult(connection, body)
251
+ if (this.options.contentInspector) {
252
+ this.options.contentInspector(result)
240
253
  }
241
- const index = this.operation.latency.getRequestIndex(id);
242
- if (result) {
243
- result.requestElapsed = elapsed;
244
- result.requestIndex = index;
245
- result.instanceIndex = this.operation.instanceIndex;
254
+ if (connection.statusCode >= 400) {
255
+ return this.finishRequest(id, `Status code ${connection.statusCode}`, result);
246
256
  }
247
- let callback;
248
- if (!this.options.requestsPerSecond) {
249
- callback = this.makeRequest.bind(this);
257
+ if (result?.customError) {
258
+ return this.finishRequest(id, `Custom error: ${result.customError}`, result);
250
259
  }
251
- this.operation.callback(error, result, callback);
252
- };
260
+ this.finishRequest(id, null, result);
261
+ });
253
262
  }
254
263
 
255
- /**
256
- * Get a function to connect the player.
257
- */
258
- getConnect(id, callback, contentInspector) {
259
- let body = '';
260
- return connection => {
261
- connection.setEncoding('utf8');
262
- connection.on('data', chunk => {
263
- body += chunk;
264
- });
265
- connection.on('error', error => {
266
- callback('Connection ' + id + ' failed: ' + error, '1');
267
- });
268
- connection.on('end', () => {
269
- const client = connection.connection || connection.client
270
- const result = {
271
- host: client._host,
272
- path: connection.req.path,
273
- method: connection.req.method,
274
- statusCode: connection.statusCode,
275
- body: body,
276
- headers: connection.headers,
277
- };
278
- if (connection.req.labels) {
279
- result.labels = connection.req.labels
280
- }
281
- if (contentInspector) {
282
- contentInspector(result)
283
- }
284
- if (connection.statusCode >= 400) {
285
- return callback('Status code ' + connection.statusCode, result);
286
- }
287
- if (result.customError) {
288
- return callback('Custom error: ' + result.customError, result);
289
- }
290
- callback(null, result);
291
- });
264
+ createResult(connection, body) {
265
+ if (!this.options.statusCallback && !this.options.contentInspector) {
266
+ return null
267
+ }
268
+ const client = connection.connection || connection.client
269
+ const result = {
270
+ host: client._host,
271
+ path: connection.req.path,
272
+ method: connection.req.method,
273
+ statusCode: connection.statusCode,
274
+ body,
275
+ headers: connection.headers,
292
276
  };
277
+ if (connection.req.labels) {
278
+ result.labels = connection.req.labels
279
+ }
280
+ return result
293
281
  }
294
282
  }
295
283
 
package/lib/latency.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as crypto from 'crypto'
1
+ import {randomUUID} from 'crypto'
2
2
  import {Result} from './result.js'
3
3
 
4
4
 
@@ -13,7 +13,7 @@ export class Latency {
13
13
  constructor(options, callback) {
14
14
  this.options = options
15
15
  this.callback = callback
16
- this.requests = {};
16
+ this.requests = new Map();
17
17
  this.partialRequests = 0;
18
18
  this.partialTime = 0;
19
19
  this.partialErrors = 0;
@@ -28,25 +28,14 @@ export class Latency {
28
28
  this.errorCodes = {};
29
29
  this.running = true;
30
30
  this.totalsShown = false;
31
- this.requestIndex = 0;
32
- this.requestIdToIndex = {};
33
- }
34
-
35
- /**
36
- * Return the index of the request. This is useful for determining the order
37
- * in which requests returned values.
38
- */
39
- getRequestIndex(requestId) {
40
- return this.requestIdToIndex[requestId];
41
31
  }
42
32
 
43
33
  /**
44
34
  * Start the request with the given id.
45
35
  */
46
36
  start(requestId) {
47
- requestId = requestId || createId();
48
- this.requests[requestId] = this.getTimeNs();
49
- this.requestIdToIndex[requestId] = this.requestIndex++;
37
+ requestId = requestId || randomUUID();
38
+ this.requests.set(requestId, this.getTimeNs())
50
39
  return requestId;
51
40
  }
52
41
 
@@ -55,15 +44,15 @@ export class Latency {
55
44
  * Accepts an optional error code signaling an error.
56
45
  */
57
46
  end(requestId, errorCode) {
58
- if (!(requestId in this.requests)) {
47
+ if (!this.requests.has(requestId)) {
59
48
  return -2;
60
49
  }
61
50
  if (!this.running) {
62
51
  return -1;
63
52
  }
64
- const elapsed = this.getElapsedMs(this.requests[requestId]);
53
+ const elapsed = this.getElapsedMs(this.requests.get(requestId));
65
54
  this.add(elapsed, errorCode);
66
- delete this.requests[requestId];
55
+ this.requests.delete(requestId);
67
56
  return elapsed;
68
57
  }
69
58
 
@@ -77,7 +66,7 @@ export class Latency {
77
66
  this.totalTime += time;
78
67
  this.totalRequests++;
79
68
  if (errorCode) {
80
- errorCode = '' + errorCode;
69
+ errorCode = String(errorCode);
81
70
  this.partialErrors++;
82
71
  this.totalErrors++;
83
72
  if (!(errorCode in this.errorCodes)) {
@@ -111,15 +100,12 @@ export class Latency {
111
100
  meanLatencyMs: Math.round(meanTime * 10) / 10,
112
101
  rps: Math.round(this.partialRequests / elapsedSeconds)
113
102
  };
114
- let percent = '';
115
- if (this.options.maxRequests) {
116
- percent = ' (' + Math.round(100 * this.totalRequests / this.options.maxRequests) + '%)';
117
- }
103
+ const requestPercent = this.getRequestPercent()
118
104
  if (!this.options.quiet) {
119
- console.info('Requests: %s%s, requests per second: %s, mean latency: %s ms', this.totalRequests, percent, result.rps, result.meanLatencyMs);
105
+ console.info(`Requests: ${this.totalRequests}${requestPercent}, requests per second: ${result.rps}, mean latency: ${result.meanLatencyMs} ms`)
120
106
  if (this.totalErrors) {
121
- percent = Math.round(100 * 10 * this.totalErrors / this.totalRequests) / 10;
122
- console.info('Errors: %s, accumulated errors: %s, %s% of total requests', this.partialErrors, this.totalErrors, percent);
107
+ const errorPercent = Math.round(100 * 10 * this.totalErrors / this.totalRequests) / 10;
108
+ console.info(`Errors: ${this.partialErrors}, accumulated errors: ${this.totalErrors}, ${errorPercent}% of total requests`)
123
109
  }
124
110
  }
125
111
  this.partialTime = 0;
@@ -128,6 +114,13 @@ export class Latency {
128
114
  this.lastShownNs = this.getTimeNs();
129
115
  }
130
116
 
117
+ getRequestPercent() {
118
+ if (!this.options.maxRequests) {
119
+ return ''
120
+ }
121
+ return ` (${Math.round(100 * this.totalRequests / this.options.maxRequests)}%)`
122
+ }
123
+
131
124
  /**
132
125
  * Returns the current high-resolution real time in nanoseconds as a big int.
133
126
  * @return {*}
@@ -194,12 +187,3 @@ export class Latency {
194
187
  }
195
188
  }
196
189
 
197
- /**
198
- * Create a unique, random token.
199
- */
200
- function createId() {
201
- const value = '' + Date.now() + Math.random();
202
- const hash = crypto.createHash('sha256');
203
- return hash.update(value).digest('hex').toLowerCase();
204
- }
205
-
package/lib/loadtest.js CHANGED
@@ -47,12 +47,12 @@ export function loadTest(options, callback) {
47
47
 
48
48
  async function loadTestAsync(options) {
49
49
  const processed = await processOptions(options)
50
- return await runOperation(processed)
50
+ return await runLoadTest(processed)
51
51
  }
52
52
 
53
- function runOperation(options) {
53
+ function runLoadTest(options) {
54
54
  return new Promise((resolve, reject) => {
55
- const operation = new Operation(options, (error, result) => {
55
+ const operation = new LoadTest(options, (error, result) => {
56
56
  if (error) {
57
57
  return reject(error)
58
58
  }
@@ -63,14 +63,14 @@ function runOperation(options) {
63
63
  }
64
64
 
65
65
  /**
66
- * Used to keep track of individual load test Operation runs.
66
+ * Used to keep track of individual load test runs.
67
67
  */
68
68
  let operationInstanceIndex = 0;
69
69
 
70
70
  /**
71
71
  * A load test operation.
72
72
  */
73
- class Operation {
73
+ class LoadTest {
74
74
  constructor(options, callback) {
75
75
  this.options = options;
76
76
  this.finalCallback = callback;
@@ -82,6 +82,7 @@ class Operation {
82
82
  this.showTimer = null;
83
83
  this.stopTimeout = null;
84
84
  this.instanceIndex = operationInstanceIndex++;
85
+ this.requestIndex = 0
85
86
  }
86
87
 
87
88
  /**
@@ -98,19 +99,21 @@ class Operation {
98
99
  }
99
100
 
100
101
  /**
101
- * Call after each operation has finished.
102
+ * Call after each request has finished.
102
103
  */
103
- callback(error, result, next) {
104
+ finishRequest(error, result, next) {
104
105
  this.completedRequests += 1;
105
106
  if (this.options.maxRequests) {
106
107
  if (this.completedRequests == this.options.maxRequests) {
107
108
  this.stop();
108
109
  }
109
110
  }
110
- if (this.running && next) {
111
+ if (this.running && !this.requestsPerSecond) {
111
112
  next();
112
113
  }
113
114
  if (this.options.statusCallback) {
115
+ result.requestIndex = this.requestIndex++
116
+ result.instanceIndex = this.instanceIndex
114
117
  this.options.statusCallback(error, result);
115
118
  }
116
119
  }
package/lib/testserver.js CHANGED
@@ -103,9 +103,9 @@ class TestServer {
103
103
  */
104
104
  listen(request, response) {
105
105
  const id = this.latency.start();
106
- request.body = '';
106
+ const bodyBuffers = []
107
107
  request.on('data', data => {
108
- request.body += data.toString();
108
+ bodyBuffers.push(data)
109
109
  });
110
110
  request.on('error', () => {
111
111
  // ignore request
@@ -113,6 +113,7 @@ class TestServer {
113
113
  this.latency.end(id, -1);
114
114
  })
115
115
  request.on('end', () => {
116
+ request.body = Buffer.concat(bodyBuffers).toString();
116
117
  this.totalRequests += 1
117
118
  const elapsedMs = Date.now() - this.debuggedTime
118
119
  if (elapsedMs > LOG_HEADERS_INTERVAL_MS) {
package/lib/websocket.js CHANGED
@@ -7,16 +7,16 @@ let latency;
7
7
  /**
8
8
  * Create a client for a websocket.
9
9
  */
10
- export function create(operation, params) {
11
- return new WebsocketClient(operation, params);
10
+ export function create(loadTest, options) {
11
+ return new WebsocketClient(loadTest, options);
12
12
  }
13
13
 
14
14
  /**
15
15
  * A client that connects to a websocket.
16
16
  */
17
17
  class WebsocketClient extends BaseClient {
18
- constructor(operation, params) {
19
- super(operation, params);
18
+ constructor(loadTest, options) {
19
+ super(loadTest, options);
20
20
  this.connection = null;
21
21
  this.lastCall = null;
22
22
  this.client = null;
@@ -30,7 +30,7 @@ class WebsocketClient extends BaseClient {
30
30
  this.client = new websocket.client();
31
31
  this.client.on('connectFailed', () => {});
32
32
  this.client.on('connect', connection => this.connect(connection));
33
- this.client.connect(this.params.url, []);
33
+ this.client.connect(this.options.url, []);
34
34
  }
35
35
 
36
36
  /**
@@ -55,7 +55,7 @@ class WebsocketClient extends BaseClient {
55
55
  * Make a single request to the server.
56
56
  */
57
57
  makeRequest() {
58
- const id = this.operation.latency.start();
58
+ const id = this.loadTest.latency.start();
59
59
  const requestFinished = this.getRequestFinisher(id);
60
60
 
61
61
  if (this.connection.connected) {
@@ -115,7 +115,7 @@ class WebsocketClient extends BaseClient {
115
115
  if (this.generateMessage) {
116
116
  message = this.generateMessage(id);
117
117
  }
118
- if (typeof this.params.requestGenerator == 'function') {
118
+ if (typeof this.options.requestGenerator == 'function') {
119
119
  // create a 'fake' object which can function like the http client
120
120
  const req = () => {
121
121
  return {
@@ -124,7 +124,7 @@ class WebsocketClient extends BaseClient {
124
124
  }
125
125
  };
126
126
  };
127
- this.params.requestGenerator(this.params, this.options, req, requestFinished);
127
+ this.options.requestGenerator(this.options, this.params, req, requestFinished);
128
128
  } else {
129
129
  this.connection.sendUTF(JSON.stringify(message));
130
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtest",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "description": "Run load tests for your web application. Mostly ab-compatible interface, with an option to force requests per second. Includes an API for automated load testing.",
6
6
  "homepage": "https://github.com/alexfernandez/loadtest",
@@ -23,7 +23,8 @@
23
23
  "websocket": "^1.0.34"
24
24
  },
25
25
  "devDependencies": {
26
- "eslint": "^8.47.0"
26
+ "eslint": "^8.47.0",
27
+ "microprofiler": "^2.0.0"
27
28
  },
28
29
  "keywords": [
29
30
  "testing",