From c16a0e5b4545f64eba10d8c74f265356ebae59e8 Mon Sep 17 00:00:00 2001 From: Andrew Carpenter Date: Sat, 1 Feb 2020 06:02:17 -0500 Subject: [PATCH] Add expectBody option and related tests (#242) * Add expectBody option and related tests * Add expectBody warning, error if used with requests * Add expectBody warning to help output as well --- README.md | 7 ++++- autocannon.js | 1 + help.txt | 3 ++ lib/httpClient.js | 10 +++++- lib/progressTracker.js | 3 ++ lib/run.js | 19 +++++++++++ test/helper.js | 2 +- test/run.test.js | 71 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 113 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f23c3397..e0dc964e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Available options: -T/--title TITLE The title to place in the results for identification. -b/--body BODY - The body of the request. + The body of the request. Note: This option needs to be used with the '-H/--headers' option in some frameworks -F/--form FORM Upload a form (multipart/form-data). The form options can be a JSON string like @@ -108,6 +108,9 @@ Available options: Server name for the SNI (Server Name Indication) TLS extension. -x/--excludeErrorStats Exclude error statistics (non 2xx http responses) from the final latency and bytes per second averages. default: false. + -E/--expectBody EXPECTED + Ensure the body matches this value. If enabled, mismatches count towards bailout. + Enabling this option will slow down the load testing. -v/--version Print the version number. -h/--help @@ -242,6 +245,7 @@ Start autocannon against the given target. * `forever`: A `Boolean` which allows you to setup an instance of autocannon that restarts indefinitely 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` * `servername`: A `String` identifying the server name for the SNI (Server Name Indication) TLS extension. _OPTIONAL_ default: `undefined`. * `excludeErrorStats`: A `Boolean` which allows you to disable tracking non 2xx code responses in latency and bytes per second calculations. _OPTIONAL_ default: `false`. + * `expectBody`: A `String` representing the expected response body. Each request whose response body is not equal to `expectBody`is counted in `mismatches`. If enabled, mismatches count towards bailout. _OPTIONAL_ * `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. * `results`: The results of the run. @@ -310,6 +314,7 @@ The results object emitted by `done` and passed to the `autocannon()` callback h * `duration`: The amount of time the test took, **in seconds**. * `errors`: The number of connection errors (including timeouts) that occurred. * `timeouts`: The number of connection timeouts that occurred. +* `mismatches`: The number of requests with a mismatched body. * `start`: A Date object representing when the test started. * `finish`: A Date object representing when the test ended. * `connections`: The amount of connections used (value of `opts.connections`). diff --git a/autocannon.js b/autocannon.js index 5a2c09f5..0ecd8cfa 100755 --- a/autocannon.js +++ b/autocannon.js @@ -58,6 +58,7 @@ function parseArguments (argvs) { idReplacement: 'I', socketPath: 'S', excludeErrorStats: 'x', + expectBody: 'E', help: 'h' }, default: { diff --git a/help.txt b/help.txt index cdb43c1e..c7b5e31c 100644 --- a/help.txt +++ b/help.txt @@ -69,6 +69,9 @@ Available options: Server name for the SNI (Server Name Indication) TLS extension. -x/--excludeErrorStats Exclude error statistics (non 2xx http responses) from the final latency and bytes per second averages. default: false. + -E/--expectBody EXPECTED + Ensure the body matches this value. If enabled, mismatches count towards bailout. + Enabling this option will slow down the load testing. --debug Print connection errors to stderr. -v/--version diff --git a/lib/httpClient.js b/lib/httpClient.js index 496a53c1..9216337b 100644 --- a/lib/httpClient.js +++ b/lib/httpClient.js @@ -19,6 +19,7 @@ function Client (opts) { this.opts.setupClient = this.opts.setupClient || noop this.opts.pipelining = this.opts.pipelining || 1 this.opts.port = this.opts.port || 80 + this.opts.expectBody = this.opts.expectBody || null this.timeout = (this.opts.timeout || 10) * 1000 this.ipc = !!this.opts.socketPath this.secure = this.opts.protocol === 'https:' @@ -85,8 +86,15 @@ function Client (opts) { this.resData[this.cer].headers = opts } - this.parser[HTTPParser.kOnBody] = (body) => { + this.parser[HTTPParser.kOnBody] = (body, start, len) => { this.emit('body', body) + + if (this.opts.expectBody) { + const bodyString = '' + body.slice(start, start + len) + if (this.opts.expectBody !== bodyString) { + return this.emit('mismatch', bodyString) + } + } } this.parser[HTTPParser.kOnMessageComplete] = () => { diff --git a/lib/progressTracker.js b/lib/progressTracker.js index d6699f21..e351d4d4 100644 --- a/lib/progressTracker.js +++ b/lib/progressTracker.js @@ -124,6 +124,9 @@ function track (instance, opts) { if (result.errors) { logToStream(`${format(result.errors)} errors (${format(result.timeouts)} timeouts)`) } + if (result.mismatches) { + logToStream(`${format(result.mismatches)} requests with mismatched body`) + } }) function logToStream (msg) { diff --git a/lib/run.js b/lib/run.js index 6067847d..cba5a717 100644 --- a/lib/run.js +++ b/lib/run.js @@ -88,6 +88,7 @@ function _run (opts, cb, tracker) { let bytes = 0 let errors = 0 let timeouts = 0 + let mismatches = 0 let totalBytes = 0 let totalRequests = 0 let totalCompletedRequests = 0 @@ -123,6 +124,7 @@ function _run (opts, cb, tracker) { url.idReplacement = opts.idReplacement url.socketPath = opts.socketPath url.servername = opts.servername + url.expectBody = opts.expectBody let clients = [] initialiseClients(clients) @@ -165,6 +167,7 @@ function _run (opts, cb, tracker) { throughput: addPercentiles(throughput, histAsObj(throughput, totalBytes)), errors: errors, timeouts: timeouts, + mismatches: mismatches, duration: Math.round((Date.now() - startTime) / 10) / 100, start: new Date(startTime), finish: new Date(), @@ -190,6 +193,7 @@ function _run (opts, cb, tracker) { }, opts.duration * 1000) errors = 0 timeouts = 0 + mismatches = 0 totalBytes = 0 totalRequests = 0 totalCompletedRequests = 0 @@ -229,6 +233,7 @@ function _run (opts, cb, tracker) { const client = new Client(url) client.on('response', onResponse) client.on('connError', onError) + client.on('mismatch', onExpectMismatch) client.on('timeout', onTimeout) client.on('request', () => { totalRequests++ }) client.on('done', onDone) @@ -259,6 +264,15 @@ function _run (opts, cb, tracker) { if (opts.bailout && errors >= opts.bailout) stop = true } + function onExpectMismatch (bpdyStr) { + for (let i = 0; i < opts.pipelining; i++) { + tracker.emit('reqMismatch', bpdyStr) + } + + mismatches++ + if (opts.bailout && mismatches >= opts.bailout) stop = true + } + // treat a timeout as a special type of error function onTimeout () { const error = new Error('request timed out') @@ -302,6 +316,11 @@ function _run (opts, cb, tracker) { return true } + if (opts.expectBody && opts.requests !== DefaultOptions.requests) { + errorCb(new Error('expectBody cannot be used in conjunction with requests')) + return true + } + if (lessThanOneError(opts.connections, 'connections')) return true if (lessThanOneError(opts.pipelining, 'pipelining factor')) return true if (greaterThanZeroError(opts.timeout, 'timeout')) return true diff --git a/test/helper.js b/test/helper.js index a8924646..07c4aad6 100644 --- a/test/helper.js +++ b/test/helper.js @@ -20,7 +20,7 @@ function startServer (opts) { function handle (req, res) { res.statusCode = statusCode - res.end('hello world') + res.end(opts.body || 'hello world') } server.unref() diff --git a/test/run.test.js b/test/run.test.js index 283b6515..6eb0e7cb 100644 --- a/test/run.test.js +++ b/test/run.test.js @@ -60,6 +60,7 @@ test('run', (t) => { t.ok(result.finish, 'finish time exists') t.equal(result.errors, 0, 'no errors') + t.equal(result.mismatches, 0, 'no mismatches') t.equal(result['1xx'], 0, '1xx codes') t.equal(result['2xx'], result.requests.total, '2xx codes') @@ -123,6 +124,7 @@ test('tracker.stop()', (t) => { t.ok(result.finish, 'finish time exists') t.equal(result.errors, 0, 'no errors') + t.equal(result.mismatches, 0, 'no mismatches') t.equal(result['1xx'], 0, '1xx codes') t.equal(result['2xx'], result.requests.total, '2xx codes') @@ -228,6 +230,20 @@ test('run should callback with an error after a bailout', (t) => { }) }) +test('run should callback with an error using expectBody and requests', (t) => { + t.plan(2) + + run({ + url: 'http://localhost:' + server.address().port, + requests: [{ body: 'something' }], + expectBody: 'hello' + }, function (err, result) { + t.ok(err, 'expectBody used with requests should cause an error') + t.notOk(result, 'results should not exist') + t.end() + }) +}) + test('run should allow users to enter timestrings to be used for duration', (t) => { t.plan(3) @@ -261,6 +277,37 @@ test('run should recognise valid urls without http at the start', (t) => { }) }) +test('run should produce count of mismatches with expectBody set', (t) => { + t.plan(2) + + run({ + url: 'http://localhost:' + server.address().port, + expectBody: 'body will not be this', + maxOverallRequests: 10 + }, function (err, result) { + t.error(err) + t.equal(result.mismatches, 10) + t.end() + }) +}) + +test('run should produce 0 mismatches with expectBody set and matches', (t) => { + t.plan(2) + + const responseBody = 'hello dave' + const server = helper.startServer({ body: responseBody }) + + run({ + url: 'http://localhost:' + server.address().port, + expectBody: responseBody, + maxOverallRequests: 10 + }, function (err, result) { + t.error(err) + t.equal(result.mismatches, 0) + t.end() + }) +}) + test('run should accept a unix socket/windows pipe', (t) => { t.plan(11) @@ -439,6 +486,30 @@ test('tracker will emit reqError with error message on error', (t) => { }) }) +test('tracker will emit reqMismatch when body does not match expectBody', (t) => { + t.plan(2) + + const responseBody = 'hello world' + const server = helper.startServer({ body: responseBody }) + + const expectBody = 'goodbye world' + + const tracker = run({ + url: `http://localhost:${server.address().port}`, + connections: 10, + duration: 15, + method: 'GET', + body: 'hello', + expectBody + }) + + tracker.once('reqMismatch', (bodyStr) => { + t.equal(bodyStr, responseBody) + t.notEqual(bodyStr, expectBody) + tracker.stop() + }) +}) + test('tracker will emit tick with current counter value', (t) => { t.plan(1)