loadtest 6.3.1 → 6.4.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
@@ -300,30 +300,9 @@ Setting this to 0 disables timeout (default).
300
300
 
301
301
  #### `-R requestGeneratorModule.js`
302
302
 
303
- Use custom request generator function from an external file.
304
-
305
- Example request generator module could look like this:
306
-
307
- ```javascript
308
- module.exports = function(params, options, client, callback) {
309
- generateMessageAsync(function(message) {
310
-
311
- if (message)
312
- {
313
- options.headers['Content-Length'] = message.length;
314
- options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
315
- }
316
- request = client(options, callback);
317
- if (message){
318
- request.write(message);
319
- }
320
-
321
- return request;
322
- }
323
- }
324
- ```
325
-
326
- See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
303
+ Use a custom request generator function from an external file.
304
+ See an example of a request generator module in [`--requestGenerator`](#requestGenerator) below.
305
+ Also see [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
327
306
  (or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
328
307
 
329
308
  #### `--agent` (deprecated)
@@ -532,7 +511,6 @@ const options = {
532
511
  const result = await loadTest(options)
533
512
  result.show()
534
513
  console.log('Tests run successfully')
535
- })
536
514
  ```
537
515
 
538
516
  The call returns a `Result` object that contains all info about the load test, also described below.
@@ -665,24 +643,20 @@ How many requests each client will send per second.
665
643
 
666
644
  #### `requestGenerator`
667
645
 
668
- Custom request generator function.
646
+ Use a custom request generator function.
647
+ The request needs to be generated synchronously and returned when this function is invoked.
669
648
 
670
649
  Example request generator function could look like this:
671
650
 
672
651
  ```javascript
673
652
  function(params, options, client, callback) {
674
- generateMessageAsync(function(message)) {
675
- request = client(options, callback);
676
-
677
- if (message)
678
- {
679
- options.headers['Content-Length'] = message.length;
680
- options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
681
- request.write(message);
682
- }
683
-
684
- request.end();
685
- }
653
+ const message = generateMessage();
654
+ const request = client(options, callback);
655
+ options.headers['Content-Length'] = message.length;
656
+ options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
657
+ request.write(message);
658
+ request.end();
659
+ return request;
686
660
  }
687
661
  ```
688
662
 
@@ -909,6 +883,11 @@ Return an HTTP error code.
909
883
  Return an HTTP error code only for the given % of requests.
910
884
  If no error code was specified, default is 500.
911
885
 
886
+ #### `logger`
887
+
888
+ A function to be called as `logger(request, response)` after every request served by the test server.
889
+ Where `request` and `response` are the usual HTTP objects.
890
+
912
891
  ### Configuration file
913
892
 
914
893
  It is possible to put configuration options in a file named `.loadtestrc` in your working directory or in a file whose name is specified in the `loadtest` entry of your `package.json`. The options in the file will be used only if they are not specified in the command line.
package/lib/httpClient.js CHANGED
@@ -9,6 +9,8 @@ import * as agentkeepalive from 'agentkeepalive'
9
9
  import * as HttpsProxyAgent from 'https-proxy-agent'
10
10
 
11
11
 
12
+ let uniqueIndex = 1
13
+
12
14
  /**
13
15
  * Create a new HTTP client.
14
16
  * Seem parameters below.
@@ -90,6 +92,19 @@ class HttpClient {
90
92
  if (this.params.secureProtocol) {
91
93
  this.options.secureProtocol = this.params.secureProtocol;
92
94
  }
95
+ // adding proxy configuration
96
+ if (this.params.proxy) {
97
+ const proxy = this.params.proxy;
98
+ const agent = new HttpsProxyAgent(proxy);
99
+ this.options.agent = agent;
100
+ }
101
+ if (this.params.indexParam) {
102
+ this.options.indexParamFinder = new RegExp(this.params.indexParam, 'g');
103
+ }
104
+ // Disable certificate checking
105
+ if (this.params.insecure === true) {
106
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
107
+ }
93
108
  }
94
109
 
95
110
  /**
@@ -129,43 +144,10 @@ class HttpClient {
129
144
  this.operation.requests += 1;
130
145
 
131
146
  const id = this.operation.latency.start();
147
+ const options = {...this.options, headers: {...this.options.headers}}
132
148
  const requestFinished = this.getRequestFinisher(id);
133
- let lib = http;
134
- if (this.options.protocol == 'https:') {
135
- lib = https;
136
- }
137
- if (this.options.protocol == 'ws:') {
138
- lib = websocket;
139
- }
140
-
141
- // adding proxy configuration
142
- if (this.params.proxy) {
143
- const proxy = this.params.proxy;
144
- const agent = new HttpsProxyAgent(proxy);
145
- this.options.agent = agent;
146
- }
147
-
148
-
149
- // Disable certificate checking
150
- if (this.params.insecure === true) {
151
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
152
- }
153
- let request, message;
154
- if (this.generateMessage) {
155
- message = this.generateMessage(id);
156
- if (typeof message === 'object') {
157
- message = JSON.stringify(message);
158
- }
159
- this.options.headers['Content-Length'] = Buffer.byteLength(message);
160
- } else {
161
- delete this.options.headers['Content-Length'];
162
- }
163
- if (typeof this.params.requestGenerator == 'function') {
164
- const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
165
- request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
166
- } else {
167
- request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
168
- }
149
+ this.customizeIndex(options)
150
+ const request = this.getRequest(id, options, requestFinished)
169
151
  if (this.params.timeout) {
170
152
  const timeout = parseInt(this.params.timeout);
171
153
  if (!timeout) {
@@ -175,8 +157,10 @@ class HttpClient {
175
157
  requestFinished('Connection timed out');
176
158
  });
177
159
  }
160
+ const message = this.getMessage(id, options)
178
161
  if (message) {
179
162
  request.write(message);
163
+ options.headers['Content-Length'] = Buffer.byteLength(message);
180
164
  }
181
165
  request.on('error', error => {
182
166
  requestFinished('Connection error: ' + error.message);
@@ -184,6 +168,54 @@ class HttpClient {
184
168
  request.end();
185
169
  }
186
170
 
171
+ customizeIndex(options) {
172
+ if (!this.options.indexParamFinder) {
173
+ return
174
+ }
175
+ options.customIndex = this.getCustomIndex()
176
+ options.path = this.options.path.replace(this.options.indexParamFinder, options.customIndex);
177
+ }
178
+
179
+ getCustomIndex() {
180
+ if (this.options.indexParamCallback instanceof Function) {
181
+ return this.options.indexParamCallback();
182
+ }
183
+ const customIndex = uniqueIndex
184
+ uniqueIndex += 1
185
+ return customIndex
186
+ }
187
+
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) {
199
+ if (!this.generateMessage) {
200
+ return
201
+ }
202
+ const candidate = this.generateMessage(id);
203
+ const message = typeof candidate === 'object' ? JSON.stringify(candidate) : candidate
204
+ if (this.options.indexParamFinder) {
205
+ return message.replace(this.options.indexParamFinder, options.customIndex);
206
+ }
207
+ return message
208
+ }
209
+
210
+ getRequest(id, options, requestFinished) {
211
+ 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);
215
+ }
216
+ return lib.request(options, this.getConnect(id, requestFinished, this.params.contentInspector));
217
+ }
218
+
187
219
  /**
188
220
  * Get a function that finishes one request and goes for the next.
189
221
  */
package/lib/loadtest.js CHANGED
@@ -23,7 +23,7 @@ https.globalAgent.maxSockets = 1000;
23
23
  * - cookies [array]: a string or an array of strings, each with name:value.
24
24
  * - headers [map]: a map with headers: {key1: value1, key2: value2}.
25
25
  * - method [string]: the method to use: POST, PUT. Default: GET, what else.
26
- * - body [string]: the contents to send along a POST or PUT request.
26
+ * - data [string]: the contents to send along a POST or PUT request.
27
27
  * - contentType [string]: the MIME type to use for the body, default text/plain.
28
28
  * - requestsPerSecond [number]: how many requests per second to send.
29
29
  * - agentKeepAlive: if true, then use connection keep-alive.
@@ -33,6 +33,7 @@ https.globalAgent.maxSockets = 1000;
33
33
  * - proxy [string]: use a proxy for requests e.g. http://localhost:8080.
34
34
  * - quiet: do not log any messages.
35
35
  * - debug: show debug messages (deprecated).
36
+ * - requestGenerator [function]: use a custom function to generate requests.
36
37
  * - `callback`: optional `function(result, error)` called if/when the test finishes;
37
38
  * if not present a promise is returned.
38
39
  */
@@ -117,33 +118,9 @@ class Operation {
117
118
  * Start a number of measuring clients.
118
119
  */
119
120
  startClients() {
120
- const url = this.options.url;
121
- const strBody = JSON.stringify(this.options.body);
122
121
  for (let index = 0; index < this.options.concurrency; index++) {
123
- if (this.options.indexParam) {
124
- let oldToken = new RegExp(this.options.indexParam, 'g');
125
- if(this.options.indexParamCallback instanceof Function) {
126
- let customIndex = this.options.indexParamCallback();
127
- this.options.url = url.replace(oldToken, customIndex);
128
- if(this.options.body) {
129
- let body = strBody.replace(oldToken, customIndex);
130
- this.options.body = JSON.parse(body);
131
- }
132
- }
133
- else {
134
- this.options.url = url.replace(oldToken, index);
135
- if(this.options.body) {
136
- let body = strBody.replace(oldToken, index);
137
- this.options.body = JSON.parse(body);
138
- }
139
- }
140
- }
141
- let constructor = httpClient.create;
142
- // TODO: || this.options.url.startsWith('wss:'))
143
- if (this.options.url.startsWith('ws:')) {
144
- constructor = websocket.create;
145
- }
146
- const client = constructor(this, this.options);
122
+ const createClient = this.getClientCreator()
123
+ const client = createClient(this, this.options);
147
124
  this.clients[index] = client;
148
125
  if (!this.options.requestsPerSecond) {
149
126
  client.start();
@@ -155,6 +132,14 @@ class Operation {
155
132
  }
156
133
  }
157
134
 
135
+ getClientCreator() {
136
+ // TODO: || this.options.url.startsWith('wss:'))
137
+ if (this.options.url.startsWith('ws:')) {
138
+ return websocket.create;
139
+ }
140
+ return httpClient.create;
141
+ }
142
+
158
143
  /**
159
144
  * Stop clients.
160
145
  */
package/lib/testserver.js CHANGED
@@ -16,6 +16,7 @@ const LOG_HEADERS_INTERVAL_MS = 5000;
16
16
  * - quiet: do not log any messages.
17
17
  * - percent: give an error (default 500) on some % of requests.
18
18
  * - error: set an HTTP error code, default is 500.
19
+ * - logger: function to log all incoming requests.
19
20
  * - `callback`: optional callback, called after the server has started.
20
21
  * If not present will return a promise.
21
22
  */
@@ -118,10 +119,10 @@ class TestServer {
118
119
  this.debug(request, elapsedMs);
119
120
  }
120
121
  if (!this.options.delay) {
121
- return this.end(response, id);
122
+ return this.end(request, response, id);
122
123
  }
123
124
  setTimeout(() => {
124
- this.end(response, id);
125
+ this.end(request, response, id);
125
126
  }, this.options.delay).unref();
126
127
  });
127
128
  }
@@ -167,7 +168,7 @@ class TestServer {
167
168
  /**
168
169
  * End the response now.
169
170
  */
170
- end(response, id) {
171
+ end(request, response, id) {
171
172
  if (this.shouldError()) {
172
173
  const code = this.options.error || 500;
173
174
  response.writeHead(code);
@@ -176,6 +177,9 @@ class TestServer {
176
177
  response.end('OK');
177
178
  }
178
179
  this.latency.end(id);
180
+ if (this.options.logger) {
181
+ this.options.logger(request, response)
182
+ }
179
183
  }
180
184
 
181
185
  shouldError() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtest",
3
- "version": "6.3.1",
3
+ "version": "6.4.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",
@@ -159,12 +159,41 @@ async function testPromise() {
159
159
  return 'Test result: ' + JSON.stringify(result)
160
160
  }
161
161
 
162
+ async function testIndexParam() {
163
+ const urls = new Map()
164
+ const bodies = new Map()
165
+ function logger(request) {
166
+ if (urls.has(request.url)) {
167
+ throw new Error(`Duplicated url ${request.url}`)
168
+ }
169
+ urls.set(request.url, true)
170
+ if (bodies.has(request.body)) {
171
+ throw new Error(`Duplicated body ${request.body}`)
172
+ }
173
+ bodies.set(request.body, true)
174
+ }
175
+ const server = await startServer({logger, ...serverOptions})
176
+ const options = {
177
+ url: `http://localhost:${PORT}/?param=index`,
178
+ maxRequests: 100,
179
+ concurrency: 10,
180
+ postBody: {
181
+ hi: 'my_index',
182
+ },
183
+ indexParam: 'index',
184
+ quiet: true,
185
+ };
186
+ await loadTest(options)
187
+ await server.close()
188
+ }
189
+
162
190
  /**
163
191
  * Run all tests.
164
192
  */
165
193
  export function test(callback) {
166
194
  testing.run([
167
- testIntegration, testIntegrationFile, testDelay, testWSIntegration, testPromise,
195
+ testIntegration, testIntegrationFile, testDelay, testWSIntegration,
196
+ testPromise, testIndexParam,
168
197
  ], 4000, callback);
169
198
  }
170
199