Skip to content

Commit

Permalink
Merge pull request #2813 from postmanlabs/feature/cli-reporter-verbos…
Browse files Browse the repository at this point in the history
…e-body

Extended CLI reporter to show more request response info in verbose mode
  • Loading branch information
codenirvana committed Aug 25, 2021
2 parents 3a898b9 + 834423f commit 5a7b888
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 88 deletions.
2 changes: 1 addition & 1 deletion .nycrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function configOverrides (testType) {
statements: 70,
branches: 55,
functions: 75,
lines: 75
lines: 70
};
default:
return {}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ master:
- >-
GH-2806 Exposed the runner on newman and the runtime run object in start
event
- >-
Added ability to see request and responses in CLI reporter `--verbose`
mode. Also added ability to see additional meta such as header count,
cookie count and body mime-types in verbose mode.
chores:
- Updated dependencies

Expand Down
15 changes: 12 additions & 3 deletions lib/reporters/cli/cli-utils-symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ subsets = {
root: '→',
sub: '↳',
ok: '✓',
error: '✖'
error: '✖',
star: '★',
up: '↑',
down: '↓'
},
encoded: {
console: {
Expand All @@ -32,7 +35,10 @@ subsets = {
root: '\u2192',
sub: '\u2514',
ok: '\u221A',
error: '\u00D7'
error: '\u00D7',
star: '\u2605',
up: '\u2191',
down: '\u2193'
},
plainText: {
console: {
Expand All @@ -45,7 +51,10 @@ subsets = {
root: 'Root',
sub: 'Sub-folder',
ok: 'Pass',
error: 'Fail'
error: 'Fail',
star: '*',
up: '^',
down: 'v'
}
};

Expand Down
166 changes: 129 additions & 37 deletions lib/reporters/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ var _ = require('lodash'),
E = '',

CACHED_TIMING_PHASE = '(cache)',
TIMING_TABLE_HEADERS = {
prepare: 'prepare',
wait: 'wait',
dns: 'dns-lookup',
tcp: 'tcp-handshake',
secureHandshake: 'ssl-handshake',
firstByte: 'transfer-start',
download: 'download',
process: 'process',
total: 'total'
},
BODY_CLIP_SIZE = 2048,

PostmanCLIReporter,
timestamp,
Expand Down Expand Up @@ -175,53 +187,133 @@ PostmanCLIReporter = function (emitter, reporterOptions, options) {
return;
}

var timingTable,
timings,
timingPhases,
size = o.response && o.response.size(),
timingHeaders = {
prepare: 'prepare',
wait: 'wait',
dns: 'dns-lookup',
tcp: 'tcp-handshake',
secureHandshake: 'ssl-handshake',
firstByte: 'transfer-start',
download: 'download',
process: 'process',
total: 'total'
};

size = size && (size.header || 0) + (size.body || 0) || 0;

print.lf(colors.gray('[%d %s, %s, %s]'), o.response.code, o.response.reason(),
util.filesize(size), util.prettyms(o.response.responseTime));

// if there are redirects, get timings for the last request sent
timings = _.last(_.get(o, 'history.execution.data'));
timings = timings && timings.timings;
if (!(o.request && o.response)) {
print.lf(colors.red('[errored]'));
print.lf(colors.red(' %s'), 'Internal error! Could not read response data.');

// print timing info of the request
if (options.verbose && timings) {
timingPhases = util.beautifyTime(sdk.Response.timingPhases(timings));
return;
}

// quickly print out basic non verbose response meta and exit
if (!options.verbose) {
print.lf(colors.gray('[%d %s, %s, %s]'), o.response.code, o.response.reason(),
util.filesize(o.response.size().total), util.prettyms(o.response.responseTime));

timingTable = new Table({
chars: _.defaults({ mid: '', middle: '' }, cliUtils.cliTableTemplate_Blank),
colAligns: _.fill(Array(_.size(timingPhases)), 'left'),
style: { 'padding-left': 2 }
});
return;
}

timingPhases = _.transform(timingHeaders, (result, header, key) => {
// this point onwards the output is verbose. a tonne of variables are created here for
// keeping the output clean and readable

let req = o.request,
res = o.response,

// set values here with abundance of caution to avoid erroring out
reqSize = util.filesize(req.size().total),
resSize = util.filesize(res.size().total),
code = res.code,
reason = res.reason(),
mime = res.contentInfo() || {},
timings = _.last(_.get(o, 'history.execution.data')),

reqHeadersLen = _.get(req, 'headers.members.length'),
resHeadersLen = _.get(res, 'headers.members.length'),

resTime = util.prettyms(res.responseTime || 0),

reqText = (options.verbose && req.body) ? req.body.toString() : E,
reqTextLen = req.size().body || Buffer.byteLength(reqText),

resText = options.verbose ? res.text() : E,
resTextLen = res.size().body || Buffer.byteLength(resText),

reqBodyMode = _.get(req, 'body.mode', ''),
resSummary = [
`${mime.contentType}`,
`${mime.mimeType}`,
`${mime.mimeFormat}`,
`${mime.charset}`
].join(` ${colors.gray(symbols.star)} `);


print.lf(SPC); // also flushes out the circling progress icon

// for clean readability of code. this section compiles the cli string for one line of
// req-res combined summary. this looks somewhat like below:
// >> 200 OK ★ 979ms time ★ 270B↑ 793B↓ size ★ 7↑ 7↓ headers ★ 0 cookies
print.lf(SPC + SPC + [
`${code} ${reason}`,
`${resTime} ${colors.gray('time')}`,
`${reqSize}${colors.gray(symbols.up)} ${resSize}${colors.gray(symbols.down)} ${colors.gray('size')}`,
`${reqHeadersLen}${colors.gray(symbols.up)} ` +
`${resHeadersLen}${colors.gray(symbols.down)} ${colors.gray('headers')}`,
`${_.get(res, 'cookies.members.length')} ${colors.gray('cookies')}`
].join(` ${colors.gray(symbols.star)} `));

// print request body
if (reqTextLen) {
// truncate very large request (is 2048 large enough?)
if (reqTextLen > BODY_CLIP_SIZE) {
reqText = reqText.substr(0, BODY_CLIP_SIZE) +
colors.brightWhite(`\n(showing ${util.filesize(BODY_CLIP_SIZE)}/${util.filesize(reqTextLen)})`);
}

reqText = wrap(reqText, ` ${colors.white(symbols.console.middle)} `);
// eslint-disable-next-line max-len
print.buffer(` ${colors.white(symbols.console.top)} ${colors.white(symbols.up)} ${reqBodyMode} ${colors.gray(symbols.star)} ${util.filesize(reqTextLen)}\n`,
colors.white(` ${symbols.console.bottom}`))
// tweak the message to ensure that its surrounding is not brightly coloured.
// also ensure to remove any blank lines generated due to util.inspect
.nobuffer(colors.gray(reqText.replace(/\n\s*\n/g, LF) + LF));

print.lf(SPC); // visual tweak: flushes out the buffer of wrapping body above
}

// print response body
if (resTextLen) {
// truncate very large response (is 2048 large enough?)
if (resTextLen > BODY_CLIP_SIZE) {
resText = resText.substr(0, BODY_CLIP_SIZE) +
colors.brightWhite(`\n(showing ${util.filesize(BODY_CLIP_SIZE)}/${util.filesize(resTextLen)})`);
}

resText = wrap(resText, ` ${colors.white(symbols.console.middle)} `);
// eslint-disable-next-line max-len
print.buffer(` ${colors.white(symbols.console.top)} ${colors.white(symbols.down)} ${resSummary} ${colors.gray(symbols.star)} ${util.filesize(resTextLen)}\n`,
colors.white(` ${symbols.console.bottom}`))
// tweak the message to ensure that its surrounding is not brightly coloured.
// also ensure to remove any blank lines generated due to util.inspect
.nobuffer(colors.gray(resText.replace(/\n\s*\n/g, LF) + LF));
}
// print the line of response body meta one liner if there is no response body
// if there is one, we would already print it across the body braces above.
else {
// we need to do some newline related shenanigans here so that the output looks clean
// in the absence of the request body block
print.lf(` ${symbols.down} ${resSummary}`);
}

// print timing info of the request
timings = timings && timings.timings; // if there are redirects, get timings for the last request sent
if (timings) {
// adds nice units to all time data in the object
let timingPhases = util.beautifyTime(sdk.Response.timingPhases(timings)),
timingTable = new Table({
chars: _.defaults({ mid: '', middle: '' }, cliUtils.cliTableTemplate_Blank),
colAligns: _.fill(Array(_.size(timingPhases)), 'left'),
style: { 'padding-left': 2 }
});

timingPhases = _.transform(TIMING_TABLE_HEADERS, (result, header, key) => {
if (_.has(timingPhases, key)) {
result.headers.push(colors.white(header));
result.values.push(colors.log(timingPhases[key] || CACHED_TIMING_PHASE));
}
}, { headers: [], values: [] });

// add name of phases in the table
timingTable.push(timingPhases.headers);

// add time of phases in the table
timingTable.push(timingPhases.values);
timingTable.push(timingPhases.headers); // add name of phases in the table
timingTable.push(timingPhases.values); // add time of phases in the table

print(LF + timingTable + LF + LF);
}
Expand Down
91 changes: 44 additions & 47 deletions test/unit/cli-reporter-symbols.test.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,49 @@
const _ = require('lodash'),

isDoubleByte = function (str) {
for (var i = 0, n = str.length; i < n; i++) {
if (str.charCodeAt(i) > 255) { return true; }
}

return false;
};

/* eslint-disable */
/**
* @attribution https://github.com/lodash/lodash/issues/2240#issuecomment-418820848
*/
const flattenKeys = (obj, path = []) =>
!_.isObject(obj)
? { [path.join('.')]: obj }
: _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {});
/* eslint-enable */

describe('unicode handling of cli symbol utility module', function () {
var cliUtilsSymbols = require('../../lib/reporters/cli/cli-utils-symbols.js');

it('should revert to text alternatives when disableUnicode parameter is set to true', function () {
var symbols = cliUtilsSymbols(true);

expect(symbols).to.eql({
console: {
top: '-',
middle: '|',
bottom: '-'
},
dot: '.',
folder: 'Folder',
root: 'Root',
sub: 'Sub-folder',
ok: 'Pass',
error: 'Fail'
});
let cliUtilsSymbols = require('../../lib/reporters/cli/cli-utils-symbols.js');

it('should have three symbol classes', function () {
expect(cliUtilsSymbols(true)).to.be.an('object');
expect(cliUtilsSymbols(false)).to.be.an('object');
});

it('should provide the platform-specific default symbol map when no options are passed', function () {
var symbols = cliUtilsSymbols(),
isWin = (/^win/).test(process.platform);

expect(symbols).to.eql(isWin ? {
console: {
top: '\u250C',
middle: '\u2502',
bottom: '\u2514'
},
dot: '.',
folder: '□',
root: '→',
sub: '└',
ok: '√',
error: '×'
} : {
console: {
top: '┌',
middle: '│',
bottom: '└'
},
dot: '.',
folder: '❏',
root: '→',
sub: '↳',
ok: '✓',
error: '✖'
});
it('should have appropriate fallback for unicode', function () {
let symbol = cliUtilsSymbols(true),
fallback = cliUtilsSymbols(false),

flattenedSymbols = flattenKeys(symbol),
flattenedPlainSymbols = flattenKeys(fallback);

expect(flattenedSymbols).to.contain.keys(flattenedPlainSymbols);
});

it('should not have unicode when not requested', function () {
let fallbackSymbols = cliUtilsSymbols(true),
flattenedPlainSymbols = flattenKeys(fallbackSymbols);

// first compute all double byte checking
for (const [key, value] of Object.entries(flattenedPlainSymbols)) {
// @todo make the assertion better so that failure here says what exactly went wrong
expect(isDoubleByte(value)).to.eql(false, key);
}
});
});
25 changes: 25 additions & 0 deletions test/unit/options.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var _ = require('lodash'),
{ VariableScope } = require('postman-collection'),
options = require('../../lib/run/options');

describe('options', function () {
Expand Down Expand Up @@ -34,6 +35,18 @@ describe('options', function () {
});
});

it('should apply directly specified env variables to environment list', function (done) {
options({
envVar: [{ key: 'test', value: 'data' }]
}, function (err, result) {
expect(err).to.be.null;
expect(result).to.have.property('environment');
expect(result.environment).to.be.an.instanceof(VariableScope);
expect(result.environment.get('test')).to.equal('data');
done();
});
});

it('should be handled correctly for globals', function (done) {
var globals = require('../../test/fixtures/run/spaces/simple-variables.json');

Expand All @@ -47,6 +60,18 @@ describe('options', function () {
});
});

it('should apply directly specified global variables to globals list', function (done) {
options({
globalVar: [{ key: 'test', value: 'data' }]
}, function (err, result) {
expect(err).to.be.null;
expect(result).to.have.property('globals');
expect(result.globals).to.be.an.instanceof(VariableScope);
expect(result.globals.get('test')).to.equal('data');
done();
});
});

it('should be handled correctly for iterationData', function (done) {
var data = require('../../test/fixtures/run/spaces/data.json');

Expand Down
Loading

0 comments on commit 5a7b888

Please sign in to comment.