loadtest 6.3.2 → 7.0.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 CHANGED
@@ -18,17 +18,9 @@ On Ubuntu or Mac OS X systems install using sudo:
18
18
 
19
19
  $ sudo npm install -g loadtest
20
20
 
21
- For access to the API just add package `loadtest` to your `package.json` devDependencies:
21
+ For access to the API just install it in your `npm` package as a dev dependency:
22
22
 
23
- ```json
24
- {
25
- ...
26
- "devDependencies": {
27
- "loadtest": "*"
28
- },
29
- ...
30
- }
31
- ```
23
+ $ npm install --save-dev loadtest
32
24
 
33
25
  ### Compatibility
34
26
 
@@ -88,24 +80,32 @@ so that you can abort deployment e.g. if 99% of the requests don't finish in 10
88
80
 
89
81
  ### Usage Don'ts
90
82
 
91
- `loadtest` saturates a single CPU pretty quickly.
92
- Do not use `loadtest` in this mode
93
- if the Node.js process is above 100% usage in `top`, which happens approx. when your load is above 1000~4000 rps.
83
+ `loadtest` performance has improved significantly,
84
+ but it is still limited.
85
+ `loadtest` saturates a single CPU pretty quickly,
86
+ so it uses half the available cores in your processor.
87
+ If you see that the Node.js processes are above 100% usage in `top`,
88
+ which happens approx. when your load is above 4000~5000 rps per core,
89
+ please adjust the number of cores.
90
+ So for instance with eight cores you can expect to get a maximum performance of
91
+ 8 * 5000 ~ 40 krps.
94
92
  (You can measure the practical limits of `loadtest` on your specific test machines by running it against a simple
95
93
  [test server](#test-server)
96
94
  and seeing when it reaches 100% CPU.)
97
- In this case try using in multi-process mode using the `--cores` parameter,
98
- see below.
99
95
 
100
- There are better tools for that use case:
96
+ If you have reached the limits of `loadtest` even after using all cores,
97
+ there are other tools that you can try.
101
98
 
99
+ * [AutoCannon](https://www.npmjs.com/package/autocannon): also an `npm` package,
100
+ awesome tool with an interface similar to `wrk`.
102
101
  * [Apache `ab`](http://httpd.apache.org/docs/2.2/programs/ab.html)
103
102
  has great performance, but it is also limited by a single CPU performance.
104
103
  Its practical limit is somewhere around ~40 krps.
105
104
  * [weighttp](http://redmine.lighttpd.net/projects/weighttp/wiki) is also `ab`-compatible
106
105
  and is supposed to be very fast (the author has not personally used it).
107
- * [wrk](https://github.com/wg/wrk) is multithreaded and fit for use when multiple CPUs are required or available.
106
+ * [wrk](https://github.com/wg/wrk) is multithreaded and highly performance.
108
107
  It may need installing from source though, and its interface is not `ab`-compatible.
108
+ * [wrk2](https://github.com/giltene/wrk2): evolution of `wrk`.
109
109
 
110
110
  ### Regular Usage
111
111
 
@@ -239,12 +239,12 @@ to provide the body of each request.
239
239
  This is useful if you want to generate request bodies dynamically and vary them for each request.
240
240
  For examples see above for `-p`.
241
241
 
242
- ##### `-r`
242
+ ##### `-r recover`
243
243
 
244
244
  Recover from errors. Always active: loadtest does not stop on errors.
245
245
  After the tests are finished, if there were errors a report with all error codes will be shown.
246
246
 
247
- #### `-s`
247
+ #### `-s secureProtocol`
248
248
 
249
249
  The TLS/SSL method to use. (e.g. TLSv1_method)
250
250
 
@@ -252,7 +252,7 @@ Example:
252
252
 
253
253
  $ loadtest -n 1000 -s TLSv1_method https://www.example.com
254
254
 
255
- #### `-V`
255
+ #### `-V version`
256
256
 
257
257
  Show version number and exit.
258
258
 
@@ -283,15 +283,19 @@ Note: --rps is not supported for websockets.
283
283
  #### `--cores number`
284
284
 
285
285
  Start `loadtest` in multi-process mode on a number of cores simultaneously.
286
- Useful when a single CPU is saturated.
287
286
  Forks the requested number of processes using the
288
287
  [Node.js cluster module](https://nodejs.org/api/cluster.html).
288
+ Default: half the available CPUs on the machine.
289
289
 
290
- In this mode the total number of requests and the rps rate are shared among all processes.
291
- The result returned is the aggregation of results from all cores.
290
+ The total number of requests and the rps rate are shared among all processes.
291
+ The result shown is the aggregation of results from all cores.
292
292
 
293
293
  Note: this option is not available in the API,
294
- where it runs just in the provided process.
294
+ since it runs just within the calling process.
295
+
296
+ **Warning**: the default value for `--cores` has changed in version 7+,
297
+ from 1 to half the available CPUs on the machine.
298
+ Set to 1 to get the previous single-process mode.
295
299
 
296
300
  #### `--timeout milliseconds`
297
301
 
@@ -496,352 +500,51 @@ However, it you try to push it beyond that, at 3 krps it will fail miserably.
496
500
 
497
501
  `loadtest` is not limited to running from the command line; it can be controlled using an API,
498
502
  thus allowing you to load test your application in your own tests.
503
+ A short introduction follows; see [complete docs for API](doc/api.md).
499
504
 
500
505
  ### Invoke Load Test
501
506
 
502
- To run a load test, just `await` for the exported function `loadTest()` with the desired options, described below:
507
+ To run a load test, invoke the exported function `loadTest()` with the desired options:
503
508
 
504
509
  ```javascript
505
510
  import {loadTest} from 'loadtest'
506
511
 
507
512
  const options = {
508
- url: 'http://localhost:8000',
509
- maxRequests: 1000,
513
+ url: 'http://localhost:8000',
514
+ maxRequests: 1000,
510
515
  }
511
516
  const result = await loadTest(options)
512
517
  result.show()
513
518
  console.log('Tests run successfully')
514
- })
515
- ```
516
-
517
- The call returns a `Result` object that contains all info about the load test, also described below.
518
- Call `result.show()` to display the results in the standard format on the console.
519
-
520
- As a legacy from before promises existed,
521
- if an optional callback is passed as second parameter then it will not behave as `async`:
522
- the callback `function(error, result)` will be invoked when the max number of requests is reached,
523
- or when the max number of seconds has elapsed.
524
-
525
- ```javascript
526
- import {loadTest} from 'loadtest'
527
-
528
- const options = {
529
- url: 'http://localhost:8000',
530
- maxRequests: 1000,
531
- }
532
- loadTest(options, function(error, result) {
533
- if (error) {
534
- return console.error('Got an error: %s', error)
535
- }
536
- result.show()
537
- console.log('Tests run successfully')
538
- })
539
- ```
540
-
541
-
542
- Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
543
- and will not call the callback.
544
-
545
- ### Result
546
-
547
- The latency result returned at the end of the load test contains a full set of data, including:
548
- mean latency, number of errors and percentiles.
549
- A simplified example follows:
550
-
551
- ```javascript
552
- {
553
- url: 'http://localhost:80/',
554
- maxRequests: 1000,
555
- maxSeconds: 0,
556
- concurrency: 10,
557
- agent: 'none',
558
- requestsPerSecond: undefined,
559
- totalRequests: 1000,
560
- percentiles: {
561
- '50': 7,
562
- '90': 10,
563
- '95': 11,
564
- '99': 15
565
- },
566
- effectiveRps: 2824,
567
- elapsedSeconds: 0.354108,
568
- meanLatencyMs: 7.72,
569
- maxLatencyMs: 20,
570
- totalErrors: 3,
571
- errorCodes: {
572
- '0': 1,
573
- '500': 2
574
- },
575
- }
576
- ```
577
-
578
- The `result` object also has a `result.show()` function
579
- that displays the results on the console in the standard format.
580
-
581
- ### Options
582
-
583
- All options but `url` are, as their name implies, optional.
584
-
585
- #### `url`
586
-
587
- The URL to invoke. Mandatory.
588
-
589
- #### `concurrency`
590
-
591
- How many clients to start in parallel.
592
-
593
- #### `maxRequests`
594
-
595
- A max number of requests; after they are reached the test will end.
596
-
597
- Note: the actual number of requests sent can be bigger if there is a concurrency level;
598
- loadtest will report just on the max number of requests.
599
-
600
- #### `maxSeconds`
601
-
602
- Max number of seconds to run the tests.
603
-
604
- Note: after the given number of seconds `loadtest` will stop sending requests,
605
- but may continue receiving tests afterwards.
606
-
607
- #### `timeout`
608
-
609
- Timeout for each generated request in milliseconds. Setting this to 0 disables timeout (default).
610
-
611
- #### `cookies`
612
-
613
- An array of cookies to send. Each cookie should be a string of the form name=value.
614
-
615
- #### `headers`
616
-
617
- A map of headers. Each header should be an entry in the map with the value given as a string.
618
- If you want to have several values for a header, write a single value separated by semicolons,
619
- like this:
620
-
621
- {
622
- accept: "text/plain;text/html"
623
- }
624
-
625
- Note: when using the API, the "host" header is not inferred from the URL but needs to be sent
626
- explicitly.
627
-
628
- #### `method`
629
-
630
- The method to use: POST, PUT. Default: GET.
631
-
632
- #### `body`
633
-
634
- The contents to send in the body of the message, for POST or PUT requests.
635
- Can be a string or an object (which will be converted to JSON).
636
-
637
- #### `contentType`
638
-
639
- The MIME type to use for the body. Default content type is `text/plain`.
640
-
641
- #### `requestsPerSecond`
642
-
643
- How many requests each client will send per second.
644
-
645
- #### `requestGenerator`
646
-
647
- Use a custom request generator function.
648
- The request needs to be generated synchronously and returned when this function is invoked.
649
-
650
- Example request generator function could look like this:
651
-
652
- ```javascript
653
- function(params, options, client, callback) {
654
- const message = generateMessage();
655
- const request = client(options, callback);
656
- options.headers['Content-Length'] = message.length;
657
- options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
658
- request.write(message);
659
- request.end();
660
- return request;
661
- }
662
- ```
663
-
664
- See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
665
- (or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
666
-
667
- #### `agentKeepAlive`
668
-
669
- Use an agent with 'Connection: Keep-alive'.
670
-
671
- Note: Uses [agentkeepalive](https://npmjs.org/package/agentkeepalive),
672
- which performs better than the default node.js agent.
673
-
674
- #### `quiet` (deprecated)
675
-
676
- Do not show any messages.
677
-
678
- Note: deprecated in version 6+, shows a warning.
679
-
680
- #### `indexParam`
681
-
682
- The given string will be replaced in the final URL with a unique index.
683
- E.g.: if URL is `http://test.com/value` and `indexParam=value`, then the URL
684
- will be:
685
-
686
- * http://test.com/1
687
- * http://test.com/2
688
- * ...
689
- * body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_1 }`
690
-
691
- #### `indexParamCallback`
692
-
693
- A function that would be executed to replace the value identified through `indexParam` through a custom value generator.
694
-
695
- E.g.: if URL is `http://test.com/value` and `indexParam=value` and
696
- ```javascript
697
- indexParamCallback: function customCallBack() {
698
- return Math.floor(Math.random() * 10); //returns a random integer from 0 to 9
699
- }
700
- ```
701
- then the URL could be:
702
-
703
- * http://test.com/1 (Randomly generated integer 1)
704
- * http://test.com/5 (Randomly generated integer 5)
705
- * http://test.com/6 (Randomly generated integer 6)
706
- * http://test.com/8 (Randomly generated integer 8)
707
- * ...
708
- * body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_<value from callback> }`
709
-
710
- #### `insecure`
711
-
712
- Allow invalid and self-signed certificates over https.
713
-
714
- #### `secureProtocol`
715
-
716
- The TLS/SSL method to use. (e.g. TLSv1_method)
717
-
718
- Example:
719
-
720
- ```javascript
721
- import {loadTest} from 'loadtest'
722
-
723
- const options = {
724
- url: 'https://www.example.com',
725
- maxRequests: 100,
726
- secureProtocol: 'TLSv1_method'
727
- }
728
-
729
- loadTest(options, function(error) {
730
- if (error) {
731
- return console.error('Got an error: %s', error)
732
- }
733
- console.log('Tests run successfully')
734
- })
735
- ```
736
-
737
- #### `statusCallback`
738
-
739
- If present, this function executes after every request operation completes. Provides immediate access to the test result while the
740
- test batch is still running. This can be used for more detailed custom logging or developing your own spreadsheet or
741
- statistical analysis of the result.
742
-
743
- The result and error passed to the callback are in the same format as the result passed to the final callback.
744
-
745
- In addition, the following three properties are added to the `result` object:
746
-
747
- - `requestElapsed`: time in milliseconds it took to complete this individual request.
748
- - `requestIndex`: 0-based index of this particular request in the sequence of all requests to be made.
749
- - `instanceIndex`: the `loadtest(...)` instance index. This is useful if you call `loadtest()` more than once.
750
-
751
- You will need to check if `error` is populated in order to determine which object to check for these properties.
752
-
753
- The second parameter contains info about the current request:
754
-
755
- ```javascript
756
- {
757
- host: 'localhost',
758
- path: '/',
759
- method: 'GET',
760
- statusCode: 200,
761
- body: '<html><body>hi</body></html>',
762
- headers: [...]
763
- }
764
- ```
765
-
766
- Example:
767
-
768
- ```javascript
769
- import {loadTest} from 'loadtest'
770
-
771
- function statusCallback(error, result, latency) {
772
- console.log('Current latency %j, result %j, error %j', latency, result, error)
773
- console.log('----')
774
- console.log('Request elapsed milliseconds: ', result.requestElapsed)
775
- console.log('Request index: ', result.requestIndex)
776
- console.log('Request loadtest() instance index: ', result.instanceIndex)
777
- }
778
-
779
- const options = {
780
- url: 'http://localhost:8000',
781
- maxRequests: 1000,
782
- statusCallback: statusCallback
783
- }
784
-
785
- loadTest(options, function(error) {
786
- if (error) {
787
- return console.error('Got an error: %s', error)
788
- }
789
- console.log('Tests run successfully')
790
- })
791
519
  ```
792
-
793
- In some situations request data needs to be available in the statusCallBack.
794
- This data can be assigned to `request.labels` in the requestGenerator:
795
- ```javascript
796
- const options = {
797
- // ...
798
- requestGenerator: (params, options, client, callback) => {
799
- // ...
800
- const randomInputData = Math.random().toString().substr(2, 8);
801
- const message = JSON.stringify({ randomInputData })
802
- const request = client(options, callback);
803
- request.labels = randomInputData;
804
- request.write(message);
805
- return request;
806
- }
807
- };
808
- ```
809
-
810
- Then in statusCallBack the labels can be accessed through `result.labels`:
811
- ```javascript
812
- function statusCallback(error, result, latency) {
813
- console.log(result.labels);
814
- }
815
- ```
816
-
817
- **Warning**: The format for `statusCallback` has changed in version 2.0.0 onwards.
818
- It used to be `statusCallback(latency, result, error)`,
819
- it has been changed to conform to the usual Node.js standard.
820
-
821
- #### `contentInspector`
822
-
823
- A function that would be executed after every request before its status be added to the final statistics.
824
520
 
825
- The is can be used when you want to mark some result with 200 http status code to be failed or error.
826
-
827
- The `result` object passed to this callback function has the same fields as the `result` object passed to `statusCallback`.
828
-
829
- `customError` can be added to mark this result as failed or error. `customErrorCode` will be provided in the final statistics, in addtion to the http status code.
830
-
831
- Example:
832
-
833
- ```javascript
834
- function contentInspector(result) {
835
- if (result.statusCode == 200) {
836
- const body = JSON.parse(result.body)
837
- // how to examine the body depends on the content that the service returns
838
- if (body.status.err_code !== 0) {
839
- result.customError = body.status.err_code + " " + body.status.msg
840
- result.customErrorCode = body.status.err_code
841
- }
842
- }
843
- },
844
- ```
521
+ Beware: if there are no `maxRequests` and no `maxSeconds`, the test will run forever.
522
+
523
+ ### `loadTest()` Parameters
524
+
525
+ A simplified list of parameters is shown below;
526
+ see [doc/api.md](doc/api.md) for the full explanations with examples.
527
+
528
+ * `url`: URL to invoke, mandatory.
529
+ * `concurrency`: how many clients to start in parallel.
530
+ * `maxRequests`: max number of requests; after they are reached the test will end.
531
+ * `maxSeconds`: max number of seconds to run the tests.
532
+ * `timeout`: timeout for each generated request in milliseconds, set to 0 to disable (default).
533
+ * `cookies`: array of cookies to send, of the form `name=value`.
534
+ * `headers`: object with headers, each with the value as string. Separate by semicolons to have multiple values.
535
+ * `method`: HTTP method to use, default `GET`.
536
+ * `body`: contents to send in the body of the message.
537
+ * `contentType`: MIME type to use for the body, default `text/plain`.
538
+ * `requestsPerSecond`: how many requests will be sent per second.
539
+ * `requestGenerator`: custom request generator function.
540
+ * `agentKeepAlive`: if true, will use 'Connection: Keep-alive'.
541
+ * `quiet`: if true, do not show any messages.
542
+ * `indexParam`: parameter to replace in URL and body with a unique index.
543
+ * `indexParamCallback`: function to generate unique indexes.
544
+ * `insecure`: allow invalid and self-signed certificates over https.
545
+ * `secureProtocol`: TLS/SSL method to use.
546
+ * `statusCallback(error, result)`: function to call after every request is completed.
547
+ * `contentInspector(result)`: function to call before aggregating statistics.
845
548
 
846
549
  ### Start Test Server
847
550
 
@@ -854,35 +557,15 @@ const server = await startServer({port: 8000})
854
557
  await server.close()
855
558
  ```
856
559
 
857
- This function returns when the server is up and running,
858
- with an HTTP server which can be `close()`d when it is no longer useful.
859
- As a legacy from before promises existed,
860
- if an optional callback is passed as second parameter then it will not behave as `async`:
861
-
862
- ```
863
- const server = startServer({port: 8000}, error => console.error(error))
864
- ```
865
-
866
- The following options are available.
867
-
868
- #### `port`
869
-
870
- Optional port to use for the server.
871
-
872
- Note: the default port is 7357, since port 80 requires special privileges.
873
-
874
- #### `delay`
875
-
876
- Wait the given number of milliseconds to answer each request.
877
-
878
- #### `error`
879
-
880
- Return an HTTP error code.
881
-
882
- #### `percent`
560
+ The following options are available:
561
+ * `port`: optional port to use for the server, default 7357.
562
+ * `delay`: milliseconds to wait before answering each request.
563
+ * `error`: HTTP status code to return, default 200 (no error).
564
+ * `percent`: return error only for the given % of requests.
565
+ * `logger(request, response)`
883
566
 
884
- Return an HTTP error code only for the given % of requests.
885
- If no error code was specified, default is 500.
567
+ A function to be called as `logger(request, response)` after every request served by the test server.
568
+ Where `request` and `response` are the usual HTTP objects.
886
569
 
887
570
  ### Configuration file
888
571
 
package/bin/loadtest.js CHANGED
@@ -5,6 +5,7 @@ import * as stdio from 'stdio'
5
5
  import {loadTest} from '../lib/loadtest.js'
6
6
  import {runTask} from '../lib/cluster.js'
7
7
  import {Result} from '../lib/result.js'
8
+ import {getHalfCores} from '../lib/cluster.js'
8
9
 
9
10
 
10
11
  const options = stdio.getopt({
@@ -34,7 +35,7 @@ const options = stdio.getopt({
34
35
  key: {args: 1, description: 'The client key to use'},
35
36
  cert: {args: 1, description: 'The client certificate to use'},
36
37
  quiet: {description: 'Do not log any messages'},
37
- cores: {args: 1, description: 'Number of cores to use', default: 1},
38
+ cores: {args: 1, description: 'Number of cores to use', default: getHalfCores()},
38
39
  agent: {description: 'Use a keep-alive http agent (deprecated)'},
39
40
  debug: {description: 'Show debug messages (deprecated)'},
40
41
  });
package/doc/api.md ADDED
@@ -0,0 +1,366 @@
1
+ # loadtest API
2
+
3
+ The `loadtest` package is a powerful tool that can be added to your package.
4
+ The API allows for easy integration in your own tests,
5
+ for instance to run automated load tests.
6
+ You can verify the performance of your server before each deployment,
7
+ and deploy only if a certain target is reached.
8
+
9
+ ## Installation
10
+
11
+ For access to the API just install it in your `npm` package as a dev dependency:
12
+
13
+ $ npm install --save-dev loadtest
14
+
15
+ ### Compatibility
16
+
17
+ See [README file](../README.md) for compatibility.
18
+
19
+ ## API
20
+
21
+ `loadtest` is not limited to running from the command line; it can be controlled using an API,
22
+ thus allowing you to load test your application in your own tests.
23
+
24
+ ### Invoke Load Test
25
+
26
+ To run a load test, just `await` for the exported function `loadTest()` with the desired options, described below:
27
+
28
+ ```javascript
29
+ import {loadTest} from 'loadtest'
30
+
31
+ const options = {
32
+ url: 'http://localhost:8000',
33
+ maxRequests: 1000,
34
+ }
35
+ const result = await loadTest(options)
36
+ result.show()
37
+ console.log('Tests run successfully')
38
+ ```
39
+
40
+ The call returns a `Result` object that contains all info about the load test, also described below.
41
+ Call `result.show()` to display the results in the standard format on the console.
42
+
43
+ As a legacy from before promises existed,
44
+ if an optional callback is passed as second parameter then it will not behave as `async`:
45
+ the callback `function(error, result)` will be invoked when the max number of requests is reached,
46
+ or when the max number of seconds has elapsed.
47
+
48
+ ```javascript
49
+ import {loadTest} from 'loadtest'
50
+
51
+ const options = {
52
+ url: 'http://localhost:8000',
53
+ maxRequests: 1000,
54
+ }
55
+ loadTest(options, function(error, result) {
56
+ if (error) {
57
+ return console.error('Got an error: %s', error)
58
+ }
59
+ result.show()
60
+ console.log('Tests run successfully')
61
+ })
62
+ ```
63
+
64
+
65
+ Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
66
+ and will not call the callback.
67
+
68
+ ### Result
69
+
70
+ The latency result returned at the end of the load test contains a full set of data, including:
71
+ mean latency, number of errors and percentiles.
72
+ A simplified example follows:
73
+
74
+ ```javascript
75
+ {
76
+ url: 'http://localhost:80/',
77
+ maxRequests: 1000,
78
+ maxSeconds: 0,
79
+ concurrency: 10,
80
+ agent: 'none',
81
+ requestsPerSecond: undefined,
82
+ totalRequests: 1000,
83
+ percentiles: {
84
+ '50': 7,
85
+ '90': 10,
86
+ '95': 11,
87
+ '99': 15
88
+ },
89
+ effectiveRps: 2824,
90
+ elapsedSeconds: 0.354108,
91
+ meanLatencyMs: 7.72,
92
+ maxLatencyMs: 20,
93
+ totalErrors: 3,
94
+ errorCodes: {
95
+ '0': 1,
96
+ '500': 2
97
+ },
98
+ }
99
+ ```
100
+
101
+ The `result` object also has a `result.show()` function
102
+ that displays the results on the console in the standard format.
103
+
104
+ ### Options
105
+
106
+ All options but `url` are, as their name implies, optional.
107
+ See also the [simplified list](../README.md#loadtest-parameters).
108
+
109
+ #### `url`
110
+
111
+ The URL to invoke. Mandatory.
112
+
113
+ #### `concurrency`
114
+
115
+ How many clients to start in parallel.
116
+
117
+ #### `maxRequests`
118
+
119
+ A max number of requests; after they are reached the test will end.
120
+
121
+ Note: the actual number of requests sent can be bigger if there is a concurrency level;
122
+ loadtest will report just on the max number of requests.
123
+
124
+ #### `maxSeconds`
125
+
126
+ Max number of seconds to run the tests.
127
+
128
+ Note: after the given number of seconds `loadtest` will stop sending requests,
129
+ but may continue receiving tests afterwards.
130
+
131
+ #### `timeout`
132
+
133
+ Timeout for each generated request in milliseconds. Setting this to 0 disables timeout (default).
134
+
135
+ #### `cookies`
136
+
137
+ An array of cookies to send. Each cookie should be a string of the form name=value.
138
+
139
+ #### `headers`
140
+
141
+ A map of headers. Each header should be an entry in the map with the value given as a string.
142
+ If you want to have several values for a header, write a single value separated by semicolons,
143
+ like this:
144
+
145
+ {
146
+ accept: "text/plain;text/html"
147
+ }
148
+
149
+ Note: when using the API, the "host" header is not inferred from the URL but needs to be sent
150
+ explicitly.
151
+
152
+ #### `method`
153
+
154
+ The method to use: POST, PUT. Default: GET.
155
+
156
+ #### `body`
157
+
158
+ The contents to send in the body of the message, for POST or PUT requests.
159
+ Can be a string or an object (which will be converted to JSON).
160
+
161
+ #### `contentType`
162
+
163
+ The MIME type to use for the body. Default content type is `text/plain`.
164
+
165
+ #### `requestsPerSecond`
166
+
167
+ How many requests will be sent per second globally.
168
+
169
+ #### `requestGenerator(params, options, client, callback)`
170
+
171
+ Use a custom request generator function.
172
+ The request needs to be generated synchronously and returned when this function is invoked.
173
+
174
+ Example request generator function could look like this:
175
+
176
+ ```javascript
177
+ function(params, options, client, callback) {
178
+ const message = generateMessage();
179
+ const request = client(options, callback);
180
+ options.headers['Content-Length'] = message.length;
181
+ options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
182
+ request.write(message);
183
+ request.end();
184
+ return request;
185
+ }
186
+ ```
187
+
188
+ See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
189
+ (or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
190
+
191
+ #### `agentKeepAlive`
192
+
193
+ Use an agent with 'Connection: Keep-alive'.
194
+
195
+ Note: Uses [agentkeepalive](https://npmjs.org/package/agentkeepalive),
196
+ which performs better than the default node.js agent.
197
+
198
+ #### `quiet`
199
+
200
+ Do not show any messages.
201
+
202
+ #### `indexParam`
203
+
204
+ The given string will be replaced in the final URL with a unique index.
205
+ E.g.: if URL is `http://test.com/value` and `indexParam=value`, then the URL
206
+ will be:
207
+
208
+ * http://test.com/1
209
+ * http://test.com/2
210
+ * ...
211
+ * body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_1 }`
212
+
213
+ #### `indexParamCallback`
214
+
215
+ A function that would be executed to replace the value identified through `indexParam` through a custom value generator.
216
+
217
+ E.g.: if URL is `http://test.com/value` and `indexParam=value` and
218
+ ```javascript
219
+ indexParamCallback: function customCallBack() {
220
+ return Math.floor(Math.random() * 10); //returns a random integer from 0 to 9
221
+ }
222
+ ```
223
+ then the URL could be:
224
+
225
+ * http://test.com/1 (Randomly generated integer 1)
226
+ * http://test.com/5 (Randomly generated integer 5)
227
+ * http://test.com/6 (Randomly generated integer 6)
228
+ * http://test.com/8 (Randomly generated integer 8)
229
+ * ...
230
+ * body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_<value from callback> }`
231
+
232
+ #### `insecure`
233
+
234
+ Allow invalid and self-signed certificates over https.
235
+
236
+ #### `secureProtocol`
237
+
238
+ The TLS/SSL method to use. (e.g. TLSv1_method)
239
+
240
+ Example:
241
+
242
+ ```javascript
243
+ import {loadTest} from 'loadtest'
244
+
245
+ const options = {
246
+ url: 'https://www.example.com',
247
+ maxRequests: 100,
248
+ secureProtocol: 'TLSv1_method'
249
+ }
250
+
251
+ loadTest(options, function(error) {
252
+ if (error) {
253
+ return console.error('Got an error: %s', error)
254
+ }
255
+ console.log('Tests run successfully')
256
+ })
257
+ ```
258
+
259
+ #### `statusCallback(error, result)`
260
+
261
+ If present, this function executes after every request operation completes. Provides immediate access to the test result while the
262
+ test batch is still running. This can be used for more detailed custom logging or developing your own spreadsheet or
263
+ statistical analysis of the result.
264
+
265
+ The `error` and `result` passed to the callback are in the same format as the result passed to the final callback:
266
+
267
+ * `error` is only populated if the request finished in error,
268
+ * `result` contains info about the current request: `host`, `path`, `method`, `statusCode`, received `body` and `headers`.
269
+ Additionally has the following parameters:
270
+ - `requestElapsed`: time in milliseconds it took to complete this individual request.
271
+ - `requestIndex`: 0-based index of this particular request in the sequence of all requests to be made.
272
+ - `instanceIndex`: the `loadtest(...)` instance index. This is useful if you call `loadtest()` more than once.
273
+
274
+ Example result:
275
+
276
+ ```javascript
277
+ {
278
+ host: 'localhost',
279
+ path: '/',
280
+ method: 'GET',
281
+ statusCode: 200,
282
+ body: '<html><body>hi</body></html>',
283
+ headers: [...],
284
+ requestElapsed: 248,
285
+ requestIndex: 8748,
286
+ instanceIndex: 5,
287
+ }
288
+ ```
289
+
290
+ See [full example](./status-callback.md).
291
+
292
+ **Warning**: The format for `statusCallback` has changed in version 7+.
293
+ Used to be `statusCallback(error, result, latency)`;
294
+ the third parameter `latency` has been removed due to performance reasons.
295
+
296
+ #### `contentInspector(result)`
297
+
298
+ A function that would be executed after every request before its status be added to the final statistics.
299
+
300
+ The is can be used when you want to mark some result with 200 http status code to be failed or error.
301
+
302
+ The `result` object passed to this callback function has the same fields as the `result` object passed to `statusCallback`.
303
+
304
+ `customError` can be added to mark this result as failed or error. `customErrorCode` will be provided in the final statistics, in addtion to the http status code.
305
+
306
+ Example:
307
+
308
+ ```javascript
309
+ function contentInspector(result) {
310
+ if (result.statusCode == 200) {
311
+ const body = JSON.parse(result.body)
312
+ // how to examine the body depends on the content that the service returns
313
+ if (body.status.err_code !== 0) {
314
+ result.customError = body.status.err_code + " " + body.status.msg
315
+ result.customErrorCode = body.status.err_code
316
+ }
317
+ }
318
+ },
319
+ ```
320
+
321
+ ### Start Test Server
322
+
323
+ To start the test server use the exported function `startServer()` with a set of options:
324
+
325
+ ```javascript
326
+ import {startServer} from 'loadtest'
327
+ const server = await startServer({port: 8000})
328
+ // do your thing
329
+ await server.close()
330
+ ```
331
+
332
+ This function returns when the server is up and running,
333
+ with an HTTP server which can be `close()`d when it is no longer useful.
334
+ As a legacy from before promises existed,
335
+ if an optional callback is passed as second parameter then it will not behave as `async`:
336
+
337
+ ```javascript
338
+ const server = startServer({port: 8000}, error => console.error(error))
339
+ ```
340
+
341
+ The following options are available.
342
+
343
+ #### `port`
344
+
345
+ Optional port to use for the server.
346
+
347
+ Note: the default port is 7357, since port 80 requires special privileges.
348
+
349
+ #### `delay`
350
+
351
+ Wait the given number of milliseconds to answer each request.
352
+
353
+ #### `error`
354
+
355
+ Return an HTTP error code.
356
+
357
+ #### `percent`
358
+
359
+ Return an HTTP error code only for the given % of requests.
360
+ If no error code was specified, default is 500.
361
+
362
+ #### `logger(request, response)`
363
+
364
+ A function to be called after every request served by the test server.
365
+ `request` and `response` are the usual HTTP objects.
366
+
@@ -0,0 +1,105 @@
1
+ # `statusCallback`
2
+
3
+ In-depth examples for the `statusCallback` API parameter.
4
+
5
+ ## `options.statusCallback(error, result)`
6
+
7
+ This function, if present, is invoked after every request is finished.
8
+ It uses the old-style convention `(error, result)`:
9
+ the `error` is only present if the request fails,
10
+ while the `result` is always present and contains info about the request.
11
+
12
+ **Warning**: The format for `statusCallback` has changed in version 7+.
13
+ The third parameter `latency` has been removed due to performance reasons.
14
+
15
+ **Warning**: The format for `statusCallback` has changed in version 2.0.0 onwards.
16
+ It used to be `statusCallback(latency, result, error)`,
17
+ it has been changed to conform to the usual Node.js standard.
18
+
19
+ ### `result` format
20
+
21
+ The `result` parameter has the following attributes, always present:
22
+
23
+ * `host`: the host where the request was sent.
24
+ * `path`: the URL path to send the request.
25
+ * `method: HTTP method used.
26
+ * `statusCode: HTTP status code, 200 is OK.
27
+ * `body: content received from the server.
28
+ * `headers: sent by the server.
29
+ * `requestElapsed`: time in milliseconds it took to complete this individual request.
30
+ * `requestIndex`: 0-based index of this particular request in the sequence of all requests to be made.
31
+ * `instanceIndex`: the `loadtest(...)` instance index. This is useful if you call `loadtest()` more than once.
32
+
33
+ ### Example Result
34
+
35
+ A sample result might look like this:
36
+
37
+ ```javascript
38
+ {
39
+ host: 'localhost',
40
+ path: '/',
41
+ method: 'GET',
42
+ statusCode: 200,
43
+ body: '<html><body>hi</body></html>',
44
+ headers: [...],
45
+ requestElapsed: 248,
46
+ requestIndex: 8748,
47
+ instanceIndex: 5,
48
+ }
49
+ ```
50
+
51
+ ## Full example
52
+
53
+ A full example of how to use the `statusCallback` follows:
54
+
55
+ ```javascript
56
+ import {loadTest} from 'loadtest'
57
+
58
+ function statusCallback(error, result, latency) {
59
+ console.log('Current latency %j, result %j, error %j', latency, result, error)
60
+ console.log('----')
61
+ console.log('Request elapsed milliseconds: ', result.requestElapsed)
62
+ console.log('Request index: ', result.requestIndex)
63
+ console.log('Request loadtest() instance index: ', result.instanceIndex)
64
+ }
65
+
66
+ const options = {
67
+ url: 'http://localhost:8000',
68
+ maxRequests: 1000,
69
+ statusCallback: statusCallback
70
+ }
71
+
72
+ loadTest(options, function(error) {
73
+ if (error) {
74
+ return console.error('Got an error: %s', error)
75
+ }
76
+ console.log('Tests run successfully')
77
+ })
78
+ ```
79
+
80
+ ### Adding Request Data
81
+
82
+ In some situations request data needs to be available in the statusCallBack.
83
+ This data can be assigned to `request.labels` in the requestGenerator:
84
+ ```javascript
85
+ const options = {
86
+ // ...
87
+ requestGenerator: (params, options, client, callback) => {
88
+ // ...
89
+ const randomInputData = Math.random().toString().substr(2, 8);
90
+ const message = JSON.stringify({ randomInputData })
91
+ const request = client(options, callback);
92
+ request.labels = randomInputData;
93
+ request.write(message);
94
+ return request;
95
+ }
96
+ };
97
+ ```
98
+
99
+ Then in statusCallBack the labels can be accessed through `result.labels`:
100
+ ```javascript
101
+ function statusCallback(error, result, latency) {
102
+ console.log(result.labels);
103
+ }
104
+ ```
105
+
package/lib/httpClient.js CHANGED
@@ -9,6 +9,8 @@ import * as agentkeepalive from 'agentkeepalive'
9
9
  import * as HttpsProxyAgent from 'https-proxy-agent'
10
10
 
11
11
 
12
+ let uniqueIndex = 1
13
+
12
14
  /**
13
15
  * Create a new HTTP client.
14
16
  * Seem parameters below.
@@ -90,6 +92,19 @@ class HttpClient {
90
92
  if (this.params.secureProtocol) {
91
93
  this.options.secureProtocol = this.params.secureProtocol;
92
94
  }
95
+ // adding proxy configuration
96
+ if (this.params.proxy) {
97
+ const proxy = this.params.proxy;
98
+ const agent = new HttpsProxyAgent(proxy);
99
+ this.options.agent = agent;
100
+ }
101
+ if (this.params.indexParam) {
102
+ this.options.indexParamFinder = new RegExp(this.params.indexParam, 'g');
103
+ }
104
+ // Disable certificate checking
105
+ if (this.params.insecure === true) {
106
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
107
+ }
93
108
  }
94
109
 
95
110
  /**
@@ -129,43 +144,10 @@ class HttpClient {
129
144
  this.operation.requests += 1;
130
145
 
131
146
  const id = this.operation.latency.start();
147
+ const options = {...this.options, headers: {...this.options.headers}}
132
148
  const requestFinished = this.getRequestFinisher(id);
133
- let lib = http;
134
- if (this.options.protocol == 'https:') {
135
- lib = https;
136
- }
137
- if (this.options.protocol == 'ws:') {
138
- lib = websocket;
139
- }
140
-
141
- // adding proxy configuration
142
- if (this.params.proxy) {
143
- const proxy = this.params.proxy;
144
- const agent = new HttpsProxyAgent(proxy);
145
- this.options.agent = agent;
146
- }
147
-
148
-
149
- // Disable certificate checking
150
- if (this.params.insecure === true) {
151
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
152
- }
153
- let request, message;
154
- if (this.generateMessage) {
155
- message = this.generateMessage(id);
156
- if (typeof message === 'object') {
157
- message = JSON.stringify(message);
158
- }
159
- this.options.headers['Content-Length'] = Buffer.byteLength(message);
160
- } else {
161
- delete this.options.headers['Content-Length'];
162
- }
163
- if (typeof this.params.requestGenerator == 'function') {
164
- const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
165
- request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
166
- } else {
167
- request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
168
- }
149
+ this.customizeIndex(options)
150
+ const request = this.getRequest(id, options, requestFinished)
169
151
  if (this.params.timeout) {
170
152
  const timeout = parseInt(this.params.timeout);
171
153
  if (!timeout) {
@@ -175,8 +157,10 @@ class HttpClient {
175
157
  requestFinished('Connection timed out');
176
158
  });
177
159
  }
160
+ const message = this.getMessage(id, options)
178
161
  if (message) {
179
162
  request.write(message);
163
+ options.headers['Content-Length'] = Buffer.byteLength(message);
180
164
  }
181
165
  request.on('error', error => {
182
166
  requestFinished('Connection error: ' + error.message);
@@ -184,6 +168,54 @@ class HttpClient {
184
168
  request.end();
185
169
  }
186
170
 
171
+ customizeIndex(options) {
172
+ if (!this.options.indexParamFinder) {
173
+ return
174
+ }
175
+ options.customIndex = this.getCustomIndex()
176
+ options.path = this.options.path.replace(this.options.indexParamFinder, options.customIndex);
177
+ }
178
+
179
+ getCustomIndex() {
180
+ if (this.options.indexParamCallback instanceof Function) {
181
+ return this.options.indexParamCallback();
182
+ }
183
+ const customIndex = uniqueIndex
184
+ uniqueIndex += 1
185
+ return customIndex
186
+ }
187
+
188
+ getLib() {
189
+ if (this.options.protocol == 'https:') {
190
+ return https;
191
+ }
192
+ if (this.options.protocol == 'ws:') {
193
+ return websocket;
194
+ }
195
+ return http;
196
+ }
197
+
198
+ getMessage(id, options) {
199
+ if (!this.generateMessage) {
200
+ return
201
+ }
202
+ const candidate = this.generateMessage(id);
203
+ const message = typeof candidate === 'object' ? JSON.stringify(candidate) : candidate
204
+ if (this.options.indexParamFinder) {
205
+ return message.replace(this.options.indexParamFinder, options.customIndex);
206
+ }
207
+ return message
208
+ }
209
+
210
+ getRequest(id, options, requestFinished) {
211
+ const lib = this.getLib()
212
+ if (typeof this.params.requestGenerator == 'function') {
213
+ const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
214
+ return this.params.requestGenerator(this.params, options, lib.request, connect);
215
+ }
216
+ return lib.request(options, this.getConnect(id, requestFinished, this.params.contentInspector));
217
+ }
218
+
187
219
  /**
188
220
  * Get a function that finishes one request and goes for the next.
189
221
  */
package/lib/loadtest.js CHANGED
@@ -16,23 +16,25 @@ https.globalAgent.maxSockets = 1000;
16
16
  * Run a load test.
17
17
  * Parameters:
18
18
  * - `options`: an object which may have:
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.
19
+ * - url: URL to access (mandatory).
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
+ * - data: 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
29
  * - agentKeepAlive: if true, then use connection keep-alive.
30
- * - indexParam [string]: string to replace with a unique index.
30
+ * - indexParam: 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
+ * - secureProtocol: TLS/SSL secure protocol method to use.
33
+ * - proxy: use a proxy for requests e.g. http://localhost:8080.
34
34
  * - quiet: do not log any messages.
35
35
  * - debug: show debug messages (deprecated).
36
+ * - requestGenerator: use a custom function to generate requests.
37
+ * - statusCallback: function called after every request.
36
38
  * - `callback`: optional `function(result, error)` called if/when the test finishes;
37
39
  * if not present a promise is returned.
38
40
  */
@@ -109,7 +111,7 @@ class Operation {
109
111
  next();
110
112
  }
111
113
  if (this.options.statusCallback) {
112
- this.options.statusCallback(error, result, this.latency.getResult());
114
+ this.options.statusCallback(error, result);
113
115
  }
114
116
  }
115
117
 
@@ -117,33 +119,9 @@ class Operation {
117
119
  * Start a number of measuring clients.
118
120
  */
119
121
  startClients() {
120
- const url = this.options.url;
121
- const strBody = JSON.stringify(this.options.body);
122
122
  for (let index = 0; index < this.options.concurrency; index++) {
123
- if (this.options.indexParam) {
124
- let oldToken = new RegExp(this.options.indexParam, 'g');
125
- if(this.options.indexParamCallback instanceof Function) {
126
- let customIndex = this.options.indexParamCallback();
127
- this.options.url = url.replace(oldToken, customIndex);
128
- if(this.options.body) {
129
- let body = strBody.replace(oldToken, customIndex);
130
- this.options.body = JSON.parse(body);
131
- }
132
- }
133
- else {
134
- this.options.url = url.replace(oldToken, index);
135
- if(this.options.body) {
136
- let body = strBody.replace(oldToken, index);
137
- this.options.body = JSON.parse(body);
138
- }
139
- }
140
- }
141
- let constructor = httpClient.create;
142
- // TODO: || this.options.url.startsWith('wss:'))
143
- if (this.options.url.startsWith('ws:')) {
144
- constructor = websocket.create;
145
- }
146
- const client = constructor(this, this.options);
123
+ const createClient = this.getClientCreator()
124
+ const client = createClient(this, this.options);
147
125
  this.clients[index] = client;
148
126
  if (!this.options.requestsPerSecond) {
149
127
  client.start();
@@ -155,6 +133,14 @@ class Operation {
155
133
  }
156
134
  }
157
135
 
136
+ getClientCreator() {
137
+ // TODO: || this.options.url.startsWith('wss:'))
138
+ if (this.options.url.startsWith('ws:')) {
139
+ return websocket.create;
140
+ }
141
+ return httpClient.create;
142
+ }
143
+
158
144
  /**
159
145
  * Stop clients.
160
146
  */
package/lib/options.js CHANGED
@@ -45,6 +45,7 @@ class Options {
45
45
  this.recover = options.recover || configuration.recover
46
46
  this.proxy = options.proxy || configuration.proxy
47
47
  this.quiet = options.quiet || configuration.quiet
48
+ this.statusCallback = options.statusCallback
48
49
  }
49
50
 
50
51
  getUrl(options) {
package/lib/testserver.js CHANGED
@@ -16,6 +16,7 @@ const LOG_HEADERS_INTERVAL_MS = 5000;
16
16
  * - quiet: do not log any messages.
17
17
  * - percent: give an error (default 500) on some % of requests.
18
18
  * - error: set an HTTP error code, default is 500.
19
+ * - logger: function to log all incoming requests.
19
20
  * - `callback`: optional callback, called after the server has started.
20
21
  * If not present will return a promise.
21
22
  */
@@ -118,10 +119,10 @@ class TestServer {
118
119
  this.debug(request, elapsedMs);
119
120
  }
120
121
  if (!this.options.delay) {
121
- return this.end(response, id);
122
+ return this.end(request, response, id);
122
123
  }
123
124
  setTimeout(() => {
124
- this.end(response, id);
125
+ this.end(request, response, id);
125
126
  }, this.options.delay).unref();
126
127
  });
127
128
  }
@@ -167,7 +168,7 @@ class TestServer {
167
168
  /**
168
169
  * End the response now.
169
170
  */
170
- end(response, id) {
171
+ end(request, response, id) {
171
172
  if (this.shouldError()) {
172
173
  const code = this.options.error || 500;
173
174
  response.writeHead(code);
@@ -176,6 +177,9 @@ class TestServer {
176
177
  response.end('OK');
177
178
  }
178
179
  this.latency.end(id);
180
+ if (this.options.logger) {
181
+ this.options.logger(request, response)
182
+ }
179
183
  }
180
184
 
181
185
  shouldError() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtest",
3
- "version": "6.3.2",
3
+ "version": "7.0.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",
@@ -159,12 +159,62 @@ async function testPromise() {
159
159
  return 'Test result: ' + JSON.stringify(result)
160
160
  }
161
161
 
162
+ async function testIndexParam() {
163
+ const urls = new Map()
164
+ const bodies = new Map()
165
+ function logger(request) {
166
+ if (urls.has(request.url)) {
167
+ throw new Error(`Duplicated url ${request.url}`)
168
+ }
169
+ urls.set(request.url, true)
170
+ if (bodies.has(request.body)) {
171
+ throw new Error(`Duplicated body ${request.body}`)
172
+ }
173
+ bodies.set(request.body, true)
174
+ }
175
+ const server = await startServer({logger, ...serverOptions})
176
+ const options = {
177
+ url: `http://localhost:${PORT}/?param=index`,
178
+ maxRequests: 100,
179
+ concurrency: 10,
180
+ postBody: {
181
+ hi: 'my_index',
182
+ },
183
+ indexParam: 'index',
184
+ quiet: true,
185
+ };
186
+ await loadTest(options)
187
+ await server.close()
188
+ }
189
+
190
+ async function testStatusCallback() {
191
+ let calls = 0
192
+ const server = await startServer(serverOptions)
193
+ const options = {
194
+ url: `http://localhost:${PORT}/`,
195
+ maxRequests: 100,
196
+ concurrency: 10,
197
+ postBody: {
198
+ hi: 'hey',
199
+ },
200
+ quiet: true,
201
+ statusCallback: (error, result) => {
202
+ testing.assertEquals(result.statusCode, 200, 'Should receive status 200')
203
+ calls += 1
204
+ }
205
+ };
206
+ await loadTest(options)
207
+ testing.assertEquals(calls, 100, 'Should have 100 calls')
208
+ await server.close()
209
+ }
210
+
162
211
  /**
163
212
  * Run all tests.
164
213
  */
165
214
  export function test(callback) {
166
215
  testing.run([
167
- testIntegration, testIntegrationFile, testDelay, testWSIntegration, testPromise,
216
+ testIntegration, testIntegrationFile, testDelay, testWSIntegration,
217
+ testPromise, testIndexParam, testStatusCallback,
168
218
  ], 4000, callback);
169
219
  }
170
220