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 +17 -38
- package/lib/httpClient.js +68 -36
- package/lib/loadtest.js +12 -27
- package/lib/testserver.js +7 -3
- package/package.json +1 -1
- package/test/integration.js +30 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
* -
|
|
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
|
-
|
|
124
|
-
|
|
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
|
+
"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",
|
package/test/integration.js
CHANGED
|
@@ -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,
|
|
195
|
+
testIntegration, testIntegrationFile, testDelay, testWSIntegration,
|
|
196
|
+
testPromise, testIndexParam,
|
|
168
197
|
], 4000, callback);
|
|
169
198
|
}
|
|
170
199
|
|