├── .github ├── dependabot.yml ├── release-drafter.yml ├── tests_checker.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .taprc ├── LICENSE ├── README.md ├── autocannon-banner.png ├── autocannon-logo-hire.png ├── autocannon-logo-icon-1000px.png ├── autocannon-logo-icon-100px.png ├── autocannon.js ├── cluster.js ├── demo.gif ├── for-zero-x.js ├── help.txt ├── lib ├── aggregateResult.js ├── defaultOptions.js ├── format.js ├── histUtil.js ├── httpClient.js ├── httpMethods.js ├── httpRequestBuilder.js ├── init.js ├── manager.js ├── multipart.js ├── noop.js ├── parseHAR.js ├── pipelinedRequestsQueue.js ├── preload │ └── autocannonDetectPort.js ├── printResult.js ├── progressTracker.js ├── requestIterator.js ├── run.js ├── runTracker.js ├── subargAliases.js ├── url.js ├── util.js ├── validate.js ├── worker.js └── worker_threads.js ├── package.json ├── samples ├── bench-multi-url.js ├── customise-individual-connection.js ├── customise-verifyBody-workers.js ├── customise-verifyBody.js ├── helpers │ ├── on-response.js │ ├── setup-request.js │ └── verify-body.js ├── init-context.js ├── modifying-request.js ├── request-context-workers.js ├── request-context.js ├── requests-sample.js ├── track-run-workers.js ├── track-run.js └── using-id-replacement.js ├── server.js └── test ├── aggregateResult.test.js ├── argumentParsing.test.js ├── basic-auth.test.js ├── cert.pem ├── cli-ipc.test.js ├── cli.test.js ├── debug.test.js ├── envPort.test.js ├── fixtures ├── example-result.json ├── httpbin-get.json ├── httpbin-post.json ├── httpbin-simple-get.json └── multi-domains.json ├── forever.test.js ├── format.test.js ├── helper.js ├── httpClient.test.js ├── httpRequestBuilder.test.js ├── j5.jpeg ├── key.pem ├── keystore.pkcs12 ├── onPort.test.js ├── parseHAR.test.js ├── pipelinedRequestsQueue.test.js ├── printResult-process.js ├── printResult-renderStatusCodes.test.js ├── printResult.test.js ├── progressTracker.test.js ├── progressTracker.test.stub.js ├── requestIterator.test.js ├── run.test.js ├── runAmount.test.js ├── runMultiServer.test.js ├── runMultipart.test.js ├── runRate.test.js ├── sampleInt.test.js ├── serial ├── autocannon.test.js ├── run.test.js ├── tap-parallel-not-ok └── wasm.test.js ├── subargAliases.test.js ├── tap-parallel-ok ├── targetProcess.js ├── url.test.js ├── utils ├── has-worker-support.js ├── on-response.js ├── setup-client.js ├── setup-request.js └── verify-body.js ├── validate.test.js └── workers.test.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: npm 9 | directory: '/' 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | comment: 'Could you please add tests to make sure this change works as expected?', 2 | fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java'] 3 | testDir: 'test' 4 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | # This allows a subsequently queued workflow run to interrupt previous runs 6 | concurrency: 7 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | test: 12 | runs-on: ${{ matrix.os }} 13 | permissions: 14 | contents: read 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | node-version: [20, 22] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | with: 24 | persist-credentials: false 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: node --version 30 | - run: npm --version 31 | - run: npm install 32 | - run: npm test 33 | env: 34 | CI: true 35 | 36 | automerge: 37 | if: > 38 | github.event_name == 'pull_request' && 39 | github.event.pull_request.user.login == 'dependabot[bot]' 40 | needs: test 41 | runs-on: ubuntu-latest 42 | permissions: 43 | pull-requests: write 44 | contents: write 45 | steps: 46 | - uses: fastify/github-action-merge-dependabot@v3 47 | with: 48 | exclude: 'chalk;pretty-bytes' 49 | github-token: ${{ secrets.github_token }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # Vim swap files 119 | *.swp 120 | 121 | # macOS files 122 | .DS_Store 123 | 124 | # lock files 125 | package-lock.json 126 | yarn.lock 127 | 128 | # editor files 129 | .vscode 130 | .idea 131 | 132 | # Compiled binary addons (http://nodejs.org/api/addons.html) 133 | build/Release 134 | 135 | profile-* 136 | 137 | .devcontainer 138 | .history -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | profile-* 36 | 37 | demo.gif 38 | server.js 39 | *.png 40 | appveyor.yml 41 | .travis.yml 42 | .github 43 | .nyc_output 44 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | ts: false 2 | jsx: false 3 | flow: false 4 | timeout: 900 5 | branches: 60 6 | functions: 60 7 | lines: 60 8 | statements: 60 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /autocannon-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/autocannon-banner.png -------------------------------------------------------------------------------- /autocannon-logo-hire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/autocannon-logo-hire.png -------------------------------------------------------------------------------- /autocannon-logo-icon-1000px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/autocannon-logo-icon-1000px.png -------------------------------------------------------------------------------- /autocannon-logo-icon-100px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/autocannon-logo-icon-100px.png -------------------------------------------------------------------------------- /cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cluster = require('cluster') 4 | const http = require('http') 5 | const numCPUs = Math.floor(require('os').cpus().length / 2) || 1 6 | 7 | if (cluster.isMaster) { 8 | console.log(`Master ${process.pid} is running`) 9 | 10 | for (let i = 0; i < numCPUs; i++) { 11 | cluster.fork() 12 | } 13 | 14 | cluster.on('exit', (worker, code, signal) => { 15 | console.log(`worker ${worker.process.pid} died`) 16 | }) 17 | } else { 18 | // Workers can share any TCP connection 19 | // In this case it is an HTTP server 20 | http.createServer((req, res) => { 21 | res.writeHead(200) 22 | res.end('hello world\n') 23 | }).listen(3000) 24 | 25 | console.log(`Worker ${process.pid} started`) 26 | } 27 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/demo.gif -------------------------------------------------------------------------------- /for-zero-x.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const autocannon = require('.') 4 | 5 | autocannon({ 6 | url: 'http://localhost:3000', 7 | connections: 10, 8 | duration: 10 9 | }, console.log) 10 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | Usage: autocannon [opts] URL 2 | 3 | URL is any valid HTTP or HTTPS URL. 4 | If the PORT environment variable is set, the URL can be a path. In that case 'http://localhost:$PORT/path' will be used as the URL. 5 | 6 | Available options: 7 | 8 | -c/--connections NUM 9 | The number of concurrent connections to use. default: 10. 10 | -p/--pipelining NUM 11 | The number of pipelined requests to use. default: 1. 12 | -d/--duration SEC 13 | The number of seconds to run the autocannon. default: 10. 14 | -a/--amount NUM 15 | The number of requests to make before exiting the benchmark. If set, duration is ignored. 16 | -L NUM 17 | The number of milliseconds to elapse between taking samples. This controls the sample interval, & therefore the total number of samples, which affects statistical analyses. default: 1. 18 | -S/--socketPath 19 | A path to a Unix Domain Socket or a Windows Named Pipe. A URL is still required to send the correct Host header and path. 20 | -w/--workers 21 | Number of worker threads to use to fire requests. 22 | -W/--warmup 23 | Use a warm up interval before starting sampling. 24 | This enables startup processes to finish and traffic to normalize before sampling begins 25 | use -c and -d sub args e.g. `--warmup [ -c 1 -d 3 ]` 26 | --on-port 27 | Start the command listed after -- on the command line. When it starts listening on a port, 28 | start sending requests to that port. A URL is still required to send requests to 29 | the correct path. The hostname can be omitted, `localhost` will be used by default. 30 | -m/--method METHOD 31 | The HTTP method to use. default: 'GET'. 32 | -t/--timeout NUM 33 | The number of seconds before timing out and resetting a connection. default: 10 34 | -T/--title TITLE 35 | The title to place in the results for identification. 36 | -b/--body BODY 37 | The body of the request. 38 | NOTE: This option needs to be used with the '-H/--headers' option in some frameworks 39 | -F/--form FORM 40 | Upload a form (multipart/form-data). The form options can be a JSON string like 41 | '{ "field 1": { "type": "text", "value": "a text value"}, "field 2": { "type": "file", "path": "path to the file" } }' 42 | or a path to a JSON file containing the form options. 43 | When uploading a file the default filename value can be overridden by using the corresponding option: 44 | '{ "field name": { "type": "file", "path": "path to the file", "options": { "filename": "myfilename" } } }' 45 | Passing the filepath to the form can be done by using the corresponding option: 46 | '{ "field name": { "type": "file", "path": "path to the file", "options": { "filepath": "/some/path/myfilename" } } }' 47 | -i/--input FILE 48 | The body of the request. See '-b/body' for more details. 49 | -H/--headers K=V 50 | The request headers. 51 | --har FILE 52 | When provided, Autocannon will use requests from the HAR file. 53 | CAUTION: you have to specify one or more domains using URL option: only the HAR requests to the same domains will be considered. 54 | NOTE: you can still add extra headers with -H/--headers but -m/--method, -F/--form, -i/--input -b/--body will be ignored. 55 | -B/--bailout NUM 56 | The number of failures before initiating a bailout. 57 | -M/--maxConnectionRequests NUM 58 | The max number of requests to make per connection to the server. 59 | -O/--maxOverallRequests NUM 60 | The max number of requests to make overall to the server. 61 | -r/--connectionRate NUM 62 | The max number of requests to make per second from an individual connection. 63 | -R/--overallRate NUM 64 | The max number of requests to make per second from all connections. 65 | connection rate will take precedence if both are set. 66 | NOTE: if using rate limiting and a very large rate is entered which cannot be met, 67 | Autocannon will do as many requests as possible per second. Also, latency data will be corrected to compensate for the effects of the coordinated omission issue. If you are not familiar with the coordinated omission issue, you should probably read [this article](http://highscalability.com/blog/2015/10/5/your-load-generator-is-probably-lying-to-you-take-the-red-pi.html) or watch this [Gil Tene's talk](https://www.youtube.com/watch?v=lJ8ydIuPFeU) on the topic. 68 | -C/--ignoreCoordinatedOmission 69 | Ignore the coordinated omission issue when requests should be sent at a fixed rate using 'connectionRate' or 'overallRate'. 70 | NOTE: it is not recommended to enable this option. 71 | When the request rate cannot be met because the server is too slow, many request latencies might be missing and Autocannon might report a misleading latency distribution. 72 | -D/--reconnectRate NUM 73 | The number of requests to make before resetting a connections connection to the 74 | server. 75 | -n/--no-progress 76 | Don't render the progress bar. default: false. 77 | -l/--latency 78 | Print all the latency data. default: false. 79 | -I/--idReplacement 80 | Enable replacement of [] with a randomly generated ID within the request body. default: false. 81 | -j/--json 82 | Print the output as newline delimited JSON. This will cause the progress bar and results not to be rendered. default: false. 83 | -f/--forever 84 | Run the benchmark forever. Efficiently restarts the benchmark on completion. default: false. 85 | -s/--servername 86 | Server name for the SNI (Server Name Indication) TLS extension. Defaults to the hostname of the URL when it is not an IP address. 87 | -x/--excludeErrorStats 88 | Exclude error statistics (non-2xx HTTP responses) from the final latency and bytes per second averages. default: false. 89 | -E/--expectBody EXPECTED 90 | Ensure the body matches this value. If enabled, mismatches count towards bailout. 91 | Enabling this option will slow down the load testing. 92 | --renderStatusCodes 93 | Print status codes and their respective statistics. 94 | --cert 95 | Path to cert chain in pem format 96 | --key 97 | Path to private key for specified cert in pem format 98 | --ca 99 | Path to trusted ca certificates for the test. This argument accepts both a single file as well as a list of files 100 | --debug 101 | Print connection errors to stderr. 102 | -v/--version 103 | Print the version number. 104 | -h/--help 105 | Print this menu. 106 | -------------------------------------------------------------------------------- /lib/aggregateResult.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { decodeHist, getHistograms, histAsObj, addPercentiles } = require('./histUtil') 4 | 5 | function aggregateResult (results, opts, histograms) { 6 | results = Array.isArray(results) ? results : [results] 7 | histograms = getHistograms(histograms) 8 | 9 | const aggregated = results.map(r => ({ 10 | ...r, 11 | latencies: decodeHist(r.latencies), 12 | requests: decodeHist(r.requests), 13 | throughput: decodeHist(r.throughput) 14 | })).reduce((acc, r) => { 15 | acc.latencies.add(r.latencies) 16 | 17 | acc.totalCompletedRequests += r.totalCompletedRequests 18 | acc.totalRequests += r.totalRequests 19 | acc.totalBytes += r.totalBytes 20 | acc.samples += r.samples 21 | 22 | acc.errors += r.errors 23 | acc.timeouts += r.timeouts 24 | acc.mismatches += r.mismatches 25 | acc.non2xx += r.non2xx 26 | acc.resets += r.resets 27 | acc['1xx'] += r['1xx'] 28 | acc['2xx'] += r['2xx'] 29 | acc['3xx'] += r['3xx'] 30 | acc['4xx'] += r['4xx'] 31 | acc['5xx'] += r['5xx'] 32 | 33 | Object.keys(r.statusCodeStats).forEach(statusCode => { 34 | if (!acc.statusCodeStats[statusCode]) { 35 | acc.statusCodeStats[statusCode] = r.statusCodeStats[statusCode] 36 | } else { 37 | acc.statusCodeStats[statusCode].count += r.statusCodeStats[statusCode].count 38 | } 39 | }) 40 | 41 | return acc 42 | }) 43 | 44 | const result = { 45 | title: opts.title, 46 | url: opts.url, 47 | socketPath: opts.socketPath, 48 | connections: opts.connections, 49 | sampleInt: opts.sampleInt, 50 | pipelining: opts.pipelining, 51 | workers: opts.workers, 52 | 53 | duration: aggregated.duration, 54 | samples: aggregated.samples, 55 | start: aggregated.start, 56 | finish: aggregated.finish, 57 | errors: aggregated.errors, 58 | timeouts: aggregated.timeouts, 59 | mismatches: aggregated.mismatches, 60 | non2xx: aggregated.non2xx, 61 | resets: aggregated.resets, 62 | '1xx': aggregated['1xx'], 63 | '2xx': aggregated['2xx'], 64 | '3xx': aggregated['3xx'], 65 | '4xx': aggregated['4xx'], 66 | '5xx': aggregated['5xx'], 67 | statusCodeStats: aggregated.statusCodeStats, 68 | 69 | latency: addPercentiles(aggregated.latencies, histAsObj(aggregated.latencies)), 70 | requests: addPercentiles(histograms.requests, histAsObj(histograms.requests, aggregated.totalCompletedRequests)), 71 | throughput: addPercentiles(histograms.throughput, histAsObj(histograms.throughput, aggregated.totalBytes)) 72 | } 73 | 74 | result.latency.totalCount = aggregated.latencies.totalCount 75 | result.requests.sent = aggregated.totalRequests 76 | 77 | if (result.requests.min >= Number.MAX_SAFE_INTEGER) result.requests.min = 0 78 | if (result.throughput.min >= Number.MAX_SAFE_INTEGER) result.throughput.min = 0 79 | if (result.latency.min >= Number.MAX_SAFE_INTEGER) result.latency.min = 0 80 | 81 | return result 82 | } 83 | 84 | module.exports = aggregateResult 85 | -------------------------------------------------------------------------------- /lib/defaultOptions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defaultOptions = { 4 | headers: {}, 5 | method: 'GET', 6 | duration: 10, 7 | connections: 10, 8 | sampleInt: 1000, 9 | pipelining: 1, 10 | timeout: 10, 11 | maxConnectionRequests: 0, 12 | maxOverallRequests: 0, 13 | connectionRate: 0, 14 | overallRate: 0, 15 | amount: 0, 16 | reconnectRate: 0, 17 | forever: false, 18 | idReplacement: false, 19 | requests: [{}], 20 | servername: undefined, 21 | excludeErrorStats: false 22 | } 23 | 24 | module.exports = defaultOptions 25 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function format (num) { 4 | if (num < 1000) { 5 | return '' + num 6 | } else { 7 | return '' + Math.round(num / 1000) + 'k' 8 | } 9 | } 10 | 11 | module.exports = format 12 | -------------------------------------------------------------------------------- /lib/histUtil.js: -------------------------------------------------------------------------------- 1 | const hdr = require('hdr-histogram-js') 2 | const histUtil = require('hdr-histogram-percentiles-obj') 3 | 4 | hdr.initWebAssemblySync() 5 | 6 | const getHistograms = ({ 7 | latencies = hdr.build({ 8 | useWebAssembly: true, 9 | bitBucketSize: 64, 10 | autoResize: true, 11 | lowestDiscernibleValue: 1, 12 | highestTrackableValue: 10000, 13 | numberOfSignificantValueDigits: 5 14 | }), 15 | requests = hdr.build({ 16 | useWebAssembly: true, 17 | bitBucketSize: 64, 18 | autoResize: true, 19 | lowestDiscernibleValue: 1, 20 | highestTrackableValue: 1000000, 21 | numberOfSignificantValueDigits: 3 22 | }), 23 | throughput = hdr.build({ 24 | useWebAssembly: true, 25 | bitBucketSize: 64, 26 | autoResize: true, 27 | lowestDiscernibleValue: 1, 28 | highestTrackableValue: 100000000000, 29 | numberOfSignificantValueDigits: 3 30 | }) 31 | } = {}) => ({ 32 | latencies, 33 | requests, 34 | throughput 35 | }) 36 | 37 | function encodeHist (h) { 38 | if (h.__custom) return null 39 | 40 | return hdr.encodeIntoCompressedBase64(h) 41 | } 42 | 43 | function decodeHist (str) { 44 | if (!str) return null 45 | 46 | return hdr.decodeFromCompressedBase64(str) 47 | } 48 | 49 | exports.getHistograms = getHistograms 50 | exports.encodeHist = encodeHist 51 | exports.decodeHist = decodeHist 52 | exports.histAsObj = histUtil.histAsObj 53 | exports.addPercentiles = histUtil.addPercentiles 54 | exports.percentiles = histUtil.percentiles 55 | -------------------------------------------------------------------------------- /lib/httpClient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const inherits = require('util').inherits 4 | const EE = require('events').EventEmitter 5 | const net = require('net') 6 | const tls = require('tls') 7 | const retimer = require('retimer') 8 | const HTTPParser = require('http-parser-js').HTTPParser 9 | const RequestIterator = require('./requestIterator') 10 | const noop = require('./noop') 11 | const clone = require('lodash.clonedeep') 12 | const PipelinedRequestsQueue = require('./pipelinedRequestsQueue') 13 | 14 | function Client (opts) { 15 | if (!(this instanceof Client)) { 16 | return new Client(opts) 17 | } 18 | 19 | this.opts = clone(opts) 20 | 21 | this.opts.setupClient = this.opts.setupClient || noop 22 | this.opts.pipelining = this.opts.pipelining || 1 23 | this.opts.port = this.opts.port || 80 24 | this.opts.expectBody = this.opts.expectBody || null 25 | this.opts.tlsOptions = this.opts.tlsOptions || {} 26 | this.timeout = (this.opts.timeout || 10) * 1000 27 | this.ipc = !!this.opts.socketPath 28 | this.secure = this.opts.protocol === 'https:' 29 | this.auth = this.opts.auth || null 30 | 31 | if (this.secure && this.opts.port === 80) { 32 | this.opts.port = 443 33 | } 34 | 35 | this.parser = new HTTPParser(HTTPParser.RESPONSE) 36 | this.requestIterator = new RequestIterator(this.opts) 37 | 38 | this.reqsMade = 0 39 | 40 | // used for request limiting 41 | this.responseMax = this.opts.responseMax 42 | 43 | // used for rate limiting 44 | this.reqsMadeThisSecond = 0 45 | this.rate = this.opts.rate 46 | 47 | // used for forcing reconnects 48 | this.reconnectRate = this.opts.reconnectRate 49 | 50 | this.pipelinedRequests = new PipelinedRequestsQueue() 51 | this.destroyed = false 52 | 53 | this.opts.setupClient(this) 54 | 55 | const handleTimeout = () => { 56 | this._destroyConnection() 57 | 58 | this.timeoutTicker.reschedule(this.timeout) 59 | 60 | this._connect() 61 | 62 | for (let i = 0; i < this.opts.pipelining; i++) { 63 | this.emit('timeout') 64 | } 65 | } 66 | 67 | if (this.rate) { 68 | this.rateInterval = setInterval(() => { 69 | this.reqsMadeThisSecond = 0 70 | if (this.paused) this._doRequest(this.cer) 71 | this.paused = false 72 | }, 1000) 73 | } 74 | 75 | this.timeoutTicker = retimer(handleTimeout, this.timeout) 76 | this.parser[HTTPParser.kOnHeaders] = () => {} 77 | this.parser[HTTPParser.kOnHeadersComplete] = (opts) => { 78 | this.emit('headers', opts) 79 | this.pipelinedRequests.setHeaders(opts) 80 | } 81 | 82 | this.parser[HTTPParser.kOnBody] = (body, start, len) => { 83 | this.pipelinedRequests.addBody(body.slice(start, start + len)) 84 | this.emit('body', body) 85 | } 86 | 87 | this.parser[HTTPParser.kOnMessageComplete] = () => { 88 | const resp = this.pipelinedRequests.terminateRequest() 89 | 90 | if (!this.destroyed && this.reconnectRate && this.reqsMade % this.reconnectRate === 0) { 91 | return this._resetConnection() 92 | } 93 | if (!this.destroyed) { 94 | this.requestIterator.recordBody(resp.req, resp.headers.statusCode, resp.body, resp.headers.headers) 95 | 96 | this.emit('response', resp.headers.statusCode, resp.bytes, resp.duration, this.rate) 97 | 98 | this._doRequest() 99 | 100 | const isFn = typeof this.opts.verifyBody === 'function' 101 | if (isFn && !this.opts.verifyBody(resp.body)) { 102 | return this.emit('mismatch', resp.body) 103 | } else if (!isFn && this.opts.expectBody && this.opts.expectBody !== resp.body) { 104 | return this.emit('mismatch', resp.body) 105 | } 106 | } 107 | } 108 | 109 | this._connect() 110 | } 111 | 112 | inherits(Client, EE) 113 | 114 | Client.prototype._connect = function () { 115 | if (this.secure) { 116 | let servername 117 | if (this.opts.servername) { 118 | servername = this.opts.servername 119 | } else if (!net.isIP(this.opts.hostname)) { 120 | servername = this.opts.hostname 121 | } 122 | 123 | if (this.ipc) { 124 | this.conn = tls.connect(this.opts.socketPath, { ...this.opts.tlsOptions, rejectUnauthorized: false }) 125 | } else { 126 | this.conn = tls.connect(this.opts.port, this.opts.hostname, { ...this.opts.tlsOptions, rejectUnauthorized: false, servername }) 127 | } 128 | } else { 129 | if (this.ipc) { 130 | this.conn = net.connect(this.opts.socketPath) 131 | } else { 132 | this.conn = net.connect(this.opts.port, this.opts.hostname) 133 | } 134 | } 135 | 136 | this.conn.on('error', (error) => { 137 | this.emit('connError', error) 138 | if (!this.destroyed) this._connect() 139 | }) 140 | 141 | this.conn.on('data', (chunk) => { 142 | this.pipelinedRequests.addByteCount(chunk.length) 143 | this.parser.execute(chunk) 144 | }) 145 | 146 | this.conn.on('end', () => { 147 | if (!this.destroyed) this._connect() 148 | }) 149 | 150 | for (let i = 0; i < this.opts.pipelining; i++) { 151 | this._doRequest() 152 | } 153 | } 154 | 155 | Client.prototype._doRequest = function () { 156 | if (!this.rate || (this.rate && this.reqsMadeThisSecond++ < this.rate)) { 157 | if (!this.destroyed && this.responseMax && this.reqsMade >= this.responseMax) { 158 | return this.destroy() 159 | } 160 | this.emit('request') 161 | if (this.reqsMade > 0) { 162 | this.requestIterator.nextRequest() 163 | if (this.requestIterator.resetted) { 164 | this.emit('reset') 165 | } 166 | } 167 | this.pipelinedRequests.insertRequest(this.requestIterator.currentRequest) 168 | this.conn.write(this.getRequestBuffer()) 169 | this.timeoutTicker.reschedule(this.timeout) 170 | this.reqsMade++ 171 | } else { 172 | this.paused = true 173 | } 174 | } 175 | 176 | Client.prototype._resetConnection = function () { 177 | this._destroyConnection() 178 | this._connect() 179 | } 180 | 181 | Client.prototype._destroyConnection = function () { 182 | this.conn.removeAllListeners('error') 183 | this.conn.removeAllListeners('end') 184 | this.conn.on('error', () => {}) 185 | this.conn.destroy() 186 | this.pipelinedRequests.clear() 187 | } 188 | 189 | Client.prototype.destroy = function () { 190 | if (!this.destroyed) { 191 | this.destroyed = true 192 | this.timeoutTicker.clear() 193 | if (this.rate) clearInterval(this.rateInterval) 194 | this.emit('done') 195 | this._destroyConnection() 196 | } 197 | } 198 | 199 | Client.prototype.getRequestBuffer = function () { 200 | return this.requestIterator.currentRequest.requestBuffer 201 | } 202 | 203 | Client.prototype.setHeaders = function (newHeaders) { 204 | this._okayToUpdateCheck() 205 | this.requestIterator.setHeaders(newHeaders) 206 | } 207 | 208 | Client.prototype.setBody = function (newBody) { 209 | this._okayToUpdateCheck() 210 | this.requestIterator.setBody(newBody) 211 | } 212 | 213 | Client.prototype.setHeadersAndBody = function (newHeaders, newBody) { 214 | this._okayToUpdateCheck() 215 | this.requestIterator.setHeadersAndBody(newHeaders, newBody) 216 | } 217 | 218 | Client.prototype.setRequest = function (newRequest) { 219 | this._okayToUpdateCheck() 220 | this.requestIterator.setRequest(newRequest) 221 | } 222 | 223 | Client.prototype.setRequests = function (newRequests) { 224 | this._okayToUpdateCheck() 225 | this.requestIterator.setRequests(newRequests) 226 | } 227 | 228 | Client.prototype._okayToUpdateCheck = function () { 229 | if (this.opts.pipelining > 1) { 230 | throw new Error('cannot update requests when the piplining factor is greater than 1') 231 | } 232 | } 233 | 234 | module.exports = Client 235 | -------------------------------------------------------------------------------- /lib/httpMethods.js: -------------------------------------------------------------------------------- 1 | // most http methods taken from RFC 7231 2 | // PATCH is defined in RFC 5789 3 | module.exports = [ 4 | 'GET', 5 | 'HEAD', 6 | 'POST', 7 | 'PUT', 8 | 'DELETE', 9 | 'CONNECT', 10 | 'OPTIONS', 11 | 'TRACE', 12 | 'PATCH' 13 | ] 14 | -------------------------------------------------------------------------------- /lib/httpRequestBuilder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const methods = require('./httpMethods') 4 | const { getPropertyCaseInsensitive } = require('./util') 5 | 6 | // this is a build request factory, that curries the build request function 7 | // and sets the default for it 8 | function requestBuilder (defaults) { 9 | // these need to be defined per request builder creation, because of the way 10 | // headers don't get deep copied 11 | const builderDefaults = { 12 | method: 'GET', 13 | path: '/', 14 | headers: {}, 15 | body: Buffer.alloc(0), 16 | hostname: 'localhost', 17 | setupRequest: reqData => reqData, 18 | port: 80 19 | } 20 | 21 | defaults = Object.assign(builderDefaults, defaults) 22 | 23 | // buildRequest takes an object, and turns it into a buffer representing the 24 | // http request. 25 | // second parameter is passed to setupRequest, when relevant 26 | // will return null if setupRequest returns falsey result 27 | return function buildRequest (reqData, context) { 28 | // below is a hack to enable deep extending of the headers so the default 29 | // headers object isn't overwritten by accident 30 | reqData = reqData || {} 31 | reqData.headers = Object.assign({}, defaults.headers, reqData.headers) 32 | 33 | reqData = Object.assign({}, defaults, reqData) 34 | 35 | reqData = reqData.setupRequest(reqData, context) 36 | if (!reqData) { 37 | return null 38 | } 39 | 40 | // for some reason some tests fail with method === undefined 41 | // the reqData.method should be set to SOMETHING in this case 42 | // cannot find reason for failure if `|| 'GET'` is taken out 43 | const method = reqData.method 44 | const path = reqData.path 45 | const headers = reqData.headers 46 | const body = reqData.body 47 | 48 | const headersDefinedHost = getPropertyCaseInsensitive(reqData.headers, 'host') 49 | let host = headersDefinedHost || reqData.host 50 | 51 | if (!host) { 52 | const hostname = reqData.hostname 53 | const port = reqData.port 54 | host = hostname + ':' + port 55 | } 56 | const baseReq = [ 57 | `${method} ${path} HTTP/1.1` 58 | ] 59 | if (!headersDefinedHost) { 60 | baseReq.push(`Host: ${host}`) 61 | } 62 | baseReq.push('Connection: keep-alive') 63 | if (reqData.auth) { 64 | const encodedAuth = Buffer.from(reqData.auth).toString('base64') 65 | headers.Authorization = `Basic ${encodedAuth}` 66 | } 67 | 68 | if (methods.indexOf(method) < 0) { 69 | throw new Error(`${method} HTTP method is not supported`) 70 | } 71 | 72 | let bodyBuf 73 | 74 | if (typeof body === 'string') { 75 | bodyBuf = Buffer.from(body) 76 | } else if (typeof body === 'number') { 77 | bodyBuf = Buffer.from(body + '') 78 | } else if (Buffer.isBuffer(body)) { 79 | bodyBuf = body 80 | } else if (body && Array.isArray(body._)) { 81 | // when the request body passed on the CLI includes brackets like for 82 | // a JSON array, the subarg parser will oddly provide the contents as 83 | // `body._`. Work around this specific issue. 84 | bodyBuf = Buffer.from(`[${body._}]`) 85 | } else if (body) { 86 | throw new Error('body must be either a string or a buffer') 87 | } 88 | 89 | if (bodyBuf && bodyBuf.length > 0) { 90 | const idCount = reqData.idReplacement 91 | ? (bodyBuf.toString().match(/\[\]/g) || []).length 92 | : 0 93 | headers['Content-Length'] = `${bodyBuf.length + (idCount * 27)}` 94 | } 95 | 96 | for (const [key, header] of Object.entries(headers)) { 97 | baseReq.push(`${key}: ${header}`) 98 | } 99 | 100 | let req = Buffer.from(baseReq.join('\r\n') + '\r\n\r\n', 'utf8') 101 | 102 | if (bodyBuf && bodyBuf.length > 0) { 103 | req = Buffer.concat([req, bodyBuf]) 104 | } 105 | 106 | return req 107 | } 108 | } 109 | 110 | module.exports = requestBuilder 111 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EE = require('events').EventEmitter 4 | const { isMainThread } = require('./worker_threads') 5 | 6 | const initWorkers = require('./manager') 7 | const validateOpts = require('./validate') 8 | const noop = require('./noop') 9 | const runTracker = require('./runTracker') 10 | const track = require('./progressTracker') 11 | 12 | function init (opts, cb) { 13 | const cbPassedIn = (typeof cb === 'function') 14 | if (!cbPassedIn && !opts.forever) { 15 | if (opts.warmup) { 16 | return runWithWarmup(opts) 17 | } else { 18 | return run(opts) 19 | } 20 | } else { 21 | return _init(opts, null, cb) 22 | } 23 | } 24 | 25 | function run (opts) { 26 | const tracker = new EE() 27 | const promise = new Promise((resolve, reject) => { 28 | _init(opts, tracker, (err, results) => { 29 | if (err) return reject(err) 30 | resolve(results) 31 | }) 32 | }) 33 | tracker.then = promise.then.bind(promise) 34 | tracker.catch = promise.catch.bind(promise) 35 | 36 | return tracker 37 | } 38 | 39 | function runWithWarmup (opts) { 40 | const warmupOpts = { 41 | ...opts, 42 | ...opts.warmup, 43 | warmupRunning: true, 44 | renderResultsTable: false 45 | } 46 | const mainTracker = new EE() 47 | const warmUpTracker = new EE() 48 | const promise = new Promise((resolve, reject) => { 49 | _init(warmupOpts, warmUpTracker, (err, warmupResults) => { 50 | if (err) return reject(err) 51 | _init(opts, mainTracker, (err, results) => { 52 | if (err) return reject(err) 53 | results.warmup = warmupResults 54 | resolve(results) 55 | }) 56 | }) 57 | }) 58 | mainTracker.then = promise.then.bind(promise) 59 | mainTracker.catch = promise.catch.bind(promise) 60 | return mainTracker 61 | } 62 | 63 | function _init (opts, tracker, cb) { 64 | const cbPassedIn = (typeof cb === 'function') 65 | cb = cb || noop 66 | 67 | tracker = tracker || new EE() 68 | 69 | opts = validateOpts(opts, cbPassedIn) 70 | 71 | function _cb (err, result) { 72 | if (err) { 73 | return cbPassedIn ? cb(err) : setImmediate(() => tracker.emit('error', err)) 74 | } 75 | 76 | tracker.emit('done', result) 77 | cb(null, result) 78 | 79 | if (!err && opts.json) { 80 | console.log(JSON.stringify(result)) 81 | } 82 | } 83 | 84 | if (opts instanceof Error) { 85 | _cb(opts) 86 | return tracker 87 | } 88 | 89 | tracker.opts = opts 90 | 91 | if (opts.workers && isMainThread) { 92 | initWorkers(opts, tracker, _cb) 93 | } else { 94 | runTracker(opts, tracker, _cb) 95 | } 96 | 97 | // if not running via command-line and 98 | // not rendering json, or if std isn't a tty, track progress 99 | if (opts[Symbol.for('internal')] && (!opts.json || !process.stdout.isTTY)) track(tracker, opts) 100 | 101 | return tracker 102 | } 103 | 104 | module.exports = init 105 | -------------------------------------------------------------------------------- /lib/manager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const EE = require('events').EventEmitter 5 | const aggregateResult = require('./aggregateResult') 6 | const { getHistograms } = require('./histUtil') 7 | const { Worker } = require('./worker_threads') 8 | 9 | function initWorkers (opts, tracker, cb) { 10 | tracker = tracker || new EE() 11 | 12 | const workers = [] 13 | const results = [] 14 | const numWorkers = +opts.workers 15 | const histograms = getHistograms() 16 | const histData = { 17 | requests: [], 18 | throughput: [] 19 | } 20 | let restart = true 21 | 22 | function reset () { 23 | workers.length = 0 24 | results.length = 0 25 | histograms.requests.reset() 26 | histograms.throughput.reset() 27 | histData.requests.length = 0 28 | histData.throughput.length = 0 29 | } 30 | 31 | function startAll () { 32 | for (const w of workers) { 33 | w.postMessage({ cmd: 'START' }) 34 | } 35 | 36 | setImmediate(() => { tracker.emit('start') }) 37 | } 38 | 39 | function handleFinish () { 40 | const result = aggregateResult(results, opts, histograms) 41 | cb(null, result) 42 | reset() 43 | 44 | if (opts.forever && restart) { 45 | setImmediate(() => { 46 | init() 47 | startAll() 48 | }) 49 | } 50 | } 51 | 52 | function stopAll () { 53 | for (const w of workers) { 54 | w.postMessage({ cmd: 'STOP' }) 55 | } 56 | reset() 57 | } 58 | 59 | const workerOpts = { 60 | ...opts, 61 | ...(opts.amount ? { amount: Math.max(Math.floor(opts.amount / numWorkers), 1) } : undefined), 62 | ...(opts.connections ? { connections: Math.max(Math.floor(opts.connections / numWorkers), 1) } : undefined) 63 | } 64 | workerOpts.a = workerOpts.amount 65 | workerOpts.c = workerOpts.connections 66 | 67 | function init () { 68 | for (let i = 0; i < numWorkers; i++) { 69 | const worker = new Worker(path.resolve(__dirname, './worker.js'), { workerData: { opts: workerOpts } }) 70 | 71 | worker.on('message', (msg) => { 72 | const { cmd, data, error } = msg 73 | 74 | if (cmd === 'RESULT') { 75 | results.push(data) 76 | 77 | if (results.length === workers.length) { 78 | handleFinish() 79 | } 80 | } else if (cmd === 'UPDATE_HIST') { 81 | const { name, value } = data 82 | histData[name].push(value) 83 | 84 | if (histData[name].length === workers.length) { 85 | const total = histData[name].reduce((acc, v) => acc + v, 0) 86 | histData[name].length = 0 87 | histograms[name].recordValue(total) 88 | } 89 | } else if (cmd === 'RESET_HIST') { 90 | const { name } = data 91 | histograms[name].reset() 92 | } else if (cmd === 'ERROR') { 93 | tracker.emit('error', error) 94 | } 95 | }) 96 | 97 | worker.on('error', (err) => { 98 | console.log('Worker error:', err) 99 | stopAll() 100 | cb(err) 101 | }) 102 | 103 | workers.push(worker) 104 | } 105 | } 106 | 107 | init() 108 | startAll() 109 | 110 | tracker.stop = () => { 111 | restart = false 112 | stopAll() 113 | } 114 | 115 | return tracker 116 | } 117 | 118 | module.exports = initWorkers 119 | -------------------------------------------------------------------------------- /lib/multipart.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { resolve, basename } = require('path') 4 | const { readFileSync } = require('fs') 5 | const FormData = require('form-data') 6 | 7 | function getFormData (string) { 8 | try { 9 | return JSON.parse(string) 10 | } catch (error) { 11 | try { 12 | const path = resolve(string) 13 | const data = readFileSync(path, 'utf8') 14 | return JSON.parse(data) 15 | } catch (error) { 16 | throw new Error('Invalid JSON or file where to get form data') 17 | } 18 | } 19 | } 20 | 21 | module.exports = (options) => { 22 | const obj = typeof options === 'string' ? getFormData(options) : options 23 | const form = new FormData() 24 | for (const key in obj) { 25 | const type = obj[key] && obj[key].type 26 | switch (type) { 27 | case 'file': { 28 | const path = obj[key] && obj[key].path 29 | if (!path) { 30 | throw new Error(`Missing key 'path' in form object for key '${key}'`) 31 | } 32 | const opts = obj[key] && obj[key].options 33 | const buffer = readFileSync(path) 34 | form.append(key, buffer, Object.assign({}, { 35 | filename: basename(path) 36 | }, opts)) 37 | break 38 | } 39 | case 'text': { 40 | const value = obj[key] && obj[key].value 41 | if (!value) { 42 | throw new Error(`Missing key 'value' in form object for key '${key}'`) 43 | } 44 | form.append(key, value) 45 | break 46 | } 47 | default: 48 | throw new Error('A \'type\' key with value \'text\' or \'file\' should be specified') 49 | } 50 | } 51 | return form 52 | } 53 | -------------------------------------------------------------------------------- /lib/noop.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | module.exports = function noop () {} 3 | -------------------------------------------------------------------------------- /lib/parseHAR.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // given we support node v8 4 | // eslint-disable-next-line n/no-deprecated-api 5 | const { parse } = require('url') 6 | 7 | function parseHAR (har) { 8 | const requestsPerOrigin = new Map() 9 | try { 10 | if (!har || typeof har !== 'object' || typeof har.log !== 'object' || !Array.isArray(har.log.entries) || !har.log.entries.length) { 11 | throw new Error('no entries found') 12 | } 13 | let i = 0 14 | for (const entry of har.log.entries) { 15 | i++ 16 | if (!entry || typeof entry !== 'object' || !entry.request || typeof entry.request !== 'object') { 17 | throw new Error(`invalid request in entry #${i}`) 18 | } 19 | const { request: { method, url, headers: headerArray, postData } } = entry 20 | // turn headers array to headers object 21 | const headers = {} 22 | if (!Array.isArray(headerArray)) { 23 | throw new Error(`invalid headers array in entry #${i}`) 24 | } 25 | let j = 0 26 | for (const header of headerArray) { 27 | j++ 28 | if (!header || typeof header !== 'object' || typeof header.name !== 'string' || typeof header.value !== 'string') { 29 | throw new Error(`invalid name or value in header #${j} of entry #${i}`) 30 | } 31 | const { name, value } = header 32 | headers[name] = value 33 | } 34 | const { path, hash, host, protocol } = parse(url) 35 | const origin = `${protocol}//${host}` 36 | 37 | let requests = requestsPerOrigin.get(origin) 38 | if (!requests) { 39 | requests = [] 40 | requestsPerOrigin.set(origin, requests) 41 | } 42 | const request = { 43 | origin, 44 | method, 45 | // only keep path & hash as our HttpClient will handle origin 46 | path: `${path}${hash || ''}`, 47 | headers 48 | } 49 | if (typeof postData === 'object' && typeof postData.text === 'string') { 50 | request.body = postData.text 51 | } 52 | requests.push(request) 53 | } 54 | } catch (err) { 55 | throw new Error(`Could not parse HAR content: ${err.message}`) 56 | } 57 | return requestsPerOrigin 58 | } 59 | 60 | exports.parseHAR = parseHAR 61 | -------------------------------------------------------------------------------- /lib/pipelinedRequestsQueue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * A queue (FIFO) to hold pipelined requests and link metadata to them as the response is received from the server. 5 | * This facilitates the handling of responses when the HTTP requests are pipelined. 6 | * A queue is the best structure for this because the sever reponses are provided in the same order as the requests 7 | * Cf. https://en.wikipedia.org/wiki/HTTP_pipelining 8 | * 9 | * /!\ it's up to you as a user to ensure that the queue is populated if using 10 | * the functionality. This implementation will fail silently if e.g. you try to 11 | * call any function accessing the queue while it's empty. 12 | */ 13 | class PipelinedRequestsQueue { 14 | constructor () { 15 | this.pendingRequests = [] 16 | } 17 | 18 | insertRequest (req) { 19 | this.pendingRequests.push({ 20 | req, 21 | bytes: 0, 22 | body: '', 23 | headers: {}, 24 | startTime: process.hrtime() 25 | }) 26 | } 27 | 28 | peek () { 29 | if (this.pendingRequests.length > 0) { 30 | return this.pendingRequests[0] 31 | } 32 | } 33 | 34 | addByteCount (count) { 35 | const req = this.peek() 36 | if (req) { 37 | req.bytes += count 38 | } 39 | } 40 | 41 | addBody (data) { 42 | const req = this.peek() 43 | if (req) { 44 | req.body += data 45 | } 46 | } 47 | 48 | setHeaders (headers) { 49 | const req = this.peek() 50 | if (req) { 51 | req.headers = headers 52 | } 53 | } 54 | 55 | size () { 56 | return this.pendingRequests.length 57 | } 58 | 59 | /** Terminates the first-in request 60 | * This will calculate the request duration, remove it from the queue and return its data 61 | **/ 62 | terminateRequest () { 63 | if (this.pendingRequests.length > 0) { 64 | const data = this.pendingRequests.shift() 65 | const hrduration = process.hrtime(data.startTime) 66 | data.duration = hrduration[0] * 1e3 + hrduration[1] / 1e6 67 | return data 68 | } 69 | } 70 | 71 | clear () { 72 | this.pendingRequests = [] 73 | } 74 | 75 | toArray () { 76 | return this.pendingRequests 77 | } 78 | } 79 | 80 | module.exports = PipelinedRequestsQueue 81 | -------------------------------------------------------------------------------- /lib/preload/autocannonDetectPort.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const onListen = require('on-net-listen') 4 | const net = require('net') 5 | 6 | const socket = net.connect(process.env.AUTOCANNON_SOCKET) 7 | 8 | onListen(function (addr) { 9 | this.destroy() 10 | const port = Buffer.from(addr.port + '') 11 | socket.write(port) 12 | }) 13 | 14 | socket.unref() 15 | -------------------------------------------------------------------------------- /lib/printResult.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Table = require('cli-table3') 4 | const Chalk = require('chalk') 5 | const testColorSupport = require('color-support') 6 | const prettyBytes = require('pretty-bytes') 7 | const { percentiles } = require('./histUtil') 8 | const format = require('./format') 9 | 10 | const defaults = { 11 | // use stderr as its progressBar's default 12 | outputStream: process.stderr, 13 | renderResultsTable: true, 14 | renderLatencyTable: false, 15 | verbose: true 16 | } 17 | 18 | class TableWithoutColor extends Table { 19 | constructor (opts = {}) { 20 | super({ ...opts, style: { head: [], border: [] } }) 21 | } 22 | } 23 | 24 | const printResult = (result, opts) => { 25 | opts = Object.assign({}, defaults, opts) 26 | let strResult = '' 27 | 28 | if (opts.verbose) { 29 | const chalk = new Chalk.Instance(testColorSupport({ stream: opts.outputStream, alwaysReturn: true })) 30 | const ColorSafeTable = chalk.level === 0 ? TableWithoutColor : Table 31 | 32 | const shortLatency = new ColorSafeTable({ 33 | head: asColor(chalk.cyan, ['Stat', '2.5%', '50%', '97.5%', '99%', 'Avg', 'Stdev', 'Max']) 34 | }) 35 | shortLatency.push(asLowRow(chalk.bold('Latency'), asMs(result.latency))) 36 | logToLocalStr('\n' + shortLatency.toString()) 37 | 38 | const requests = new ColorSafeTable({ 39 | head: asColor(chalk.cyan, ['Stat', '1%', '2.5%', '50%', '97.5%', 'Avg', 'Stdev', 'Min']) 40 | }) 41 | 42 | requests.push(asHighRow(chalk.bold('Req/Sec'), asNumber(result.requests))) 43 | requests.push(asHighRow(chalk.bold('Bytes/Sec'), asBytes(result.throughput))) 44 | logToLocalStr(requests.toString()) 45 | 46 | if (opts.renderStatusCodes === true) { 47 | const statusCodeStats = new ColorSafeTable({ 48 | head: asColor(chalk.cyan, ['Code', 'Count']) 49 | }) 50 | Object.keys(result.statusCodeStats).forEach(statusCode => { 51 | const stats = result.statusCodeStats[statusCode] 52 | const colorize = colorizeByStatusCode(chalk, statusCode) 53 | statusCodeStats.push([colorize(statusCode), stats.count]) 54 | }) 55 | logToLocalStr(statusCodeStats.toString()) 56 | } 57 | logToLocalStr('') 58 | if (result.sampleInt === 1000) { 59 | logToLocalStr('Req/Bytes counts sampled once per second.') 60 | } else { 61 | logToLocalStr('Req/Bytes counts sampled every ' + result.sampleInt / 1000 + ' seconds.') 62 | } 63 | logToLocalStr('# of samples: ' + result.samples) 64 | logToLocalStr('') 65 | 66 | if (opts.renderLatencyTable) { 67 | const latencies = new ColorSafeTable({ 68 | head: asColor(chalk.cyan, ['Percentile', 'Latency (ms)']) 69 | }) 70 | percentiles.map((perc) => { 71 | const key = `p${perc}`.replace('.', '_') 72 | return [ 73 | chalk.bold('' + perc), 74 | result.latency[key] 75 | ] 76 | }).forEach(row => { 77 | latencies.push(row) 78 | }) 79 | logToLocalStr(latencies.toString()) 80 | logToLocalStr('') 81 | } 82 | } 83 | 84 | if (result.non2xx) { 85 | logToLocalStr(`${result['2xx']} 2xx responses, ${result.non2xx} non 2xx responses`) 86 | } 87 | logToLocalStr(`${format(result.requests.sent)} requests in ${result.duration}s, ${prettyBytes(result.throughput.total)} read`) 88 | if (result.errors) { 89 | logToLocalStr(`${format(result.errors)} errors (${format(result.timeouts)} timeouts)`) 90 | } 91 | if (result.mismatches) { 92 | logToLocalStr(`${format(result.mismatches)} requests with mismatched body`) 93 | } 94 | if (result.resets) { 95 | logToLocalStr(`request pipeline was reset ${format(result.resets)} ${result.resets === 1 ? 'time' : 'times'}`) 96 | } 97 | 98 | function logToLocalStr (msg) { 99 | strResult += msg + '\n' 100 | } 101 | 102 | return strResult 103 | } 104 | 105 | // create a table row for stats where low values is better 106 | function asLowRow (name, stat) { 107 | return [ 108 | name, 109 | stat.p2_5, 110 | stat.p50, 111 | stat.p97_5, 112 | stat.p99, 113 | stat.average, 114 | stat.stddev, 115 | typeof stat.max === 'string' ? stat.max : Math.floor(stat.max * 100) / 100 116 | ] 117 | } 118 | 119 | // create a table row for stats where high values is better 120 | function asHighRow (name, stat) { 121 | return [ 122 | name, 123 | stat.p1, 124 | stat.p2_5, 125 | stat.p50, 126 | stat.p97_5, 127 | stat.average, 128 | stat.stddev, 129 | typeof stat.min === 'string' ? stat.min : Math.floor(stat.min * 100) / 100 130 | ] 131 | } 132 | 133 | function asColor (colorise, row) { 134 | return row.map((entry) => colorise(entry)) 135 | } 136 | 137 | function asMs (stat) { 138 | const result = Object.create(null) 139 | for (const k of Object.keys(stat)) { 140 | result[k] = `${stat[k]} ms` 141 | } 142 | result.max = typeof stat.max === 'string' ? stat.max : `${Math.floor(stat.max * 100) / 100} ms` 143 | 144 | return result 145 | } 146 | 147 | function asNumber (stat) { 148 | const result = Object.create(null) 149 | for (const k of Object.keys(stat)) { 150 | result[k] = stat[k].toLocaleString(undefined, { 151 | // to show all digits 152 | maximumFractionDigits: 20 153 | }) 154 | } 155 | 156 | return result 157 | } 158 | 159 | function asBytes (stat) { 160 | const result = Object.create(stat) 161 | 162 | for (const p of percentiles) { 163 | const key = `p${p}`.replace('.', '_') 164 | result[key] = prettyBytes(stat[key]) 165 | } 166 | 167 | result.average = prettyBytes(stat.average) 168 | result.stddev = prettyBytes(stat.stddev) 169 | result.max = prettyBytes(stat.max) 170 | result.min = prettyBytes(stat.min) 171 | return result 172 | } 173 | 174 | function colorizeByStatusCode (chalk, statusCode) { 175 | const codeClass = Math.floor(parseInt(statusCode) / 100) - 1 176 | return [chalk.cyan, chalk.cyan, chalk.cyan, chalk.redBright, chalk.redBright][codeClass] 177 | } 178 | 179 | module.exports = printResult 180 | -------------------------------------------------------------------------------- /lib/progressTracker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints out the test result details. It doesn't have not much business logic. 3 | * We skip test coverage for this file 4 | */ 5 | /* istanbul ignore file */ 6 | 'use strict' 7 | 8 | const ProgressBar = require('progress') 9 | const Chalk = require('chalk') 10 | const testColorSupport = require('color-support') 11 | const charSpinner = require('char-spinner') 12 | const printResult = require('./printResult') 13 | const { isMainThread } = require('./worker_threads') 14 | const defaults = { 15 | // use stderr as its progressBar's default 16 | outputStream: process.stderr, 17 | renderProgressBar: true, 18 | renderResultsTable: true, 19 | renderLatencyTable: false 20 | } 21 | 22 | function track (instance, opts) { 23 | if (!instance) { 24 | throw new Error('instance required for tracking') 25 | } 26 | 27 | opts = Object.assign({}, defaults, opts) 28 | 29 | const chalk = new Chalk.Instance(testColorSupport({ stream: opts.outputStream, alwaysReturn: true })) 30 | // this default needs to be set after chalk is setup, because chalk is now local to this func 31 | const runningDescription = opts.warmupRunning ? 'warmup' : 'running' 32 | opts.progressBarString = opts.progressBarString || `${chalk.green(runningDescription)} [:bar] :percent` 33 | 34 | const iOpts = instance.opts 35 | let durationProgressBar 36 | let amountProgressBar 37 | let addedListeners = false 38 | let spinner 39 | 40 | instance.on('start', () => { 41 | if (opts.renderProgressBar && isMainThread) { 42 | const socketPath = iOpts.socketPath ? ` (${iOpts.socketPath})` : '' 43 | let msg = `${iOpts.connections} connections` 44 | 45 | if (iOpts.pipelining > 1) { 46 | msg += ` with ${iOpts.pipelining} pipelining factor` 47 | } 48 | 49 | if (iOpts.workers) { 50 | msg += `\n${iOpts.workers} workers` 51 | } 52 | 53 | const runningType = opts.warmupRunning ? 'warmup' : 'test' 54 | const message = iOpts.amount 55 | ? `Running ${iOpts.amount} requests ${runningType} @ ${iOpts.url}${socketPath}\n${msg}\n` 56 | : `Running ${iOpts.duration}s ${runningType} @ ${iOpts.url}${socketPath}\n${msg}\n` 57 | 58 | logToStream(message) 59 | 60 | if (iOpts.workers) { 61 | showSpinner() 62 | } else { 63 | if (iOpts.amount) { 64 | amountProgressBar = trackAmount(instance, opts, iOpts) 65 | } else { 66 | durationProgressBar = trackDuration(instance, opts, iOpts) 67 | } 68 | } 69 | 70 | addListener() 71 | } 72 | }) 73 | 74 | instance.on('done', (result) => { 75 | // the code below this `if` just renders the results table... 76 | // if the user doesn't want to render the table, we can just return early 77 | if (opts.renderResultsTable === false) return 78 | 79 | const tableStrResult = printResult(result, opts) 80 | opts.outputStream.write(tableStrResult) 81 | }) 82 | 83 | function showSpinner () { 84 | spinner = charSpinner() 85 | } 86 | 87 | function hideSpinner () { 88 | if (spinner) { 89 | clearInterval(spinner) 90 | spinner = null 91 | } 92 | } 93 | 94 | function addListener () { 95 | // add listeners for progress bar to instance here so they aren't 96 | // added on restarting, causing listener leaks 97 | if (addedListeners) { 98 | return 99 | } 100 | 101 | addedListeners = true 102 | 103 | // TODO: think about if workers can report progress every second, that 104 | // way we could have a progress bar per worker. 105 | if (iOpts.workers) { 106 | // using `prependOnceListener` to make sure that we hide the spinner 107 | // before writing anything else. 108 | // if we print anything else to the output, the stale spinner text 109 | // is left uncleared and clutters the output. 110 | instance.prependOnceListener('done', hideSpinner) 111 | return 112 | } 113 | 114 | // note: Attempted to curry the functions below, but that breaks the functionality 115 | // as they use the scope/closure of the progress bar variables to allow them to be reset 116 | if (opts.outputStream.isTTY) { 117 | if (!iOpts.amount) { // duration progress bar 118 | instance.on('tick', () => { durationProgressBar.tick() }) 119 | instance.on('done', () => { durationProgressBar.tick(iOpts.duration - 1) }) 120 | process.once('SIGINT', () => { durationProgressBar.tick(iOpts.duration - 1) }) 121 | } else { // amount progress bar 122 | instance.on('response', () => { amountProgressBar.tick() }) 123 | instance.on('reqError', () => { amountProgressBar.tick() }) 124 | instance.on('done', () => { amountProgressBar.tick(iOpts.amount - 1) }) 125 | process.once('SIGINT', () => { amountProgressBar.tick(iOpts.amount - 1) }) 126 | } 127 | } 128 | } 129 | 130 | function logToStream (msg) { 131 | if (!isMainThread) return 132 | 133 | opts.outputStream.write(msg + '\n') 134 | } 135 | } 136 | 137 | function trackDuration (instance, opts, iOpts) { 138 | // if switch needed needed to avoid 139 | // https://github.com/mcollina/autocannon/issues/60 140 | if (!opts.outputStream.isTTY) return 141 | 142 | const progressBar = new ProgressBar(opts.progressBarString, { 143 | width: 20, 144 | incomplete: ' ', 145 | total: iOpts.duration, 146 | clear: true, 147 | stream: opts.outputStream 148 | }) 149 | 150 | progressBar.tick(0) 151 | return progressBar 152 | } 153 | 154 | function trackAmount (instance, opts, iOpts) { 155 | // if switch needed needed to avoid 156 | // https://github.com/mcollina/autocannon/issues/60 157 | if (!opts.outputStream.isTTY) return 158 | 159 | const progressBar = new ProgressBar(opts.progressBarString, { 160 | width: 20, 161 | incomplete: ' ', 162 | total: iOpts.amount, 163 | clear: true, 164 | stream: opts.outputStream 165 | }) 166 | 167 | progressBar.tick(0) 168 | return progressBar 169 | } 170 | 171 | module.exports = track 172 | -------------------------------------------------------------------------------- /lib/requestIterator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hyperid = require('hyperid') 4 | const inherits = require('util').inherits 5 | const requestBuilder = require('./httpRequestBuilder') 6 | const clone = require('lodash.clonedeep') 7 | const chunk = require('lodash.chunk') 8 | const flatten = require('lodash.flatten') 9 | 10 | const toHeaderKeyValue = (rawHeaders) => { 11 | const tupleHeaders = chunk(rawHeaders, 2) 12 | const headers = {} 13 | tupleHeaders.forEach((val) => { 14 | const currentValue = headers[val[0]] 15 | if (!currentValue) { 16 | headers[val[0]] = val[1] 17 | } else { 18 | headers[val[0]] = flatten([currentValue, val[1]]) 19 | } 20 | }) 21 | return headers 22 | } 23 | 24 | function RequestIterator (opts) { 25 | if (!(this instanceof RequestIterator)) { 26 | return new RequestIterator(opts) 27 | } 28 | 29 | this.hyperid = hyperid({ urlSafe: true }) 30 | this.resetted = false 31 | this.headers = {} 32 | this.initialContext = opts.initialContext || {} 33 | this.resetContext() 34 | this.reqDefaults = opts 35 | this.requestBuilder = requestBuilder(opts) 36 | this.setRequests(opts.requests) 37 | } 38 | 39 | inherits(RequestIterator, Object) 40 | 41 | RequestIterator.prototype.resetContext = function () { 42 | this.context = clone(this.initialContext) 43 | } 44 | 45 | RequestIterator.prototype.nextRequest = function () { 46 | this.resetted = false 47 | ++this.currentRequestIndex 48 | // when looping over available request, clear context for a fresh start 49 | if (this.currentRequestIndex === this.requests.length) { 50 | this.resetContext() 51 | this.currentRequestIndex = 0 52 | } 53 | this.currentRequest = this.requests[this.currentRequestIndex] 54 | // only builds if it has dynamic setup 55 | if (this.reqDefaults.idReplacement || typeof this.currentRequest.setupRequest === 'function') { 56 | this.rebuildRequest() 57 | } 58 | return this.currentRequest 59 | } 60 | 61 | RequestIterator.prototype.nextRequestBuffer = function () { 62 | // get next request 63 | this.nextRequest() 64 | return this.currentRequest.requestBuffer 65 | } 66 | 67 | RequestIterator.prototype.setRequests = function (newRequests) { 68 | this.resetted = false 69 | this.requests = newRequests || [{}] 70 | this.currentRequestIndex = 0 71 | // build all request which don't have dynamic setup, except if it's the first one 72 | this.requests.forEach((request, i) => { 73 | this.currentRequest = request 74 | if (i === 0 || typeof request.setupRequest !== 'function') { 75 | this.rebuildRequest() 76 | } 77 | }) 78 | this.currentRequest = this.requests[0] 79 | } 80 | 81 | RequestIterator.prototype.setHeaders = function (newHeaders) { 82 | this.headers = newHeaders || {} 83 | this.currentRequest.headers = this.headers 84 | this.rebuildRequest() 85 | } 86 | 87 | RequestIterator.prototype.setBody = function (newBody) { 88 | this.currentRequest.body = newBody || Buffer.alloc(0) 89 | this.rebuildRequest() 90 | } 91 | 92 | RequestIterator.prototype.setHeadersAndBody = function (newHeaders, newBody) { 93 | this.headers = newHeaders || {} 94 | this.currentRequest.headers = this.headers 95 | this.currentRequest.body = newBody || Buffer.alloc(0) 96 | this.rebuildRequest() 97 | } 98 | 99 | RequestIterator.prototype.setRequest = function (newRequest) { 100 | this.currentRequest = newRequest || {} 101 | this.rebuildRequest() 102 | } 103 | 104 | RequestIterator.prototype.rebuildRequest = function () { 105 | let data 106 | this.resetted = false 107 | if (this.currentRequest) { 108 | this.currentRequest.headers = this.currentRequest.headers || this.headers 109 | data = this.requestBuilder(this.currentRequest, this.context) 110 | if (data) { 111 | const hyperid = this.hyperid() 112 | this.currentRequest.requestBuffer = this.reqDefaults.idReplacement 113 | ? Buffer.from(data.toString() 114 | .replace(/\[\]/g, hyperid) 115 | // in the first line only (the url), replace encoded id placeholders 116 | .replace(/^.+/, m => m.replace(/\[%3Cid%3E]/g, hyperid))) 117 | : data 118 | } else if (this.currentRequestIndex === 0) { 119 | // when first request fails to build, we can not reset pipeline, or it'll never end 120 | throw new Error('First setupRequest() failed did not returned valid request. Stopping') 121 | } else { 122 | this.currentRequestIndex = this.requests.length - 1 123 | this.nextRequest() 124 | this.resetted = true 125 | } 126 | } 127 | return data 128 | } 129 | 130 | RequestIterator.prototype.recordBody = function (request, status, body, headers) { 131 | if (request && typeof request.onResponse === 'function') { 132 | request.onResponse(status, body, this.context, toHeaderKeyValue(headers || [])) 133 | } 134 | } 135 | 136 | module.exports = RequestIterator 137 | -------------------------------------------------------------------------------- /lib/runTracker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EE = require('events').EventEmitter 4 | const run = require('./run') 5 | const noop = require('./noop') 6 | 7 | function runTracker (opts, tracker, cb) { 8 | cb = cb || noop 9 | tracker = tracker || new EE() 10 | 11 | run(opts, tracker, cb) 12 | 13 | return tracker 14 | } 15 | 16 | module.exports = runTracker 17 | -------------------------------------------------------------------------------- /lib/subargAliases.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const clone = require('lodash.clonedeep') 4 | 5 | const subArgAlias = { 6 | warmup: { 7 | c: 'connections', 8 | d: 'duration' 9 | } 10 | } 11 | 12 | // Expects args to have already been processed by subarg 13 | function generateSubargAliases (args) { 14 | const aliasedArgs = clone(args) 15 | 16 | function isParentAliasInArgs (argKey) { 17 | return aliasedArgs[argKey] 18 | } 19 | 20 | function isSubargAnAlias (parentAlias, subArg) { 21 | return parentAlias[subArg] 22 | } 23 | 24 | function mapAliasForSubarg (parentAlias, parentKey) { 25 | const parentArgs = aliasedArgs[parentKey] 26 | for (const subArg in parentArgs) { 27 | if (isSubargAnAlias(parentAlias, subArg)) { 28 | parentArgs[parentAlias[subArg]] = parentArgs[subArg] 29 | } 30 | } 31 | } 32 | 33 | for (const parentKey in subArgAlias) { 34 | const parentAlias = subArgAlias[parentKey] 35 | if (isParentAliasInArgs(parentKey)) { 36 | mapAliasForSubarg(parentAlias, parentKey) 37 | } 38 | } 39 | 40 | return aliasedArgs 41 | } 42 | 43 | module.exports = generateSubargAliases 44 | -------------------------------------------------------------------------------- /lib/url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * check the url is not an empty string or empty array 5 | * @param url 6 | */ 7 | function checkURL (url) { 8 | return (typeof url === 'string' && url) || 9 | (Array.isArray(url) && url.length > 0) 10 | } 11 | 12 | /** 13 | * 14 | * @param url 15 | * @param asArray 16 | * @returns 17 | */ 18 | function ofURL (url, asArray) { 19 | if (Array.isArray(url)) return url 20 | 21 | if (typeof url === 'string') { 22 | return { 23 | map (fn) { 24 | if (asArray) return [fn(url)] 25 | return fn(url) 26 | } 27 | } 28 | } 29 | 30 | throw new Error('url should only be a string or an array of string') 31 | } 32 | 33 | exports.checkURL = checkURL 34 | exports.ofURL = ofURL 35 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | const hasWorkerSupport = semver.gte(process.versions.node, '11.7.0') 5 | 6 | function getPropertyCaseInsensitive (obj, key) { 7 | for (const objKey of Object.keys(obj)) { 8 | if (objKey.toLowerCase() === key.toLowerCase()) return obj[objKey] 9 | } 10 | } 11 | 12 | module.exports = { hasWorkerSupport, getPropertyCaseInsensitive } 13 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defaultOptions = require('./defaultOptions') 4 | const timestring = require('timestring') 5 | const { checkURL } = require('./url') 6 | const multipart = require('./multipart') 7 | const { parseHAR } = require('./parseHAR') 8 | const { hasWorkerSupport } = require('./util') 9 | 10 | const isValidFn = (opt) => (!opt || typeof opt === 'function' || typeof opt === 'string') 11 | 12 | const lessThanOneError = (label) => new Error(`${label} can not be less than 1`) 13 | const greaterThanZeroError = (label) => new Error(`${label} must be greater than 0`) 14 | const minIfPresent = (val, min) => val !== null && val < min 15 | 16 | function safeRequire (path) { 17 | if (typeof path === 'string') { 18 | try { 19 | return require(path) 20 | } catch (err) {} 21 | } 22 | 23 | return path 24 | } 25 | 26 | function defaultOpts (opts) { 27 | const setupClient = opts.workers ? opts.setupClient : safeRequire(opts.setupClient) 28 | const verifyBody = opts.workers ? opts.verifyBody : safeRequire(opts.verifyBody) 29 | 30 | const requests = opts.requests 31 | ? opts.requests.map((r) => { 32 | const setupRequest = opts.workers ? r.setupRequest : safeRequire(r.setupRequest) 33 | const onResponse = opts.workers ? r.onResponse : safeRequire(r.onResponse) 34 | 35 | return { 36 | ...r, 37 | ...(setupRequest ? { setupRequest } : undefined), 38 | ...(onResponse ? { onResponse } : undefined) 39 | } 40 | }) 41 | : undefined 42 | 43 | return { 44 | ...defaultOptions, 45 | ...opts, 46 | ...(setupClient ? { setupClient } : undefined), 47 | ...(verifyBody ? { verifyBody } : undefined), 48 | ...(requests ? { requests } : undefined) 49 | } 50 | } 51 | 52 | module.exports = function validateOpts (opts, cbPassedIn) { 53 | if (opts.workers && !hasWorkerSupport) return new Error('Please use node >= 11.7.0 for workers support') 54 | // these need to be validated before defaulting 55 | if (minIfPresent(opts.bailout, 1)) return lessThanOneError('bailout threshold') 56 | if (minIfPresent(opts.connectionRate, 1)) return lessThanOneError('connectionRate') 57 | if (minIfPresent(opts.overallRate, 1)) return lessThanOneError('bailout overallRate') 58 | if (minIfPresent(opts.amount, 1)) return lessThanOneError('amount') 59 | if (minIfPresent(opts.maxConnectionRequests, 1)) return lessThanOneError('maxConnectionRequests') 60 | if (minIfPresent(opts.maxOverallRequests, 1)) return lessThanOneError('maxOverallRequests') 61 | 62 | if (opts.form) { 63 | opts.method = opts.method || 'POST' 64 | } 65 | 66 | // fill in defaults after 67 | opts = defaultOpts(opts) 68 | 69 | if (opts.json === true) { 70 | opts.renderProgressBar = opts.renderResultsTable = opts.renderLatencyTable = false 71 | } 72 | 73 | if (opts.requests) { 74 | if (opts.requests.some(r => !isValidFn(r.setupRequest))) { 75 | return new Error('Invalid option setupRequest, please provide a function (or file path when in workers mode)') 76 | } 77 | 78 | if (opts.requests.some(r => !isValidFn(r.onResponse))) { 79 | return new Error('Invalid option onResponse, please provide a function (or file path when in workers mode)') 80 | } 81 | } 82 | 83 | if (!isValidFn(opts.setupClient)) { 84 | return new Error('Invalid option setupClient, please provide a function (or file path when in workers mode)') 85 | } 86 | 87 | if (!isValidFn(opts.verifyBody)) { 88 | return new Error('Invalid option verifyBody, please provide a function (or file path when in workers mode)') 89 | } 90 | 91 | if (!checkURL(opts.url) && !opts.socketPath) { 92 | return new Error('url or socketPath option required') 93 | } 94 | 95 | if (typeof opts.duration === 'string') { 96 | if (/[a-zA-Z]/.exec(opts.duration)) { 97 | try { 98 | opts.duration = timestring(opts.duration) 99 | } catch (error) { 100 | return error 101 | } 102 | } else { 103 | opts.duration = Number(opts.duration.trim()) 104 | } 105 | } 106 | 107 | if (typeof opts.duration !== 'number') { 108 | return new Error('duration entered was in an invalid format') 109 | } 110 | 111 | if (opts.duration < 0) { 112 | return new Error('duration can not be less than 0') 113 | } 114 | 115 | opts.sampleInt = parseFloat(opts.sampleInt) 116 | 117 | if (isNaN(opts.sampleInt)) { 118 | return new Error('sample interval entered was in an invalid format') 119 | } 120 | 121 | if (opts.sampleInt < 0) { 122 | return new Error('sample interval can not be less than 0') 123 | } 124 | 125 | if (opts.expectBody && opts.requests !== defaultOptions.requests) { 126 | return new Error('expectBody cannot be used in conjunction with requests') 127 | } 128 | 129 | if (opts.form) { 130 | try { 131 | // Parse multipart upfront to make sure there's no errors 132 | const data = multipart(opts.form) 133 | opts.form = opts.workers ? opts.form : data // but use parsed data only if not in workers mode 134 | } catch (error) { 135 | return error 136 | } 137 | } 138 | 139 | opts.harRequests = new Map() 140 | if (opts.har) { 141 | try { 142 | opts.harRequests = parseHAR(opts.har) 143 | } catch (error) { 144 | return error 145 | } 146 | } 147 | 148 | if (opts.connections < 1) return lessThanOneError('connections') 149 | if (opts.pipelining < 1) return lessThanOneError('pipelining factor') 150 | if (opts.timeout < 1) return greaterThanZeroError('timeout') 151 | 152 | if (opts.ignoreCoordinatedOmission && !opts.connectionRate && !opts.overallRate) { 153 | return new Error('ignoreCoordinatedOmission makes no sense without connectionRate or overallRate') 154 | } 155 | 156 | if (opts.forever && cbPassedIn) { 157 | return new Error('should not use the callback parameter when the `forever` option is set to true. Use the `done` event on this event emitter') 158 | } 159 | 160 | if (opts.forever && opts.workers) { 161 | return new Error('Using `forever` option isn\'t currently supported with workers') 162 | } 163 | 164 | return opts 165 | } 166 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { isMainThread, parentPort, workerData } = require('worker_threads') 4 | const multipart = require('./multipart') 5 | const run = require('./run') 6 | 7 | const createHist = (name) => ({ 8 | __custom: true, 9 | recordValue: v => updateHist(name, v), 10 | destroy: () => {}, 11 | reset: () => resetHist(name) 12 | }) 13 | 14 | const updateHist = (name, value) => { 15 | parentPort.postMessage({ 16 | cmd: 'UPDATE_HIST', 17 | data: { name, value } 18 | }) 19 | } 20 | 21 | const resetHist = (name) => { 22 | parentPort.postMessage({ 23 | cmd: 'RESET_HIST', 24 | data: { name } 25 | }) 26 | } 27 | 28 | function runTracker (opts, cb) { 29 | const tracker = run({ 30 | ...opts, 31 | ...(opts.form ? { form: multipart(opts.form) } : undefined), 32 | ...(opts.setupClient ? { setupClient: require(opts.setupClient) } : undefined), 33 | ...(opts.verifyBody ? { verifyBody: require(opts.verifyBody) } : undefined), 34 | requests: opts.requests 35 | ? opts.requests.map(r => ({ 36 | ...r, 37 | ...(r.setupRequest ? { setupRequest: require(r.setupRequest) } : undefined), 38 | ...(r.onResponse ? { onResponse: require(r.onResponse) } : undefined) 39 | })) 40 | : undefined, 41 | histograms: { 42 | requests: createHist('requests'), 43 | throughput: createHist('throughput') 44 | } 45 | }, null, cb) 46 | 47 | tracker.on('tick', (data) => { 48 | parentPort.postMessage({ cmd: 'TICK', data }) 49 | }) 50 | 51 | return { 52 | stop: tracker.stop 53 | } 54 | } 55 | 56 | if (!isMainThread) { 57 | const { opts } = workerData 58 | let tracker 59 | 60 | parentPort.on('message', (msg) => { 61 | const { cmd } = msg 62 | 63 | if (cmd === 'START') { 64 | tracker = runTracker(opts, (error, data) => { 65 | parentPort.postMessage({ cmd: error ? 'ERROR' : 'RESULT', error, data }) 66 | parentPort.close() 67 | }) 68 | } else if (cmd === 'STOP') { 69 | tracker.stop() 70 | parentPort.close() 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /lib/worker_threads.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let workerThreads = {} 4 | 5 | try { 6 | workerThreads = require('worker_threads') 7 | } catch (err) { 8 | if (err) { 9 | // we don't need the error but can't have catch block 10 | // without err as node 8 doesn't support that 11 | } 12 | 13 | workerThreads = { 14 | isMainThread: true 15 | } 16 | } 17 | 18 | module.exports = workerThreads 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocannon", 3 | "version": "8.0.0", 4 | "description": "Fast HTTP benchmarking tool written in Node.js", 5 | "main": "autocannon.js", 6 | "bin": { 7 | "autocannon": "autocannon.js" 8 | }, 9 | "scripts": { 10 | "test": "standard && tap test/serial/*.test.js test/*.test.js", 11 | "standard:fix": "standard --fix" 12 | }, 13 | "pre-commit": [ 14 | "test" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/mcollina/autocannon.git" 19 | }, 20 | "keywords": [ 21 | "http", 22 | "soak", 23 | "load", 24 | "fast", 25 | "wrk", 26 | "ab", 27 | "test" 28 | ], 29 | "author": "Matteo Collina ", 30 | "contributors": [ 31 | "Glen Keane ", 32 | "Donald Robertson " 34 | ], 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/mcollina/autocannon/issues" 38 | }, 39 | "homepage": "https://github.com/mcollina/autocannon#readme", 40 | "devDependencies": { 41 | "ansi-regex": "^5.0.1", 42 | "bl": "^6.0.0", 43 | "busboy": "^0.3.1", 44 | "pre-commit": "^1.1.2", 45 | "proxyquire": "^2.1.3", 46 | "sinon": "^15.0.0", 47 | "split2": "^4.0.0", 48 | "standard": "^17.0.0", 49 | "tap": "^16.0.0", 50 | "why-is-node-running": "^2.3.0" 51 | }, 52 | "dependencies": { 53 | "@minimistjs/subarg": "^1.0.0", 54 | "chalk": "^4.1.0", 55 | "char-spinner": "^1.0.1", 56 | "cli-table3": "^0.6.0", 57 | "color-support": "^1.1.1", 58 | "cross-argv": "^2.0.0", 59 | "form-data": "^4.0.0", 60 | "has-async-hooks": "^1.0.0", 61 | "hdr-histogram-js": "^3.0.0", 62 | "hdr-histogram-percentiles-obj": "^3.0.0", 63 | "http-parser-js": "^0.5.2", 64 | "hyperid": "^3.0.0", 65 | "lodash.chunk": "^4.2.0", 66 | "lodash.clonedeep": "^4.5.0", 67 | "lodash.flatten": "^4.4.0", 68 | "manage-path": "^2.0.0", 69 | "on-net-listen": "^1.1.1", 70 | "pretty-bytes": "^5.4.1", 71 | "progress": "^2.0.3", 72 | "reinterval": "^1.1.0", 73 | "retimer": "^3.0.0", 74 | "semver": "^7.3.2", 75 | "timestring": "^6.0.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /samples/bench-multi-url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | function createHandler (serverName) { 7 | return function (req, res) { 8 | console.log(serverName + ' received request') 9 | res.end('hello world') 10 | } 11 | } 12 | 13 | const server1 = http.createServer(createHandler('server1')) 14 | const server2 = http.createServer(createHandler('server2')) 15 | 16 | server1.listen(0, startBench) 17 | server2.listen(0, startBench) 18 | 19 | function startBench () { 20 | const url = [ 21 | 'http://localhost:' + server1.address().port, 22 | 'http://localhost:' + server2.address().port 23 | ] 24 | 25 | // same with run the follow command in cli 26 | // autocannon -d 10 -c 2 http://localhost:xxxx http://localhost:yyyy 27 | autocannon({ 28 | url, 29 | // connection number should n times of the number of server 30 | connections: 2, 31 | duration: 10, 32 | requests: [ 33 | { 34 | method: 'GET', 35 | path: '/' 36 | } 37 | ] 38 | }, finishedBench) 39 | 40 | function finishedBench (err, res) { 41 | console.log('finished bench', err, res) 42 | process.exit(1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /samples/customise-individual-connection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | let connection = 0 11 | 12 | function handle (req, res) { 13 | res.end('hello world') 14 | } 15 | 16 | function startBench () { 17 | const url = 'http://localhost:' + server.address().port 18 | 19 | autocannon({ 20 | url, 21 | connections: 1000, 22 | duration: 10, 23 | setupClient 24 | }, finishedBench) 25 | 26 | function setupClient (client) { 27 | client.setBody('connection number', connection++) 28 | } 29 | 30 | function finishedBench (err, res) { 31 | console.log('finished bench', err, res) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/customise-verifyBody-workers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const path = require('path') 5 | const autocannon = require('../autocannon') 6 | 7 | const server = http.createServer(handle) 8 | 9 | server.listen(0, startBench) 10 | 11 | function handle (req, res) { 12 | res.end('hello world') 13 | } 14 | 15 | function startBench () { 16 | const url = 'http://localhost:' + server.address().port 17 | 18 | autocannon({ 19 | url, 20 | connections: 1000, 21 | duration: 10, 22 | workers: 2, 23 | verifyBody: path.join(__dirname, 'helpers', 'verify-body') 24 | }, finishedBench) 25 | 26 | function finishedBench (err, res) { 27 | console.log('finished bench', err, res) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/customise-verifyBody.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | res.end('hello world') 12 | } 13 | 14 | function startBench () { 15 | const url = 'http://localhost:' + server.address().port 16 | 17 | autocannon({ 18 | url, 19 | connections: 1000, 20 | duration: 10, 21 | verifyBody 22 | }, finishedBench) 23 | 24 | function verifyBody (body) { 25 | return body.indexOf('') > -1 26 | } 27 | 28 | function finishedBench (err, res) { 29 | console.log('finished bench', err, res) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/helpers/on-response.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (status, body, context) => { 4 | if (status === 200) { 5 | context.user = JSON.parse(body) 6 | } // on error, you may abort the benchmark 7 | } 8 | -------------------------------------------------------------------------------- /samples/helpers/setup-request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (req, context) => ({ 4 | ...req, 5 | path: `/user/${context.user.id}`, 6 | body: JSON.stringify({ 7 | ...context.user, 8 | lastName: 'Doe' 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /samples/helpers/verify-body.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (body) => { 4 | return true 5 | } 6 | -------------------------------------------------------------------------------- /samples/init-context.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | const body = [] 12 | // this route handler simply returns whatever it gets from response 13 | req 14 | .on('data', chunk => body.push(chunk)) 15 | .on('end', () => res.end(Buffer.concat(body))) 16 | } 17 | 18 | function startBench () { 19 | const url = 'http://localhost:' + server.address().port 20 | 21 | autocannon({ 22 | url, 23 | connections: 1, 24 | amount: 1, 25 | initialContext: { user: { firstName: 'Salman' } }, 26 | requests: [ 27 | { 28 | // use data from context 29 | method: 'PUT', 30 | setupRequest: (req, context) => ({ 31 | ...req, 32 | path: `/user/${context.user.id}`, 33 | body: JSON.stringify({ 34 | ...context.user, 35 | lastName: 'Mitha' 36 | }) 37 | }) 38 | } 39 | ] 40 | }, finishedBench) 41 | 42 | function finishedBench (err, res) { 43 | console.log('finished bench', err, res) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/modifying-request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | res.end('hello world') 12 | } 13 | 14 | function startBench () { 15 | const url = 'http://localhost:' + server.address().port 16 | 17 | const instance = autocannon({ 18 | url, 19 | connections: 1000, 20 | duration: 10 21 | }, finishedBench) 22 | 23 | let message = 0 24 | // modify the body on future requests 25 | instance.on('response', function (client, statusCode, returnBytes, responseTime) { 26 | client.setBody('message ' + message++) 27 | }) 28 | 29 | let headers = 0 30 | // modify the headers on future requests 31 | // this wipes any existing headers out with the new ones 32 | instance.on('response', function (client, statusCode, returnBytes, responseTime) { 33 | const newHeaders = {} 34 | newHeaders[`header${headers++}`] = `headerValue${headers++}` 35 | client.setHeaders(newHeaders) 36 | }) 37 | 38 | function finishedBench (err, res) { 39 | console.log('finished bench', err, res) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/request-context-workers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const path = require('path') 5 | const autocannon = require('../autocannon') 6 | 7 | const server = http.createServer(handle) 8 | 9 | server.listen(0, startBench) 10 | 11 | function handle (req, res) { 12 | const body = [] 13 | // this route handler simply returns whatever it gets from response 14 | req 15 | .on('data', chunk => body.push(chunk)) 16 | .on('end', () => res.end(Buffer.concat(body))) 17 | } 18 | 19 | function startBench () { 20 | const url = 'http://localhost:' + server.address().port 21 | 22 | autocannon({ 23 | url, 24 | duration: 2, 25 | workers: 2, 26 | requests: [ 27 | { 28 | // let's create a new user 29 | method: 'POST', 30 | path: '/users', 31 | body: JSON.stringify({ firstName: 'Jane', id: 10 }), 32 | onResponse: path.join(__dirname, 'helpers', 'on-response') 33 | }, 34 | { 35 | // now we'll give them a last name 36 | method: 'PUT', 37 | setupRequest: path.join(__dirname, 'helpers', 'setup-request') 38 | } 39 | ] 40 | }, finishedBench) 41 | 42 | function finishedBench (err, res) { 43 | console.log('finished bench', err, res) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/request-context.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | const body = [] 12 | // this route handler simply returns whatever it gets from response 13 | req 14 | .on('data', chunk => body.push(chunk)) 15 | .on('end', () => res.end(Buffer.concat(body))) 16 | } 17 | 18 | function startBench () { 19 | const url = 'http://localhost:' + server.address().port 20 | 21 | autocannon({ 22 | url, 23 | requests: [ 24 | { 25 | // let's create a new user 26 | method: 'POST', 27 | path: '/users', 28 | body: JSON.stringify({ firstName: 'Jane', id: 10 }), 29 | onResponse: (status, body, context) => { 30 | if (status === 200) { 31 | context.user = JSON.parse(body) 32 | } // on error, you may abort the benchmark 33 | } 34 | }, 35 | { 36 | // now we'll give them a last name 37 | method: 'PUT', 38 | setupRequest: (req, context) => ({ 39 | ...req, 40 | path: `/user/${context.user.id}`, 41 | body: JSON.stringify({ 42 | ...context.user, 43 | lastName: 'Doe' 44 | }) 45 | }) 46 | } 47 | ] 48 | }, finishedBench) 49 | 50 | function finishedBench (err, res) { 51 | console.log('finished bench', err, res) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /samples/requests-sample.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | res.end('hello world') 12 | } 13 | 14 | function startBench () { 15 | const url = 'http://localhost:' + server.address().port 16 | 17 | autocannon({ 18 | url, 19 | connections: 1000, 20 | duration: 10, 21 | headers: { 22 | // by default we add an auth token to all requests 23 | auth: 'A Pregenerated auth token' 24 | }, 25 | requests: [ 26 | { 27 | method: 'POST', // this should be a post for logging in 28 | path: '/login', 29 | body: 'valid login details', 30 | // overwrite our default headers, 31 | // so we don't add an auth token 32 | // for this request 33 | headers: {} 34 | }, 35 | { 36 | path: '/mySecretDetails' 37 | // this will automatically add the pregenerated auth token 38 | }, 39 | { 40 | method: 'GET', // this should be a put for modifying secret details 41 | path: '/mySecretDetails', 42 | headers: { // let submit some json? 43 | 'Content-type': 'application/json; charset=utf-8' 44 | }, 45 | // we need to stringify the json first 46 | body: JSON.stringify({ 47 | name: 'my new name' 48 | }), 49 | setupRequest: reqData => { 50 | reqData.method = 'PUT' // we are overriding the method 'GET' to 'PUT' here 51 | return reqData 52 | } 53 | } 54 | ] 55 | }, finishedBench) 56 | 57 | function finishedBench (err, res) { 58 | console.log('finished bench', err, res) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /samples/track-run-workers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | res.end('hello world') 12 | } 13 | 14 | function startBench () { 15 | const instance = autocannon({ 16 | connections: 100, 17 | duration: 2, 18 | url: 'http://localhost:' + server.address().port, 19 | workers: 2 20 | }, finishedBench) 21 | 22 | autocannon.track(instance) 23 | 24 | // this is used to kill the instance on CTRL-C 25 | process.once('SIGINT', () => { 26 | instance.stop() 27 | }) 28 | 29 | function finishedBench (err, res) { 30 | console.log('finished bench', err, res) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/track-run.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | res.end('hello world') 12 | } 13 | 14 | function startBench () { 15 | const instance = autocannon({ 16 | url: 'http://localhost:' + server.address().port 17 | }, finishedBench) 18 | 19 | autocannon.track(instance) 20 | 21 | // this is used to kill the instance on CTRL-C 22 | process.once('SIGINT', () => { 23 | instance.stop() 24 | }) 25 | 26 | function finishedBench (err, res) { 27 | console.log('finished bench', err, res) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/using-id-replacement.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const autocannon = require('../autocannon') 5 | 6 | const server = http.createServer(handle) 7 | 8 | server.listen(0, startBench) 9 | 10 | function handle (req, res) { 11 | res.end('hello world') 12 | } 13 | 14 | function startBench () { 15 | const url = 'http://localhost:' + server.address().port 16 | 17 | autocannon({ 18 | url, 19 | connections: 1000, 20 | duration: 10, 21 | requests: [ 22 | { 23 | method: 'POST', 24 | path: '/register', 25 | headers: { 26 | 'Content-type': 'application/json; charset=utf-8' 27 | }, 28 | body: JSON.stringify({ 29 | name: 'New User', 30 | email: 'new-[]@user.com' // [] will be replaced with generated HyperID at run time 31 | }) 32 | } 33 | ], 34 | idReplacement: true 35 | }, finishedBench) 36 | 37 | function finishedBench (err, res) { 38 | console.log('finished bench', err, res) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const https = require('https') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | const options = { 9 | key: fs.readFileSync(path.join(__dirname, 'test', '/key.pem')), 10 | cert: fs.readFileSync(path.join(__dirname, 'test', '/cert.pem')), 11 | passphrase: 'test' 12 | } 13 | const server = http.createServer(handle) 14 | const server2 = https.createServer(options, handle) 15 | 16 | server.listen(3000) 17 | server2.listen(3001) 18 | 19 | function handle (req, res) { 20 | res.end('hello world') 21 | } 22 | -------------------------------------------------------------------------------- /test/aggregateResult.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const { startServer } = require('./helper') 3 | const autocannon = require('../autocannon') 4 | const aggregateResult = autocannon.aggregateResult 5 | const server = startServer() 6 | const url = 'http://localhost:' + server.address().port 7 | 8 | test('exec separate autocannon instances with skipAggregateResult, then aggregateResult afterwards', async (t) => { 9 | t.plan(2) 10 | 11 | const opts = { 12 | url, 13 | connections: 1, 14 | maxOverallRequests: 10, 15 | skipAggregateResult: true 16 | } 17 | 18 | const results = await Promise.all([ 19 | autocannon(opts), 20 | autocannon(opts) 21 | ]) 22 | 23 | const aggregateResults = aggregateResult(results, opts) 24 | 25 | t.equal(aggregateResults['2xx'], 20) 26 | t.equal(aggregateResults.requests.total, 20) 27 | }) 28 | 29 | test('aggregateResult must be passed opts with at least a URL or socketPath property', async (t) => { 30 | t.plan(2) 31 | t.throws(() => aggregateResult([]), 'url or socketPath option required') 32 | t.throws(() => aggregateResult([], {}), 'url or socketPath option required') 33 | }) 34 | -------------------------------------------------------------------------------- /test/argumentParsing.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const Autocannon = require('../autocannon') 5 | const fs = require('fs') 6 | 7 | test('parse argument', (t) => { 8 | t.plan(4) 9 | 10 | const args = Autocannon.parseArguments([ 11 | '-H', 'X-Http-Method-Override=GET', 12 | '-m', 'POST', 13 | '-b', 'the body', 14 | 'http://localhost/foo/bar' 15 | ]) 16 | 17 | t.equal(args.url, 'http://localhost/foo/bar') 18 | t.strictSame(args.headers, { 'X-Http-Method-Override': 'GET' }) 19 | t.equal(args.method, 'POST') 20 | t.equal(args.body, 'the body') 21 | }) 22 | 23 | test('parse argument with multiple headers', (t) => { 24 | t.plan(3) 25 | 26 | const args = Autocannon.parseArguments([ 27 | '-H', 'header1=value1', 28 | '-H', 'header2=value2', 29 | '-H', 'header3=value3', 30 | '-H', 'header4=value4', 31 | '-H', 'header5=value5', 32 | 'http://localhost/foo/bar' 33 | ]) 34 | 35 | t.equal(args.url, 'http://localhost/foo/bar') 36 | t.strictSame(args.headers, { 37 | header1: 'value1', 38 | header2: 'value2', 39 | header3: 'value3', 40 | header4: 'value4', 41 | header5: 'value5' 42 | }) 43 | t.equal(args.method, 'GET') 44 | }) 45 | 46 | test('parse argument with multiple complex headers', (t) => { 47 | t.plan(3) 48 | 49 | const args = Autocannon.parseArguments([ 50 | '-H', 'header1=value1;data=asd', 51 | '-H', 'header2=value2;data=asd', 52 | '-H', 'header3=value3;data=asd', 53 | '-H', 'header4=value4;data=asd', 54 | '-H', 'header5=value5;data=asd', 55 | 'http://localhost/foo/bar' 56 | ]) 57 | 58 | t.equal(args.url, 'http://localhost/foo/bar') 59 | t.strictSame(args.headers, { 60 | header1: 'value1;data=asd', 61 | header2: 'value2;data=asd', 62 | header3: 'value3;data=asd', 63 | header4: 'value4;data=asd', 64 | header5: 'value5;data=asd' 65 | }) 66 | t.equal(args.method, 'GET') 67 | }) 68 | 69 | test('parse argument with multiple headers in standard notation', (t) => { 70 | t.plan(3) 71 | 72 | const args = Autocannon.parseArguments([ 73 | '-H', 'header1: value1', 74 | '-H', 'header2: value2', 75 | '-H', 'header3: value3', 76 | '-H', 'header4: value4', 77 | '-H', 'header5: value5', 78 | 'http://localhost/foo/bar' 79 | ]) 80 | 81 | t.equal(args.url, 'http://localhost/foo/bar') 82 | t.strictSame(args.headers, { 83 | header1: ' value1', 84 | header2: ' value2', 85 | header3: ' value3', 86 | header4: ' value4', 87 | header5: ' value5' 88 | }) 89 | t.equal(args.method, 'GET') 90 | }) 91 | 92 | test('parse argument with multiple complex headers in standard notation', (t) => { 93 | t.plan(3) 94 | 95 | const args = Autocannon.parseArguments([ 96 | '-H', 'header1:value1;data=asd', 97 | '-H', 'header2:value2;data=asd', 98 | '-H', 'header3:value3;data=asd', 99 | '-H', 'header4:value4;data=asd', 100 | '-H', 'header5:value5;data=asd', 101 | 'http://localhost/foo/bar' 102 | ]) 103 | 104 | t.equal(args.url, 'http://localhost/foo/bar') 105 | t.strictSame(args.headers, { 106 | header1: 'value1;data=asd', 107 | header2: 'value2;data=asd', 108 | header3: 'value3;data=asd', 109 | header4: 'value4;data=asd', 110 | header5: 'value5;data=asd' 111 | }) 112 | t.equal(args.method, 'GET') 113 | }) 114 | 115 | test('parse argument with "=" in value header', (t) => { 116 | t.plan(1) 117 | 118 | const args = Autocannon.parseArguments([ 119 | '-H', 'header1=foo=bar', 120 | 'http://localhost/foo/bar' 121 | ]) 122 | 123 | t.strictSame(args.headers, { 124 | header1: 'foo=bar' 125 | }) 126 | }) 127 | 128 | test('parse argument ending space in value header', (t) => { 129 | t.plan(1) 130 | 131 | const args = Autocannon.parseArguments([ 132 | '-H', 'header1=foo=bar ', 133 | 'http://localhost/foo/bar' 134 | ]) 135 | 136 | t.strictSame(args.headers, { 137 | header1: 'foo=bar ' 138 | }) 139 | }) 140 | 141 | test('parse argument with ":" in value header', (t) => { 142 | t.plan(1) 143 | 144 | const args = Autocannon.parseArguments([ 145 | '-H', 'header1=foo:bar', 146 | 'http://localhost/foo/bar' 147 | ]) 148 | 149 | t.strictSame(args.headers, { 150 | header1: 'foo:bar' 151 | }) 152 | }) 153 | 154 | test('parse argument not correctly formatted header', (t) => { 155 | t.plan(1) 156 | 157 | t.throws(() => { 158 | Autocannon.parseArguments([ 159 | '-H', 'header1', 160 | 'http://localhost/foo/bar' 161 | ]) 162 | }, /An HTTP header was not correctly formatted/) 163 | }) 164 | 165 | test('parse argument with multiple url', (t) => { 166 | t.plan(2) 167 | const args = Autocannon.parseArguments([ 168 | 'localhost/foo/bar', 169 | 'http://localhost/baz/qux' 170 | ]) 171 | 172 | t.equal(args.url[0], 'http://localhost/foo/bar') 173 | t.equal(args.url[1], 'http://localhost/baz/qux') 174 | }) 175 | 176 | test('parse argument with input file and multiple workers', (t) => { 177 | t.plan(3) 178 | 179 | const inputPath = 'help.txt' 180 | const args = Autocannon.parseArguments([ 181 | '-m', 'POST', 182 | '-w', '2', 183 | '-a', 10, 184 | '-i', inputPath, 185 | '-H', 'Content-Type=application/json', 186 | 'http://localhost/foo/bar' 187 | ]) 188 | 189 | t.equal(args.url, 'http://localhost/foo/bar') 190 | t.equal(args.method, 'POST') 191 | t.equal(args.body, fs.readFileSync(inputPath, 'utf8')) 192 | }) 193 | 194 | test('parse argument with cert, key and multiple ca paths', (t) => { 195 | t.plan(5) 196 | 197 | const certPath = 'test/cert.pem' 198 | const keyPath = 'test/key.pem' 199 | const caPath1 = 'help.txt' 200 | const caPath2 = 'package.json' 201 | const args = Autocannon.parseArguments([ 202 | '-m', 'POST', 203 | '--cert', certPath, 204 | '--key', keyPath, 205 | '--ca', '[', caPath1, caPath2, ']', 206 | 'http://localhost/foo/bar' 207 | ]) 208 | 209 | t.equal(args.url, 'http://localhost/foo/bar') 210 | t.equal(args.method, 'POST') 211 | t.same(args.tlsOptions.cert, fs.readFileSync(certPath)) 212 | t.same(args.tlsOptions.key, fs.readFileSync(keyPath)) 213 | t.same(args.tlsOptions.ca, [fs.readFileSync(caPath1), fs.readFileSync(caPath2)]) 214 | }) 215 | 216 | test('parse argument with cert, key and single ca path', (t) => { 217 | t.plan(5) 218 | 219 | const certPath = 'test/cert.pem' 220 | const keyPath = 'test/key.pem' 221 | const caPath = 'help.txt' 222 | const args = Autocannon.parseArguments([ 223 | '-m', 'POST', 224 | '--cert', certPath, 225 | '--key', keyPath, 226 | '--ca', caPath, 227 | 'http://localhost/foo/bar' 228 | ]) 229 | 230 | t.equal(args.url, 'http://localhost/foo/bar') 231 | t.equal(args.method, 'POST') 232 | t.same(args.tlsOptions.cert, fs.readFileSync(certPath)) 233 | t.same(args.tlsOptions.key, fs.readFileSync(keyPath)) 234 | t.same(args.tlsOptions.ca, [fs.readFileSync(caPath)]) 235 | }) 236 | -------------------------------------------------------------------------------- /test/basic-auth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const split = require('split2') 5 | const path = require('path') 6 | const childProcess = require('child_process') 7 | const helper = require('./helper') 8 | 9 | const lines = [ 10 | /Running 1s test @ .*$/, 11 | /10 connections.*$/, 12 | /$/, 13 | /.*/, 14 | /$/, 15 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 16 | /.*/, 17 | /Latency.*$/, 18 | /$/, 19 | /.*/, 20 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 21 | /.*/, 22 | /Req\/Sec.*$/, 23 | /.*/, 24 | /Bytes\/Sec.*$/, 25 | /.*/, 26 | /$/, 27 | /Req\/Bytes counts sampled once per second.*$/, 28 | /# of samples: 10*$/, 29 | /$/, 30 | /.* requests in ([0-9]|\.)+s, .* read/ 31 | ] 32 | 33 | t.plan(lines.length * 2) 34 | 35 | const server = helper.startBasicAuthServer() 36 | const url = 'http://foo:bar@localhost:' + server.address().port 37 | 38 | const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', url], { 39 | cwd: __dirname, 40 | env: process.env, 41 | stdio: ['ignore', 'pipe', 'pipe'], 42 | detached: false 43 | }) 44 | 45 | t.teardown(() => { 46 | child.kill() 47 | }) 48 | 49 | child 50 | .stderr 51 | .pipe(split()) 52 | .on('data', (line) => { 53 | const regexp = lines.shift() 54 | t.ok(regexp, 'we are expecting this line') 55 | t.ok(regexp.test(line), 'line matches ' + regexp) 56 | }) 57 | -------------------------------------------------------------------------------- /test/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEVzCCAz+gAwIBAgIJAJn02lrTTFjxMA0GCSqGSIb3DQEBBQUAMHoxCzAJBgNV 3 | BAYTAklFMRIwEAYDVQQIEwl3YXRlcmZvcmQxDTALBgNVBAcTBHRlc3QxDTALBgNV 4 | BAoTBHRlc3QxDTALBgNVBAsTBHRlc3QxDTALBgNVBAMTBHRlc3QxGzAZBgkqhkiG 5 | 9w0BCQEWDHRlc0B0ZXN0LmNvbTAeFw0xNjA2MjcxNDE4MDdaFw0xOTAzMjMxNDE4 6 | MDdaMHoxCzAJBgNVBAYTAklFMRIwEAYDVQQIEwl3YXRlcmZvcmQxDTALBgNVBAcT 7 | BHRlc3QxDTALBgNVBAoTBHRlc3QxDTALBgNVBAsTBHRlc3QxDTALBgNVBAMTBHRl 8 | c3QxGzAZBgkqhkiG9w0BCQEWDHRlc0B0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEB 9 | BQADggEPADCCAQoCggEBAK0PhkEuPPEQNc1/96rapuEazVaa5p74QAn4PNOPIKaz 10 | XWyLheBF78N320w6jB4eqAe3o6XMtt28iK+q+HejLZt7v+m6c7lHDtfcLSG8CEJ3 11 | dfwR/iOfCLRlDeZyWvxouf9/s3FSAM5VqKb9kmc/Pt2+opWlX1cZvdfkg/lzSHUu 12 | FwmuxOAONKt2dPiEvDSiSs99Kv0+jSgMmy+4D8LGyvxFCQu67bh6a2zGEEYAcAib 13 | Rpw+Fb/AK8VYPW528SaWHRT7CcDgzdXaMfos3EWOQ/Cc0Q+MgqVfSmqTEUPXAc41 14 | Y4Lvvl5GSHQ4lve3jIR05xenxcMIZ8BP7fJ3BfjXCxsCAwEAAaOB3zCB3DAdBgNV 15 | HQ4EFgQUYtl9YCe7XZ4F0MvA627f+BOJoVYwgawGA1UdIwSBpDCBoYAUYtl9YCe7 16 | XZ4F0MvA627f+BOJoVahfqR8MHoxCzAJBgNVBAYTAklFMRIwEAYDVQQIEwl3YXRl 17 | cmZvcmQxDTALBgNVBAcTBHRlc3QxDTALBgNVBAoTBHRlc3QxDTALBgNVBAsTBHRl 18 | c3QxDTALBgNVBAMTBHRlc3QxGzAZBgkqhkiG9w0BCQEWDHRlc0B0ZXN0LmNvbYIJ 19 | AJn02lrTTFjxMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJN7pnlv 20 | SascD2V0I+9wirpuuNnfUP5YCFaAjky4DREV+DRPXEL2tzTQqsEeFts8BHBH+Jz3 21 | ytYP5NZSjuToF9czu8v3+mPCSqzdOFKruJbl/lAokLJWan8Z3qfXWZQL79C2I7Ih 22 | hSBnH/O+jZz9FPRJ2ydR8DB0LdGVKkQFvZynPZOh7D4NKvrEgFad4p6EBFshO+8N 23 | 1ALfR/2mrJOkBHfHPVWMmy6DoXWyVijPuLaa+l2TzdQJycl6CAJw6F7tPoO75qKY 24 | MAcIKOW5F9Zv7I3aqmoLDOaOh43FeT2JLvODe2TIaytWckoFesGadEgvAzCAXC4r 25 | ArqQX2nVUdasOnQ= 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /test/cli-ipc.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const split = require('split2') 5 | const os = require('os') 6 | const path = require('path') 7 | const childProcess = require('child_process') 8 | const helper = require('./helper') 9 | 10 | const win = process.platform === 'win32' 11 | 12 | const lines = [ 13 | /Running 1s test @ http:\/\/example.com\/foo \([^)]*\)$/, 14 | /10 connections.*$/, 15 | /$/, 16 | /.*/, 17 | /$/, 18 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 19 | /.*/, 20 | /Latency.*$/, 21 | /$/, 22 | /.*/, 23 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 24 | /.*/, 25 | /Req\/Sec.*$/, 26 | /.*/, 27 | /Bytes\/Sec.*$/, 28 | /.*/, 29 | /$/, 30 | /Req\/Bytes counts sampled once per second.*$/, 31 | /# of samples: 10*$/, 32 | /$/, 33 | /.* requests in ([0-9]|\.)+s, .* read/ 34 | ] 35 | 36 | if (!win) { 37 | // If not Windows we can predict exactly how many lines there will be. On 38 | // Windows we rely on t.end() being called. 39 | t.plan(lines.length) 40 | } 41 | 42 | t.autoend(false) 43 | t.teardown(function () { 44 | child.kill() 45 | }) 46 | 47 | const socketPath = win 48 | ? path.join('\\\\?\\pipe', process.cwd(), 'autocannon-' + Date.now()) 49 | : path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock') 50 | 51 | helper.startServer({ socketPath }) 52 | 53 | const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', '-S', socketPath, 'example.com/foo'], { 54 | cwd: __dirname, 55 | env: process.env, 56 | stdio: ['ignore', 'pipe', 'pipe'], 57 | detached: false 58 | }) 59 | 60 | // For handling the last line on Windows 61 | let errorLine = false 62 | let failsafeTimer 63 | 64 | child 65 | .stderr 66 | .pipe(split()) 67 | .on('data', (line) => { 68 | let regexp = lines.shift() 69 | const lastLine = lines.length === 0 70 | 71 | if (regexp) { 72 | t.ok(regexp.test(line), 'line matches ' + regexp) 73 | 74 | if (lastLine && win) { 75 | // We can't be sure the error line is outputted on Windows, so in case 76 | // this really is the last line, we'll set a timer to auto-end the test 77 | // in case there are no more lines. 78 | failsafeTimer = setTimeout(function () { 79 | t.end() 80 | }, 1000) 81 | } 82 | } else if (!errorLine && win) { 83 | // On Windows a few errors are expected. We'll accept a 1% error rate on 84 | // the pipe. 85 | errorLine = true 86 | clearTimeout(failsafeTimer) 87 | regexp = /^(\d+) errors \(0 timeouts\)$/ 88 | const match = line.match(regexp) 89 | t.ok(match, 'line matches ' + regexp) 90 | const errors = Number(match[1]) 91 | t.ok(errors / 15000 < 0.01, `should have less than 1% errors on Windows (had ${errors} errors)`) 92 | t.end() 93 | } else { 94 | throw new Error('Unexpected line: ' + JSON.stringify(line)) 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /test/debug.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const Autocannon = require('../autocannon') 5 | 6 | test('debug works', (t) => { 7 | t.plan(5) 8 | 9 | const args = Autocannon.parseArguments([ 10 | '-H', 'X-Http-Method-Override=GET', 11 | '-m', 'POST', 12 | '-b', 'the body', 13 | '--debug', 14 | 'http://localhost/foo/bar' 15 | ]) 16 | 17 | t.equal(args.url, 'http://localhost/foo/bar') 18 | t.strictSame(args.headers, { 'X-Http-Method-Override': 'GET' }) 19 | t.equal(args.method, 'POST') 20 | t.equal(args.body, 'the body') 21 | t.equal(args.debug, true) 22 | }) 23 | -------------------------------------------------------------------------------- /test/envPort.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const why = require('why-is-node-running') 4 | const t = require('tap') 5 | const split = require('split2') 6 | const path = require('path') 7 | const childProcess = require('child_process') 8 | const helper = require('./helper') 9 | 10 | setInterval(function () { 11 | console.log(why()) 12 | }, 30000).unref() 13 | 14 | const lines = [ 15 | /Running 1s test @ .*$/, 16 | /10 connections.*$/, 17 | /$/, 18 | /.*/, 19 | /$/, 20 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 21 | /.*/, 22 | /Latency.*$/, 23 | /$/, 24 | /.*/, 25 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 26 | /.*/, 27 | /Req\/Sec.*$/, 28 | /$/, 29 | /Bytes\/Sec.*$/, 30 | /.*/, 31 | /$/, 32 | /Req\/Bytes counts sampled once per second.*$/, 33 | /# of samples: 10*$/, 34 | /$/, 35 | /.* requests in ([0-9]|\.)+s, .* read/ 36 | ] 37 | 38 | t.plan(lines.length * 2 + 2) 39 | 40 | const server = helper.startServer() 41 | const port = server.address().port 42 | const url = '/path' // no hostname 43 | 44 | const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', url], { 45 | cwd: __dirname, 46 | env: Object.assign({}, process.env, { 47 | PORT: port 48 | }), 49 | stdio: ['ignore', 'pipe', 'pipe'], 50 | detached: false 51 | }) 52 | 53 | t.teardown(() => { 54 | try { 55 | child.kill() 56 | } catch {} 57 | }) 58 | 59 | child 60 | .stderr 61 | .pipe(split()) 62 | .on('data', (line) => { 63 | const regexp = lines.shift() 64 | t.ok(regexp, 'we are expecting this line') 65 | t.ok(regexp.test(line), 'line matches ' + regexp) 66 | }) 67 | .on('end', () => { 68 | t.ok(server.autocannonConnects > 0, 'targeted the correct port') 69 | }) 70 | 71 | const noPortChild = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), url], { 72 | cwd: __dirname, 73 | env: process.env, 74 | stdio: ['ignore', 'pipe', 'pipe'], 75 | detached: false 76 | }) 77 | 78 | noPortChild.on('exit', (code) => { 79 | t.equal(code, 1, 'should exit with error when a hostless URL is passed and no PORT var is available') 80 | }) 81 | -------------------------------------------------------------------------------- /test/fixtures/example-result.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "example result", 3 | "url": "https://httpbin.org/get", 4 | "requests": { 5 | "average": 250, 6 | "mean": 250, 7 | "stddev": 117, 8 | "min": 133, 9 | "max": 367, 10 | "total": 500, 11 | "p0_001": 133, 12 | "p0_01": 133, 13 | "p0_1": 133, 14 | "p1": 133, 15 | "p2_5": 133, 16 | "p10": 133, 17 | "p25": 133, 18 | "p50": 133, 19 | "p75": 367, 20 | "p90": 367, 21 | "p97_5": 367, 22 | "p99": 367, 23 | "p99_9": 367, 24 | "p99_99": 367, 25 | "p99_999": 367, 26 | "sent": 500 27 | }, 28 | "latency": { 29 | "average": 100.1, 30 | "mean": 100.1, 31 | "stddev": 84.74, 32 | "min": 47, 33 | "max": 424, 34 | "p0_001": 47, 35 | "p0_01": 47, 36 | "p0_1": 47, 37 | "p1": 53, 38 | "p2_5": 55, 39 | "p10": 58, 40 | "p25": 59, 41 | "p50": 67, 42 | "p75": 107, 43 | "p90": 126, 44 | "p97_5": 378, 45 | "p99": 386, 46 | "p99_9": 424, 47 | "p99_99": 424, 48 | "p99_999": 424, 49 | "totalCount": 500 50 | }, 51 | "throughput": { 52 | "average": 52468, 53 | "mean": 52468, 54 | "stddev": 24556, 55 | "min": 27915, 56 | "max": 77030, 57 | "total": 104945, 58 | "p0_001": 27919, 59 | "p0_01": 27919, 60 | "p0_1": 27919, 61 | "p1": 27919, 62 | "p2_5": 27919, 63 | "p10": 27919, 64 | "p25": 27919, 65 | "p50": 27919, 66 | "p75": 77055, 67 | "p90": 77055, 68 | "p97_5": 77055, 69 | "p99": 77055, 70 | "p99_9": 77055, 71 | "p99_99": 77055, 72 | "p99_999": 77055 73 | }, 74 | "errors": 0, 75 | "timeouts": 0, 76 | "mismatches": 0, 77 | "duration": 2.06, 78 | "sampleInt": 1000, 79 | "samples": 10, 80 | "start": "2020-11-06T09:38:31.027Z", 81 | "finish": "2020-11-06T09:38:33.089Z", 82 | "connections": 40, 83 | "pipelining": 1, 84 | "non2xx": 0, 85 | "resets": 0, 86 | "1xx": 0, 87 | "2xx": 500, 88 | "3xx": 0, 89 | "4xx": 0, 90 | "5xx": 0, 91 | "statusCodeStats": { 92 | "200": { "count": "500" }, 93 | "302": { "count": "0" }, 94 | "401": { "count": "0" }, 95 | "403": { "count": "0" } 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /test/fixtures/httpbin-get.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "Firefox", 6 | "version": "80.0.1" 7 | }, 8 | "browser": { 9 | "name": "Firefox", 10 | "version": "80.0.1" 11 | }, 12 | "pages": [ 13 | { 14 | "startedDateTime": "2020-09-28T20:47:18.735+02:00", 15 | "id": "page_1", 16 | "title": "https://httpbin.org/post", 17 | "pageTimings": { 18 | "onContentLoad": 170, 19 | "onLoad": 187 20 | } 21 | } 22 | ], 23 | "entries": [ 24 | { 25 | "pageref": "page_2", 26 | "startedDateTime": "2020-09-28T22:03:15.885+02:00", 27 | "request": { 28 | "bodySize": 0, 29 | "method": "GET", 30 | "url": "https://httpbin.org/get", 31 | "httpVersion": "HTTP/2", 32 | "headers": [ 33 | { 34 | "name": "Host", 35 | "value": "httpbin.org" 36 | }, 37 | { 38 | "name": "User-Agent", 39 | "value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0" 40 | }, 41 | { 42 | "name": "Accept", 43 | "value": "*/*" 44 | }, 45 | { 46 | "name": "Accept-Language", 47 | "value": "fr,en;q=0.5" 48 | }, 49 | { 50 | "name": "Accept-Encoding", 51 | "value": "gzip, deflate, br" 52 | }, 53 | { 54 | "name": "Referer", 55 | "value": "https://httpbin.org/" 56 | }, 57 | { 58 | "name": "DNT", 59 | "value": "1" 60 | }, 61 | { 62 | "name": "Connection", 63 | "value": "keep-alive" 64 | } 65 | ], 66 | "cookies": [], 67 | "queryString": [], 68 | "headersSize": 272 69 | }, 70 | "response": { 71 | "status": 200, 72 | "statusText": "OK", 73 | "httpVersion": "HTTP/2", 74 | "headers": [ 75 | { 76 | "name": "date", 77 | "value": "Mon, 28 Sep 2020 20:03:16 GMT" 78 | }, 79 | { 80 | "name": "content-type", 81 | "value": "application/json" 82 | }, 83 | { 84 | "name": "content-length", 85 | "value": "461" 86 | }, 87 | { 88 | "name": "server", 89 | "value": "gunicorn/19.9.0" 90 | }, 91 | { 92 | "name": "access-control-allow-origin", 93 | "value": "*" 94 | }, 95 | { 96 | "name": "access-control-allow-credentials", 97 | "value": "true" 98 | }, 99 | { 100 | "name": "X-Firefox-Spdy", 101 | "value": "h2" 102 | } 103 | ], 104 | "cookies": [], 105 | "content": { 106 | "mimeType": "application/json", 107 | "size": 461, 108 | "text": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f724184-c2b2dc0aa856f9d04dac5d35\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get\"\n}\n" 109 | }, 110 | "redirectURL": "", 111 | "headersSize": 224, 112 | "bodySize": 685 113 | }, 114 | "cache": {}, 115 | "timings": { 116 | "blocked": 184, 117 | "dns": 8, 118 | "connect": 85, 119 | "ssl": 88, 120 | "send": 0, 121 | "wait": 86, 122 | "receive": 0 123 | }, 124 | "time": 451, 125 | "_securityState": "secure", 126 | "serverIPAddress": "35.170.21.246", 127 | "connection": "443" 128 | }, 129 | { 130 | "pageref": "page_1", 131 | "startedDateTime": "2020-09-28T21:46:29.772+02:00", 132 | "request": { 133 | "bodySize": 0, 134 | "method": "GET", 135 | "url": "https://httpbin.org/get?from=10&size=20&sort=+name", 136 | "httpVersion": "HTTP/2", 137 | "headers": [ 138 | { 139 | "name": "Host", 140 | "value": "httpbin.org" 141 | }, 142 | { 143 | "name": "User-Agent", 144 | "value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0" 145 | }, 146 | { 147 | "name": "Accept", 148 | "value": "*/*" 149 | }, 150 | { 151 | "name": "Accept-Language", 152 | "value": "fr,en;q=0.5" 153 | }, 154 | { 155 | "name": "Accept-Encoding", 156 | "value": "gzip, deflate, br" 157 | }, 158 | { 159 | "name": "Referer", 160 | "value": "https://httpbin.org/" 161 | }, 162 | { 163 | "name": "DNT", 164 | "value": "1" 165 | }, 166 | { 167 | "name": "Connection", 168 | "value": "keep-alive" 169 | }, 170 | { 171 | "name": "TE", 172 | "value": "Trailers" 173 | } 174 | ], 175 | "cookies": [], 176 | "queryString": [ 177 | { 178 | "name": "from", 179 | "value": "10" 180 | }, 181 | { 182 | "name": "size", 183 | "value": "20" 184 | }, 185 | { 186 | "name": "sort", 187 | "value": " name" 188 | } 189 | ], 190 | "headersSize": 299 191 | }, 192 | "response": { 193 | "status": 200, 194 | "statusText": "OK", 195 | "httpVersion": "HTTP/2", 196 | "headers": [ 197 | { 198 | "name": "date", 199 | "value": "Mon, 28 Sep 2020 19:46:29 GMT" 200 | }, 201 | { 202 | "name": "content-type", 203 | "value": "application/json" 204 | }, 205 | { 206 | "name": "content-length", 207 | "value": "549" 208 | }, 209 | { 210 | "name": "server", 211 | "value": "gunicorn/19.9.0" 212 | }, 213 | { 214 | "name": "access-control-allow-origin", 215 | "value": "*" 216 | }, 217 | { 218 | "name": "access-control-allow-credentials", 219 | "value": "true" 220 | }, 221 | { 222 | "name": "X-Firefox-Spdy", 223 | "value": "h2" 224 | } 225 | ], 226 | "cookies": [], 227 | "content": { 228 | "mimeType": "application/json", 229 | "size": 549, 230 | "text": "{\n \"args\": {\n \"from\": \"10\", \n \"size\": \"20\", \n \"sort\": \" name\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723d95-9675a1b77254a636c2f09fa3\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get?from=10&size=20&sort=+name\"\n}\n" 231 | }, 232 | "redirectURL": "", 233 | "headersSize": 224, 234 | "bodySize": 773 235 | }, 236 | "cache": {}, 237 | "timings": { 238 | "blocked": 0, 239 | "dns": 0, 240 | "connect": 0, 241 | "ssl": 0, 242 | "send": 0, 243 | "wait": 168, 244 | "receive": 0 245 | }, 246 | "time": 168, 247 | "_securityState": "secure", 248 | "serverIPAddress": "35.170.21.246", 249 | "connection": "443" 250 | } 251 | ] 252 | } 253 | } -------------------------------------------------------------------------------- /test/fixtures/httpbin-simple-get.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "Firefox", 6 | "version": "80.0.1" 7 | }, 8 | "browser": { 9 | "name": "Firefox", 10 | "version": "80.0.1" 11 | }, 12 | "entries": [ 13 | { 14 | "startedDateTime": "2020-09-28T22:03:15.885+02:00", 15 | "request": { 16 | "bodySize": 0, 17 | "method": "GET", 18 | "url": "https://httpbin.org/get", 19 | "httpVersion": "HTTP/2", 20 | "headers": [ 21 | { 22 | "name": "Host", 23 | "value": "httpbin.org" 24 | }, 25 | { 26 | "name": "User-Agent", 27 | "value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0" 28 | }, 29 | { 30 | "name": "Accept", 31 | "value": "*/*" 32 | }, 33 | { 34 | "name": "Accept-Language", 35 | "value": "fr,en;q=0.5" 36 | }, 37 | { 38 | "name": "Accept-Encoding", 39 | "value": "gzip, deflate, br" 40 | }, 41 | { 42 | "name": "Referer", 43 | "value": "https://httpbin.org/" 44 | }, 45 | { 46 | "name": "DNT", 47 | "value": "1" 48 | }, 49 | { 50 | "name": "Connection", 51 | "value": "keep-alive" 52 | } 53 | ], 54 | "cookies": [], 55 | "queryString": [], 56 | "headersSize": 272 57 | }, 58 | "response": { 59 | "status": 200, 60 | "statusText": "OK", 61 | "httpVersion": "HTTP/2", 62 | "headers": [ 63 | { 64 | "name": "date", 65 | "value": "Mon, 28 Sep 2020 20:03:16 GMT" 66 | }, 67 | { 68 | "name": "content-type", 69 | "value": "application/json" 70 | }, 71 | { 72 | "name": "content-length", 73 | "value": "461" 74 | }, 75 | { 76 | "name": "server", 77 | "value": "gunicorn/19.9.0" 78 | }, 79 | { 80 | "name": "access-control-allow-origin", 81 | "value": "*" 82 | }, 83 | { 84 | "name": "access-control-allow-credentials", 85 | "value": "true" 86 | }, 87 | { 88 | "name": "X-Firefox-Spdy", 89 | "value": "h2" 90 | } 91 | ], 92 | "cookies": [], 93 | "content": { 94 | "mimeType": "application/json", 95 | "size": 461, 96 | "text": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f724184-c2b2dc0aa856f9d04dac5d35\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get\"\n}\n" 97 | }, 98 | "redirectURL": "", 99 | "headersSize": 224, 100 | "bodySize": 685 101 | }, 102 | "cache": {}, 103 | "timings": { 104 | "blocked": 184, 105 | "dns": 8, 106 | "connect": 85, 107 | "ssl": 88, 108 | "send": 0, 109 | "wait": 86, 110 | "receive": 0 111 | }, 112 | "time": 451, 113 | "_securityState": "secure", 114 | "serverIPAddress": "35.170.21.246", 115 | "connection": "443" 116 | } 117 | ] 118 | } 119 | } -------------------------------------------------------------------------------- /test/forever.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const initJob = require('../lib/init') 5 | const helper = require('./helper') 6 | const server = helper.startServer() 7 | 8 | test('running with forever set to true and passing in a callback should cause an error to be returned in the callback', (t) => { 9 | t.plan(2) 10 | 11 | initJob({ 12 | url: `http://localhost:${server.address().port}`, 13 | forever: true 14 | }, (err, res) => { 15 | t.ok(err, 'should be error when callback passed to run') 16 | t.notOk(res, 'should not exist') 17 | t.end() 18 | }) 19 | }) 20 | 21 | test('run forever should run until .stop() is called', (t) => { 22 | t.plan(3) 23 | let numRuns = 0 24 | 25 | const instance = initJob({ 26 | url: `http://localhost:${server.address().port}`, 27 | duration: 0.5, 28 | forever: true 29 | }) 30 | 31 | instance.on('done', (results) => { 32 | t.ok(results, 'should have gotten results') 33 | if (++numRuns === 2) { 34 | instance.stop() 35 | setTimeout(() => { 36 | t.ok(true, 'should have reached here without the callback being called again') 37 | t.end() 38 | }, 1000) 39 | } 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/format.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const format = require('../lib/format') 5 | 6 | const pairs = { 7 | 2: 2, 8 | '2k': 2000, 9 | '4k': 4042, 10 | '2300k': 2300000 11 | } 12 | 13 | Object.keys(pairs).forEach((expected) => { 14 | const original = pairs[expected] 15 | test(`format ${original} into ${expected}`, (t) => { 16 | t.equal(expected, format(original)) 17 | t.end() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const https = require('https') 5 | const tls = require('tls') 6 | const fs = require('fs') 7 | const path = require('path') 8 | const BusBoy = require('busboy') 9 | 10 | function startServer (opts) { 11 | opts = opts || {} 12 | 13 | if (!Array.isArray(opts.responses)) { 14 | opts.responses = [] 15 | } 16 | 17 | const fixedStatusCode = opts.statusCode || 200 18 | const server = http.createServer(handle) 19 | server.autocannonConnects = 0 20 | server.autocannonRequests = 0 21 | 22 | server.on('connection', () => { server.autocannonConnects++ }) 23 | 24 | server.listen(opts.socketPath || 0) 25 | 26 | function handle (req, res) { 27 | let { statusCode, body, headers } = opts.responses[server.autocannonRequests] || {} 28 | 29 | server.autocannonRequests++ 30 | 31 | if (!statusCode) { 32 | statusCode = fixedStatusCode 33 | } 34 | 35 | if (!body) { 36 | body = opts.body || 'hello world' 37 | } 38 | 39 | res.statusCode = statusCode 40 | const reply = () => { 41 | const bodyToWrite = typeof body === 'function' ? body(req) : body 42 | 43 | if (headers) { 44 | res.writeHead(statusCode, headers) 45 | } 46 | res.end(statusCode < 200 ? undefined : bodyToWrite) 47 | } 48 | 49 | if (opts.delayResponse) { 50 | setTimeout(reply, opts.delayResponse) 51 | } else { 52 | reply() 53 | } 54 | } 55 | 56 | server.unref() 57 | 58 | return server 59 | } 60 | 61 | function startTrailerServer () { 62 | const server = http.createServer(handle) 63 | 64 | function handle (req, res) { 65 | res.writeHead(200, { 'Content-Type': 'text/plain', Trailer: 'Content-MD5' }) 66 | res.write('hello ') 67 | res.addTrailers({ 'Content-MD5': '7895bf4b8828b55ceaf47747b4bca667' }) 68 | res.end('world') 69 | } 70 | 71 | server.listen(0) 72 | 73 | server.unref() 74 | 75 | return server 76 | } 77 | 78 | // this server won't reply to requests 79 | function startTimeoutServer () { 80 | const server = http.createServer(() => {}) 81 | 82 | server.listen(0) 83 | server.unref() 84 | 85 | return server 86 | } 87 | 88 | // this server destroys the socket on connection, should result in ECONNRESET 89 | function startSocketDestroyingServer () { 90 | const server = http.createServer(handle) 91 | 92 | function handle (req, res) { 93 | res.destroy() 94 | server.close() 95 | } 96 | 97 | server.listen(0) 98 | server.unref() 99 | 100 | return server 101 | } 102 | 103 | // this server won't reply to requests 104 | function startHttpsServer (opts = {}) { 105 | const options = { 106 | key: fs.readFileSync(path.join(__dirname, '/key.pem')), 107 | cert: fs.readFileSync(path.join(__dirname, '/cert.pem')), 108 | passphrase: 'test' 109 | } 110 | 111 | const server = https.createServer(options, handle) 112 | 113 | server.listen(opts.socketPath || 0) 114 | 115 | function handle (req, res) { 116 | res.end('hello world') 117 | } 118 | 119 | server.unref() 120 | 121 | return server 122 | } 123 | 124 | // this server will echo the SNI Server Name and emailAddress from the client certificate in a HTTP header 125 | function startTlsServer () { 126 | const key = fs.readFileSync(path.join(__dirname, '/key.pem')) 127 | const cert = fs.readFileSync(path.join(__dirname, '/cert.pem')) 128 | const passphrase = 'test' 129 | 130 | const options = { 131 | key, 132 | cert, 133 | passphrase, 134 | requestCert: true, 135 | rejectUnauthorized: false 136 | } 137 | 138 | const server = tls.createServer(options, handle) 139 | 140 | server.listen(0) 141 | 142 | function handle (socket) { 143 | const servername = socket.servername || '' 144 | const certificate = socket.getPeerCertificate() 145 | const email = (certificate && certificate.subject && certificate.subject.emailAddress) || '' 146 | socket.on('data', function (data) { 147 | // Assume this is a http get request and send back the servername in an otherwise empty reponse. 148 | socket.write('HTTP/1.1 200 OK\n') 149 | socket.write('X-servername: ' + servername + '\n') 150 | if (email) { 151 | socket.write('X-email: ' + email + '\n') 152 | } 153 | socket.write('Content-Length: 0\n\n') 154 | socket.setEncoding('utf8') 155 | socket.pipe(socket) 156 | }) 157 | 158 | socket.on('error', noop) 159 | } 160 | 161 | server.unref() 162 | 163 | return server 164 | } 165 | 166 | function startMultipartServer (opts = {}, test = () => {}) { 167 | const server = http.createServer(handle) 168 | const allowed = ['POST', 'PUT'] 169 | function handle (req, res) { 170 | if (allowed.includes(req.method)) { 171 | const bboy = new BusBoy({ headers: req.headers, ...opts }) 172 | const fileData = [] 173 | const payload = {} 174 | bboy.on('file', (fieldname, file, filename, encoding, mimetype) => { 175 | payload[fieldname] = { 176 | filename, 177 | encoding, 178 | mimetype 179 | } 180 | file.on('data', data => fileData.push(data)) 181 | }) 182 | bboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { 183 | payload[fieldname] = val 184 | }) 185 | bboy.on('finish', () => { 186 | res.statusCode = fileData.length ? 201 : 400 187 | res.write(JSON.stringify(payload)) 188 | res.end() 189 | test(payload) 190 | }) 191 | req.pipe(bboy) 192 | } else { 193 | res.statusCode = 404 194 | res.write(JSON.stringify({})) 195 | res.end() 196 | } 197 | } 198 | 199 | server.listen(0) 200 | server.unref() 201 | 202 | return server 203 | } 204 | function startBasicAuthServer () { 205 | const server = http.createServer(handle) 206 | 207 | function handle (req, res) { 208 | if (!req.headers.authorization || req.headers.authorization.indexOf('Basic ') === -1) { 209 | res.writeHead(401) 210 | return res.end() 211 | } 212 | 213 | res.writeHead(200) 214 | res.end('hello world') 215 | } 216 | 217 | server.listen(0) 218 | server.unref() 219 | 220 | return server 221 | } 222 | 223 | function customizeHAR (fixturePath, replaced, domain) { 224 | const har = JSON.parse(JSON.stringify(require(fixturePath))) 225 | for (const entry of har.log.entries) { 226 | entry.request.url = entry.request.url.replace(replaced, domain) 227 | } 228 | return har 229 | } 230 | 231 | module.exports.startServer = startServer 232 | module.exports.startTimeoutServer = startTimeoutServer 233 | module.exports.startSocketDestroyingServer = startSocketDestroyingServer 234 | module.exports.startHttpsServer = startHttpsServer 235 | module.exports.startTrailerServer = startTrailerServer 236 | module.exports.startTlsServer = startTlsServer 237 | module.exports.startMultipartServer = startMultipartServer 238 | module.exports.startBasicAuthServer = startBasicAuthServer 239 | module.exports.customizeHAR = customizeHAR 240 | 241 | function noop () {} 242 | -------------------------------------------------------------------------------- /test/httpRequestBuilder.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const helper = require('./helper') 5 | const server = helper.startServer() 6 | const RequestBuilder = require('../lib/httpRequestBuilder') 7 | const httpMethods = require('../lib/httpMethods') 8 | 9 | test('request builder should create a request with sensible defaults', (t) => { 10 | t.plan(1) 11 | 12 | const opts = server.address() 13 | 14 | const build = RequestBuilder(opts) 15 | 16 | const result = build() 17 | t.same(result, 18 | Buffer.from(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`), 19 | 'request is okay') 20 | }) 21 | 22 | test('request builder should allow default overwriting', (t) => { 23 | t.plan(1) 24 | 25 | const opts = server.address() 26 | opts.method = 'POST' 27 | 28 | const build = RequestBuilder(opts) 29 | 30 | const result = build() 31 | t.same(result, 32 | Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`), 33 | 'request is okay') 34 | }) 35 | 36 | test('request builder should allow per build overwriting', (t) => { 37 | t.plan(1) 38 | 39 | const opts = server.address() 40 | opts.method = 'POST' 41 | 42 | const build = RequestBuilder(opts) 43 | 44 | const result = build({ method: 'GET' }) 45 | 46 | t.same(result, 47 | Buffer.from(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`), 48 | 'request is okay') 49 | }) 50 | 51 | test('request builder should throw on unknown http method', (t) => { 52 | t.plan(1) 53 | 54 | const opts = server.address() 55 | 56 | const build = RequestBuilder(opts) 57 | 58 | t.throws(() => build({ method: 'UNKNOWN' })) 59 | }) 60 | 61 | test('request builder should accept all valid standard http methods', (t) => { 62 | t.plan(httpMethods.length) 63 | httpMethods.forEach((method) => { 64 | const opts = server.address() 65 | 66 | const build = RequestBuilder(opts) 67 | 68 | t.doesNotThrow(() => build({ method }), `${method} should be usable by the request builded`) 69 | }) 70 | t.end() 71 | }) 72 | 73 | test('request builder should add a Content-Length header when the body buffer exists as a default override', (t) => { 74 | t.plan(1) 75 | 76 | const opts = server.address() 77 | opts.method = 'POST' 78 | opts.body = 'body' 79 | 80 | const build = RequestBuilder(opts) 81 | 82 | const result = build() 83 | t.same(result, 84 | Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\nbody`), 85 | 'request is okay') 86 | }) 87 | 88 | test('request builder should add a Content-Length header when the body buffer exists as per build override', (t) => { 89 | t.plan(1) 90 | 91 | const opts = server.address() 92 | opts.method = 'POST' 93 | 94 | const build = RequestBuilder(opts) 95 | 96 | const result = build({ body: 'body' }) 97 | t.same(result, 98 | Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\nbody`), 99 | 'request is okay') 100 | }) 101 | 102 | test('request builder should add only one HOST header', (t) => { 103 | t.plan(1) 104 | 105 | const opts = server.address() 106 | opts.method = 'POST' 107 | opts.headers = { 108 | Host: 'example.com' 109 | } 110 | 111 | const build = RequestBuilder(opts) 112 | 113 | const result = build({ body: 'body' }) 114 | t.same(result, 115 | Buffer.from('POST / HTTP/1.1\r\nConnection: keep-alive\r\nHost: example.com\r\nContent-Length: 4\r\n\r\nbody'), 116 | 'request is okay') 117 | }) 118 | 119 | test('request builder should add a Content-Length header with correct calculated value when the body buffer exists and idReplacement is enabled as a default override', (t) => { 120 | t.plan(1) 121 | 122 | const opts = server.address() 123 | opts.method = 'POST' 124 | opts.body = '[]' 125 | opts.idReplacement = true 126 | 127 | const build = RequestBuilder(opts) 128 | 129 | const result = build() 130 | t.same(result, 131 | Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 33\r\n\r\n[]`), 132 | 'request is okay') 133 | }) 134 | 135 | test('request builder should add a Content-Length header with value "[]" when the body buffer exists and idReplacement is enabled as a per build override', (t) => { 136 | t.plan(1) 137 | 138 | const opts = server.address() 139 | opts.method = 'POST' 140 | opts.body = '[]' 141 | 142 | const build = RequestBuilder(opts) 143 | 144 | const result = build({ idReplacement: true }) 145 | t.same(result, 146 | Buffer.from(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 33\r\n\r\n[]`), 147 | 'request is okay') 148 | }) 149 | 150 | test('request builder should allow http basic authentication', (t) => { 151 | t.plan(1) 152 | 153 | const opts = server.address() 154 | opts.auth = 'username:password' 155 | 156 | const build = RequestBuilder(opts) 157 | 158 | const result = build() 159 | t.same(result, 160 | Buffer.from(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nAuthorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=\r\n\r\n`), 161 | 'request is okay') 162 | }) 163 | 164 | test('should throw error if body is not a string or a buffer', (t) => { 165 | t.plan(1) 166 | 167 | const opts = server.address() 168 | 169 | const build = RequestBuilder(opts) 170 | 171 | try { 172 | build({ body: [] }) 173 | } catch (error) { 174 | t.equal(error.message, 'body must be either a string or a buffer') 175 | } 176 | }) 177 | -------------------------------------------------------------------------------- /test/j5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/test/j5.jpeg -------------------------------------------------------------------------------- /test/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,081E2A4DFAF06358 4 | 5 | yfmHgUwn2RV+CGBYKJMBfLu+EQCKIOprjUxSBPaytTwc6NsOeVjOb/iAcVvr0D6B 6 | a6Kc8BrIMfVVjABghufAnJXav+aB6Q/LkfaqJp6AfVI86WMaCBb0Fzkg8IYYSbgy 7 | JiskTYmV1IrjYfR6TEtQmeFP0DMh1RU9VLIfehEFRMU7LcDh5Ah3bsZE5muBSlws 8 | 98IVBwEwEXoQdVxLJmz+kR3fEjXYmgNZQzP6GFb3jQENV3XijQffgQ4ajREB5EZL 9 | Zag+K0vwbFZORqWNfI5VFVVWOEErRF0BSDFZWJP8jbiSX4tNyEFRzO2dgdjGycqt 10 | 3dtWbofY28Hjf36P3ee2mMHe3V8XZUtCtfufiYFLKHedHShg7FolfSSMmIfNmSgE 11 | jCnTFNU4Fsq8I/q6ywfVh3CP/rjEe/I4GAU7wql0PW7LXjAHqRjLshJ09mX5J3w8 12 | +tvsrYiBLsgmtdnX47LDJDyeYZY5gbBaz9XsNd432z1M6lRILsT48RYwXEz5oHBE 13 | El1oKNko/PHljDkTph/xI1jUlkYJ7w9//stao/9VLzn0XQG839+lz1MNi/opmpUi 14 | 3P9Z3+yTbdwibRHLmgCR6PNE4EJdfv0VNantsxiSATyhIS/xPGhU4lich8gMNDcv 15 | 9IcUSCTkmVGy8ksJITt/m7mespQ8Y1oD3xvXoaCbTJ8aig6EQ5flYwmmiIHrWSol 16 | 5w5iKjkNbdNYftAg+RB6+EwrNz43oHLYRMB3dckYc1L+otd/X+lmlTlqOo8WDgh1 17 | cH4MI4cwjdYmFdp5Nhfd9/nWFtayqcj59IBgEAMzafbzcmhma6oJD9GsxSnw9eRY 18 | vb3b7fhtcqeKDgqfN4TklX/7kkoLpRVMEdW4uNyU0US1QC5gTxiFRW3/XGhaQJ6o 19 | quE2UP3qySp7kHsz++P6mya2xF2JNcKh1zxxF+wSnjymUxaWXz1LKGQs4iOjU8Xg 20 | HkPD3wTxtMXXZw/0LGMu7XLMFWMV3nuMGtt/I/GGtCbKbFkDOOI4imZWpmsvvO0s 21 | liMhhtCwJTqkYvlxPtfcLSKFJrwcCeb2xHcLEDAaRa5xUbUt3P60l7Ujyv3p+bVi 22 | sItGE6Mk8kGXglmZGBA0RIGhXlX7r6pNryqMUy0CU5/YniJFCKoaU9WOG8VwHynY 23 | psqvZ1umAtl4twe7SxF0znLdOGiYsOOQls5wcz1rDfj5IXRcbrWEeLwYD9b0oXms 24 | TvgkcfHlutZ+M+NtP7wGFBen1X68HsYBhvhLrVbox5bgMZVrXCN4+HiBO4VAwq+m 25 | c2akVF9kQfoR5iGMhzzgskH8c7TWuF7IDqA8KBuFKcicBHK/Ns/1ljycWr07cLJc 26 | WabX25aI0x58+ise5fJAQvNW6Oq2Jvt3qcUXnr2OMnR6WUQ7amgVC7JAdfRoahYU 27 | 2WJl6jkCANIv5mY7ubEgVgXnuYYL4ljScpMZLER3cLW4uQnRXWo4zciz06+BadfI 28 | PjGb9xt+3EE4VB64O3yI9yBoDAM/lL0oadB8TPxFLN1NZA3gixbE6dXd5jGD9YxJ 29 | rJNu8xR4MUttiHA8+aVw84kiqsY8OzrjbJnVr2SSXARYw4dnz8DfxhPCt6hMCWnw 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /test/keystore.pkcs12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/test/keystore.pkcs12 -------------------------------------------------------------------------------- /test/onPort.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { test } = require('tap') 5 | const spawn = require('child_process').spawn 6 | const split = require('split2') 7 | const hasAsyncHooks = require('has-async-hooks') 8 | 9 | test('--on-port flag', { skip: !hasAsyncHooks() }, (t) => { 10 | const lines = [ 11 | /Running 1s test @ .*$/, 12 | /10 connections.*$/, 13 | /$/, 14 | /.*/, 15 | /$/, 16 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 17 | /.*/, 18 | /Latency.*$/, 19 | /$/, 20 | /.*/, 21 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 22 | /.*/, 23 | /Req\/Sec.*$/, 24 | /$/, 25 | /Bytes\/Sec.*$/, 26 | /.*/, 27 | /$/, 28 | /Req\/Bytes counts sampled once per second.*$/, 29 | /# of samples: 10*$/, 30 | /$/, 31 | /.* requests in ([0-9]|\.)+s, .* read/ 32 | ] 33 | 34 | t.plan(lines.length * 2) 35 | 36 | const child = spawn(process.execPath, [ 37 | path.join(__dirname, '..'), 38 | '-c', '10', 39 | '-d', '1', 40 | '--on-port', '/', 41 | '--', 'node', path.join(__dirname, './targetProcess') 42 | ], { 43 | cwd: __dirname, 44 | env: process.env, 45 | stdio: ['ignore', 'pipe', 'pipe'], 46 | detached: false 47 | }) 48 | 49 | t.teardown(() => { 50 | child.kill() 51 | }) 52 | 53 | child 54 | .stderr 55 | .pipe(split()) 56 | .on('data', (line) => { 57 | const regexp = lines.shift() 58 | t.ok(regexp, 'we are expecting this line') 59 | t.ok(regexp.test(line), 'line matches ' + regexp) 60 | }) 61 | }) 62 | 63 | test('assume --on-port flag if -- node is set', { skip: !hasAsyncHooks() }, (t) => { 64 | const lines = [ 65 | /Running 1s test @ .*$/, 66 | /10 connections.*$/, 67 | /$/, 68 | /.*/, 69 | /$/, 70 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 71 | /.*/, 72 | /Latency.*$/, 73 | /$/, 74 | /.*/, 75 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 76 | /.*/, 77 | /Req\/Sec.*$/, 78 | /$/, 79 | /Bytes\/Sec.*$/, 80 | /.*/, 81 | /$/, 82 | /Req\/Bytes counts sampled once per second.*$/, 83 | /# of samples: 10*$/, 84 | /$/, 85 | /.* requests in ([0-9]|\.)+s, .* read/ 86 | ] 87 | 88 | t.plan(lines.length * 2) 89 | 90 | const child = spawn(process.execPath, [ 91 | path.join(__dirname, '..'), 92 | '-c', '10', 93 | '-d', '1', 94 | '/', 95 | '--', 'node', path.join(__dirname, './targetProcess') 96 | ], { 97 | cwd: __dirname, 98 | env: process.env, 99 | stdio: ['ignore', 'pipe', 'pipe'], 100 | detached: false 101 | }) 102 | 103 | t.teardown(() => { 104 | child.kill() 105 | }) 106 | 107 | child 108 | .stderr 109 | .pipe(split()) 110 | .on('data', (line) => { 111 | const regexp = lines.shift() 112 | t.ok(regexp, 'we are expecting this line') 113 | t.ok(regexp.test(line), 'line matches ' + regexp + `actual: ${line} expected: ${regexp}`) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/parseHAR.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const { parseHAR } = require('../lib/parseHAR') 5 | 6 | test('should throw on empty HAR', (t) => { 7 | t.plan(1) 8 | 9 | t.throws(() => parseHAR(null), /Could not parse HAR content: no entries found/) 10 | t.end() 11 | }) 12 | 13 | test('should throw on HAR with no entries', (t) => { 14 | t.plan(1) 15 | 16 | t.throws(() => parseHAR({ 17 | log: { 18 | version: '1.2', 19 | creator: { 20 | name: 'Firefox', 21 | version: '80.0.1' 22 | }, 23 | pages: [ 24 | { 25 | startedDateTime: '2020-09-28T16:43:28.987+02:00', 26 | id: 'page_1', 27 | title: 'mcollina/autocannon: fast HTTP/1.1 benchmarking tool written in Node.js', 28 | pageTimings: { 29 | onContentLoad: 1234, 30 | onLoad: 1952 31 | } 32 | } 33 | ] 34 | } 35 | }), /Could not parse HAR content: no entries found/) 36 | t.end() 37 | }) 38 | 39 | test('should throw on HAR with empty entries', (t) => { 40 | t.plan(1) 41 | 42 | t.throws(() => parseHAR({ 43 | log: { 44 | version: '1.2', 45 | creator: { 46 | name: 'Firefox', 47 | version: '80.0.1' 48 | }, 49 | pages: [ 50 | { 51 | startedDateTime: '2020-09-28T16:43:28.987+02:00', 52 | id: 'page_1', 53 | title: 'mcollina/autocannon: fast HTTP/1.1 benchmarking tool written in Node.js', 54 | pageTimings: { 55 | onContentLoad: 1234, 56 | onLoad: 1952 57 | } 58 | } 59 | ], 60 | entries: [] 61 | } 62 | }), /Could not parse HAR content: no entries found/) 63 | t.end() 64 | }) 65 | 66 | test('should parse and return GET entries', (t) => { 67 | t.plan(1) 68 | 69 | t.strictSame(parseHAR(require('./fixtures/httpbin-get.json')).get('https://httpbin.org'), [{ 70 | method: 'GET', 71 | origin: 'https://httpbin.org', 72 | path: '/get', 73 | headers: { 74 | Host: 'httpbin.org', 75 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 76 | Accept: '*/*', 77 | 'Accept-Language': 'fr,en;q=0.5', 78 | 'Accept-Encoding': 'gzip, deflate, br', 79 | Referer: 'https://httpbin.org/', 80 | DNT: '1', 81 | Connection: 'keep-alive' 82 | } 83 | }, { 84 | method: 'GET', 85 | origin: 'https://httpbin.org', 86 | path: '/get?from=10&size=20&sort=+name', 87 | headers: { 88 | Host: 'httpbin.org', 89 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 90 | Accept: '*/*', 91 | 'Accept-Language': 'fr,en;q=0.5', 92 | 'Accept-Encoding': 'gzip, deflate, br', 93 | Referer: 'https://httpbin.org/', 94 | DNT: '1', 95 | Connection: 'keep-alive', 96 | TE: 'Trailers' 97 | } 98 | }]) 99 | t.end() 100 | }) 101 | 102 | test('should throw on HAR with invalid entries', (t) => { 103 | t.plan(2) 104 | 105 | t.throws(() => parseHAR({ 106 | log: { 107 | entries: ['invalid'] 108 | } 109 | }), /Could not parse HAR content: invalid request in entry #1/) 110 | t.throws(() => parseHAR({ 111 | log: { 112 | entries: [{ request: { headers: [], url: 'http://localhost' } }, { request: null }] 113 | } 114 | }), /Could not parse HAR content: invalid request in entry #2/) 115 | 116 | t.end() 117 | }) 118 | 119 | test('should throw on HAR with invalid headers', (t) => { 120 | t.plan(2) 121 | const url = 'http://localhost' 122 | 123 | t.throws(() => parseHAR({ 124 | log: { 125 | entries: [{ request: { headers: [], url } }, { request: { headers: ['foo'], url } }] 126 | } 127 | }), /Could not parse HAR content: invalid name or value in header #1 of entry #2/) 128 | t.throws(() => parseHAR({ 129 | log: { 130 | entries: [{ request: { headers: null } }] 131 | } 132 | }), /Could not parse HAR content: invalid headers array in entry #1/) 133 | t.end() 134 | }) 135 | 136 | test('should parse and return POST entries', (t) => { 137 | t.plan(1) 138 | 139 | t.strictSame(parseHAR(require('./fixtures/httpbin-post.json')).get('https://httpbin.org'), [{ 140 | method: 'POST', 141 | origin: 'https://httpbin.org', 142 | path: '/post', 143 | headers: { 144 | Host: 'httpbin.org', 145 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 146 | Accept: '*/*', 147 | 'Accept-Language': 'fr,en;q=0.5', 148 | 'Accept-Encoding': 'gzip, deflate, br', 149 | Referer: 'https://httpbin.org/', 150 | 'Content-Type': 'multipart/form-data; boundary=---------------------------31420230025845772252453285324', 151 | Origin: 'https://httpbin.org', 152 | 'Content-Length': '362', 153 | DNT: '1', 154 | Connection: 'keep-alive', 155 | TE: 'Trailers' 156 | }, 157 | body: '-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name="text"\r\n\r\na text value\r\n-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name="file"; filename="blob"\r\nContent-Type: application/octet-stream\r\n\r\nHello World!\n\r\n-----------------------------31420230025845772252453285324--\r\n' 158 | }, { 159 | method: 'POST', 160 | origin: 'https://httpbin.org', 161 | path: '/post', 162 | headers: { 163 | Host: 'httpbin.org', 164 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 165 | Accept: '*/*', 166 | 'Accept-Language': 'fr,en;q=0.5', 167 | 'Accept-Encoding': 'gzip, deflate, br', 168 | Referer: 'https://httpbin.org/', 169 | 'Content-Type': 'application/x-www-form-urlencoded', 170 | Origin: 'https://httpbin.org', 171 | 'Content-Length': '27', 172 | DNT: '1', 173 | Connection: 'keep-alive', 174 | TE: 'Trailers' 175 | }, 176 | body: 'text=a+text+value&number=10' 177 | }]) 178 | t.end() 179 | }) 180 | 181 | test('should split requests per origin', (t) => { 182 | t.plan(2) 183 | 184 | const requetsPerOrigin = parseHAR(require('./fixtures/multi-domains.json')) 185 | t.strictSame(requetsPerOrigin.get('https://httpbin.org'), [{ 186 | method: 'POST', 187 | origin: 'https://httpbin.org', 188 | path: '/post', 189 | headers: { 190 | Host: 'httpbin.org', 191 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 192 | Accept: '*/*', 193 | 'Accept-Language': 'fr,en;q=0.5', 194 | 'Accept-Encoding': 'gzip, deflate, br', 195 | Referer: 'https://httpbin.org/', 196 | 'Content-Type': 'multipart/form-data; boundary=---------------------------31420230025845772252453285324', 197 | Origin: 'https://httpbin.org', 198 | 'Content-Length': '362', 199 | DNT: '1', 200 | Connection: 'keep-alive', 201 | TE: 'Trailers' 202 | }, 203 | body: '-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name="text"\r\n\r\na text value\r\n-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name="file"; filename="blob"\r\nContent-Type: application/octet-stream\r\n\r\nHello World!\n\r\n-----------------------------31420230025845772252453285324--\r\n' 204 | }, { 205 | method: 'GET', 206 | origin: 'https://httpbin.org', 207 | path: '/get?from=10&size=20&sort=+name', 208 | headers: { 209 | Host: 'httpbin.org', 210 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 211 | Accept: '*/*', 212 | 'Accept-Language': 'fr,en;q=0.5', 213 | 'Accept-Encoding': 'gzip, deflate, br', 214 | Referer: 'https://httpbin.org/', 215 | DNT: '1', 216 | Connection: 'keep-alive', 217 | TE: 'Trailers' 218 | } 219 | }]) 220 | 221 | t.strictSame(requetsPerOrigin.get('https://github.com'), [{ 222 | method: 'POST', 223 | origin: 'https://github.com', 224 | path: '/', 225 | headers: { 226 | Host: 'github.com', 227 | 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0', 228 | Accept: '*/*', 229 | 'Accept-Language': 'fr,en;q=0.5', 230 | 'Accept-Encoding': 'gzip, deflate, br', 231 | Referer: 'https://github.com/', 232 | 'Content-Type': 'application/x-www-form-urlencoded', 233 | Origin: 'https://github.com', 234 | 'Content-Length': '27', 235 | DNT: '1', 236 | Connection: 'keep-alive', 237 | TE: 'Trailers' 238 | }, 239 | body: 'text=a+text+value&number=10' 240 | }]) 241 | t.end() 242 | }) 243 | -------------------------------------------------------------------------------- /test/pipelinedRequestsQueue.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const PipelinedRequestsQueue = require('../lib/pipelinedRequestsQueue') 5 | 6 | test('PipelinedRequestsQueue measures time precisely', (t) => { 7 | t.plan(2) 8 | 9 | const delay = 42 10 | const queue = new PipelinedRequestsQueue() 11 | 12 | const startTime = process.hrtime() 13 | queue.insertRequest() 14 | setTimeout(() => { 15 | const data = queue.terminateRequest() 16 | 17 | t.ok(data.duration > delay, `Calculated duration ${data.duration} should not be less than the induced delay ${delay}`) 18 | 19 | const hrduration = process.hrtime(startTime) 20 | const maxExpectedDuration = Math.ceil(hrduration[0] * 1e3 + hrduration[1] / 1e6) 21 | t.ok(data.duration <= maxExpectedDuration, `Calculated duration ${data.duration} should be less than the max expected duration ${maxExpectedDuration}`) 22 | }, delay) 23 | }) 24 | 25 | test('PipelinedRequestsQueue is a queue/FIFO', (t) => { 26 | const COUNT = 3 27 | t.plan(COUNT) 28 | 29 | const queue = new PipelinedRequestsQueue() 30 | 31 | let count = COUNT 32 | while (count > 0) { 33 | queue.insertRequest(count--) 34 | } 35 | 36 | count = COUNT 37 | while (count > 0) { 38 | t.equal(queue.terminateRequest().req, count--) 39 | } 40 | }) 41 | 42 | test('PipelinedRequestsQueue.clear() empties the queue', (t) => { 43 | t.plan(5) 44 | 45 | const queue = new PipelinedRequestsQueue() 46 | t.equal(queue.size(), 0) 47 | t.equal(queue.toArray().length, 0) 48 | queue.insertRequest() 49 | queue.insertRequest() 50 | queue.insertRequest() 51 | t.equal(queue.size(), 3) 52 | t.equal(queue.toArray().length, 3) 53 | queue.clear() 54 | t.equal(queue.terminateRequest(), undefined) 55 | }) 56 | 57 | test('PipelinedRequestsQueue methods set values to the request in first-in-last-out order', (t) => { 58 | t.plan(6) 59 | 60 | const queue = new PipelinedRequestsQueue() 61 | queue.insertRequest(1) 62 | queue.insertRequest(2) 63 | 64 | queue.addBody('1') 65 | queue.addByteCount(1) 66 | queue.setHeaders({ val: '1' }) 67 | 68 | const req1 = queue.terminateRequest() 69 | t.equal(req1.req, 1) 70 | t.equal(req1.body, '1') 71 | t.same(req1.headers, { val: '1' }) 72 | 73 | queue.addBody('2') 74 | queue.addByteCount(2) 75 | queue.setHeaders({ val: '2' }) 76 | 77 | const req2 = queue.terminateRequest() 78 | t.equal(req2.req, 2) 79 | t.equal(req2.body, '2') 80 | t.same(req2.headers, { val: '2' }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/printResult-process.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const autocannon = require('../autocannon') 4 | const exampleResult = require('./fixtures/example-result.json') 5 | const crossArgv = require('cross-argv') 6 | 7 | let opts = null 8 | 9 | if (process.argv.length > 2) { 10 | const args = crossArgv(process.argv.slice(2)) 11 | opts = autocannon.parseArguments(args) 12 | } 13 | 14 | const resultStr = autocannon.printResult(exampleResult, opts) 15 | process.stderr.write(resultStr) 16 | -------------------------------------------------------------------------------- /test/printResult-renderStatusCodes.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const split = require('split2') 5 | const path = require('path') 6 | const childProcess = require('child_process') 7 | 8 | test('should stdout (print) the result', (t) => { 9 | const lines = [ 10 | /.*/, 11 | /$/, 12 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 13 | /.*/, 14 | /Latency.*$/, 15 | /$/, 16 | /.*/, 17 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 18 | /.*/, 19 | /Req\/Sec.*$/, 20 | /.*/, 21 | /Bytes\/Sec.*$/, 22 | /.*/, 23 | /.*/, 24 | /Code.*Count.*$/, 25 | /.*/, 26 | /200.*500.*$/, 27 | /.*/, 28 | /302.*0.*$/, 29 | /.*/, 30 | /401.*0.*$/, 31 | /.*/, 32 | /403.*0.*$/, 33 | /.*/, 34 | /$/, 35 | /Req\/Bytes counts sampled once per second.*$/, 36 | /# of samples: 10.*$/, 37 | /$/, 38 | /.* requests in ([0-9]|\.)+s, .* read/ 39 | ] 40 | 41 | t.plan(lines.length * 2) 42 | 43 | const child = childProcess.spawn(process.execPath, [path.join(__dirname, 'printResult-process.js'), '--renderStatusCodes', 'http://127.0.0.1'], { 44 | cwd: __dirname, 45 | env: process.env, 46 | stdio: ['ignore', 'pipe', 'pipe'], 47 | detached: false 48 | }) 49 | 50 | t.teardown(() => { 51 | child.kill() 52 | }) 53 | 54 | child 55 | .stderr 56 | .pipe(split()) 57 | .on('data', (line) => { 58 | const regexp = lines.shift() 59 | t.ok(regexp, 'we are expecting this line') 60 | t.ok(regexp.test(line), 'line matches ' + regexp) 61 | }) 62 | .on('end', t.end) 63 | }) 64 | -------------------------------------------------------------------------------- /test/printResult.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const split = require('split2') 5 | const path = require('path') 6 | const childProcess = require('child_process') 7 | const { Writable } = require('stream') 8 | const ansiRegex = require('ansi-regex') 9 | const printResult = require('../lib/printResult') 10 | 11 | test('should stdout (print) the result', (t) => { 12 | const lines = [ 13 | /.*/, 14 | /$/, 15 | /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, 16 | /.*/, 17 | /Latency.*$/, 18 | /$/, 19 | /.*/, 20 | /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, 21 | /.*/, 22 | /Req\/Sec.*$/, 23 | /.*/, 24 | /Bytes\/Sec.*$/, 25 | /.*/, 26 | /$/, 27 | /Req\/Bytes counts sampled once per second.*$/, 28 | /# of samples: 10*$/, 29 | /$/, 30 | /.* requests in ([0-9]|\.)+s, .* read/ 31 | ] 32 | 33 | t.plan(lines.length * 2) 34 | 35 | const child = childProcess.spawn(process.execPath, [path.join(__dirname, 'printResult-process.js')], { 36 | cwd: __dirname, 37 | env: process.env, 38 | stdio: ['ignore', 'pipe', 'pipe'], 39 | detached: false 40 | }) 41 | 42 | t.teardown(() => { 43 | child.kill() 44 | }) 45 | 46 | child 47 | .stderr 48 | .pipe(split()) 49 | .on('data', (line) => { 50 | const regexp = lines.shift() 51 | t.ok(regexp, 'we are expecting this line') 52 | t.ok(regexp.test(line), 'line matches ' + regexp) 53 | }) 54 | .on('end', t.end) 55 | }) 56 | 57 | test('verify amount of total requests', (t) => { 58 | t.plan(1) 59 | 60 | // arrange 61 | const connections = 10 62 | const pipelining = 2 63 | const result = { 64 | connections, 65 | pipelining, 66 | latency: {}, 67 | requests: { 68 | sent: connections * pipelining 69 | }, 70 | throughput: { 71 | average: 3319, 72 | mean: 3319, 73 | stddev: 0, 74 | min: 3318, 75 | max: 3318, 76 | total: 3318, 77 | p0_001: 3319, 78 | p0_01: 3319, 79 | p0_1: 3319, 80 | p1: 3319, 81 | p2_5: 3319, 82 | p10: 3319, 83 | p25: 3319, 84 | p50: 3319, 85 | p75: 3319, 86 | p90: 3319, 87 | p97_5: 3319, 88 | p99: 3319, 89 | p99_9: 3319, 90 | p99_99: 3319, 91 | p99_999: 3319 92 | } 93 | } 94 | 95 | // act 96 | const output = printResult(result, { }) 97 | 98 | // assert 99 | const expectedRequests = connections * pipelining 100 | t.match(output.includes(`${expectedRequests} requests in`), true) 101 | }) 102 | 103 | test('should not print when verbose(V=0) is false', (t) => { 104 | t.plan(1) 105 | 106 | const connections = 10 107 | const pipelining = 2 108 | const result = { 109 | connections, 110 | pipelining, 111 | latency: {}, 112 | requests: { 113 | sent: connections * pipelining 114 | }, 115 | throughput: { 116 | average: 3319, 117 | mean: 3319, 118 | stddev: 0, 119 | min: 3318, 120 | max: 3318, 121 | total: 3318, 122 | p0_001: 3319, 123 | p0_01: 3319, 124 | p0_1: 3319, 125 | p1: 3319, 126 | p2_5: 3319, 127 | p10: 3319, 128 | p25: 3319, 129 | p50: 3319, 130 | p75: 3319, 131 | p90: 3319, 132 | p97_5: 3319, 133 | p99: 3319, 134 | p99_9: 3319, 135 | p99_99: 3319, 136 | p99_999: 3319 137 | } 138 | } 139 | 140 | // act 141 | const output = printResult(result, { verbose: false }) 142 | t.ok(output.split('\n').length === 2) 143 | }) 144 | 145 | test('should print with color when color is supported', (t) => { 146 | t.plan(1) 147 | 148 | const connections = 10 149 | const pipelining = 2 150 | const result = { 151 | connections, 152 | pipelining, 153 | latency: {}, 154 | requests: { 155 | sent: connections * pipelining 156 | }, 157 | throughput: { 158 | average: 3319, 159 | mean: 3319, 160 | stddev: 0, 161 | min: 3318, 162 | max: 3318, 163 | total: 3318, 164 | p0_001: 3319, 165 | p0_01: 3319, 166 | p0_1: 3319, 167 | p1: 3319, 168 | p2_5: 3319, 169 | p10: 3319, 170 | p25: 3319, 171 | p50: 3319, 172 | p75: 3319, 173 | p90: 3319, 174 | p97_5: 3319, 175 | p99: 3319, 176 | p99_9: 3319, 177 | p99_99: 3319, 178 | p99_999: 3319 179 | } 180 | } 181 | 182 | const { FORCE_COLOR, NO_COLOR, COLOR, CI, COLORTERM } = process.env 183 | delete process.env.FORCE_COLOR 184 | delete process.env.NO_COLOR 185 | delete process.env.COLOR 186 | delete process.env.CI 187 | process.env.COLORTERM = 'truecolor' 188 | const outputStream = new Writable({ 189 | write () {} 190 | }) 191 | outputStream.isTTY = true 192 | 193 | // act 194 | const output = printResult(result, { outputStream }) 195 | t.ok(ansiRegex().test(output)) 196 | 197 | // cleanup 198 | process.env.FORCE_COLOR = FORCE_COLOR 199 | process.env.NO_COLOR = NO_COLOR 200 | process.env.COLOR = COLOR 201 | process.env.CI = CI 202 | process.env.COLORTERM = COLORTERM 203 | }) 204 | 205 | test('should not print with any color when color is not supported', (t) => { 206 | t.plan(1) 207 | 208 | const connections = 10 209 | const pipelining = 2 210 | const result = { 211 | connections, 212 | pipelining, 213 | latency: {}, 214 | requests: { 215 | sent: connections * pipelining 216 | }, 217 | throughput: { 218 | average: 3319, 219 | mean: 3319, 220 | stddev: 0, 221 | min: 3318, 222 | max: 3318, 223 | total: 3318, 224 | p0_001: 3319, 225 | p0_01: 3319, 226 | p0_1: 3319, 227 | p1: 3319, 228 | p2_5: 3319, 229 | p10: 3319, 230 | p25: 3319, 231 | p50: 3319, 232 | p75: 3319, 233 | p90: 3319, 234 | p97_5: 3319, 235 | p99: 3319, 236 | p99_9: 3319, 237 | p99_99: 3319, 238 | p99_999: 3319 239 | } 240 | } 241 | 242 | const { FORCE_COLOR, NO_COLOR, COLOR, CI, COLORTERM } = process.env 243 | delete process.env.FORCE_COLOR 244 | delete process.env.NO_COLOR 245 | delete process.env.COLOR 246 | delete process.env.CI 247 | process.env.COLORTERM = 'truecolor' 248 | const outputStream = new Writable({ 249 | write () {} 250 | }) 251 | 252 | // act 253 | const output = printResult(result, { outputStream }) 254 | t.ok(!ansiRegex().test(output)) 255 | 256 | // cleanup 257 | process.env.FORCE_COLOR = FORCE_COLOR 258 | process.env.NO_COLOR = NO_COLOR 259 | process.env.COLOR = COLOR 260 | process.env.CI = CI 261 | process.env.COLORTERM = COLORTERM 262 | }) 263 | -------------------------------------------------------------------------------- /test/progressTracker.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const helper = require('./helper') 4 | const test = require('tap').test 5 | const { defaultMaxListeners } = require('events') 6 | const { Writable } = require('stream') 7 | const sinon = require('sinon') 8 | const autocannon = require('../autocannon') 9 | const { hasWorkerSupport } = require('../lib/util') 10 | 11 | test(`should not emit warnings when using >= ${defaultMaxListeners} workers`, { skip: !hasWorkerSupport }, t => { 12 | const server = helper.startServer() 13 | 14 | const instance = autocannon({ 15 | url: `http://localhost:${server.address().port}`, 16 | workers: defaultMaxListeners, 17 | duration: 1 18 | }) 19 | 20 | setTimeout(() => { 21 | instance.stop() 22 | t.notOk(emitWarningSpy.called) 23 | emitWarningSpy.restore() 24 | t.end() 25 | }, 2000) 26 | 27 | const emitWarningSpy = sinon.spy(process, 'emitWarning') 28 | 29 | autocannon.track(instance, { 30 | outputStream: new Writable({ 31 | write () {} 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/progressTracker.test.stub.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const helper = require('./helper') 4 | const test = require('tap').test 5 | const progressTracker = require('../lib/progressTracker') 6 | const autocannon = require('../autocannon') 7 | 8 | test('progress tracker should throw if no instance is provided', t => { 9 | t.plan(1) 10 | try { 11 | progressTracker(null, {}) 12 | } catch (error) { 13 | t.same(error.message, 'instance required for tracking') 14 | } 15 | }) 16 | 17 | test('should work', t => { 18 | const server = helper.startServer({ statusCode: 404 }) 19 | const instance = autocannon({ 20 | url: `http://localhost:${server.address().port}`, 21 | pipelining: 2 22 | }, console.log) 23 | 24 | setTimeout(() => { 25 | instance.stop() 26 | t.end() 27 | }, 2000) 28 | 29 | autocannon.track(instance, { 30 | renderProgressBar: true, 31 | renderLatencyTable: true 32 | }) 33 | }) 34 | 35 | test('should work with amount', t => { 36 | const server = helper.startServer() 37 | const instance = autocannon({ 38 | url: `http://localhost:${server.address().port}`, 39 | pipelining: 1, 40 | amount: 10 41 | }, process.stdout) 42 | 43 | setTimeout(() => { 44 | instance.stop() 45 | t.end() 46 | }, 2000) 47 | autocannon.track(instance, { 48 | renderProgressBar: true 49 | }) 50 | t.pass() 51 | }) 52 | 53 | test('should log mismatches', t => { 54 | const server = helper.startServer() 55 | const instance = autocannon({ 56 | url: `http://localhost:${server.address().port}`, 57 | pipelining: 1, 58 | amount: 10, 59 | expectBody: 'modified' 60 | }, console.log) 61 | 62 | setTimeout(() => { 63 | instance.stop() 64 | t.end() 65 | }, 2000) 66 | autocannon.track(instance, { 67 | renderProgressBar: true 68 | }) 69 | t.pass() 70 | }) 71 | 72 | test('should log resets', t => { 73 | const server = helper.startServer() 74 | const instance = autocannon({ 75 | url: `http://localhost:${server.address().port}`, 76 | connections: 1, 77 | amount: 10, 78 | requests: [ 79 | { method: 'GET' }, 80 | { 81 | method: 'GET', 82 | // falsey result will reset 83 | setupRequest: () => {} 84 | }, 85 | { method: 'GET' } 86 | ] 87 | }, console.log) 88 | 89 | setTimeout(() => { 90 | instance.stop() 91 | t.end() 92 | }, 2000) 93 | autocannon.track(instance, { 94 | renderProgressBar: true 95 | }) 96 | t.pass() 97 | }) 98 | -------------------------------------------------------------------------------- /test/runAmount.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const initJob = require('../lib/init') 5 | const helper = require('./helper') 6 | const timeoutServer = helper.startTimeoutServer() 7 | const server = helper.startServer() 8 | 9 | test('run should only send the expected number of requests', (t) => { 10 | t.plan(10) 11 | 12 | let done = false 13 | 14 | initJob({ 15 | url: `http://localhost:${server.address().port}`, 16 | duration: 1, 17 | connections: 100, 18 | amount: 50146 19 | }, (err, res) => { 20 | t.error(err) 21 | t.equal(res.requests.total + res.timeouts, 50146, 'results should match the amount') 22 | t.equal(res.requests.sent, 50146, 'totalRequests should match the amount') 23 | done = true 24 | }) 25 | 26 | setTimeout(() => { 27 | t.notOk(done) 28 | }, 1000) 29 | 30 | initJob({ 31 | url: `http://localhost:${server.address().port}`, 32 | connections: 2, 33 | maxConnectionRequests: 10 34 | }, (err, res) => { 35 | t.error(err) 36 | t.equal(res.requests.total, 20, 'results should match max connection requests * connections') 37 | t.equal(res.requests.sent, 20, 'totalRequests should match the expected amount') 38 | }) 39 | 40 | initJob({ 41 | url: `http://localhost:${server.address().port}`, 42 | connections: 2, 43 | maxOverallRequests: 10 44 | }, (err, res) => { 45 | t.error(err) 46 | t.equal(res.requests.total, 10, 'results should match max overall requests') 47 | t.equal(res.requests.sent, 10, 'totalRequests should match the expected amount') 48 | }) 49 | }) 50 | 51 | test('should shutdown after all amounts timeout', (t) => { 52 | t.plan(5) 53 | 54 | initJob({ 55 | url: `http://localhost:${timeoutServer.address().port}`, 56 | amount: 10, 57 | timeout: 2, 58 | connections: 10 59 | }, (err, res) => { 60 | t.error(err) 61 | t.equal(res.errors, 10) 62 | t.equal(res.timeouts, 10) 63 | t.equal(res.requests.sent, 10, 'totalRequests should match the expected amount') 64 | t.equal(res.requests.total, 0, 'total completed requests should be 0') 65 | }) 66 | }) 67 | 68 | test('should reconnect twice to the server with a reset rate of 10 for 20 connections', (t) => { 69 | t.plan(3) 70 | const testServer = helper.startServer() 71 | 72 | initJob({ 73 | url: 'localhost:' + testServer.address().port, 74 | connections: 1, 75 | amount: 20, 76 | reconnectRate: 2 77 | }, (err, res) => { 78 | t.error(err) 79 | t.equal(res.requests.sent, 20, 'totalRequests should match the expected amount') 80 | t.equal(testServer.autocannonConnects, 10, 'should have connected to the server 10 times after dropping the connection every second request') 81 | t.end() 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/runMultiServer.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const initJob = require('../lib/init') 3 | const helper = require('./helper') 4 | 5 | const server1 = helper.startServer({ body: 'from server1' }) 6 | const server2 = helper.startServer({ body: 'from server2' }) 7 | const server3 = helper.startServer({ body: 'from server3' }) 8 | 9 | test('should receive the message from different server', (t) => { 10 | t.plan(3) 11 | 12 | const instance = initJob({ 13 | url: [ 14 | server1, 15 | server2, 16 | server3 17 | ].map(server => `http://localhost:${server.address().port}`), 18 | duration: 1, 19 | connections: 3 20 | }) 21 | 22 | let receivedServer1 = false 23 | let receivedServer2 = false 24 | let receivedServer3 = false 25 | 26 | instance.on('response', (client) => { 27 | if (!receivedServer1 && client.parser.chunk.toString().includes('from server1')) { 28 | receivedServer1 = true 29 | t.pass() 30 | } 31 | if (!receivedServer2 && client.parser.chunk.toString().includes('from server2')) { 32 | receivedServer2 = true 33 | t.pass() 34 | } 35 | if (!receivedServer3 && client.parser.chunk.toString().includes('from server3')) { 36 | receivedServer3 = true 37 | t.pass() 38 | } 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/runMultipart.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const { tmpdir } = require('os') 5 | const { join } = require('path') 6 | const { writeFile } = require('fs') 7 | const { promisify } = require('util') 8 | const initJob = require('../lib/init') 9 | const helper = require('./helper') 10 | const writef = promisify(writeFile) 11 | const { hasWorkerSupport } = require('../lib/util') 12 | 13 | test('run should return an error with invalid form options', async t => { 14 | const cases = [ 15 | { 16 | name: 'invalid JSON', 17 | value: 'u', 18 | message: 'Invalid JSON or file where to get form data' 19 | }, 20 | { 21 | name: 'non existing JSON file', 22 | value: 'nonexisting.json', 23 | message: 'Invalid JSON or file where to get form data' 24 | }, 25 | { 26 | name: 'JSON options missing path key in file type', 27 | value: '{ "image": { "type": "file" }}', 28 | message: 'Missing key \'path\' in form object for key \'image\'' 29 | }, 30 | { 31 | name: 'JS Object missing path key in file type', 32 | value: { image: { type: 'file' } }, 33 | message: 'Missing key \'path\' in form object for key \'image\'' 34 | }, 35 | { 36 | name: 'JSON options missing value in text type', 37 | value: '{ "image": { "type": "text" }}', 38 | message: 'Missing key \'value\' in form object for key \'image\'' 39 | }, 40 | { 41 | name: 'JS Object missing value in text type', 42 | value: { image: { type: 'text' } }, 43 | message: 'Missing key \'value\' in form object for key \'image\'' 44 | }, 45 | { 46 | name: 'JSON options with not supported type', 47 | value: '{ "image": { "type": "random" }}', 48 | message: 'A \'type\' key with value \'text\' or \'file\' should be specified' 49 | }, 50 | { 51 | name: 'JS Object with not supported type', 52 | value: { image: { type: 'random' } }, 53 | message: 'A \'type\' key with value \'text\' or \'file\' should be specified' 54 | } 55 | ] 56 | 57 | const server = helper.startMultipartServer() 58 | t.teardown(() => server.close()) 59 | 60 | for (const c of cases) { 61 | t.test(c.name, async t => { 62 | const [err] = await new Promise((resolve) => { 63 | initJob({ 64 | url: 'http://localhost:' + server.address().port, 65 | connections: 1, 66 | amount: 1, 67 | form: c.value 68 | }, (err, res) => { 69 | resolve([err, res]) 70 | }) 71 | }) 72 | await t.test('error', t => { 73 | t.not(null, err) 74 | t.equal(c.message, err.message, `mismatching error message ${err.message}`) 75 | t.end() 76 | }) 77 | }) 78 | } 79 | }) 80 | 81 | test('run should take form options as a JSON string or a JS Object', async t => { 82 | const form = { 83 | image: { 84 | type: 'file', 85 | path: require.resolve('./j5.jpeg') 86 | }, 87 | name: { 88 | type: 'text', 89 | value: 'j5' 90 | } 91 | } 92 | const string = JSON.stringify(form) 93 | const temp = tmpdir() 94 | const jsonFile = join(temp, 'multipart.json') 95 | 96 | await writef(jsonFile, string, 'utf8') 97 | 98 | const cases = [ 99 | { 100 | name: 'from string', 101 | value: string 102 | }, 103 | { 104 | name: 'from json file', 105 | value: jsonFile 106 | }, 107 | { 108 | name: 'from JS Object', 109 | value: form 110 | } 111 | ] 112 | const allCases = [...cases, ...cases.map(c => ({ ...c, workers: true }))] 113 | 114 | for (const c of allCases) { 115 | t.test(c.name, { skip: c.workers && !hasWorkerSupport }, async t => { 116 | const server = helper.startMultipartServer(null, payload => { 117 | t.equal('j5', payload.name) 118 | t.equal('j5.jpeg', payload.image.filename) 119 | }) 120 | t.teardown(() => server.close()) 121 | const [err, res] = await new Promise((resolve) => { 122 | initJob({ 123 | url: 'http://localhost:' + server.address().port, 124 | connections: 1, 125 | amount: 1, 126 | form: c.value, 127 | workers: c.workers ? 1 : undefined // use only one worker coz we're checking for 1 req 128 | }, (err, res) => { 129 | resolve([err, res]) 130 | }) 131 | }) 132 | await t.test('result', t => { 133 | t.equal(null, err) 134 | t.equal(0, res.errors, 'result should not have errors') 135 | t.equal(1, res['2xx'], 'result status code should be 2xx') 136 | t.equal(0, res.non2xx, 'result status code should be 2xx') 137 | t.end() 138 | }) 139 | }) 140 | } 141 | }) 142 | 143 | test('run should use a custom method if `options.method` is passed', t => { 144 | const server = helper.startMultipartServer(null, payload => { 145 | t.equal('j5', payload.name) 146 | t.equal('j5.jpeg', payload.image.filename) 147 | }) 148 | t.teardown(() => server.close()) 149 | 150 | const form = { 151 | image: { 152 | type: 'file', 153 | path: require.resolve('./j5.jpeg') 154 | }, 155 | name: { 156 | type: 'text', 157 | value: 'j5' 158 | } 159 | } 160 | initJob({ 161 | url: 'http://localhost:' + server.address().port, 162 | method: 'PUT', 163 | connections: 1, 164 | amount: 1, 165 | form 166 | }, (err, res) => { 167 | t.equal(null, err) 168 | t.equal(0, res.errors, 'result should not have errors') 169 | t.equal(1, res['2xx'], 'result status code should be 2xx') 170 | t.equal(0, res.non2xx, 'result status code should be 2xx') 171 | t.end() 172 | }) 173 | }) 174 | 175 | test('run should set filename', t => { 176 | const server = helper.startMultipartServer(null, payload => { 177 | t.equal('j5', payload.name) 178 | t.equal('j5.jpeg', payload.image.filename) 179 | }) 180 | t.teardown(() => server.close()) 181 | 182 | const form = { 183 | image: { 184 | type: 'file', 185 | path: require.resolve('./j5.jpeg') 186 | }, 187 | name: { 188 | type: 'text', 189 | value: 'j5' 190 | } 191 | } 192 | initJob({ 193 | url: 'http://localhost:' + server.address().port, 194 | method: 'POST', 195 | connections: 1, 196 | amount: 1, 197 | form 198 | }, (err, res) => { 199 | t.equal(null, err) 200 | t.equal(0, res.errors, 'result should not have errors') 201 | t.equal(1, res['2xx'], 'result status code should be 2xx') 202 | t.equal(0, res.non2xx, 'result status code should be 2xx') 203 | t.end() 204 | }) 205 | }) 206 | 207 | test('run should allow overriding filename', t => { 208 | const server = helper.startMultipartServer(null, payload => { 209 | t.equal('j5', payload.name) 210 | t.equal('testfilename.jpeg', payload.image.filename) 211 | }) 212 | t.teardown(() => server.close()) 213 | 214 | const form = { 215 | image: { 216 | type: 'file', 217 | path: require.resolve('./j5.jpeg'), 218 | options: { 219 | filename: 'testfilename.jpeg' 220 | } 221 | }, 222 | name: { 223 | type: 'text', 224 | value: 'j5' 225 | } 226 | } 227 | initJob({ 228 | url: 'http://localhost:' + server.address().port, 229 | method: 'POST', 230 | connections: 1, 231 | amount: 1, 232 | form 233 | }, (err, res) => { 234 | t.equal(null, err) 235 | t.equal(0, res.errors, 'result should not have errors') 236 | t.equal(1, res['2xx'], 'result status code should be 2xx') 237 | t.equal(0, res.non2xx, 'result status code should be 2xx') 238 | t.end() 239 | }) 240 | }) 241 | 242 | test('run should allow overriding filename with file path', t => { 243 | const server = helper.startMultipartServer({ preservePath: true }, payload => { 244 | t.equal('j5', payload.name) 245 | t.equal('some/path/testfilename.jpeg', payload.image.filename) 246 | }) 247 | t.teardown(() => server.close()) 248 | 249 | const form = { 250 | image: { 251 | type: 'file', 252 | path: require.resolve('./j5.jpeg'), 253 | options: { 254 | filepath: 'some/path/testfilename.jpeg' 255 | } 256 | }, 257 | name: { 258 | type: 'text', 259 | value: 'j5' 260 | } 261 | } 262 | initJob({ 263 | url: 'http://localhost:' + server.address().port, 264 | method: 'POST', 265 | connections: 1, 266 | amount: 1, 267 | form 268 | }, (err, res) => { 269 | t.equal(null, err) 270 | t.equal(0, res.errors, 'result should not have errors') 271 | t.equal(1, res['2xx'], 'result status code should be 2xx') 272 | t.equal(0, res.non2xx, 'result status code should be 2xx') 273 | t.end() 274 | }) 275 | }) 276 | -------------------------------------------------------------------------------- /test/runRate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const initJob = require('../lib/init') 5 | const helper = require('./helper') 6 | const server = helper.startServer() 7 | 8 | test('run should only send the expected number of requests per second - scenario 1', (t) => { 9 | t.plan(3) 10 | 11 | initJob({ 12 | url: `http://localhost:${server.address().port}`, 13 | connections: 2, 14 | overallRate: 10, 15 | amount: 40, 16 | sampleInt: 1000 17 | }, (err, res) => { 18 | t.error(err) 19 | 20 | t.equal(Math.floor(res.duration), 4, 'should have take 4 seconds to send 10 requests per seconds') 21 | t.equal(res.requests.average, 10, 'should have sent 10 requests per second on average') 22 | }) 23 | }) 24 | 25 | test('run should only send the expected number of requests per second - scenario 2', (t) => { 26 | t.plan(3) 27 | 28 | initJob({ 29 | url: `http://localhost:${server.address().port}`, 30 | connections: 2, 31 | connectionRate: 10, 32 | amount: 40, 33 | sampleInt: 1000 34 | }, (err, res) => { 35 | t.error(err) 36 | t.equal(Math.floor(res.duration), 2, 'should have taken 2 seconds to send 10 requests per connection with 2 connections') 37 | t.equal(res.requests.average, 20, 'should have sent 20 requests per second on average with two connections') 38 | }) 39 | }) 40 | 41 | test('run should only send the expected number of requests per second - scenario 3', (t) => { 42 | t.plan(3) 43 | 44 | initJob({ 45 | url: `http://localhost:${server.address().port}`, 46 | connections: 15, 47 | overallRate: 10, 48 | amount: 40, 49 | sampleInt: 1000 50 | }, (err, res) => { 51 | t.error(err) 52 | t.equal(Math.floor(res.duration), 4, 'should have take 4 seconds to send 10 requests per seconds') 53 | t.equal(res.requests.average, 10, 'should have sent 10 requests per second on average') 54 | }) 55 | }) 56 | 57 | test('run should compensate for coordinated omission when the expected number of requests per second is too high', (t) => { 58 | t.plan(2) 59 | 60 | initJob({ 61 | url: `http://localhost:${server.address().port}`, 62 | connections: 100, 63 | connectionRate: 1000, 64 | duration: 1 65 | }, (err, res) => { 66 | t.error(err) 67 | t.not(res.latency.totalCount, res.requests.total, 'should have recorded additionnal latencies') 68 | }) 69 | }) 70 | 71 | test('run should not compensate for coordinated omission when this feature is disabled', (t) => { 72 | t.plan(2) 73 | 74 | initJob({ 75 | url: `http://localhost:${server.address().port}`, 76 | connections: 100, 77 | connectionRate: 1000, 78 | ignoreCoordinatedOmission: true, 79 | duration: 1 80 | }, (err, res) => { 81 | t.error(err) 82 | t.equal(res.latency.totalCount, res.requests.total, 'should not have recorded additionnal latencies') 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/sampleInt.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const validate = require('../lib/validate') 5 | const parseArguments = require('../autocannon').parseArguments 6 | const initJob = require('../lib/init') 7 | const printResult = require('../lib/printResult') 8 | 9 | test('validate should return an error', (t) => { 10 | t.plan(2) 11 | 12 | const args = { 13 | sampleInt: 'hello', 14 | url: 'https://github.com/mcollina/autocannon' 15 | } 16 | 17 | const result = validate(args) 18 | 19 | t.ok(result instanceof Error) 20 | t.equal(result.message, 'sample interval entered was in an invalid format') 21 | }) 22 | 23 | test('validate should return an error', (t) => { 24 | t.plan(2) 25 | 26 | const args = { 27 | sampleInt: -1, 28 | url: 'https://github.com/mcollina/autocannon' 29 | } 30 | 31 | const result = validate(args) 32 | 33 | t.ok(result instanceof Error) 34 | t.equal(result.message, 'sample interval can not be less than 0') 35 | }) 36 | 37 | test('validate should not return an error', (t) => { 38 | t.plan(2) 39 | 40 | const args = { 41 | sampleInt: 2, 42 | url: 'https://github.com/mcollina/autocannon' 43 | } 44 | 45 | const result = validate(args) 46 | 47 | t.ok(!(result instanceof Error)) 48 | t.equal(result.sampleInt, 2) 49 | }) 50 | 51 | test('parseArguments should accept value in ms (2000)', (t) => { 52 | t.plan(1) 53 | 54 | const args = [ 55 | '-L', 2000, 56 | 'https://github.com/mcollina/autocannon' 57 | ] 58 | 59 | const result = parseArguments(args) 60 | 61 | t.equal(result.sampleInt, 2000) 62 | }) 63 | 64 | test('run should return sampleInt == 2000 & samples == 3', (t) => { 65 | t.plan(2) 66 | 67 | initJob({ 68 | duration: 6, 69 | sampleInt: 2000, 70 | url: 'https://github.com/mcollina/autocannon' 71 | }, (err, res) => { 72 | if (err) { 73 | console.err(err) 74 | } 75 | t.equal(res.sampleInt, 2000) 76 | t.equal(res.samples, 3) 77 | }) 78 | }) 79 | 80 | test('printResult should print the sample interval (2) & the total samples (3)', (t) => { 81 | t.plan(2) 82 | 83 | const result = { 84 | duration: 6, 85 | sampleInt: 2000, 86 | samples: 3, 87 | url: 'https://github.com/mcollina/autocannon', 88 | latency: {}, 89 | requests: {}, 90 | throughput: { 91 | average: 3319, 92 | mean: 3319, 93 | stddev: 0, 94 | min: 3318, 95 | max: 3318, 96 | total: 3318, 97 | p0_001: 3319, 98 | p0_01: 3319, 99 | p0_1: 3319, 100 | p1: 3319, 101 | p2_5: 3319, 102 | p10: 3319, 103 | p25: 3319, 104 | p50: 3319, 105 | p75: 3319, 106 | p90: 3319, 107 | p97_5: 3319, 108 | p99: 3319, 109 | p99_9: 3319, 110 | p99_99: 3319, 111 | p99_999: 3319 112 | } 113 | } 114 | 115 | const output = printResult(result, {}) 116 | 117 | t.ok(output.includes('Req/Bytes counts sampled every 2 seconds.')) 118 | t.ok(output.includes('# of samples: 3')) 119 | }) 120 | -------------------------------------------------------------------------------- /test/serial/autocannon.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const childProcess = require('child_process') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const test = require('tap').test 7 | 8 | const { promisify } = require('util') 9 | 10 | const exec = promisify(childProcess.exec).bind(childProcess) 11 | const proxyquire = require('proxyquire') 12 | 13 | const baseDir = path.join(__dirname, '..', '..') 14 | 15 | test('should print error if url.URL is not a function', t => { 16 | t.plan(2) 17 | 18 | const _error = console.error 19 | const _exit = process.exit 20 | 21 | process.exit = (code) => { 22 | t.equal(code, 1) 23 | process.exit = _exit 24 | t.end() 25 | } 26 | console.error = (obj) => { 27 | t.equal( 28 | obj, 29 | 'autocannon requires the WHATWG URL API, but it is not available. Please upgrade to Node 6.13+.' 30 | ) 31 | console.error = _error 32 | } 33 | proxyquire('../..', { 34 | url: { 35 | URL: null 36 | } 37 | }) 38 | }) 39 | 40 | test('should print version if invoked with --version', async t => { 41 | t.plan(1) 42 | const res = await exec(`node ${baseDir}/autocannon.js --version`) 43 | t.ok(res.stdout.match(/autocannon v(\d+\.\d+\.\d+)/)) 44 | }) 45 | 46 | test('should print help if invoked with --help', async t => { 47 | t.plan(1) 48 | const help = fs.readFileSync(path.join(baseDir, 'help.txt'), 'utf8') 49 | const res = await exec(`node ${baseDir}/autocannon.js --help`) 50 | t.same(res.stderr.trim(), help.trim()) // console.error adds \n at the end of print 51 | }) 52 | 53 | test('should print help if no url is provided', async t => { 54 | t.plan(1) 55 | const help = fs.readFileSync(path.join(baseDir, 'help.txt'), 'utf8') 56 | const res = await exec(`node ${baseDir}/autocannon.js`) 57 | t.same(res.stderr.trim(), help.trim()) // console.error adds \n at the end of print 58 | }) 59 | 60 | test('start should console an error when a promise is caught', (t) => { 61 | const Autocannon = t.mock('../../autocannon', { 62 | '../../lib/init': () => new Promise((resolve, reject) => { 63 | reject(new Error('Test Error')) 64 | }) 65 | }) 66 | 67 | t.plan(1) 68 | 69 | const _error = console.error 70 | console.error = (obj) => { 71 | t.equal( 72 | obj, 73 | 'Test Error' 74 | ) 75 | console.error = _error 76 | } 77 | 78 | Autocannon.start( 79 | Autocannon.parseArguments([ 80 | '-d', '1', 81 | '-c', '1', 82 | 'http://localhost/foo/bar' 83 | ]) 84 | ) 85 | }) 86 | 87 | test('start should console an error when one is thrown without a promise', (t) => { 88 | const Autocannon = t.mock('../../autocannon', { 89 | '../../lib/init': () => { throw new Error('Test Error') } 90 | }) 91 | 92 | t.plan(1) 93 | 94 | const _error = console.error 95 | console.error = (obj) => { 96 | t.equal( 97 | obj, 98 | 'Test Error' 99 | ) 100 | console.error = _error 101 | } 102 | 103 | Autocannon.start( 104 | Autocannon.parseArguments([ 105 | '-d', '1', 106 | '-c', '1', 107 | '--forever', 108 | 'http://localhost/foo/bar' 109 | ]) 110 | ) 111 | }) 112 | 113 | test('start should console an error when --on-port is used without async hooks', (t) => { 114 | const Autocannon = t.mock('../../autocannon', { 115 | 'has-async-hooks': () => false, 116 | child_process: { 117 | spawn: () => {} 118 | }, 119 | '../../lib/init': () => {}, 120 | net: { 121 | createServer: () => ({ 122 | listen: () => {}, 123 | on: () => {} 124 | }) 125 | } 126 | }) 127 | 128 | t.plan(2) 129 | 130 | const _exit = process.exit 131 | process.exit = (code) => { 132 | t.equal(code, 1) 133 | process.exit = _exit 134 | t.end() 135 | } 136 | const _error = console.error 137 | console.error = (obj) => { 138 | t.equal( 139 | obj, 140 | 'The --on-port flag requires the async_hooks builtin module, but it is not available. Please upgrade to Node 8.1+.' 141 | ) 142 | console.error = _error 143 | } 144 | 145 | Autocannon.start( 146 | Autocannon.parseArguments([ 147 | '-d', '1', 148 | '-c', '1', 149 | '--on-port', 150 | 'http://localhost/foo/bar' 151 | ]) 152 | ) 153 | }) 154 | 155 | test('When there is a port, createChannel should try to unlink the socketPath', (t) => { 156 | const Autocannon = t.mock('../../autocannon', { 157 | 'has-async-hooks': () => true, 158 | child_process: { 159 | spawn: () => {} 160 | }, 161 | '../../lib/init': () => {}, 162 | net: { 163 | createServer: () => ({ 164 | listen: () => {}, 165 | on: (eventName, cb) => { 166 | t.equal(eventName, 'close') 167 | cb() 168 | } 169 | }) 170 | } 171 | }) 172 | 173 | t.plan(1) 174 | 175 | Autocannon.start( 176 | Autocannon.parseArguments([ 177 | '-d', '1', 178 | '-c', '1', 179 | '--on-port', 180 | 'http://localhost/foo/bar' 181 | ]) 182 | ) 183 | }) 184 | 185 | test('createChannel should try to unlink the socketPath', (t) => { 186 | const Autocannon = t.mock('../../autocannon', { 187 | 'has-async-hooks': () => true, 188 | child_process: { 189 | spawn: () => {} 190 | }, 191 | '../../lib/init': () => {}, 192 | net: { 193 | createServer: () => ({ 194 | listen: () => {}, 195 | on: (_, cb) => { 196 | t.equal(_, 'close') 197 | cb() 198 | } 199 | }) 200 | } 201 | }) 202 | 203 | t.plan(1) 204 | 205 | Autocannon.start( 206 | Autocannon.parseArguments([ 207 | '-d', '1', 208 | '-c', '1', 209 | '--on-port', 210 | 'http://localhost/foo/bar' 211 | ]) 212 | ) 213 | }) 214 | -------------------------------------------------------------------------------- /test/serial/run.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const initJob = require('../../lib/init') 4 | 5 | test('should log error on connection error', t => { 6 | t.plan(1) 7 | console.error = function (obj) { 8 | t.type(obj, Error) 9 | console.error = () => {} 10 | } 11 | initJob({ 12 | url: 'http://unknownhost', 13 | connections: 2, 14 | duration: 5, 15 | title: 'title321', 16 | debug: true 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/serial/tap-parallel-not-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/test/serial/tap-parallel-not-ok -------------------------------------------------------------------------------- /test/serial/wasm.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const initJob = require('../../lib/init') 5 | const helper = require('../helper') 6 | 7 | test('should clean up HdrHistogram WASM memory at each run', async (t) => { 8 | const server = helper.startServer() 9 | const runTwentyTimes = (resolve, reject, numberOfRuns = 0) => { 10 | initJob({ 11 | url: 'http://localhost:' + server.address().port, 12 | connections: 1, 13 | amount: 1 14 | }, (result) => { 15 | // should get error " url or socketPath option required" 16 | // we can ignore this error, we just want run() to execute 17 | // and to instantiate new WASM histograms 18 | if (numberOfRuns < 20) { 19 | runTwentyTimes(resolve, reject, ++numberOfRuns) 20 | } else { 21 | resolve() 22 | } 23 | }) 24 | } 25 | const lotsOfRuns = [] 26 | for (let index = 0; index < 50; index++) { 27 | lotsOfRuns.push(new Promise(runTwentyTimes)) 28 | } 29 | 30 | await Promise.all(lotsOfRuns) 31 | 32 | // if the process has not crashed, we are good \o/ 33 | t.end() 34 | }) 35 | -------------------------------------------------------------------------------- /test/subargAliases.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const generateSubArgAliases = require('../lib/subargAliases') 5 | 6 | test('generateSubArgAliases Should generate warmup aliases', (t) => { 7 | t.plan(4) 8 | 9 | const args = { 10 | connections: 1, 11 | duration: 2, 12 | warmup: { 13 | c: 3, 14 | d: 4 15 | } 16 | } 17 | 18 | const result = generateSubArgAliases(args) 19 | 20 | t.equal(result.connections, 1) 21 | t.equal(result.duration, 2) 22 | t.equal(result.warmup.connections, 3) 23 | t.equal(result.warmup.duration, 4) 24 | }) 25 | 26 | test('generateSubArgAliases should not process aliases that are not defined in subargAliases.js', (t) => { 27 | t.plan(5) 28 | 29 | const args = { 30 | connections: 1, 31 | warmup: { 32 | c: 3, 33 | T: 'A title' 34 | } 35 | } 36 | 37 | const result = generateSubArgAliases(args) 38 | 39 | t.equal(result.connections, 1) 40 | t.equal(result.warmup.connections, 3) 41 | t.equal(result.warmup.c, 3) 42 | t.equal(result.warmup.T, 'A title') 43 | t.equal(Object.keys(result.warmup).length, 3) 44 | }) 45 | -------------------------------------------------------------------------------- /test/tap-parallel-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcollina/autocannon/225e3a30a6adb8111cd8cf60759bd601936764be/test/tap-parallel-ok -------------------------------------------------------------------------------- /test/targetProcess.js: -------------------------------------------------------------------------------- 1 | const server = require('./helper').startServer() 2 | 3 | server.ref() 4 | -------------------------------------------------------------------------------- /test/url.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | // const { ofUrl, checkUrl } = require('../lib/url') 5 | const { ofURL, checkURL } = require('../lib/url') 6 | 7 | test('checkURL should return true if a populated string is passed', (t) => { 8 | t.plan(1) 9 | 10 | const result = checkURL('foo') 11 | t.ok(result) 12 | }) 13 | 14 | test('checkURL should return false if an empty string is passed', (t) => { 15 | t.plan(1) 16 | 17 | const result = checkURL('') 18 | t.notOk(result) 19 | }) 20 | 21 | test('checkURL should return true if a populated array is passed', (t) => { 22 | t.plan(1) 23 | 24 | const result = checkURL(['foo']) 25 | t.ok(result) 26 | }) 27 | 28 | test('checkURL should return false if an empty array is passed', (t) => { 29 | t.plan(1) 30 | 31 | const result = checkURL([]) 32 | t.notOk(result) 33 | }) 34 | 35 | test('ofUrl should return the array if the passed in url is an array', (t) => { 36 | t.plan(1) 37 | 38 | const result = ofURL(['foo', 'bar']) 39 | t.same(result, ['foo', 'bar']) 40 | }) 41 | 42 | test('When ofUrl is passed a string ofUrl should return an object containing a map function that accepts an url', (t) => { 43 | t.plan(1) 44 | 45 | const result = ofURL('foo', false) 46 | const mappedResult = result.map((url) => url) 47 | t.same(mappedResult, 'foo') 48 | }) 49 | 50 | test('When ofUrl is passed a string and asArray=true ofUrl should return an object containing a map function that returns an array', (t) => { 51 | t.plan(1) 52 | 53 | const result = ofURL('foo', true) 54 | const mappedResult = result.map((url) => url) 55 | t.same(mappedResult, ['foo']) 56 | }) 57 | 58 | test('ofUrl Should throw an error when passed an invalid url type', (t) => { 59 | t.plan(1) 60 | 61 | t.throws(() => ofURL(123), 'url should only be a string or an array of string') 62 | }) 63 | -------------------------------------------------------------------------------- /test/utils/has-worker-support.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | 5 | module.exports = semver.gte(process.versions.node, '11.7.0') 6 | -------------------------------------------------------------------------------- /test/utils/on-response.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (status, body, context) => { 4 | context.foo = 'bar=baz' 5 | } 6 | -------------------------------------------------------------------------------- /test/utils/setup-client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (client) => { 4 | client.setHeaders({ custom: 'my-header' }) 5 | } 6 | -------------------------------------------------------------------------------- /test/utils/setup-request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (req, context) => ({ 4 | ...req, 5 | path: `/test-123?some=thing&${context.foo}` 6 | }) 7 | -------------------------------------------------------------------------------- /test/utils/verify-body.js: -------------------------------------------------------------------------------- 1 | module.exports = (body) => { 2 | return false 3 | } 4 | -------------------------------------------------------------------------------- /test/validate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const validateOpts = require('../lib/validate') 5 | const helper = require('./helper') 6 | const { hasWorkerSupport } = require('../lib/util') 7 | 8 | test('validateOpts should not return an error with only an url passed in', (t) => { 9 | t.plan(1) 10 | 11 | const result = validateOpts({ url: 'http://localhost' }) 12 | t.ok(!(result instanceof Error)) 13 | }) 14 | 15 | test('validateOpts should return an error when workers option is present and hasWorkerSupport is false', (t) => { 16 | const validateOpts = t.mock('../lib/validate', { 17 | '../lib/util': { hasWorkerSupport: false } 18 | }) 19 | t.plan(2) 20 | 21 | const result = validateOpts({ workers: 1 }) 22 | t.ok(result instanceof Error) 23 | t.equal(result.message, 'Please use node >= 11.7.0 for workers support') 24 | }) 25 | 26 | test('validateOpts should return an error when bailout is less than 1', (t) => { 27 | const validateOpts = t.mock('../lib/validate', { 28 | '../lib/util': { hasWorkerSupport: false } 29 | }) 30 | t.plan(2) 31 | 32 | const result = validateOpts({ bailout: 0 }) 33 | t.ok(result instanceof Error) 34 | t.equal(result.message, 'bailout threshold can not be less than 1') 35 | }) 36 | 37 | test('validateOpts should return an error when connectionRate is less than 1', (t) => { 38 | t.plan(2) 39 | 40 | const result = validateOpts({ connectionRate: 0 }) 41 | t.ok(result instanceof Error) 42 | t.equal(result.message, 'connectionRate can not be less than 1') 43 | }) 44 | 45 | test('validateOpts should return an error when overallRate is less than 1', (t) => { 46 | t.plan(2) 47 | 48 | const result = validateOpts({ overallRate: 0 }) 49 | t.ok(result instanceof Error) 50 | t.equal(result.message, 'bailout overallRate can not be less than 1') 51 | }) 52 | 53 | test('validateOpts should return an error when amount is less than 1', (t) => { 54 | t.plan(2) 55 | 56 | const result = validateOpts({ amount: 0 }) 57 | t.ok(result instanceof Error) 58 | t.equal(result.message, 'amount can not be less than 1') 59 | }) 60 | 61 | test('validateOpts should return an error when maxConnectionRequests is less than 1', (t) => { 62 | t.plan(2) 63 | 64 | const result = validateOpts({ url: 'http://localhost', maxConnectionRequests: 0 }) 65 | t.ok(result instanceof Error) 66 | t.equal(result.message, 'maxConnectionRequests can not be less than 1') 67 | }) 68 | 69 | test('validateOpts should return an error when maxOverallRequests is less than 1', (t) => { 70 | t.plan(2) 71 | 72 | const result = validateOpts({ maxOverallRequests: 0 }) 73 | t.ok(result instanceof Error) 74 | t.equal(result.message, 'maxOverallRequests can not be less than 1') 75 | }) 76 | 77 | test('validateOpts should return an error when requests does not contain a valid setupRequest function', (t) => { 78 | t.plan(2) 79 | 80 | const result = validateOpts({ url: 'http://localhost', requests: [{ setupRequest: 123 }] }) 81 | t.ok(result instanceof Error) 82 | t.equal(result.message, 'Invalid option setupRequest, please provide a function (or file path when in workers mode)') 83 | }) 84 | 85 | test('validateOpts should return an error when requests does not contain a valid onResponse function', (t) => { 86 | t.plan(2) 87 | 88 | const result = validateOpts({ url: 'http://localhost', requests: [{ onResponse: 123 }] }) 89 | t.ok(result instanceof Error) 90 | t.equal(result.message, 'Invalid option onResponse, please provide a function (or file path when in workers mode)') 91 | }) 92 | 93 | test('validateOpts should return an error when setupClient is not a valid function', (t) => { 94 | t.plan(2) 95 | 96 | const result = validateOpts({ url: 'http://localhost', setupClient: 123 }) 97 | t.ok(result instanceof Error) 98 | t.equal(result.message, 'Invalid option setupClient, please provide a function (or file path when in workers mode)') 99 | }) 100 | 101 | test('validateOpts should return an error if neither url or socket path are provided', (t) => { 102 | t.plan(2) 103 | 104 | const result = validateOpts({ }) 105 | t.ok(result instanceof Error) 106 | t.equal(result.message, 'url or socketPath option required') 107 | }) 108 | 109 | test('validateOpts should convert a duration that is a string representation of a number into a number', (t) => { 110 | t.plan(1) 111 | 112 | const result = validateOpts({ url: 'http://localhost', duration: '100' }) 113 | t.equal(result.duration, 100) 114 | }) 115 | 116 | test('validateOpts should convert a duration that is a timestring into a number', (t) => { 117 | t.plan(1) 118 | 119 | const result = validateOpts({ url: 'http://localhost', duration: '2 weeks' }) 120 | t.equal(result.duration, 1209600) 121 | }) 122 | 123 | test('validateOpts should return an error if duration is in an invalid format', (t) => { 124 | t.plan(1) 125 | 126 | const result = validateOpts({ url: 'http://localhost', duration: '2 dsweeks' }) 127 | t.ok(result instanceof Error) 128 | }) 129 | 130 | test('validateOpts should return an error if duration less than 0', (t) => { 131 | t.plan(2) 132 | 133 | const result = validateOpts({ url: 'http://localhost', duration: -1 }) 134 | t.ok(result instanceof Error) 135 | t.equal(result.message, 'duration can not be less than 0') 136 | }) 137 | 138 | test('validateOpts should return an error if expectBody is used in conjunction with requests', (t) => { 139 | t.plan(2) 140 | 141 | const result = validateOpts({ url: 'http://localhost', expectBody: 'foo', requests: [] }) 142 | t.ok(result instanceof Error) 143 | t.equal(result.message, 'expectBody cannot be used in conjunction with requests') 144 | }) 145 | 146 | test('validateOpts should parse a multipart form correctly', (t) => { 147 | t.plan(1) 148 | 149 | const result = validateOpts({ url: 'http://localhost', form: '{ "field 1": { "type": "text", "value": "a text value"} }' }) 150 | t.ok(result.form) 151 | }) 152 | 153 | test('validateOpts should return an error if a multipart form is incorrectly formatted', (t) => { 154 | t.plan(1) 155 | 156 | const result = validateOpts({ url: 'http://localhost', form: 'invalid form' }) 157 | t.ok(result instanceof Error) 158 | }) 159 | 160 | test('validateOpts should parse a HAR request successfully', (t) => { 161 | t.plan(1) 162 | 163 | const har = helper.customizeHAR('./fixtures/httpbin-get.json', 'https://httpbin.org', 'http://localhost') 164 | const result = validateOpts({ url: 'http://localhost', har }) 165 | t.ok(result.har) 166 | }) 167 | 168 | test('validateOpts should return an error if a HAR request is unsuccessful', (t) => { 169 | t.plan(1) 170 | 171 | const result = validateOpts({ url: 'http://localhost', har: 'invalid har' }) 172 | t.ok(result instanceof Error) 173 | }) 174 | 175 | test('validateOpts should return an error when connections is less than 1', (t) => { 176 | t.plan(2) 177 | 178 | const result = validateOpts({ url: 'http://localhost', connections: 0 }) 179 | t.ok(result instanceof Error) 180 | t.equal(result.message, 'connections can not be less than 1') 181 | }) 182 | 183 | test('validateOpts should return an error when ignoreCoordinatedOmission used without connectionRate or overallRate', (t) => { 184 | t.plan(2) 185 | 186 | const result = validateOpts({ 187 | url: 'http://localhost', 188 | ignoreCoordinatedOmission: true 189 | }) 190 | t.ok(result instanceof Error) 191 | t.equal(result.message, 'ignoreCoordinatedOmission makes no sense without connectionRate or overallRate') 192 | }) 193 | 194 | test('validateOpts is successful when ignoreCoordinatedOmission is used with connectionRate', (t) => { 195 | t.plan(1) 196 | 197 | const result = validateOpts({ 198 | url: 'http://localhost', 199 | ignoreCoordinatedOmission: true, 200 | connectionRate: 1 201 | }) 202 | t.ok(result.ignoreCoordinatedOmission) 203 | }) 204 | 205 | test('validateOpts is successful when ignoreCoordinatedOmission is used with overallRate', (t) => { 206 | t.plan(1) 207 | 208 | const result = validateOpts({ 209 | url: 'http://localhost', 210 | ignoreCoordinatedOmission: true, 211 | overallRate: 1 212 | }) 213 | t.ok(result.ignoreCoordinatedOmission) 214 | }) 215 | 216 | test('validateOpts should return an error when forever is used with cbPassedIn', (t) => { 217 | t.plan(2) 218 | 219 | const result = validateOpts({ url: 'http://localhost', forever: true }, () => {}) 220 | t.ok(result instanceof Error) 221 | t.equal(result.message, 'should not use the callback parameter when the `forever` option is set to true. Use the `done` event on this event emitter') 222 | }) 223 | 224 | test('validateOpts should return an error when forever is used with workers', { skip: !hasWorkerSupport }, (t) => { 225 | t.plan(2) 226 | 227 | const result = validateOpts({ url: 'http://localhost', forever: true, workers: 2 }) 228 | t.ok(result instanceof Error) 229 | t.equal(result.message, 'Using `forever` option isn\'t currently supported with workers') 230 | }) 231 | 232 | test('validateOpts should not set render options by default', (t) => { 233 | t.plan(3) 234 | 235 | const result = validateOpts({ url: 'http://localhost' }) 236 | t.equal(result.renderProgressBar, undefined) 237 | t.equal(result.renderResultsTable, undefined) 238 | t.equal(result.renderLatencyTable, undefined) 239 | }) 240 | 241 | test('validateOpts should disable render options when json is true', (t) => { 242 | t.plan(3) 243 | 244 | const result = validateOpts({ url: 'http://localhost', json: true }) 245 | t.equal(result.renderProgressBar, false) 246 | t.equal(result.renderResultsTable, false) 247 | t.equal(result.renderLatencyTable, false) 248 | }) 249 | -------------------------------------------------------------------------------- /test/workers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | const test = require('tap').test 6 | const http = require('http') 7 | const initJob = require('../lib/init') 8 | const { hasWorkerSupport } = require('../lib/util') 9 | const helper = require('./helper') 10 | const httpsServer = helper.startHttpsServer() 11 | 12 | test('returns error when no worker support was found', (t) => { 13 | const server = helper.startServer() 14 | initJob({ 15 | url: 'http://localhost:' + server.address().port, 16 | connections: 3, 17 | workers: 3, 18 | amount: 6, 19 | title: 'with-workers' 20 | }, function (err, result) { 21 | if (hasWorkerSupport) { 22 | t.error(err) 23 | } else { 24 | t.equal(err.message, 'Please use node >= 11.7.0 for workers support') 25 | } 26 | 27 | t.end() 28 | }) 29 | }) 30 | 31 | test('init with workers', { skip: !hasWorkerSupport }, (t) => { 32 | const server = helper.startServer() 33 | initJob({ 34 | url: 'http://localhost:' + server.address().port, 35 | connections: 3, 36 | workers: 3, 37 | amount: 6, 38 | title: 'with-workers' 39 | }, function (err, result) { 40 | t.error(err) 41 | 42 | t.ok(result.workers === 3, 'correct worker count') 43 | t.equal(result.title, 'with-workers', 'title should be what was passed in') 44 | t.equal(result.connections, 3, 'connections is the same') 45 | t.equal(result.pipelining, 1, 'pipelining is the default') 46 | 47 | t.ok(result.latency, 'latency exists') 48 | t.type(result.latency.average, 'number', 'latency.average exists') 49 | t.type(result.latency.stddev, 'number', 'latency.stddev exists') 50 | t.ok(result.latency.min >= 0, 'latency.min exists') 51 | t.type(result.latency.max, 'number', 'latency.max exists') 52 | t.type(result.latency.p2_5, 'number', 'latency.p2_5 (2.5%) exists') 53 | t.type(result.latency.p50, 'number', 'latency.p50 (50%) exists') 54 | t.type(result.latency.p97_5, 'number', 'latency.p97_5 (97.5%) exists') 55 | t.type(result.latency.p99, 'number', 'latency.p99 (99%) exists') 56 | 57 | t.ok(result.requests, 'requests exists') 58 | t.type(result.requests.average, 'number', 'requests.average exists') 59 | t.type(result.requests.stddev, 'number', 'requests.stddev exists') 60 | t.type(result.requests.min, 'number', 'requests.min exists') 61 | t.type(result.requests.max, 'number', 'requests.max exists') 62 | t.ok(result.requests.total === 6, 'requests.total exists') 63 | t.type(result.requests.sent, 'number', 'sent exists') 64 | t.ok(result.requests.sent >= result.requests.total, 'total requests made should be more than or equal to completed requests total') 65 | t.type(result.requests.p1, 'number', 'requests.p1 (1%) exists') 66 | t.type(result.requests.p2_5, 'number', 'requests.p2_5 (2.5%) exists') 67 | t.type(result.requests.p50, 'number', 'requests.p50 (50%) exists') 68 | t.type(result.requests.p97_5, 'number', 'requests.p97_5 (97.5%) exists') 69 | 70 | t.ok(result.throughput, 'throughput exists') 71 | t.type(result.throughput.average, 'number', 'throughput.average exists') 72 | t.type(result.throughput.stddev, 'number', 'throughput.stddev exists') 73 | t.type(result.throughput.min, 'number', 'throughput.min exists') 74 | t.type(result.throughput.max, 'number', 'throughput.max exists') 75 | t.type(result.throughput.total, 'number', 'throughput.total exists') 76 | t.type(result.throughput.p1, 'number', 'throughput.p1 (1%) exists') 77 | t.type(result.throughput.p2_5, 'number', 'throughput.p2_5 (2.5%) exists') 78 | t.type(result.throughput.p50, 'number', 'throughput.p50 (50%) exists') 79 | t.type(result.throughput.p97_5, 'number', 'throughput.p97_5 (97.5%) exists') 80 | 81 | t.ok(result.start, 'start time exists') 82 | t.ok(result.finish, 'finish time exists') 83 | 84 | t.equal(result.errors, 0, 'no errors') 85 | t.equal(result.mismatches, 0, 'no mismatches') 86 | t.equal(result.resets, 0, 'no resets') 87 | 88 | t.equal(result['1xx'], 0, '1xx codes') 89 | t.equal(result['2xx'], result.requests.total, '2xx codes') 90 | t.equal(result['3xx'], 0, '3xx codes') 91 | t.equal(result['4xx'], 0, '4xx codes') 92 | t.equal(result['5xx'], 0, '5xx codes') 93 | t.equal(result.non2xx, 0, 'non 2xx codes') 94 | 95 | t.end() 96 | }) 97 | }) 98 | 99 | test('setupRequest and onResponse work with workers', { skip: !hasWorkerSupport }, (t) => { 100 | const server = http.createServer((req, res) => { 101 | // it's not easy to assert things within setupRequest and onResponse 102 | // when in workers mode. So, we set something in onResponse and use in the 103 | // next Request and make sure it exist or we return 404. 104 | if (req.method === 'GET' && req.url !== '/test-123?some=thing&bar=baz') { 105 | res.statusCode = 404 106 | res.end('NOT OK') 107 | return 108 | } 109 | 110 | res.end('OK') 111 | }) 112 | server.listen(0) 113 | server.unref() 114 | 115 | initJob({ 116 | url: 'http://localhost:' + server.address().port, 117 | connections: 2, 118 | amount: 4, 119 | workers: 1, 120 | requests: [ 121 | { 122 | method: 'PUT', 123 | onResponse: path.join(__dirname, './utils/on-response') 124 | }, 125 | { 126 | method: 'GET', 127 | setupRequest: path.join(__dirname, './utils/setup-request') 128 | } 129 | ] 130 | }, function (err, result) { 131 | t.error(err) 132 | 133 | t.equal(4, result['2xx'], 'should have 4 ok requests') 134 | t.equal(0, result['4xx'], 'should not have any 404s') 135 | t.end() 136 | }) 137 | }) 138 | 139 | test('verifyBody work with workers', { skip: !hasWorkerSupport }, (t) => { 140 | const server = http.createServer((req, res) => { 141 | // it's not easy to assert things within setupRequest and onResponse 142 | // when in workers mode. So, we set something in onResponse and use in the 143 | // next Request and make sure it exist or we return 404. 144 | if (req.method === 'GET' && req.url !== '/test-123?some=thing&bar=baz') { 145 | res.statusCode = 404 146 | res.end('NOT OK') 147 | return 148 | } 149 | 150 | res.end('OK') 151 | }) 152 | server.listen(0) 153 | server.unref() 154 | 155 | initJob({ 156 | url: 'http://localhost:' + server.address().port, 157 | connections: 2, 158 | amount: 4, 159 | workers: 1, 160 | verifyBody: path.join(__dirname, './utils/verify-body') 161 | }, function (err, result) { 162 | t.error(err) 163 | 164 | t.equal(4, result.mismatches, 'should have 4 mismatches requests') 165 | t.end() 166 | }) 167 | }) 168 | 169 | test('setupClient works with workers', { skip: !hasWorkerSupport }, (t) => { 170 | const server = http.createServer((req, res) => { 171 | if (req.headers.custom !== 'my-header') { 172 | res.statusCode = 404 173 | res.end('NOT OK') 174 | return 175 | } 176 | res.end('OK') 177 | }) 178 | server.listen(0) 179 | server.unref() 180 | 181 | initJob({ 182 | url: 'http://localhost:' + server.address().port, 183 | connections: 2, 184 | amount: 2, 185 | workers: 1, 186 | setupClient: path.join(__dirname, './utils/setup-client') 187 | }, function (err, result) { 188 | t.error(err) 189 | 190 | t.equal(2, result['2xx'], 'should have 2 ok requests') 191 | t.equal(0, result['4xx'], 'should not have any 404s') 192 | t.end() 193 | }) 194 | }) 195 | 196 | test('tlsOptions using pfx work as intended in workers', { skip: !hasWorkerSupport }, (t) => { 197 | initJob({ 198 | url: 'https://localhost:' + httpsServer.address().port, 199 | connections: 1, 200 | amount: 1, 201 | workers: 2, 202 | tlsOptions: { 203 | pfx: fs.readFileSync(path.join(__dirname, '/keystore.pkcs12')), 204 | passphrase: 'test' 205 | } 206 | }, function (err, result) { 207 | t.error(err) 208 | t.ok(result, 'requests are ok') 209 | t.end() 210 | }) 211 | }) 212 | --------------------------------------------------------------------------------