loadtest 6.1.0 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 call the exported function `loadTest()` with a set of options and an optional callback:
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 an HTTP server which can be `close()`d when it is no longer useful.
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 the optional callback is passed then it will not behave as `async`:
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
- showResult(options, result)
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
 
@@ -36,20 +37,21 @@ class HttpClient {
36
37
  */
37
38
  init() {
38
39
  this.options = urlLib.parse(this.params.url);
39
- this.options.headers = {};
40
- if (this.params.headers) {
41
- this.options.headers = this.params.headers;
42
- }
40
+ this.options.headers = this.params.headers || {}
43
41
  if (this.params.cert && this.params.key) {
44
42
  this.options.cert = this.params.cert;
45
43
  this.options.key = this.params.key;
46
44
  }
47
45
  this.options.agent = false;
46
+ if (this.params.requestsPerSecond) {
47
+ // rps for each client is total / concurrency (# of clients)
48
+ this.options.requestsPerSecond = this.params.requestsPerSecond / this.params.concurrency
49
+ }
48
50
  if (this.params.agentKeepAlive) {
49
- const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive;
51
+ const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
50
52
  let maxSockets = 10;
51
- if (this.params.requestsPerSecond) {
52
- maxSockets += Math.floor(this.params.requestsPerSecond);
53
+ if (this.options.requestsPerSecond) {
54
+ maxSockets += Math.floor(this.options.requestsPerSecond);
53
55
  }
54
56
  this.options.agent = new KeepAlive({
55
57
  maxSockets: maxSockets,
@@ -71,7 +73,7 @@ class HttpClient {
71
73
  } else if (typeof this.params.body == 'function') {
72
74
  this.generateMessage = this.params.body;
73
75
  } else {
74
- console.error('Unrecognized body: %s', typeof this.params.body);
76
+ throw new Error(`Unrecognized body: ${typeof this.params.body}`);
75
77
  }
76
78
  this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
77
79
  }
@@ -81,7 +83,7 @@ class HttpClient {
81
83
  } else if (typeof this.params.cookies == 'string') {
82
84
  this.options.headers.Cookie = this.params.cookies;
83
85
  } else {
84
- console.error('Invalid cookies %j, please use an array or a string', this.params.cookies);
86
+ throw new Error(`Invalid cookies ${JSON.stringify(this.params.cookies)}, please use an array or a string`);
85
87
  }
86
88
  }
87
89
  addUserAgent(this.options.headers);
@@ -94,10 +96,15 @@ class HttpClient {
94
96
  * Start the HTTP client.
95
97
  */
96
98
  start() {
97
- if (!this.params.requestsPerSecond) {
99
+ if (this.stopped) {
100
+ // solves testing issue: with requestsPerSecond clients are started at random,
101
+ // so sometimes they are stopped before they have even started
102
+ return
103
+ }
104
+ if (!this.options.requestsPerSecond) {
98
105
  return this.makeRequest();
99
106
  }
100
- const interval = 1000 / this.params.requestsPerSecond;
107
+ const interval = 1000 / this.options.requestsPerSecond;
101
108
  this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
102
109
  }
103
110
 
@@ -105,6 +112,7 @@ class HttpClient {
105
112
  * Stop the HTTP client.
106
113
  */
107
114
  stop() {
115
+ this.stopped = true
108
116
  if (this.requestTimer) {
109
117
  this.requestTimer.stop();
110
118
  }
@@ -133,7 +141,6 @@ class HttpClient {
133
141
  // adding proxy configuration
134
142
  if (this.params.proxy) {
135
143
  const proxy = this.params.proxy;
136
- //console.log('using proxy server %j', proxy);
137
144
  const agent = new HttpsProxyAgent(proxy);
138
145
  this.options.agent = agent;
139
146
  }
@@ -154,7 +161,8 @@ class HttpClient {
154
161
  delete this.options.headers['Content-Length'];
155
162
  }
156
163
  if (typeof this.params.requestGenerator == 'function') {
157
- request = this.params.requestGenerator(this.params, this.options, lib.request, this.getConnect(id, requestFinished, this.params.contentInspector));
164
+ const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
165
+ request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
158
166
  } else {
159
167
  request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
160
168
  }
@@ -205,7 +213,7 @@ class HttpClient {
205
213
  result.instanceIndex = this.operation.instanceIndex;
206
214
  }
207
215
  let callback;
208
- if (!this.params.requestsPerSecond) {
216
+ if (!this.options.requestsPerSecond) {
209
217
  callback = this.makeRequest.bind(this);
210
218
  }
211
219
  this.operation.callback(error, result, callback);
package/lib/latency.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as crypto from 'crypto'
2
- import {showResult} from './show.js'
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 elapsedSeconds = this.getElapsed(this.initialTime) / 1000;
178
- const meanTime = this.totalTime / this.totalRequests;
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
- showResult(this.options, result)
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: 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.
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(null, error))
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(options)
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,152 +1,131 @@
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 packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
12
- if (!options.url) {
13
- throw new Error('Missing URL in options')
14
- }
15
- options.concurrency = options.concurrency || 1;
16
- if (options.requestsPerSecond) {
17
- options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
18
- }
19
- if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
20
- throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
21
- }
22
- if (options.url.startsWith('ws:')) {
23
- if (options.requestsPerSecond) {
24
- throw new Error(`"requestsPerSecond" not supported for WebSockets`);
11
+ const processed = new Options(options)
12
+ await processed.process()
13
+ return processed
14
+ }
15
+
16
+ class Options {
17
+ constructor(options) {
18
+ this.url = this.getUrl(options)
19
+ const configuration = loadConfig();
20
+ this.concurrency = options.concurrency || configuration.concurrency || 1
21
+ const rps = options.rps ? parseFloat(options.rps) : null
22
+ this.requestsPerSecond = options.requestsPerSecond || rps || configuration.requestsPerSecond
23
+ this.agentKeepAlive = options.keepalive || options.agent || options.agentKeepAlive || configuration.agentKeepAlive;
24
+ this.indexParam = options.index || options.indexParam || configuration.indexParam;
25
+ this.method = options.method || configuration.method || 'GET'
26
+ this.readBodyAndMethod(options, configuration)
27
+ this.key = null
28
+ this.keyFile = options.key || configuration.key
29
+ this.cert = null
30
+ this.certFile = options.cert || configuration.cert
31
+ this.headers = configuration.headers || {}
32
+ this.headers['host'] = urlLib.parse(options.url).host;
33
+ this.headers['accept'] = '*/*';
34
+ if (options.headers) {
35
+ addHeaders(options.headers, this.headers);
25
36
  }
37
+ this.requestGenerator = options.requestGenerator || configuration.requestGenerator
38
+ this.maxRequests = options.maxRequests || configuration.maxRequests
39
+ this.maxSeconds = options.maxSeconds || configuration.maxSeconds
40
+ this.cookies = options.cookies || configuration.cookies
41
+ this.contentType = options.contentType || configuration.contentType
42
+ this.timeout = options.timeout || configuration.timeout
43
+ this.secureProtocol = options.secureProtocol || configuration.secureProtocol
44
+ this.insecure = options.insecure || configuration.insecure
45
+ this.recover = options.recover || configuration.recover
46
+ this.proxy = options.proxy || configuration.proxy
47
+ this.quiet = options.quiet || configuration.quiet
26
48
  }
27
- const configuration = loadConfig();
28
49
 
29
- options.agentKeepAlive = options.keepalive || options.agent || configuration.agentKeepAlive;
30
- options.indexParam = options.index || configuration.indexParam;
31
-
32
- //TODO: add index Param
33
- // Allow a post body string in options
34
- // Ex -P '{"foo": "bar"}'
35
- if (options.postBody) {
36
- options.method = 'POST';
37
- options.body = options.postBody;
38
- }
39
- if (options.postFile) {
40
- options.method = 'POST';
41
- options.body = await readBody(options.postFile, '-p');
42
- }
43
- if (options.data) {
44
- options.body = options.data
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
+ getUrl(options) {
51
+ if (!options.url) {
52
+ throw new Error('Missing URL in options')
50
53
  }
51
- }
52
- if (options.putFile) {
53
- options.method = 'PUT';
54
- options.body = await readBody(options.putFile, '-u');
55
- }
56
- if (options.patchBody) {
57
- options.method = 'PATCH';
58
- options.body = options.patchBody;
59
- }
60
- if (options.patchFile) {
61
- options.method = 'PATCH';
62
- options.body = await readBody(options.patchFile, '-a');
63
- }
64
- if (!options.method) {
65
- options.method = configuration.method;
66
- }
67
- if (!options.body) {
68
- if(configuration.body) {
69
- options.body = configuration.body;
70
- } else if (configuration.file) {
71
- options.body = await readBody(configuration.file, 'configuration.request.file');
54
+ if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
55
+ throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
72
56
  }
57
+ if (options.url.startsWith('ws:')) {
58
+ if (options.requestsPerSecond) {
59
+ throw new Error(`"requestsPerSecond" not supported for WebSockets`);
60
+ }
61
+ }
62
+ return options.url
73
63
  }
74
- options.requestsPerSecond = options.rps ? parseFloat(options.rps) : configuration.requestsPerSecond;
75
- if (!options.key) {
76
- options.key = configuration.key;
77
- }
78
- if (options.key) {
79
- options.key = await readFile(options.key)
80
- }
81
- if (!options.cert) {
82
- options.cert = configuration.cert;
83
- }
84
- if (options.cert) {
85
- options.cert = await readFile(options.cert);
86
- }
87
-
88
- const defaultHeaders = options.headers || !configuration.headers ? {} : configuration.headers;
89
- defaultHeaders['host'] = urlLib.parse(options.url).host;
90
- defaultHeaders['user-agent'] = 'loadtest/' + packageJson.version;
91
- defaultHeaders['accept'] = '*/*';
92
-
93
- if (options.headers) {
94
- addHeaders(options.headers, defaultHeaders);
95
- console.log('headers: %s, %j', typeof defaultHeaders, defaultHeaders);
96
- }
97
- options.headers = defaultHeaders;
98
64
 
99
- if (!options.requestGenerator) {
100
- options.requestGenerator = configuration.requestGenerator;
101
- }
102
- if (typeof options.requestGenerator == 'string') {
103
- options.requestGenerator = await import(options.requestGenerator)
65
+ readBodyAndMethod(options, configuration) {
66
+ if (options.data) {
67
+ this.body = options.data
68
+ }
69
+ // Allow a post body string in options
70
+ // Ex -P '{"foo": "bar"}'
71
+ if (options.postBody) {
72
+ this.method = 'POST';
73
+ this.body = options.postBody
74
+ }
75
+ if (options.postFile) {
76
+ this.method = 'POST';
77
+ this.bodyFile = options.postFile
78
+ }
79
+ if (options.putFile) {
80
+ this.method = 'PUT';
81
+ this.bodyFile = options.putFile
82
+ }
83
+ if (options.patchBody) {
84
+ this.method = 'PATCH';
85
+ this.body = options.patchBody
86
+ }
87
+ if (options.patchFile) {
88
+ this.method = 'PATCH';
89
+ this.bodyFile = options.patchFile
90
+ }
91
+ // sanity check
92
+ if (acceptedMethods.indexOf(this.method) === -1) {
93
+ throw new Error(`Invalid method ${this.method}`)
94
+ }
95
+ if (!options.body) {
96
+ if (configuration.body) {
97
+ this.body = configuration.body;
98
+ } else if (configuration.file) {
99
+ this.bodyFile = configuration.file
100
+ }
101
+ }
104
102
  }
105
103
 
106
- // Use configuration file for other values
107
- if(!options.maxRequests) {
108
- options.maxRequests = configuration.maxRequests;
109
- }
110
- if(!options.concurrency) {
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;
104
+ async process() {
105
+ const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
106
+ this.headers['user-agent'] = 'loadtest/' + packageJson.version;
107
+ if (this.keyFile) {
108
+ this.key = await readFile(this.keyFile)
109
+ }
110
+ if (this.certFile) {
111
+ this.cert = await readFile(this.certFile);
112
+ }
113
+ if (typeof this.requestGenerator == 'string') {
114
+ this.requestGenerator = await import(this.requestGenerator)
115
+ }
116
+ if (this.bodyFile) {
117
+ this.body = await readBody(this.bodyFile);
118
+ }
136
119
  }
137
120
  }
138
121
 
139
- async function readBody(filename, option) {
122
+ async function readBody(filename) {
140
123
  if (typeof filename !== 'string') {
141
- throw new Error(`Invalid file to open with ${option}: ${filename}`);
124
+ throw new Error(`Invalid file to open for body: ${filename}`);
142
125
  }
143
-
144
126
  if (path.extname(filename) === '.js') {
145
127
  return await import(new URL(filename, `file://${process.cwd()}/`))
146
128
  }
147
-
148
- const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
149
-
150
- return ret;
129
+ return await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
151
130
  }
152
131
 
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.1.0",
3
+ "version": "6.2.1",
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.3",
22
+ "testing": "^3.1.0",
23
23
  "websocket": "^1.0.34"
24
24
  },
25
25
  "devDependencies": {
@@ -12,7 +12,7 @@ function testBodyGenerator(callback) {
12
12
  }
13
13
  const options = {
14
14
  url: 'http://localhost:' + PORT,
15
- requestsPerSecond: 100,
15
+ requestsPerSecond: 1000,
16
16
  maxRequests: 100,
17
17
  concurrency: 10,
18
18
  postFile: 'sample/post-file.js',
@@ -112,16 +112,16 @@ function testWSIntegration(callback) {
112
112
  */
113
113
  function testDelay(callback) {
114
114
  const delay = 10;
115
- let options = {
115
+ const serverOptions = {
116
116
  port: PORT + 1,
117
- delay: delay,
117
+ delay,
118
118
  quiet: true,
119
119
  };
120
- const server = startServer(options, error => {
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: 100,
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
-