loadtest 6.0.0 → 6.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 +31 -21
- package/bin/loadtest.js +4 -2
- package/lib/latency.js +16 -51
- package/lib/loadtest.js +40 -26
- package/lib/options.js +2 -6
- package/lib/show.js +47 -0
- package/lib/testserver.js +28 -23
- package/package.json +1 -1
- package/sample/request-generator.js +2 -2
- package/sample/request-generator.ts +2 -2
- package/test/body-generator.js +2 -1
- package/test/integration.js +36 -8
- package/test/latency.js +1 -1
- package/test/loadtest.js +14 -5
- package/test/request-generator.js +2 -1
- package/test/testserver.js +1 -0
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ but the resulting figure is much more robust.
|
|
|
84
84
|
`loadtest` is also quite extensible.
|
|
85
85
|
Using the provided API it is very easy to integrate loadtest with your package, and run programmatic load tests.
|
|
86
86
|
loadtest makes it very easy to run load tests as part of systems tests, before deploying a new version of your software.
|
|
87
|
-
The
|
|
87
|
+
The result includes mean response times and percentiles,
|
|
88
88
|
so that you can abort deployment e.g. if 99% of the requests don't finish in 10 ms or less.
|
|
89
89
|
|
|
90
90
|
### Usage Don'ts
|
|
@@ -111,6 +111,7 @@ The following parameters are compatible with Apache ab.
|
|
|
111
111
|
#### `-n requests`
|
|
112
112
|
|
|
113
113
|
Number of requests to send out.
|
|
114
|
+
Default is no limit; will keep on sending if not specified.
|
|
114
115
|
|
|
115
116
|
Note: the total number of requests sent can be bigger than the parameter if there is a concurrency parameter;
|
|
116
117
|
loadtest will report just the first `n`.
|
|
@@ -119,6 +120,7 @@ loadtest will report just the first `n`.
|
|
|
119
120
|
|
|
120
121
|
loadtest will create a certain number of clients; this parameter controls how many.
|
|
121
122
|
Requests from them will arrive concurrently to the server.
|
|
123
|
+
Default value is 1.
|
|
122
124
|
|
|
123
125
|
Note: requests are not sent in parallel (from different processes),
|
|
124
126
|
but concurrently (a second request may be sent before the first has been answered).
|
|
@@ -126,6 +128,7 @@ but concurrently (a second request may be sent before the first has been answere
|
|
|
126
128
|
#### `-t timelimit`
|
|
127
129
|
|
|
128
130
|
Max number of seconds to wait until requests no longer go out.
|
|
131
|
+
Default is no limit; will keep on sending if not specified.
|
|
129
132
|
|
|
130
133
|
Note: this is different than Apache `ab`, which stops _receiving_ requests after the given seconds.
|
|
131
134
|
|
|
@@ -175,15 +178,17 @@ Send the string as the PATCH body. E.g.: `-A '{"key": "a9acf03f"}'`
|
|
|
175
178
|
|
|
176
179
|
#### `-m method`
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
Set method that will be sent to the test URL.
|
|
182
|
+
Accepts: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`,
|
|
183
|
+
and lowercase versions. Default is `GET`.
|
|
184
|
+
Example: `-m POST`.
|
|
180
185
|
|
|
181
|
-
#### `--data
|
|
186
|
+
#### `--data body`
|
|
182
187
|
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
Add some data to send in the body. It does not support method GET.
|
|
189
|
+
Requires setting the method with `-m` and the type with `-T`.
|
|
190
|
+
Example: `--data '{"username": "test", "password": "test"}' -T 'application/x-www-form-urlencoded' -m POST`
|
|
185
191
|
|
|
186
|
-
It required `-m` and `-T 'application/x-www-form-urlencoded'`
|
|
187
192
|
|
|
188
193
|
#### `-p POST-file`
|
|
189
194
|
|
|
@@ -256,6 +261,7 @@ The following parameters are _not_ compatible with Apache ab.
|
|
|
256
261
|
|
|
257
262
|
Controls the number of requests per second that are sent.
|
|
258
263
|
Can be fractional, e.g. `--rps 0.5` sends one request every two seconds.
|
|
264
|
+
Not used by default: each request is sent as soon as the previous one is responded.
|
|
259
265
|
|
|
260
266
|
Note: Concurrency doesn't affect the final number of requests per second,
|
|
261
267
|
since rps will be shared by all the clients. E.g.:
|
|
@@ -309,17 +315,15 @@ Open connections using keep-alive.
|
|
|
309
315
|
|
|
310
316
|
Note: instead of using the default agent, this option is now an alias for `-k`.
|
|
311
317
|
|
|
312
|
-
#### `--quiet`
|
|
318
|
+
#### `--quiet`
|
|
313
319
|
|
|
314
320
|
Do not show any messages.
|
|
315
321
|
|
|
316
|
-
Note: deprecated in version 6+, shows a warning.
|
|
317
|
-
|
|
318
322
|
#### `--debug` (deprecated)
|
|
319
323
|
|
|
320
324
|
Show debug messages.
|
|
321
325
|
|
|
322
|
-
Note: deprecated in version 6
|
|
326
|
+
Note: deprecated in version 6+.
|
|
323
327
|
|
|
324
328
|
#### `--insecure`
|
|
325
329
|
|
|
@@ -383,7 +387,7 @@ with concurrency 10 (only relevant results are shown):
|
|
|
383
387
|
99% 14 ms
|
|
384
388
|
100% 35997 ms (longest request)
|
|
385
389
|
|
|
386
|
-
|
|
390
|
+
The result was quite erratic, with some requests taking up to 36 seconds;
|
|
387
391
|
this suggests that Node.js is queueing some requests for a long time, and answering them irregularly.
|
|
388
392
|
Now we will try a fixed rate of 1000 rps:
|
|
389
393
|
|
|
@@ -438,10 +442,10 @@ We now know that our server can accept 500 rps without problems.
|
|
|
438
442
|
Not bad for a single-process naïve Node.js server...
|
|
439
443
|
We may refine our results further to find at which point from 500 to 1000 rps our server breaks down.
|
|
440
444
|
|
|
441
|
-
But instead let us research how to improve the
|
|
445
|
+
But instead let us research how to improve the result.
|
|
442
446
|
One obvious candidate is to add keep-alive to the requests so we don't have to create
|
|
443
447
|
a new connection for every request.
|
|
444
|
-
The
|
|
448
|
+
The result (with the same test server) is impressive:
|
|
445
449
|
|
|
446
450
|
$ loadtest http://localhost:7357/ -t 20 -c 10 -k
|
|
447
451
|
...
|
|
@@ -659,11 +663,11 @@ loadTest(options, function(error) {
|
|
|
659
663
|
|
|
660
664
|
#### `statusCallback`
|
|
661
665
|
|
|
662
|
-
If present, this function executes after every request operation completes. Provides immediate access to test
|
|
666
|
+
If present, this function executes after every request operation completes. Provides immediate access to the test result while the
|
|
663
667
|
test batch is still running. This can be used for more detailed custom logging or developing your own spreadsheet or
|
|
664
|
-
statistical analysis of
|
|
668
|
+
statistical analysis of the result.
|
|
665
669
|
|
|
666
|
-
The
|
|
670
|
+
The result and error passed to the callback are in the same format as the result passed to the final callback.
|
|
667
671
|
|
|
668
672
|
In addition, the following three properties are added to the `result` object:
|
|
669
673
|
|
|
@@ -754,9 +758,9 @@ function contentInspector(result) {
|
|
|
754
758
|
},
|
|
755
759
|
```
|
|
756
760
|
|
|
757
|
-
###
|
|
761
|
+
### Result
|
|
758
762
|
|
|
759
|
-
The latency
|
|
763
|
+
The latency result passed to your callback at the end of the load test contains a full set of data, including:
|
|
760
764
|
mean latency, number of errors and percentiles.
|
|
761
765
|
An example follows:
|
|
762
766
|
|
|
@@ -796,14 +800,20 @@ The second parameter contains info about the current request:
|
|
|
796
800
|
|
|
797
801
|
### Start Test Server
|
|
798
802
|
|
|
799
|
-
To start the test server use the exported function `startServer()` with a set of options
|
|
803
|
+
To start the test server use the exported function `startServer()` with a set of options:
|
|
800
804
|
|
|
801
805
|
```javascript
|
|
802
806
|
import {startServer} from 'loadtest'
|
|
803
|
-
const server = startServer({port: 8000})
|
|
807
|
+
const server = await startServer({port: 8000})
|
|
804
808
|
```
|
|
805
809
|
|
|
806
810
|
This function returns an HTTP server which can be `close()`d when it is no longer useful.
|
|
811
|
+
As a legacy from before promises existed,
|
|
812
|
+
if the optional callback is passed then it will not behave as `async`:
|
|
813
|
+
|
|
814
|
+
```
|
|
815
|
+
const server = startServer({port: 8000}, error => console.error(error))
|
|
816
|
+
```
|
|
807
817
|
|
|
808
818
|
The following options are available.
|
|
809
819
|
|
package/bin/loadtest.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {readFile} from 'fs/promises'
|
|
4
4
|
import * as stdio from 'stdio'
|
|
5
5
|
import {loadTest} from '../lib/loadtest.js'
|
|
6
|
+
import {showResult} from '../lib/show.js'
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
const options = stdio.getopt({
|
|
@@ -31,8 +32,8 @@ const options = stdio.getopt({
|
|
|
31
32
|
insecure: {description: 'Allow self-signed certificates over https'},
|
|
32
33
|
key: {args: 1, description: 'The client key to use'},
|
|
33
34
|
cert: {args: 1, description: 'The client certificate to use'},
|
|
35
|
+
quiet: {description: 'Do not log any messages'},
|
|
34
36
|
agent: {description: 'Use a keep-alive http agent (deprecated)'},
|
|
35
|
-
quiet: {description: 'Do not log any messages (deprecated)'},
|
|
36
37
|
debug: {description: 'Show debug messages (deprecated)'}
|
|
37
38
|
});
|
|
38
39
|
|
|
@@ -52,7 +53,8 @@ async function processAndRun(options) {
|
|
|
52
53
|
}
|
|
53
54
|
options.url = options.args[0];
|
|
54
55
|
try {
|
|
55
|
-
loadTest(options)
|
|
56
|
+
const result = await loadTest(options)
|
|
57
|
+
showResult(options, result)
|
|
56
58
|
} catch(error) {
|
|
57
59
|
console.error(error.message)
|
|
58
60
|
help()
|
package/lib/latency.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as crypto from 'crypto'
|
|
2
|
+
import {showResult} from './show.js'
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Latency measurements. Options can be:
|
|
6
7
|
* - maxRequests: max number of requests to measure before stopping.
|
|
7
8
|
* - maxSeconds: max seconds, alternative to max requests.
|
|
8
|
-
* An optional callback(error,
|
|
9
|
-
* or the
|
|
9
|
+
* An optional callback(error, result) will be called with an error,
|
|
10
|
+
* or the result after max is reached.
|
|
10
11
|
*/
|
|
11
12
|
export class Latency {
|
|
12
13
|
constructor(options, callback) {
|
|
@@ -106,7 +107,7 @@ export class Latency {
|
|
|
106
107
|
showPartial() {
|
|
107
108
|
const elapsedSeconds = this.getElapsed(this.lastShown) / 1000;
|
|
108
109
|
const meanTime = this.partialTime / this.partialRequests || 0.0;
|
|
109
|
-
const
|
|
110
|
+
const result = {
|
|
110
111
|
meanLatencyMs: Math.round(meanTime * 10) / 10,
|
|
111
112
|
rps: Math.round(this.partialRequests / elapsedSeconds)
|
|
112
113
|
};
|
|
@@ -114,10 +115,12 @@ export class Latency {
|
|
|
114
115
|
if (this.options.maxRequests) {
|
|
115
116
|
percent = ' (' + Math.round(100 * this.totalRequests / this.options.maxRequests) + '%)';
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
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);
|
|
120
|
+
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);
|
|
123
|
+
}
|
|
121
124
|
}
|
|
122
125
|
this.partialTime = 0;
|
|
123
126
|
this.partialRequests = 0;
|
|
@@ -163,14 +166,14 @@ export class Latency {
|
|
|
163
166
|
finish() {
|
|
164
167
|
this.running = false;
|
|
165
168
|
if (this.callback) {
|
|
166
|
-
return this.callback(null, this.
|
|
169
|
+
return this.callback(null, this.getResult());
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
/**
|
|
171
|
-
* Get final
|
|
174
|
+
* Get final result.
|
|
172
175
|
*/
|
|
173
|
-
|
|
176
|
+
getResult() {
|
|
174
177
|
const elapsedSeconds = this.getElapsed(this.initialTime) / 1000;
|
|
175
178
|
const meanTime = this.totalTime / this.totalRequests;
|
|
176
179
|
return {
|
|
@@ -215,53 +218,15 @@ export class Latency {
|
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
/**
|
|
218
|
-
* Show final
|
|
221
|
+
* Show final result.
|
|
219
222
|
*/
|
|
220
223
|
show() {
|
|
221
224
|
if (this.totalsShown) {
|
|
222
225
|
return;
|
|
223
226
|
}
|
|
224
227
|
this.totalsShown = true;
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
console.info('Target URL: %s', this.options.url);
|
|
228
|
-
if (this.options.maxRequests) {
|
|
229
|
-
console.info('Max requests: %s', this.options.maxRequests);
|
|
230
|
-
} else if (this.options.maxSeconds) {
|
|
231
|
-
console.info('Max time (s): %s', this.options.maxSeconds);
|
|
232
|
-
}
|
|
233
|
-
console.info('Concurrency level: %s', this.options.concurrency);
|
|
234
|
-
let agent = 'none';
|
|
235
|
-
if (this.options.agentKeepAlive) {
|
|
236
|
-
agent = 'keepalive';
|
|
237
|
-
}
|
|
238
|
-
console.info('Agent: %s', agent);
|
|
239
|
-
if (this.options.requestsPerSecond) {
|
|
240
|
-
console.info('Requests per second: %s', this.options.requestsPerSecond * this.options.concurrency);
|
|
241
|
-
}
|
|
242
|
-
console.info('');
|
|
243
|
-
console.info('Completed requests: %s', results.totalRequests);
|
|
244
|
-
console.info('Total errors: %s', results.totalErrors);
|
|
245
|
-
console.info('Total time: %s s', results.totalTimeSeconds);
|
|
246
|
-
console.info('Requests per second: %s', results.rps);
|
|
247
|
-
console.info('Mean latency: %s ms', results.meanLatencyMs);
|
|
248
|
-
console.info('');
|
|
249
|
-
console.info('Percentage of the requests served within a certain time');
|
|
250
|
-
|
|
251
|
-
Object.keys(results.percentiles).forEach(percentile => {
|
|
252
|
-
console.info(' %s% %s ms', percentile, results.percentiles[percentile]);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
256
|
-
if (results.totalErrors) {
|
|
257
|
-
console.info('');
|
|
258
|
-
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
259
|
-
console.info('');
|
|
260
|
-
Object.keys(results.errorCodes).forEach(errorCode => {
|
|
261
|
-
const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
|
|
262
|
-
console.info(' %s%s: %s errors', padding, errorCode, results.errorCodes[errorCode]);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
228
|
+
const result = this.getResult();
|
|
229
|
+
showResult(this.options, result)
|
|
265
230
|
}
|
|
266
231
|
}
|
|
267
232
|
|
package/lib/loadtest.js
CHANGED
|
@@ -14,33 +14,47 @@ https.globalAgent.maxSockets = 1000;
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Run a load test.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
17
|
+
* Parameters:
|
|
18
|
+
* - `options`: an object which may have:
|
|
19
|
+
* - url: mandatory URL to access.
|
|
20
|
+
* - concurrency: how many concurrent clients to use.
|
|
21
|
+
* - maxRequests: how many requests to send
|
|
22
|
+
* - maxSeconds: how long to run the tests.
|
|
23
|
+
* - cookies: a string or an array of strings, each with name:value.
|
|
24
|
+
* - headers: a map with headers: {key1: value1, key2: value2}.
|
|
25
|
+
* - method: the method to use: POST, PUT. Default: GET, what else.
|
|
26
|
+
* - body: the contents to send along a POST or PUT request.
|
|
27
|
+
* - contentType: the MIME type to use for the body, default text/plain.
|
|
28
|
+
* - requestsPerSecond: how many requests per second to send.
|
|
29
|
+
* - agentKeepAlive: if true, then use connection keep-alive.
|
|
30
|
+
* - indexParam: string to replace with a unique index.
|
|
31
|
+
* - insecure: allow https using self-signed certs.
|
|
32
|
+
* - quiet: do not log any messages.
|
|
33
|
+
* - debug: show debug messages (deprecated).
|
|
34
|
+
* - `callback`: optional `function(result, error)` called if/when the test finishes;
|
|
35
|
+
* if not present a promise is returned.
|
|
34
36
|
*/
|
|
35
37
|
export function loadTest(options, callback) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (!callback) {
|
|
39
|
+
return loadTestAsync(options)
|
|
40
|
+
}
|
|
41
|
+
loadTestAsync(options).then(result => callback(null, result)).catch(error => callback(null, error))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadTestAsync(options) {
|
|
45
|
+
await processOptions(options)
|
|
46
|
+
return await runOperation(options)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runOperation(options) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const operation = new Operation(options, (error, result) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
return reject(error)
|
|
54
|
+
}
|
|
55
|
+
return resolve(result)
|
|
56
|
+
});
|
|
42
57
|
operation.start();
|
|
43
|
-
return operation;
|
|
44
58
|
})
|
|
45
59
|
}
|
|
46
60
|
|
|
@@ -93,7 +107,7 @@ class Operation {
|
|
|
93
107
|
next();
|
|
94
108
|
}
|
|
95
109
|
if (this.options.statusCallback) {
|
|
96
|
-
this.options.statusCallback(error, result, this.latency.
|
|
110
|
+
this.options.statusCallback(error, result, this.latency.getResult());
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -156,7 +170,7 @@ class Operation {
|
|
|
156
170
|
this.clients[index].stop();
|
|
157
171
|
});
|
|
158
172
|
if (this.finalCallback) {
|
|
159
|
-
const result = this.latency.
|
|
173
|
+
const result = this.latency.getResult();
|
|
160
174
|
result.instanceIndex = this.instanceIndex;
|
|
161
175
|
this.finalCallback(null, result);
|
|
162
176
|
} else {
|
package/lib/options.js
CHANGED
|
@@ -7,11 +7,7 @@ import {addHeaders} from '../lib/headers.js'
|
|
|
7
7
|
import {loadConfig} from '../lib/config.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
export function processOptions(options
|
|
11
|
-
processOptionsAsync(options).then(result => callback(null, result)).catch(error => callback(error))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function processOptionsAsync(options) {
|
|
10
|
+
export async function processOptions(options) {
|
|
15
11
|
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
|
|
16
12
|
if (!options.url) {
|
|
17
13
|
throw new Error('Missing URL in options')
|
|
@@ -45,7 +41,7 @@ async function processOptionsAsync(options) {
|
|
|
45
41
|
options.body = await readBody(options.postFile, '-p');
|
|
46
42
|
}
|
|
47
43
|
if (options.data) {
|
|
48
|
-
options.body =
|
|
44
|
+
options.body = options.data
|
|
49
45
|
}
|
|
50
46
|
if (options.method) {
|
|
51
47
|
const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
|
package/lib/show.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Show result of a load test.
|
|
5
|
+
*/
|
|
6
|
+
export function showResult(options, result) {
|
|
7
|
+
console.info('');
|
|
8
|
+
console.info('Target URL: %s', options.url);
|
|
9
|
+
if (options.maxRequests) {
|
|
10
|
+
console.info('Max requests: %s', options.maxRequests);
|
|
11
|
+
} else if (options.maxSeconds) {
|
|
12
|
+
console.info('Max time (s): %s', options.maxSeconds);
|
|
13
|
+
}
|
|
14
|
+
console.info('Concurrency level: %s', options.concurrency);
|
|
15
|
+
let agent = 'none';
|
|
16
|
+
if (options.agentKeepAlive) {
|
|
17
|
+
agent = 'keepalive';
|
|
18
|
+
}
|
|
19
|
+
console.info('Agent: %s', agent);
|
|
20
|
+
if (options.requestsPerSecond) {
|
|
21
|
+
console.info('Requests per second: %s', options.requestsPerSecond * options.concurrency);
|
|
22
|
+
}
|
|
23
|
+
console.info('');
|
|
24
|
+
console.info('Completed requests: %s', result.totalRequests);
|
|
25
|
+
console.info('Total errors: %s', result.totalErrors);
|
|
26
|
+
console.info('Total time: %s s', result.totalTimeSeconds);
|
|
27
|
+
console.info('Requests per second: %s', result.rps);
|
|
28
|
+
console.info('Mean latency: %s ms', result.meanLatencyMs);
|
|
29
|
+
console.info('');
|
|
30
|
+
console.info('Percentage of the requests served within a certain time');
|
|
31
|
+
|
|
32
|
+
Object.keys(result.percentiles).forEach(percentile => {
|
|
33
|
+
console.info(' %s% %s ms', percentile, result.percentiles[percentile]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.info(' 100% %s ms (longest request)', result.maxLatencyMs);
|
|
37
|
+
if (result.totalErrors) {
|
|
38
|
+
console.info('');
|
|
39
|
+
Object.keys(result.errorCodes).forEach(errorCode => {
|
|
40
|
+
const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
|
|
41
|
+
console.info(' %s%s: %s errors', padding, errorCode, result.errorCodes[errorCode]);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
package/lib/testserver.js
CHANGED
|
@@ -23,7 +23,7 @@ class TestServer {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Start the server.
|
|
26
|
-
*
|
|
26
|
+
* The callback parameter will be called after the server has started.
|
|
27
27
|
*/
|
|
28
28
|
start(callback) {
|
|
29
29
|
if (this.options.socket) {
|
|
@@ -45,10 +45,8 @@ class TestServer {
|
|
|
45
45
|
return this.createError('Could not start server on port ' + this.port + ': ' + error, callback);
|
|
46
46
|
});
|
|
47
47
|
this.server.listen(this.port, () => {
|
|
48
|
-
console.info(`Listening on http://localhost:${this.port}/`)
|
|
49
|
-
|
|
50
|
-
callback();
|
|
51
|
-
}
|
|
48
|
+
if (!this.options.quiet) console.info(`Listening on http://localhost:${this.port}/`)
|
|
49
|
+
callback(null, this.server)
|
|
52
50
|
});
|
|
53
51
|
this.wsServer.on('request', request => {
|
|
54
52
|
// explicity omitting origin check here.
|
|
@@ -61,19 +59,16 @@ class TestServer {
|
|
|
61
59
|
}
|
|
62
60
|
});
|
|
63
61
|
connection.on('close', () => {
|
|
64
|
-
console.info('Peer %s disconnected', connection.remoteAddress);
|
|
62
|
+
if (!this.options.quiet) console.info('Peer %s disconnected', connection.remoteAddress);
|
|
65
63
|
});
|
|
66
64
|
});
|
|
67
|
-
return this.server
|
|
65
|
+
return this.server
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
/**
|
|
71
69
|
* Log an error, or send to the callback if present.
|
|
72
70
|
*/
|
|
73
71
|
createError(message, callback) {
|
|
74
|
-
if (!callback) {
|
|
75
|
-
return console.error(message);
|
|
76
|
-
}
|
|
77
72
|
callback(message);
|
|
78
73
|
}
|
|
79
74
|
|
|
@@ -106,7 +101,7 @@ class TestServer {
|
|
|
106
101
|
*/
|
|
107
102
|
socketListen(socket) {
|
|
108
103
|
socket.on('error', error => {
|
|
109
|
-
console.error('socket error: %s', error);
|
|
104
|
+
if (!this.options.quiet) console.error('socket error: %s', error);
|
|
110
105
|
socket.end();
|
|
111
106
|
});
|
|
112
107
|
socket.on('data', data => this.readData(data));
|
|
@@ -116,16 +111,16 @@ class TestServer {
|
|
|
116
111
|
* Read some data off the socket.
|
|
117
112
|
*/
|
|
118
113
|
readData(data) {
|
|
119
|
-
console.info('data: %s', data);
|
|
114
|
+
if (!this.options.quiet) console.info('data: %s', data);
|
|
120
115
|
}
|
|
121
116
|
|
|
122
117
|
/**
|
|
123
118
|
* Debug headers and other interesting information: POST body.
|
|
124
119
|
*/
|
|
125
120
|
debug(request) {
|
|
126
|
-
console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
|
|
121
|
+
if (!this.options.quiet) console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
|
|
127
122
|
if (request.body) {
|
|
128
|
-
console.info('Body: %s', request.body);
|
|
123
|
+
if (!this.options.quiet) console.info('Body: %s', request.body);
|
|
129
124
|
}
|
|
130
125
|
}
|
|
131
126
|
|
|
@@ -152,7 +147,7 @@ class TestServer {
|
|
|
152
147
|
}
|
|
153
148
|
const percent = parseInt(this.options.percent, 10);
|
|
154
149
|
if (!percent) {
|
|
155
|
-
console.error('Invalid error percent %s', this.options.percent);
|
|
150
|
+
if (!this.options.quiet) console.error('Invalid error percent %s', this.options.percent);
|
|
156
151
|
return false;
|
|
157
152
|
}
|
|
158
153
|
return (Math.random() < percent / 100);
|
|
@@ -160,16 +155,26 @@ class TestServer {
|
|
|
160
155
|
}
|
|
161
156
|
|
|
162
157
|
/**
|
|
163
|
-
* Start a test server.
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
158
|
+
* Start a test server. Parameters:
|
|
159
|
+
* - `options`, can contain:
|
|
160
|
+
* - port: the port to use, default 7357.
|
|
161
|
+
* - delay: wait the given milliseconds before answering.
|
|
162
|
+
* - quiet: do not log any messages.
|
|
163
|
+
* - percent: give an error (default 500) on some % of requests.
|
|
164
|
+
* - error: set an HTTP error code, default is 500.
|
|
165
|
+
* - `callback`: optional callback, called after the server has started.
|
|
166
|
+
* If not present will return a promise.
|
|
170
167
|
*/
|
|
171
168
|
export function startServer(options, callback) {
|
|
172
169
|
const server = new TestServer(options);
|
|
173
|
-
|
|
170
|
+
if (callback) {
|
|
171
|
+
return server.start(callback)
|
|
172
|
+
}
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
server.start((error, result) => {
|
|
175
|
+
if (error) return reject(error)
|
|
176
|
+
return resolve(result)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
174
179
|
}
|
|
175
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtest",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.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",
|
|
@@ -25,11 +25,11 @@ const options = {
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
loadTest(options, (error,
|
|
28
|
+
loadTest(options, (error, result) => {
|
|
29
29
|
if (error) {
|
|
30
30
|
return console.error('Got an error: %s', error);
|
|
31
31
|
}
|
|
32
|
-
console.log(
|
|
32
|
+
console.log(result);
|
|
33
33
|
console.log('Tests run successfully');
|
|
34
34
|
});
|
|
35
35
|
|
|
@@ -25,9 +25,9 @@ const options: loadtest.LoadTestOptions = {
|
|
|
25
25
|
},
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
loadtest.loadTest(options, (error,
|
|
28
|
+
loadtest.loadTest(options, (error, result) => {
|
|
29
29
|
if (error) {
|
|
30
30
|
return console.error(`Got an error: ${error}`)
|
|
31
31
|
}
|
|
32
|
-
console.log("Tests run successfully", {
|
|
32
|
+
console.log("Tests run successfully", {result})
|
|
33
33
|
})
|
package/test/body-generator.js
CHANGED
|
@@ -6,7 +6,7 @@ const PORT = 10453;
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
function testBodyGenerator(callback) {
|
|
9
|
-
const server = startServer({port: PORT}, error => {
|
|
9
|
+
const server = startServer({port: PORT, quiet: true}, error => {
|
|
10
10
|
if (error) {
|
|
11
11
|
return callback('Could not start test server');
|
|
12
12
|
}
|
|
@@ -16,6 +16,7 @@ function testBodyGenerator(callback) {
|
|
|
16
16
|
maxRequests: 100,
|
|
17
17
|
concurrency: 10,
|
|
18
18
|
postFile: 'sample/post-file.js',
|
|
19
|
+
quiet: true,
|
|
19
20
|
}
|
|
20
21
|
loadTest(options, (error, result) => {
|
|
21
22
|
if (error) {
|
package/test/integration.js
CHANGED
|
@@ -4,13 +4,17 @@ import {join} from 'path'
|
|
|
4
4
|
import {loadTest, startServer} from '../index.js'
|
|
5
5
|
|
|
6
6
|
const PORT = 10408;
|
|
7
|
+
const serverOptions = {
|
|
8
|
+
port: PORT,
|
|
9
|
+
quiet: true,
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Run an integration test.
|
|
11
15
|
*/
|
|
12
16
|
function testIntegration(callback) {
|
|
13
|
-
const server = startServer(
|
|
17
|
+
const server = startServer(serverOptions, error => {
|
|
14
18
|
if (error) {
|
|
15
19
|
return callback(error);
|
|
16
20
|
}
|
|
@@ -22,6 +26,7 @@ function testIntegration(callback) {
|
|
|
22
26
|
body: {
|
|
23
27
|
hi: 'there',
|
|
24
28
|
},
|
|
29
|
+
quiet: true,
|
|
25
30
|
};
|
|
26
31
|
loadTest(options, (error, result) => {
|
|
27
32
|
if (error) {
|
|
@@ -31,7 +36,7 @@ function testIntegration(callback) {
|
|
|
31
36
|
if (error) {
|
|
32
37
|
return callback(error);
|
|
33
38
|
}
|
|
34
|
-
return callback(null, 'Test
|
|
39
|
+
return callback(null, 'Test result: ' + JSON.stringify(result));
|
|
35
40
|
});
|
|
36
41
|
});
|
|
37
42
|
});
|
|
@@ -42,12 +47,13 @@ function testIntegration(callback) {
|
|
|
42
47
|
* Run an integration test using configuration file.
|
|
43
48
|
*/
|
|
44
49
|
function testIntegrationFile(callback) {
|
|
45
|
-
const server = startServer(
|
|
50
|
+
const server = startServer(serverOptions, error => {
|
|
46
51
|
if (error) {
|
|
47
52
|
return callback(error);
|
|
48
53
|
}
|
|
49
54
|
execFile('node',
|
|
50
|
-
[join('./', 'bin', 'loadtest.js'), `http://localhost:${PORT}/`,
|
|
55
|
+
[join('./', 'bin', 'loadtest.js'), `http://localhost:${PORT}/`,
|
|
56
|
+
'-n', '100', '--quiet'],
|
|
51
57
|
(error, stdout) => {
|
|
52
58
|
if (error) {
|
|
53
59
|
return callback(error);
|
|
@@ -56,7 +62,7 @@ function testIntegrationFile(callback) {
|
|
|
56
62
|
if (error) {
|
|
57
63
|
return callback(error);
|
|
58
64
|
}
|
|
59
|
-
return callback(null, 'Test
|
|
65
|
+
return callback(null, 'Test result: ' + stdout);
|
|
60
66
|
});
|
|
61
67
|
});
|
|
62
68
|
});
|
|
@@ -68,7 +74,7 @@ function testIntegrationFile(callback) {
|
|
|
68
74
|
* Run an integration test.
|
|
69
75
|
*/
|
|
70
76
|
function testWSIntegration(callback) {
|
|
71
|
-
const server = startServer(
|
|
77
|
+
const server = startServer(serverOptions, error => {
|
|
72
78
|
if (error) {
|
|
73
79
|
return callback(error);
|
|
74
80
|
}
|
|
@@ -85,6 +91,7 @@ function testWSIntegration(callback) {
|
|
|
85
91
|
type: 'ping',
|
|
86
92
|
hi: 'there',
|
|
87
93
|
},
|
|
94
|
+
quiet: true,
|
|
88
95
|
};
|
|
89
96
|
loadTest(options, (error, result) => {
|
|
90
97
|
if (error) {
|
|
@@ -94,7 +101,7 @@ function testWSIntegration(callback) {
|
|
|
94
101
|
if (error) {
|
|
95
102
|
return callback(error);
|
|
96
103
|
}
|
|
97
|
-
return callback(null, 'Test
|
|
104
|
+
return callback(null, 'Test result: ' + JSON.stringify(result));
|
|
98
105
|
});
|
|
99
106
|
});
|
|
100
107
|
});
|
|
@@ -108,6 +115,7 @@ function testDelay(callback) {
|
|
|
108
115
|
let options = {
|
|
109
116
|
port: PORT + 1,
|
|
110
117
|
delay: delay,
|
|
118
|
+
quiet: true,
|
|
111
119
|
};
|
|
112
120
|
const server = startServer(options, error => {
|
|
113
121
|
if (error) {
|
|
@@ -116,6 +124,7 @@ function testDelay(callback) {
|
|
|
116
124
|
options = {
|
|
117
125
|
url: 'http://localhost:' + (PORT + 1),
|
|
118
126
|
maxRequests: 10,
|
|
127
|
+
quiet: true,
|
|
119
128
|
};
|
|
120
129
|
loadTest(options, (error, result) => {
|
|
121
130
|
if (error) {
|
|
@@ -133,10 +142,29 @@ function testDelay(callback) {
|
|
|
133
142
|
});
|
|
134
143
|
}
|
|
135
144
|
|
|
145
|
+
async function testPromise() {
|
|
146
|
+
const server = await startServer(serverOptions)
|
|
147
|
+
const options = {
|
|
148
|
+
url: 'http://localhost:' + PORT,
|
|
149
|
+
maxRequests: 100,
|
|
150
|
+
concurrency: 10,
|
|
151
|
+
method: 'POST',
|
|
152
|
+
body: {
|
|
153
|
+
hi: 'there',
|
|
154
|
+
},
|
|
155
|
+
quiet: true,
|
|
156
|
+
};
|
|
157
|
+
const result = await loadTest(options)
|
|
158
|
+
await server.close()
|
|
159
|
+
return 'Test result: ' + JSON.stringify(result)
|
|
160
|
+
}
|
|
161
|
+
|
|
136
162
|
/**
|
|
137
163
|
* Run all tests.
|
|
138
164
|
*/
|
|
139
165
|
export function test(callback) {
|
|
140
|
-
testing.run([
|
|
166
|
+
testing.run([
|
|
167
|
+
testIntegration, testIntegrationFile, testDelay, testWSIntegration, testPromise,
|
|
168
|
+
], 4000, callback);
|
|
141
169
|
}
|
|
142
170
|
|
package/test/latency.js
CHANGED
|
@@ -49,7 +49,7 @@ function testLatencyPercentiles(callback) {
|
|
|
49
49
|
};
|
|
50
50
|
const latency = new Latency(options, error => {
|
|
51
51
|
testing.check(error, 'Error while testing latency percentiles', callback);
|
|
52
|
-
const percentiles = latency.
|
|
52
|
+
const percentiles = latency.getResult().percentiles;
|
|
53
53
|
|
|
54
54
|
Object.keys(percentiles).forEach(percentile => {
|
|
55
55
|
testing.assert(percentiles[percentile] !== false, 'Empty percentile for %s', percentile, callback);
|
package/test/loadtest.js
CHANGED
|
@@ -10,6 +10,7 @@ function testMaxSeconds(callback) {
|
|
|
10
10
|
url: 'http://localhost:7357/',
|
|
11
11
|
maxSeconds: 0.1,
|
|
12
12
|
concurrency: 1,
|
|
13
|
+
quiet: true,
|
|
13
14
|
};
|
|
14
15
|
loadTest(options, callback);
|
|
15
16
|
}
|
|
@@ -23,6 +24,7 @@ function testWSEcho(callback) {
|
|
|
23
24
|
url: 'ws://localhost:7357/',
|
|
24
25
|
maxSeconds: 0.1,
|
|
25
26
|
concurrency: 1,
|
|
27
|
+
quiet: true,
|
|
26
28
|
};
|
|
27
29
|
loadTest(options, callback);
|
|
28
30
|
}
|
|
@@ -32,7 +34,8 @@ function testIndexParam(callback) {
|
|
|
32
34
|
url: 'http://localhost:7357/replace',
|
|
33
35
|
concurrency:1,
|
|
34
36
|
maxSeconds: 0.1,
|
|
35
|
-
indexParam: "replace"
|
|
37
|
+
indexParam: "replace",
|
|
38
|
+
quiet: true,
|
|
36
39
|
};
|
|
37
40
|
loadTest(options, callback);
|
|
38
41
|
}
|
|
@@ -43,7 +46,8 @@ function testIndexParamWithBody(callback) {
|
|
|
43
46
|
concurrency:1,
|
|
44
47
|
maxSeconds: 0.1,
|
|
45
48
|
indexParam: "replace",
|
|
46
|
-
body: '{"id": "replace"}'
|
|
49
|
+
body: '{"id": "replace"}',
|
|
50
|
+
quiet: true,
|
|
47
51
|
};
|
|
48
52
|
loadTest(options, callback);
|
|
49
53
|
}
|
|
@@ -57,7 +61,8 @@ function testIndexParamWithCallback(callback) {
|
|
|
57
61
|
indexParamCallback: function() {
|
|
58
62
|
//https://gist.github.com/6174/6062387
|
|
59
63
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
60
|
-
}
|
|
64
|
+
},
|
|
65
|
+
quiet: true,
|
|
61
66
|
};
|
|
62
67
|
loadTest(options, callback);
|
|
63
68
|
}
|
|
@@ -72,7 +77,8 @@ function testIndexParamWithCallbackAndBody(callback) {
|
|
|
72
77
|
indexParamCallback: function() {
|
|
73
78
|
//https://gist.github.com/6174/6062387
|
|
74
79
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
75
|
-
}
|
|
80
|
+
},
|
|
81
|
+
quiet: true,
|
|
76
82
|
};
|
|
77
83
|
loadTest(options, callback);
|
|
78
84
|
}
|
|
@@ -82,6 +88,9 @@ function testIndexParamWithCallbackAndBody(callback) {
|
|
|
82
88
|
* Run all tests.
|
|
83
89
|
*/
|
|
84
90
|
export function test(callback) {
|
|
85
|
-
testing.run([
|
|
91
|
+
testing.run([
|
|
92
|
+
testMaxSeconds, testWSEcho, testIndexParam, testIndexParamWithBody,
|
|
93
|
+
testIndexParamWithCallback, testIndexParamWithCallbackAndBody,
|
|
94
|
+
], callback);
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -6,7 +6,7 @@ const PORT = 10453;
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
function testRequestGenerator(callback) {
|
|
9
|
-
const server = startServer({port: PORT}, error => {
|
|
9
|
+
const server = startServer({port: PORT, quiet: true}, error => {
|
|
10
10
|
if (error) {
|
|
11
11
|
return callback('Could not start test server');
|
|
12
12
|
}
|
|
@@ -24,6 +24,7 @@ function testRequestGenerator(callback) {
|
|
|
24
24
|
request.write(message);
|
|
25
25
|
return request;
|
|
26
26
|
},
|
|
27
|
+
quiet: true,
|
|
27
28
|
};
|
|
28
29
|
loadTest(options, (error, result) => {
|
|
29
30
|
if (error) {
|
package/test/testserver.js
CHANGED