diff --git a/README.md b/README.md index 6b2a9826..91fee0a8 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Available options: Don't render the progress bar. default: false. -l/--latency Print all the latency data. default: false. + -I/--idReplacement + Enable replacement of [] with a randomly generated ID within the request body. default: false. -j/--json Print the output as newline delimited json. This will cause the progress bar and results not to be rendered. default: false. -f/--forever @@ -131,7 +133,7 @@ Start autocannon against the given target. * `bailout`: The threshold of the number of errors when making the requests to the server before this instance bail's out. This instance will take all existing results so far and aggregate them into the results. If none passed here, the instance will ignore errors and never bail out. _OPTIONAL_ default: `undefined`. * `method`: The http method to use. _OPTIONAL_ `default: 'GET'`. * `title`: A `String` to be added to the results for identification. _OPTIONAL_ default: `undefined`. - * `body`: A `String` or a `Buffer` containing the body of the request. Leave undefined for an empty body. _OPTIONAL_ default: `undefined`. + * `body`: A `String` or a `Buffer` containing the body of the request. Insert one or more randomly generated IDs into the body by including `[]` where the randomly generated ID should be inserted. This can be useful in soak testing POST endpoints where one or more fields must be unique. Leave undefined for an empty body. _OPTIONAL_ default: `undefined`. * `headers`: An `Object` containing the headers of the request. _OPTIONAL_ default: `{}`. * `setupClient`: A `Function` which will be passed the `Client` object for each connection to be made. This can be used to customise each individual connection headers and body using the API shown below. The changes you make to the client in this function will take precedence over the default `body` and `headers` you pass in here. There is an example of this in the samples folder. _OPTIONAL_ default: `function noop () {}`. * `maxConnectionRequests`: A `Number` stating the max requests to make per connection. `amount` takes precedence if both are set. _OPTIONAL_ @@ -140,6 +142,7 @@ Start autocannon against the given target. * `overallRate`: A `Number` stating the rate of requests to make per second from all connections. `conenctionRate` takes precedence if both are set. No rate limiting by default. _OPTIONAL_ * `reconnectRate`: A `Number` which makes the individual connections disconnect and reconnect to the server whenever it has sent that number of requests. _OPTIONAL_ * `requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. The `Object`s in this array can have `body`, `headers`, `method`, or `path` attributes, which overwrite those that are passed in this `opts` object. Therefore, the ones in this (`opts`) object take precedence and should be viewed as defaults. Check the samples folder for an example of how this might be used. _OPTIONAL_. + * `idReplacement`: A `Boolean` which enables the replacement of `[]` tags within the request body with a randomly generated ID, allowing for unique fields to be sent with requests. Check out [an example of programatic usage](./samples/using-id-replacement.js) can be found in the samples. _OPTIONAL_ default: `false` * `forever`: A `Boolean` which allows you to setup an instance of autocannon that restarts indefinatly after emiting results with the `done` event. Useful for efficiently restarting your instance. To stop running forever, you must cause a `SIGINT` or call the `.stop()` function on your instance. _OPTIONAL_ default: `false` * `cb`: The callback which is called on completion of a benchmark. Takes the following params. _OPTIONAL_. * `err`: If there was an error encountered with the run. diff --git a/autocannon.js b/autocannon.js index dae2aca6..673f0ca7 100755 --- a/autocannon.js +++ b/autocannon.js @@ -14,7 +14,7 @@ module.exports.track = track function start () { const argv = minimist(process.argv.slice(2), { - boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'forever'], + boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'forever', 'idReplacement'], alias: { connections: 'c', pipelining: 'p', @@ -37,6 +37,7 @@ function start () { title: 'T', version: 'v', forever: 'f', + idReplacement: 'I', help: 'h' }, default: { @@ -49,7 +50,8 @@ function start () { renderProgressBar: true, json: false, forever: false, - method: 'GET' + method: 'GET', + idReplacement: false } }) diff --git a/lib/httpRequestBuilder.js b/lib/httpRequestBuilder.js index f1891612..1d2ccd1a 100644 --- a/lib/httpRequestBuilder.js +++ b/lib/httpRequestBuilder.js @@ -60,7 +60,10 @@ function requestBuilder (defaults) { } if (bodyBuf && bodyBuf.length > 0) { - headers['Content-Length'] = '' + bodyBuf.length + const idCount = reqData.idReplacement + ? (bodyBuf.toString().match(/\[\]/g) || []).length + : 0 + headers['Content-Length'] = `${bodyBuf.length + (idCount * 27)}` } let req = Object.keys(headers) diff --git a/lib/requestIterator.js b/lib/requestIterator.js index 001140c2..3fb6ba92 100644 --- a/lib/requestIterator.js +++ b/lib/requestIterator.js @@ -1,3 +1,6 @@ +'use strict' + +const hyperid = require('hyperid')(true) const inherits = require('util').inherits const requestBuilder = require('./httpRequestBuilder') @@ -28,9 +31,11 @@ RequestIterator.prototype.nextRequestBuffer = function () { RequestIterator.prototype.move = function () { // get the current buffer and proceed to next request - const ret = this.currentRequest.requestBuffer + let ret = this.currentRequest.requestBuffer this.nextRequest() - return ret + return this.reqDefaults.idReplacement + ? new Buffer(ret.toString().replace(/\[\]/g, hyperid())) + : ret } RequestIterator.prototype.setRequests = function (newRequests) { diff --git a/lib/run.js b/lib/run.js index dcde2895..6707d4ef 100644 --- a/lib/run.js +++ b/lib/run.js @@ -26,6 +26,7 @@ const defaultOptions = { amount: 0, reconnectRate: 0, forever: false, + idReplacement: false, requests: [{}] } @@ -84,6 +85,7 @@ function run (opts, cb) { url.reconnectRate = opts.reconnectRate url.responseMax = amount || opts.maxConnectionRequests || opts.maxOverallRequests url.rate = opts.connectionRate || opts.overallRate + url.idReplacement = opts.idReplacement let clients = [] initialiseClients(clients) diff --git a/package.json b/package.json index 60b20dbe..2cfab011 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ ], "author": "Matteo Collina ", "contributors": [ - "Glen Keane (http://glenkeane.me/)" + "Glen Keane (http://glenkeane.me/)", + "Donald Robertson ]@user.com' // [] will be replaced with generated HyperID at run time + }) + } + ], + idReplacement: true + }, finishedBench) + + function finishedBench (err, res) { + console.log('finished bench', err, res) + } +} diff --git a/test/httpRequestBuilder.test.js b/test/httpRequestBuilder.test.js index d3f20be6..4fc4f22a 100644 --- a/test/httpRequestBuilder.test.js +++ b/test/httpRequestBuilder.test.js @@ -69,3 +69,63 @@ test('request builder should accept all valid standard http methods', (t) => { }) t.end() }) + +test('request builder should add a Content-Length header when the body buffer exists as a default override', (t) => { + t.plan(1) + + const opts = server.address() + opts.method = 'POST' + opts.body = 'body' + + const build = RequestBuilder(opts) + + const result = build() + t.same(result, + new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\nbody\r\n`), + 'request is okay') +}) + +test('request builder should add a Content-Length header when the body buffer exists as per build override', (t) => { + t.plan(1) + + const opts = server.address() + opts.method = 'POST' + + const build = RequestBuilder(opts) + + const result = build({ body: 'body' }) + t.same(result, + new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 4\r\n\r\nbody\r\n`), + 'request is okay') +}) + +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) => { + t.plan(1) + + const opts = server.address() + opts.method = 'POST' + opts.body = '[]' + opts.idReplacement = true + + const build = RequestBuilder(opts) + + const result = build() + t.same(result, + new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 33\r\n\r\n[]\r\n`), + 'request is okay') +}) + +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) => { + t.plan(1) + + const opts = server.address() + opts.method = 'POST' + opts.body = '[]' + + const build = RequestBuilder(opts) + + const result = build({ idReplacement: true }) + t.same(result, + new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 33\r\n\r\n[]\r\n`), + 'request is okay') +}) diff --git a/test/requestIterator.test.js b/test/requestIterator.test.js index 323674f4..f9fbc102 100644 --- a/test/requestIterator.test.js +++ b/test/requestIterator.test.js @@ -143,3 +143,37 @@ test('request iterator should allow for rebuilding the current request', (t) => iterator.setRequest() // this should build default request t.same(iterator.currentRequest.requestBuffer, request5Res, 'request was okay') }) + +test('request iterator should not replace any [] tags with generated IDs when calling move with idReplacement disabled', (t) => { + t.plan(2) + + const opts = server.address() + opts.method = 'POST' + opts.body = '[]' + opts.requests = [{}] + + const iterator = new RequestIterator(opts.requests, opts) + const result = iterator.move().toString().trim() + + const contentLength = result.split('Content-Length: ')[1].slice(0, 1) + t.equal(contentLength, '6', 'Content-Length was incorrect') + + const body = result.split('Content-Length: 6')[1].trim() + t.equal(body, '[]', '[] should be present in body') +}) + +test('request iterator should replace all [] tags with generated IDs when calling move with idReplacement enabled', (t) => { + t.plan(2) + + const opts = server.address() + opts.method = 'POST' + opts.body = '[]' + opts.requests = [{}] + opts.idReplacement = true + + const iterator = new RequestIterator(opts.requests, opts) + const result = iterator.move().toString().trim() + + t.equal(result.includes('[]'), false, 'One or more [] tags were not replaced') + t.equal(result.slice(-1), '0', 'Generated ID should end with request number') +})