loadtest 6.3.2 → 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
@@ -511,7 +511,6 @@ const options = {
511
511
  const result = await loadTest(options)
512
512
  result.show()
513
513
  console.log('Tests run successfully')
514
- })
515
514
  ```
516
515
 
517
516
  The call returns a `Result` object that contains all info about the load test, also described below.
@@ -884,6 +883,11 @@ Return an HTTP error code.
884
883
  Return an HTTP error code only for the given % of requests.
885
884
  If no error code was specified, default is 500.
886
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
+
887
891
  ### Configuration file
888
892
 
889
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.2",
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