loadtest 6.1.0 → 6.2.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 +80 -46
- package/bin/loadtest.js +1 -2
- package/lib/httpClient.js +21 -10
- package/lib/latency.js +4 -16
- package/lib/loadtest.js +18 -17
- package/lib/options.js +52 -96
- package/lib/result.js +68 -0
- package/package.json +2 -2
- package/test/body-generator.js +1 -1
- package/test/integration.js +4 -4
- package/test/loadtest.js +29 -7
- package/test/request-generator.js +1 -1
- package/lib/show.js +0 -47
package/README.md
CHANGED
|
@@ -481,7 +481,28 @@ thus allowing you to load test your application in your own tests.
|
|
|
481
481
|
|
|
482
482
|
### Invoke Load Test
|
|
483
483
|
|
|
484
|
-
To run a load test, just
|
|
484
|
+
To run a load test, just `await` for the exported function `loadTest()` with the desired options, described below:
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
import {loadTest} from 'loadtest'
|
|
488
|
+
|
|
489
|
+
const options = {
|
|
490
|
+
url: 'http://localhost:8000',
|
|
491
|
+
maxRequests: 1000,
|
|
492
|
+
}
|
|
493
|
+
const result = await loadTest(options)
|
|
494
|
+
result.show()
|
|
495
|
+
console.log('Tests run successfully')
|
|
496
|
+
})
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The call returns a `Result` object that contains all info about the load test, also described below.
|
|
500
|
+
Call `result.show()` to display the results in the standard format on the console.
|
|
501
|
+
|
|
502
|
+
As a legacy from before promises existed,
|
|
503
|
+
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
504
|
+
the callback `function(error, result)` will be invoked when the max number of requests is reached,
|
|
505
|
+
or when the max number of seconds has elapsed.
|
|
485
506
|
|
|
486
507
|
```javascript
|
|
487
508
|
import {loadTest} from 'loadtest'
|
|
@@ -494,16 +515,51 @@ loadTest(options, function(error, result) {
|
|
|
494
515
|
if (error) {
|
|
495
516
|
return console.error('Got an error: %s', error)
|
|
496
517
|
}
|
|
518
|
+
result.show()
|
|
497
519
|
console.log('Tests run successfully')
|
|
498
520
|
})
|
|
499
521
|
```
|
|
500
522
|
|
|
501
|
-
The callback `function(error, result)` will be invoked when the max number of requests is reached,
|
|
502
|
-
or when the max number of seconds has elapsed.
|
|
503
523
|
|
|
504
524
|
Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
|
|
505
525
|
and will not call the callback.
|
|
506
526
|
|
|
527
|
+
### Result
|
|
528
|
+
|
|
529
|
+
The latency result returned at the end of the load test contains a full set of data, including:
|
|
530
|
+
mean latency, number of errors and percentiles.
|
|
531
|
+
An example follows:
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
{
|
|
535
|
+
url: 'http://localhost:80/',
|
|
536
|
+
maxRequests: 1000,
|
|
537
|
+
maxSeconds: 0,
|
|
538
|
+
concurrency: 10,
|
|
539
|
+
agent: 'none',
|
|
540
|
+
requestsPerSecond: undefined,
|
|
541
|
+
totalRequests: 1000,
|
|
542
|
+
percentiles: {
|
|
543
|
+
'50': 7,
|
|
544
|
+
'90': 10,
|
|
545
|
+
'95': 11,
|
|
546
|
+
'99': 15
|
|
547
|
+
},
|
|
548
|
+
rps: 2824,
|
|
549
|
+
totalTimeSeconds: 0.354108,
|
|
550
|
+
meanLatencyMs: 7.72,
|
|
551
|
+
maxLatencyMs: 20,
|
|
552
|
+
totalErrors: 3,
|
|
553
|
+
errorCodes: {
|
|
554
|
+
'0': 1,
|
|
555
|
+
'500': 2
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The `result` object also has a `result.show()` function
|
|
561
|
+
that displays the results on the console in the standard format.
|
|
562
|
+
|
|
507
563
|
### Options
|
|
508
564
|
|
|
509
565
|
All options but `url` are, as their name implies, optional.
|
|
@@ -591,6 +647,9 @@ function(params, options, client, callback) {
|
|
|
591
647
|
}
|
|
592
648
|
```
|
|
593
649
|
|
|
650
|
+
See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
|
|
651
|
+
(or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
|
|
652
|
+
|
|
594
653
|
#### `agentKeepAlive`
|
|
595
654
|
|
|
596
655
|
Use an agent with 'Connection: Keep-alive'.
|
|
@@ -677,6 +736,19 @@ In addition, the following three properties are added to the `result` object:
|
|
|
677
736
|
|
|
678
737
|
You will need to check if `error` is populated in order to determine which object to check for these properties.
|
|
679
738
|
|
|
739
|
+
The second parameter contains info about the current request:
|
|
740
|
+
|
|
741
|
+
```javascript
|
|
742
|
+
{
|
|
743
|
+
host: 'localhost',
|
|
744
|
+
path: '/',
|
|
745
|
+
method: 'GET',
|
|
746
|
+
statusCode: 200,
|
|
747
|
+
body: '<html><body>hi</body></html>',
|
|
748
|
+
headers: [...]
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
680
752
|
Example:
|
|
681
753
|
|
|
682
754
|
```javascript
|
|
@@ -703,7 +775,6 @@ loadTest(options, function(error) {
|
|
|
703
775
|
console.log('Tests run successfully')
|
|
704
776
|
})
|
|
705
777
|
```
|
|
706
|
-
|
|
707
778
|
|
|
708
779
|
In some situations request data needs to be available in the statusCallBack.
|
|
709
780
|
This data can be assigned to `request.labels` in the requestGenerator:
|
|
@@ -757,46 +828,6 @@ function contentInspector(result) {
|
|
|
757
828
|
}
|
|
758
829
|
},
|
|
759
830
|
```
|
|
760
|
-
|
|
761
|
-
### Result
|
|
762
|
-
|
|
763
|
-
The latency result passed to your callback at the end of the load test contains a full set of data, including:
|
|
764
|
-
mean latency, number of errors and percentiles.
|
|
765
|
-
An example follows:
|
|
766
|
-
|
|
767
|
-
```javascript
|
|
768
|
-
{
|
|
769
|
-
totalRequests: 1000,
|
|
770
|
-
percentiles: {
|
|
771
|
-
'50': 7,
|
|
772
|
-
'90': 10,
|
|
773
|
-
'95': 11,
|
|
774
|
-
'99': 15
|
|
775
|
-
},
|
|
776
|
-
rps: 2824,
|
|
777
|
-
totalTimeSeconds: 0.354108,
|
|
778
|
-
meanLatencyMs: 7.72,
|
|
779
|
-
maxLatencyMs: 20,
|
|
780
|
-
totalErrors: 3,
|
|
781
|
-
errorCodes: {
|
|
782
|
-
'0': 1,
|
|
783
|
-
'500': 2
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
The second parameter contains info about the current request:
|
|
789
|
-
|
|
790
|
-
```javascript
|
|
791
|
-
{
|
|
792
|
-
host: 'localhost',
|
|
793
|
-
path: '/',
|
|
794
|
-
method: 'GET',
|
|
795
|
-
statusCode: 200,
|
|
796
|
-
body: '<html><body>hi</body></html>',
|
|
797
|
-
headers: [...]
|
|
798
|
-
}
|
|
799
|
-
```
|
|
800
831
|
|
|
801
832
|
### Start Test Server
|
|
802
833
|
|
|
@@ -805,11 +836,14 @@ To start the test server use the exported function `startServer()` with a set of
|
|
|
805
836
|
```javascript
|
|
806
837
|
import {startServer} from 'loadtest'
|
|
807
838
|
const server = await startServer({port: 8000})
|
|
839
|
+
// do your thing
|
|
840
|
+
await server.close()
|
|
808
841
|
```
|
|
809
842
|
|
|
810
|
-
This function returns
|
|
843
|
+
This function returns when the server is up and running,
|
|
844
|
+
with an HTTP server which can be `close()`d when it is no longer useful.
|
|
811
845
|
As a legacy from before promises existed,
|
|
812
|
-
if
|
|
846
|
+
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
813
847
|
|
|
814
848
|
```
|
|
815
849
|
const server = startServer({port: 8000}, error => console.error(error))
|
package/bin/loadtest.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
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'
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
const options = stdio.getopt({
|
|
@@ -54,7 +53,7 @@ async function processAndRun(options) {
|
|
|
54
53
|
options.url = options.args[0];
|
|
55
54
|
try {
|
|
56
55
|
const result = await loadTest(options)
|
|
57
|
-
|
|
56
|
+
result.show()
|
|
58
57
|
} catch(error) {
|
|
59
58
|
console.error(error.message)
|
|
60
59
|
help()
|
package/lib/httpClient.js
CHANGED
|
@@ -28,6 +28,7 @@ class HttpClient {
|
|
|
28
28
|
constructor(operation, params) {
|
|
29
29
|
this.operation = operation
|
|
30
30
|
this.params = params
|
|
31
|
+
this.stopped = false
|
|
31
32
|
this.init();
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -45,11 +46,15 @@ class HttpClient {
|
|
|
45
46
|
this.options.key = this.params.key;
|
|
46
47
|
}
|
|
47
48
|
this.options.agent = false;
|
|
49
|
+
if (this.params.requestsPerSecond) {
|
|
50
|
+
// rps for each client is total / concurrency (# of clients)
|
|
51
|
+
this.options.requestsPerSecond = this.params.requestsPerSecond / this.params.concurrency
|
|
52
|
+
}
|
|
48
53
|
if (this.params.agentKeepAlive) {
|
|
49
|
-
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive;
|
|
54
|
+
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
|
|
50
55
|
let maxSockets = 10;
|
|
51
|
-
if (this.
|
|
52
|
-
maxSockets += Math.floor(this.
|
|
56
|
+
if (this.options.requestsPerSecond) {
|
|
57
|
+
maxSockets += Math.floor(this.options.requestsPerSecond);
|
|
53
58
|
}
|
|
54
59
|
this.options.agent = new KeepAlive({
|
|
55
60
|
maxSockets: maxSockets,
|
|
@@ -71,7 +76,7 @@ class HttpClient {
|
|
|
71
76
|
} else if (typeof this.params.body == 'function') {
|
|
72
77
|
this.generateMessage = this.params.body;
|
|
73
78
|
} else {
|
|
74
|
-
|
|
79
|
+
throw new Error(`Unrecognized body: ${typeof this.params.body}`);
|
|
75
80
|
}
|
|
76
81
|
this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
|
|
77
82
|
}
|
|
@@ -81,7 +86,7 @@ class HttpClient {
|
|
|
81
86
|
} else if (typeof this.params.cookies == 'string') {
|
|
82
87
|
this.options.headers.Cookie = this.params.cookies;
|
|
83
88
|
} else {
|
|
84
|
-
|
|
89
|
+
throw new Error(`Invalid cookies ${JSON.stringify(this.params.cookies)}, please use an array or a string`);
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
addUserAgent(this.options.headers);
|
|
@@ -94,10 +99,15 @@ class HttpClient {
|
|
|
94
99
|
* Start the HTTP client.
|
|
95
100
|
*/
|
|
96
101
|
start() {
|
|
97
|
-
if (
|
|
102
|
+
if (this.stopped) {
|
|
103
|
+
// solves testing issue: with requestsPerSecond clients are started at random,
|
|
104
|
+
// so sometimes they are stopped before they have even started
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (!this.options.requestsPerSecond) {
|
|
98
108
|
return this.makeRequest();
|
|
99
109
|
}
|
|
100
|
-
const interval = 1000 / this.
|
|
110
|
+
const interval = 1000 / this.options.requestsPerSecond;
|
|
101
111
|
this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
|
|
102
112
|
}
|
|
103
113
|
|
|
@@ -105,6 +115,7 @@ class HttpClient {
|
|
|
105
115
|
* Stop the HTTP client.
|
|
106
116
|
*/
|
|
107
117
|
stop() {
|
|
118
|
+
this.stopped = true
|
|
108
119
|
if (this.requestTimer) {
|
|
109
120
|
this.requestTimer.stop();
|
|
110
121
|
}
|
|
@@ -133,7 +144,6 @@ class HttpClient {
|
|
|
133
144
|
// adding proxy configuration
|
|
134
145
|
if (this.params.proxy) {
|
|
135
146
|
const proxy = this.params.proxy;
|
|
136
|
-
//console.log('using proxy server %j', proxy);
|
|
137
147
|
const agent = new HttpsProxyAgent(proxy);
|
|
138
148
|
this.options.agent = agent;
|
|
139
149
|
}
|
|
@@ -154,7 +164,8 @@ class HttpClient {
|
|
|
154
164
|
delete this.options.headers['Content-Length'];
|
|
155
165
|
}
|
|
156
166
|
if (typeof this.params.requestGenerator == 'function') {
|
|
157
|
-
|
|
167
|
+
const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
|
|
168
|
+
request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
|
|
158
169
|
} else {
|
|
159
170
|
request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
|
|
160
171
|
}
|
|
@@ -205,7 +216,7 @@ class HttpClient {
|
|
|
205
216
|
result.instanceIndex = this.operation.instanceIndex;
|
|
206
217
|
}
|
|
207
218
|
let callback;
|
|
208
|
-
if (!this.
|
|
219
|
+
if (!this.options.requestsPerSecond) {
|
|
209
220
|
callback = this.makeRequest.bind(this);
|
|
210
221
|
}
|
|
211
222
|
this.operation.callback(error, result, callback);
|
package/lib/latency.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as crypto from 'crypto'
|
|
2
|
-
import {
|
|
2
|
+
import {Result} from './result.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -174,19 +174,8 @@ export class Latency {
|
|
|
174
174
|
* Get final result.
|
|
175
175
|
*/
|
|
176
176
|
getResult() {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
totalRequests: this.totalRequests,
|
|
181
|
-
totalErrors: this.totalErrors,
|
|
182
|
-
totalTimeSeconds: elapsedSeconds,
|
|
183
|
-
rps: Math.round(this.totalRequests / elapsedSeconds),
|
|
184
|
-
meanLatencyMs: Math.round(meanTime * 10) / 10,
|
|
185
|
-
maxLatencyMs: this.maxLatencyMs,
|
|
186
|
-
minLatencyMs: this.minLatencyMs,
|
|
187
|
-
percentiles: this.computePercentiles(),
|
|
188
|
-
errorCodes: this.errorCodes
|
|
189
|
-
};
|
|
177
|
+
const result = new Result(this.options, this)
|
|
178
|
+
return result
|
|
190
179
|
}
|
|
191
180
|
|
|
192
181
|
/**
|
|
@@ -226,11 +215,10 @@ export class Latency {
|
|
|
226
215
|
}
|
|
227
216
|
this.totalsShown = true;
|
|
228
217
|
const result = this.getResult();
|
|
229
|
-
|
|
218
|
+
result.show()
|
|
230
219
|
}
|
|
231
220
|
}
|
|
232
221
|
|
|
233
|
-
|
|
234
222
|
/**
|
|
235
223
|
* Create a unique, random token.
|
|
236
224
|
*/
|
package/lib/loadtest.js
CHANGED
|
@@ -16,19 +16,21 @@ https.globalAgent.maxSockets = 1000;
|
|
|
16
16
|
* Run a load test.
|
|
17
17
|
* Parameters:
|
|
18
18
|
* - `options`: an object which may have:
|
|
19
|
-
* - url:
|
|
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.
|
|
19
|
+
* - url [string]: URL to access (mandatory).
|
|
20
|
+
* - concurrency [number]: how many concurrent clients to use.
|
|
21
|
+
* - maxRequests [number]: how many requests to send
|
|
22
|
+
* - maxSeconds [number]: how long to run the tests.
|
|
23
|
+
* - cookies [array]: a string or an array of strings, each with name:value.
|
|
24
|
+
* - headers [map]: a map with headers: {key1: value1, key2: value2}.
|
|
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.
|
|
27
|
+
* - contentType [string]: the MIME type to use for the body, default text/plain.
|
|
28
|
+
* - requestsPerSecond [number]: how many requests per second to send.
|
|
29
29
|
* - agentKeepAlive: if true, then use connection keep-alive.
|
|
30
|
-
* - indexParam: string to replace with a unique index.
|
|
30
|
+
* - indexParam [string]: string to replace with a unique index.
|
|
31
31
|
* - insecure: allow https using self-signed certs.
|
|
32
|
+
* - secureProtocol [string]: TLS/SSL secure protocol method to use.
|
|
33
|
+
* - proxy [string]: use a proxy for requests e.g. http://localhost:8080.
|
|
32
34
|
* - quiet: do not log any messages.
|
|
33
35
|
* - debug: show debug messages (deprecated).
|
|
34
36
|
* - `callback`: optional `function(result, error)` called if/when the test finishes;
|
|
@@ -38,12 +40,12 @@ export function loadTest(options, callback) {
|
|
|
38
40
|
if (!callback) {
|
|
39
41
|
return loadTestAsync(options)
|
|
40
42
|
}
|
|
41
|
-
loadTestAsync(options).then(result => callback(null, result)).catch(error => callback(
|
|
43
|
+
loadTestAsync(options).then(result => callback(null, result)).catch(error => callback(error))
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
async function loadTestAsync(options) {
|
|
45
|
-
await processOptions(options)
|
|
46
|
-
return await runOperation(
|
|
47
|
+
const processed = await processOptions(options)
|
|
48
|
+
return await runOperation(processed)
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function runOperation(options) {
|
|
@@ -157,15 +159,14 @@ class Operation {
|
|
|
157
159
|
* Stop clients.
|
|
158
160
|
*/
|
|
159
161
|
stop() {
|
|
162
|
+
this.running = false;
|
|
163
|
+
this.latency.running = false;
|
|
160
164
|
if (this.showTimer) {
|
|
161
165
|
this.showTimer.stop();
|
|
162
166
|
}
|
|
163
167
|
if (this.stopTimeout) {
|
|
164
168
|
clearTimeout(this.stopTimeout);
|
|
165
169
|
}
|
|
166
|
-
this.running = false;
|
|
167
|
-
this.latency.running = false;
|
|
168
|
-
|
|
169
170
|
Object.keys(this.clients).forEach(index => {
|
|
170
171
|
this.clients[index].stop();
|
|
171
172
|
});
|
package/lib/options.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import {readFile} from 'fs/promises'
|
|
4
2
|
import * as path from 'path'
|
|
5
3
|
import * as urlLib from 'url'
|
|
6
4
|
import {addHeaders} from '../lib/headers.js'
|
|
7
5
|
import {loadConfig} from '../lib/config.js'
|
|
8
6
|
|
|
7
|
+
const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
export async function processOptions(options) {
|
|
11
|
+
const processed = {}
|
|
11
12
|
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
|
|
12
13
|
if (!options.url) {
|
|
13
14
|
throw new Error('Missing URL in options')
|
|
14
15
|
}
|
|
15
|
-
options.concurrency = options.concurrency || 1;
|
|
16
|
-
if (options.requestsPerSecond) {
|
|
17
|
-
options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
|
|
18
|
-
}
|
|
19
16
|
if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
|
|
20
17
|
throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
|
|
21
18
|
}
|
|
@@ -24,129 +21,88 @@ export async function processOptions(options) {
|
|
|
24
21
|
throw new Error(`"requestsPerSecond" not supported for WebSockets`);
|
|
25
22
|
}
|
|
26
23
|
}
|
|
24
|
+
processed.url = options.url
|
|
27
25
|
const configuration = loadConfig();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
processed.concurrency = options.concurrency || configuration.concurrency || 1
|
|
27
|
+
const rps = options.rps ? parseFloat(options.rps) : null
|
|
28
|
+
processed.requestsPerSecond = options.requestsPerSecond || rps || configuration.requestsPerSecond
|
|
29
|
+
processed.agentKeepAlive = options.keepalive || options.agent || options.agentKeepAlive || configuration.agentKeepAlive;
|
|
30
|
+
processed.indexParam = options.index || options.indexParam || configuration.indexParam;
|
|
31
|
+
processed.method = options.method || configuration.method || 'GET'
|
|
33
32
|
// Allow a post body string in options
|
|
34
33
|
// Ex -P '{"foo": "bar"}'
|
|
35
34
|
if (options.postBody) {
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
processed.method = 'POST';
|
|
36
|
+
processed.body = options.postBody;
|
|
38
37
|
}
|
|
39
38
|
if (options.postFile) {
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
processed.method = 'POST';
|
|
40
|
+
processed.body = await readBody(options.postFile, '-p');
|
|
42
41
|
}
|
|
43
42
|
if (options.data) {
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
if (options.method) {
|
|
47
|
-
const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
|
|
48
|
-
if (acceptedMethods.indexOf(options.method) === -1) {
|
|
49
|
-
options.method = 'GET';
|
|
50
|
-
}
|
|
43
|
+
processed.body = options.data
|
|
51
44
|
}
|
|
52
45
|
if (options.putFile) {
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
processed.method = 'PUT';
|
|
47
|
+
processed.body = await readBody(options.putFile, '-u');
|
|
55
48
|
}
|
|
56
49
|
if (options.patchBody) {
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
processed.method = 'PATCH';
|
|
51
|
+
processed.body = options.patchBody;
|
|
59
52
|
}
|
|
60
53
|
if (options.patchFile) {
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
processed.method = 'PATCH';
|
|
55
|
+
processed.body = await readBody(options.patchFile, '-a');
|
|
63
56
|
}
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
// sanity check
|
|
58
|
+
if (acceptedMethods.indexOf(processed.method) === -1) {
|
|
59
|
+
throw new Error(`Invalid method ${processed.method}`)
|
|
66
60
|
}
|
|
67
61
|
if (!options.body) {
|
|
68
62
|
if(configuration.body) {
|
|
69
|
-
|
|
63
|
+
processed.body = configuration.body;
|
|
70
64
|
} else if (configuration.file) {
|
|
71
|
-
|
|
65
|
+
processed.body = await readBody(configuration.file, 'configuration.request.file');
|
|
72
66
|
}
|
|
73
67
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
options.key = configuration.key;
|
|
68
|
+
if (options.key || configuration.key) {
|
|
69
|
+
processed.key = await readFile(options.key || configuration.key)
|
|
77
70
|
}
|
|
78
|
-
if (options.
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
if (!options.cert) {
|
|
82
|
-
options.cert = configuration.cert;
|
|
83
|
-
}
|
|
84
|
-
if (options.cert) {
|
|
85
|
-
options.cert = await readFile(options.cert);
|
|
71
|
+
if (options.cert || configuration.cert) {
|
|
72
|
+
processed.cert = await readFile(options.cert || configuration.cert);
|
|
86
73
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
defaultHeaders['accept'] = '*/*';
|
|
74
|
+
processed.headers = configuration.headers || {}
|
|
75
|
+
processed.headers['host'] = urlLib.parse(options.url).host;
|
|
76
|
+
processed.headers['user-agent'] = 'loadtest/' + packageJson.version;
|
|
77
|
+
processed.headers['accept'] = '*/*';
|
|
92
78
|
|
|
93
79
|
if (options.headers) {
|
|
94
|
-
addHeaders(options.headers,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
options.concurrency = configuration.concurrency;
|
|
112
|
-
}
|
|
113
|
-
if(!options.maxSeconds) {
|
|
114
|
-
options.maxSeconds = configuration.maxSeconds;
|
|
115
|
-
}
|
|
116
|
-
if(!options.timeout && configuration.timeout) {
|
|
117
|
-
options.timeout = configuration.timeout;
|
|
118
|
-
}
|
|
119
|
-
if(!options.contentType) {
|
|
120
|
-
options.contentType = configuration.contentType;
|
|
121
|
-
}
|
|
122
|
-
if(!options.cookies) {
|
|
123
|
-
options.cookies = configuration.cookies;
|
|
124
|
-
}
|
|
125
|
-
if(!options.secureProtocol) {
|
|
126
|
-
options.secureProtocol = configuration.secureProtocol;
|
|
127
|
-
}
|
|
128
|
-
if(!options.insecure) {
|
|
129
|
-
options.insecure = configuration.insecure;
|
|
130
|
-
}
|
|
131
|
-
if(!options.recover) {
|
|
132
|
-
options.recover = configuration.recover;
|
|
133
|
-
}
|
|
134
|
-
if(!options.proxy) {
|
|
135
|
-
options.proxy = configuration.proxy;
|
|
136
|
-
}
|
|
80
|
+
addHeaders(options.headers, processed.headers);
|
|
81
|
+
}
|
|
82
|
+
processed.requestGenerator = options.requestGenerator || configuration.requestGenerator
|
|
83
|
+
if (typeof processed.requestGenerator == 'string') {
|
|
84
|
+
processed.requestGenerator = await import(processed.requestGenerator)
|
|
85
|
+
}
|
|
86
|
+
processed.maxRequests = options.maxRequests || configuration.maxRequests
|
|
87
|
+
processed.maxSeconds = options.maxSeconds || configuration.maxSeconds
|
|
88
|
+
processed.cookies = options.cookies || configuration.cookies
|
|
89
|
+
processed.contentType = options.contentType || configuration.contentType
|
|
90
|
+
processed.timeout = options.timeout || configuration.timeout
|
|
91
|
+
processed.secureProtocol = options.secureProtocol || configuration.secureProtocol
|
|
92
|
+
processed.insecure = options.insecure || configuration.insecure
|
|
93
|
+
processed.recover = options.recover || configuration.recover
|
|
94
|
+
processed.proxy = options.proxy || configuration.proxy
|
|
95
|
+
processed.quiet = options.quiet || configuration.quiet
|
|
96
|
+
return processed
|
|
137
97
|
}
|
|
138
98
|
|
|
139
99
|
async function readBody(filename, option) {
|
|
140
100
|
if (typeof filename !== 'string') {
|
|
141
101
|
throw new Error(`Invalid file to open with ${option}: ${filename}`);
|
|
142
102
|
}
|
|
143
|
-
|
|
144
103
|
if (path.extname(filename) === '.js') {
|
|
145
104
|
return await import(new URL(filename, `file://${process.cwd()}/`))
|
|
146
105
|
}
|
|
147
|
-
|
|
148
|
-
const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
|
|
149
|
-
|
|
150
|
-
return ret;
|
|
106
|
+
return await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
|
|
151
107
|
}
|
|
152
108
|
|
package/lib/result.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of a load test.
|
|
5
|
+
*/
|
|
6
|
+
export class Result {
|
|
7
|
+
constructor(options, latency) {
|
|
8
|
+
// options
|
|
9
|
+
this.url = options.url
|
|
10
|
+
this.maxRequests = options.maxRequests
|
|
11
|
+
this.maxSeconds = options.maxSeconds
|
|
12
|
+
this.concurrency = options.concurrency
|
|
13
|
+
this.agent = options.agentKeepAlive ? 'keepalive' : 'none';
|
|
14
|
+
this.requestsPerSecond = options.requestsPerSecond
|
|
15
|
+
// results
|
|
16
|
+
this.elapsedSeconds = latency.getElapsed(latency.initialTime) / 1000
|
|
17
|
+
const meanTime = latency.totalTime / latency.totalRequests
|
|
18
|
+
this.totalRequests = latency.totalRequests
|
|
19
|
+
this.totalErrors = latency.totalErrors
|
|
20
|
+
this.totalTimeSeconds = this.elapsedSeconds
|
|
21
|
+
this.rps = Math.round(latency.totalRequests / this.elapsedSeconds)
|
|
22
|
+
this.meanLatencyMs = Math.round(meanTime * 10) / 10
|
|
23
|
+
this.maxLatencyMs = latency.maxLatencyMs
|
|
24
|
+
this.minLatencyMs = latency.minLatencyMs
|
|
25
|
+
this.percentiles = latency.computePercentiles()
|
|
26
|
+
this.errorCodes = latency.errorCodes
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Show result of a load test.
|
|
31
|
+
*/
|
|
32
|
+
show() {
|
|
33
|
+
console.info('');
|
|
34
|
+
console.info('Target URL: %s', this.url);
|
|
35
|
+
if (this.maxRequests) {
|
|
36
|
+
console.info('Max requests: %s', this.maxRequests);
|
|
37
|
+
} else if (this.maxSeconds) {
|
|
38
|
+
console.info('Max time (s): %s', this.maxSeconds);
|
|
39
|
+
}
|
|
40
|
+
console.info('Concurrency level: %s', this.concurrency);
|
|
41
|
+
console.info('Agent: %s', this.agent);
|
|
42
|
+
if (this.requestsPerSecond) {
|
|
43
|
+
console.info('Requests per second: %s', this.requestsPerSecond);
|
|
44
|
+
}
|
|
45
|
+
console.info('');
|
|
46
|
+
console.info('Completed requests: %s', this.totalRequests);
|
|
47
|
+
console.info('Total errors: %s', this.totalErrors);
|
|
48
|
+
console.info('Total time: %s s', this.totalTimeSeconds);
|
|
49
|
+
console.info('Requests per second: %s', this.rps);
|
|
50
|
+
console.info('Mean latency: %s ms', this.meanLatencyMs);
|
|
51
|
+
console.info('');
|
|
52
|
+
console.info('Percentage of the requests served within a certain time');
|
|
53
|
+
|
|
54
|
+
Object.keys(this.percentiles).forEach(percentile => {
|
|
55
|
+
console.info(' %s% %s ms', percentile, this.percentiles[percentile]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
59
|
+
if (this.totalErrors) {
|
|
60
|
+
console.info('');
|
|
61
|
+
Object.keys(this.errorCodes).forEach(errorCode => {
|
|
62
|
+
const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
|
|
63
|
+
console.info(' %s%s: %s errors', padding, errorCode, this.errorCodes[errorCode]);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtest",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"confinode": "^2.1.1",
|
|
20
20
|
"https-proxy-agent": "^2.2.1",
|
|
21
21
|
"stdio": "0.2.7",
|
|
22
|
-
"testing": "^3.0
|
|
22
|
+
"testing": "^3.1.0",
|
|
23
23
|
"websocket": "^1.0.34"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
package/test/body-generator.js
CHANGED
package/test/integration.js
CHANGED
|
@@ -112,16 +112,16 @@ function testWSIntegration(callback) {
|
|
|
112
112
|
*/
|
|
113
113
|
function testDelay(callback) {
|
|
114
114
|
const delay = 10;
|
|
115
|
-
|
|
115
|
+
const serverOptions = {
|
|
116
116
|
port: PORT + 1,
|
|
117
|
-
delay
|
|
117
|
+
delay,
|
|
118
118
|
quiet: true,
|
|
119
119
|
};
|
|
120
|
-
const server = startServer(
|
|
120
|
+
const server = startServer(serverOptions, error => {
|
|
121
121
|
if (error) {
|
|
122
122
|
return callback(error);
|
|
123
123
|
}
|
|
124
|
-
options = {
|
|
124
|
+
const options = {
|
|
125
125
|
url: 'http://localhost:' + (PORT + 1),
|
|
126
126
|
maxRequests: 10,
|
|
127
127
|
quiet: true,
|
package/test/loadtest.js
CHANGED
|
@@ -2,9 +2,6 @@ import testing from 'testing'
|
|
|
2
2
|
import {loadTest} from '../lib/loadtest.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* A load test with max seconds.
|
|
7
|
-
*/
|
|
8
5
|
function testMaxSeconds(callback) {
|
|
9
6
|
const options = {
|
|
10
7
|
url: 'http://localhost:7357/',
|
|
@@ -15,10 +12,6 @@ function testMaxSeconds(callback) {
|
|
|
15
12
|
loadTest(options, callback);
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A load test with max seconds.
|
|
21
|
-
*/
|
|
22
15
|
function testWSEcho(callback) {
|
|
23
16
|
const options = {
|
|
24
17
|
url: 'ws://localhost:7357/',
|
|
@@ -83,6 +76,34 @@ function testIndexParamWithCallbackAndBody(callback) {
|
|
|
83
76
|
loadTest(options, callback);
|
|
84
77
|
}
|
|
85
78
|
|
|
79
|
+
function testError(callback) {
|
|
80
|
+
const options = {
|
|
81
|
+
maxSeconds: 0.1,
|
|
82
|
+
concurrency: 1,
|
|
83
|
+
quiet: true,
|
|
84
|
+
};
|
|
85
|
+
loadTest(options, (error) => {
|
|
86
|
+
if (!error) {
|
|
87
|
+
return callback('Should error without URL')
|
|
88
|
+
}
|
|
89
|
+
return callback(false, 'OK')
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A load test with keep-alive.
|
|
95
|
+
*/
|
|
96
|
+
function testKeepAlive(callback) {
|
|
97
|
+
const options = {
|
|
98
|
+
url: 'http://localhost:7357/',
|
|
99
|
+
maxSeconds: 0.1,
|
|
100
|
+
concurrency: 1,
|
|
101
|
+
quiet: true,
|
|
102
|
+
keepalive: true,
|
|
103
|
+
};
|
|
104
|
+
loadTest(options, callback)
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
|
|
87
108
|
/**
|
|
88
109
|
* Run all tests.
|
|
@@ -91,6 +112,7 @@ export function test(callback) {
|
|
|
91
112
|
testing.run([
|
|
92
113
|
testMaxSeconds, testWSEcho, testIndexParam, testIndexParamWithBody,
|
|
93
114
|
testIndexParamWithCallback, testIndexParamWithCallbackAndBody,
|
|
115
|
+
testError, testKeepAlive,
|
|
94
116
|
], callback);
|
|
95
117
|
}
|
|
96
118
|
|
|
@@ -13,7 +13,7 @@ function testRequestGenerator(callback) {
|
|
|
13
13
|
const options = {
|
|
14
14
|
url: 'http://localhost:' + PORT,
|
|
15
15
|
method: 'POST',
|
|
16
|
-
requestsPerSecond:
|
|
16
|
+
requestsPerSecond: 1000,
|
|
17
17
|
maxRequests: 100,
|
|
18
18
|
concurrency: 10,
|
|
19
19
|
requestGenerator: (params, options, client, callback) => {
|
package/lib/show.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
|