Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render specific status codes (issue: #519) #534

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ Available options:
Enabling this option will slow down the load testing.
--renderStatusCodes
Print status codes and their respective statistics.
--renderOnlyStatusCode NUM/STRING
Print the specified status code and their respective statistics. This can be used by supplying a single status code, or multiple comma-separated status codes.
Example: --renderOnlyStatusCode=200 or --renderOnlyStatusCode=301,302,404
--cert
Path to cert chain in pem format
--key
Expand Down
1 change: 1 addition & 0 deletions autocannon.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const defaults = {
renderLatencyTable: false,
renderProgressBar: true,
renderStatusCodes: false,
renderOnlyStatusCode: null,
json: false,
forever: false,
method: 'GET',
Expand Down
3 changes: 3 additions & 0 deletions help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ Available options:
Enabling this option will slow down the load testing.
--renderStatusCodes
Print status codes and their respective statistics.
--renderOnlyStatusCode NUM/STRING
Print the specified status code and their respective statistics. This can be used by supplying a single status code, or multiple comma-separated status codes.
Example: --renderOnlyStatusCode=200 or --renderOnlyStatusCode=301,302,404
--cert
Path to cert chain in pem format
--key
Expand Down
2 changes: 2 additions & 0 deletions lib/printResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const printResult = (result, opts) => {
head: asColor(chalk.cyan, ['Code', 'Count'])
})
Object.keys(result.statusCodeStats).forEach(statusCode => {
if (Array.isArray(opts.renderOnlyStatusCode) && !opts.renderOnlyStatusCode.includes(parseInt(statusCode, 10))) return

const stats = result.statusCodeStats[statusCode]
const colorize = colorizeByStatusCode(chalk, statusCode)
statusCodeStats.push([colorize(statusCode), stats.count])
Expand Down
23 changes: 23 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const isValidFn = (opt) => (!opt || typeof opt === 'function' || typeof opt ===

const lessThanOneError = (label) => new Error(`${label} can not be less than 1`)
const greaterThanZeroError = (label) => new Error(`${label} must be greater than 0`)
const valueBetweenError = (label, min, max) => new Error(`${label} must be between ${min} and ${max}`)
const minIfPresent = (val, min) => val !== null && val < min
const betweenIfPresent = (val, min, max) => val !== null && val !== undefined && val >= min && val <= max

function safeRequire (path) {
if (typeof path === 'string') {
Expand Down Expand Up @@ -149,6 +151,27 @@ module.exports = function validateOpts (opts, cbPassedIn) {
if (opts.pipelining < 1) return lessThanOneError('pipelining factor')
if (opts.timeout < 1) return greaterThanZeroError('timeout')

if (opts.renderOnlyStatusCode) {
if (!opts.renderStatusCodes) return new Error('renderStatusCodes must be enabled to use renderOnlyStatusCode')
if (!/^[/d]?[\d,]*$/.test(opts.renderOnlyStatusCode)) return new Error('renderOnlyStatusCode must be a valid status code or comma separated list of status codes')

let statusCodeOutOfBounds = false
const arrayOfStatusCodes = opts.renderOnlyStatusCode.toString().split(',')
const transformedRenderOnlyStatusCode = []

for (const statusCode of arrayOfStatusCodes) {
const code = parseInt(statusCode, 10)
if (!betweenIfPresent(code, 100, 599)) {
statusCodeOutOfBounds = true
break
}
transformedRenderOnlyStatusCode.push(code)
}

if (statusCodeOutOfBounds) return valueBetweenError('renderOnlyStatusCode', 100, 599)
opts.renderOnlyStatusCode = transformedRenderOnlyStatusCode
}

if (opts.ignoreCoordinatedOmission && !opts.connectionRate && !opts.overallRate) {
return new Error('ignoreCoordinatedOmission makes no sense without connectionRate or overallRate')
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "autocannon",
"version": "8.0.0",
"version": "8.1.0",
"description": "Fast HTTP benchmarking tool written in Node.js",
"main": "autocannon.js",
"bin": {
Expand Down
99 changes: 99 additions & 0 deletions test/fixtures/example-result-non2xx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"title": "example result with non2xx responses",
"url": "http://localhost:3000/test3",
"connections": 1,
"sampleInt": 1000,
"pipelining": 1,
"duration": 1.01,
"samples": 1,
"start": "2025-01-14T19:06:15.608Z",
"finish": "2025-01-14T19:06:16.618Z",
"errors": 0,
"timeouts": 0,
"mismatches": 0,
"non2xx": 7559,
"resets": 0,
"1xx": 1906,
"2xx": 1843,
"3xx": 1858,
"4xx": 1896,
"5xx": 1899,
"statusCodeStats": {
"101": { "count": 1906 },
"200": { "count": 1843 },
"301": { "count": 58 },
"302": { "count": 1800 },
"404": { "count": 1896 },
"500": { "count": 1899 }
},
"latency": {
"average": 0.01,
"mean": 0.01,
"stddev": 0.06,
"min": 5,
"max": 5,
"p0_001": 0,
"p0_01": 0,
"p0_1": 0,
"p1": 0,
"p2_5": 0,
"p10": 0,
"p25": 0,
"p50": 0,
"p75": 0,
"p90": 0,
"p97_5": 0,
"p99": 0,
"p99_9": 0,
"p99_99": 5,
"p99_999": 5,
"totalCount": 9402
},
"requests": {
"average": 9404,
"mean": 9404,
"stddev": 0,
"min": 9402,
"max": 9402,
"total": 9402,
"p0_001": 9407,
"p0_01": 9407,
"p0_1": 9407,
"p1": 9407,
"p2_5": 9407,
"p10": 9407,
"p25": 9407,
"p50": 9407,
"p75": 9407,
"p90": 9407,
"p97_5": 9407,
"p99": 9407,
"p99_9": 9407,
"p99_99": 9407,
"p99_999": 9407,
"sent": 9403
},
"throughput": {
"average": 1303040,
"mean": 1303040,
"stddev": 0,
"min": 1303103,
"max": 1303103,
"total": 1303103,
"p0_001": 1303551,
"p0_01": 1303551,
"p0_1": 1303551,
"p1": 1303551,
"p2_5": 1303551,
"p10": 1303551,
"p25": 1303551,
"p50": 1303551,
"p75": 1303551,
"p90": 1303551,
"p97_5": 1303551,
"p99": 1303551,
"p99_9": 1303551,
"p99_99": 1303551,
"p99_999": 1303551
}
}
11 changes: 10 additions & 1 deletion test/printResult-process.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

const autocannon = require('../autocannon')
const exampleResult = require('./fixtures/example-result.json')
const exampleResultWithNon2xxResponses = require('./fixtures/example-result-non2xx.json')
const crossArgv = require('cross-argv')
const validateOpts = require('../lib/validate')

let opts = null

Expand All @@ -11,5 +13,12 @@ if (process.argv.length > 2) {
opts = autocannon.parseArguments(args)
}

const resultStr = autocannon.printResult(exampleResult, opts)
let fixture = exampleResult

if (opts?.renderOnlyStatusCode) {
fixture = exampleResultWithNon2xxResponses
opts.renderOnlyStatusCode = validateOpts(opts).renderOnlyStatusCode
}

const resultStr = autocannon.printResult(fixture, opts)
process.stderr.write(resultStr)
112 changes: 112 additions & 0 deletions test/printResult-renderOnlyStatusCode.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict'

const test = require('tap').test
const split = require('split2')
const path = require('path')
const childProcess = require('child_process')

test('should stdout (print) the result (multiple status codes)', (t) => {
const lines = [
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/.*/,
/Code.*Count.*$/,
/.*/,
/200.*1843.*$/,
/.*/,
/302.*1800.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 1.*$/,
/$/,
/.* 2xx responses, ([\d])+ non 2xx responses/,
/.* requests in ([0-9]|\.)+s, .* read/
]

t.plan(lines.length * 2)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, 'printResult-process.js'), '--renderStatusCodes', '--renderOnlyStatusCode=200,302', 'http://127.0.0.1'], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})

t.teardown(() => {
child.kill()
})

child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})
.on('end', t.end)
})

test('should stdout (print) the result (single status code)', (t) => {
const lines = [
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/.*/,
/Code.*Count.*$/,
/.*/,
/301.*58.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 1.*$/,
/$/,
/.* 2xx responses, ([\d])+ non 2xx responses/,
/.* requests in ([0-9]|\.)+s, .* read/
]

t.plan(lines.length * 2)

const child = childProcess.spawn(process.execPath, [path.join(__dirname, 'printResult-process.js'), '--renderStatusCodes', '--renderOnlyStatusCode=301', 'http://127.0.0.1'], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})

t.teardown(() => {
child.kill()
})

child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})
.on('end', t.end)
})
61 changes: 61 additions & 0 deletions test/validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,64 @@ test('validateOpts should disable render options when json is true', (t) => {
t.equal(result.renderResultsTable, false)
t.equal(result.renderLatencyTable, false)
})

