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 +5 -1
- 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
|
@@ -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
|
-
|
|
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
|
|