test('validateOpts should return an error if renderOnlyStatusCode is used without renderStatusCodes', (t) => {
t.plan(2)

const result = validateOpts({ url: 'http://localhost', renderOnlyStatusCode: 200 })
t.ok(result instanceof Error)
t.equal(result.message, 'renderStatusCodes must be enabled to use renderOnlyStatusCode')
})

test('validateOpts should return an error if renderOnlyStatusCode is not a valid status code', (t) => {
t.plan(12)

// Invalid status code
const result = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: 'foo' })
t.ok(result instanceof Error)
t.equal(result.message, 'renderOnlyStatusCode must be a valid status code or comma separated list of status codes')

// Out of bounds status code
const result2 = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: 600 })
t.ok(result2 instanceof Error)
t.equal(result2.message, 'renderOnlyStatusCode must be between 100 and 599')

// Out of bounds status code in comma separated list
const result3 = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: '200,10' })
t.ok(result3 instanceof Error)
t.equal(result3.message, 'renderOnlyStatusCode must be between 100 and 599')

// Valid status code, with forgotten comma at the end
const result4 = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: '200,302,' })
t.ok(result4 instanceof Error)
t.equal(result4.message, 'renderOnlyStatusCode must be between 100 and 599')

// Valid status code, with non-numerical characters
const result5 = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: '20O' })
t.ok(result5 instanceof Error)
t.equal(result5.message, 'renderOnlyStatusCode must be a valid status code or comma separated list of status codes')

// Valid status code, with non-numerical characters in comma separated list
const result6 = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: '200,302{' })
t.ok(result6 instanceof Error)
t.equal(result6.message, 'renderOnlyStatusCode must be a valid status code or comma separated list of status codes')
})

test('validateOpts should return undefined for renderOnlyStatusCode if it is not set', (t) => {
t.plan(1)

const result = validateOpts({ url: 'http://localhost', renderStatusCodes: true })
t.equal(result.renderOnlyStatusCode, undefined)
})

test('validateOpts should return a valid array of status codes for renderOnlyStatusCode', (t) => {
t.plan(2)

// Multiple status codes
const result = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: '200,302' })
t.same(result.renderOnlyStatusCode, [200, 302])

// Single status code
const result2 = validateOpts({ url: 'http://localhost', renderStatusCodes: true, renderOnlyStatusCode: '200' })
t.same(result2.renderOnlyStatusCode, [200])
})