assert: improve error messages
From now on all error messages produced by `assert` in strict mode will produce a error diff. PR-URL: https://github.com/nodejs/node/pull/17615 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
cb88f35a73
commit
2d9e87695e
@ -17,6 +17,9 @@ For more information about the used equality comparisons see
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/REPLACEME
|
||||
description: Added error diffs to the strict mode
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/17002
|
||||
description: Added strict mode to the assert module.
|
||||
@ -26,12 +29,42 @@ When using the `strict mode`, any `assert` function will use the equality used i
|
||||
the strict function mode. So [`assert.deepEqual()`][] will, for example, work the
|
||||
same as [`assert.deepStrictEqual()`][].
|
||||
|
||||
On top of that, error messages which involve objects produce an error diff
|
||||
instead of displaying both objects. That is not the case for the legacy mode.
|
||||
|
||||
It can be accessed using:
|
||||
|
||||
```js
|
||||
const assert = require('assert').strict;
|
||||
```
|
||||
|
||||
Example error diff (the `expected`, `actual`, and `Lines skipped` will be on a
|
||||
single row):
|
||||
|
||||
```js
|
||||
const assert = require('assert').strict;
|
||||
|
||||
assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
|
||||
```
|
||||
|
||||
```diff
|
||||
AssertionError [ERR_ASSERTION]: Input A expected to deepStrictEqual input B:
|
||||
+ expected
|
||||
- actual
|
||||
... Lines skipped
|
||||
|
||||
[
|
||||
[
|
||||
...
|
||||
2,
|
||||
- 3
|
||||
+ '3'
|
||||
],
|
||||
...
|
||||
5
|
||||
]
|
||||
```
|
||||
|
||||
## Legacy mode
|
||||
|
||||
> Stability: 0 - Deprecated: Use strict mode instead.
|
||||
|
@ -48,6 +48,10 @@ const meta = [
|
||||
|
||||
const escapeFn = (str) => meta[str.charCodeAt(0)];
|
||||
|
||||
const ERR_DIFF_DEACTIVATED = 0;
|
||||
const ERR_DIFF_NOT_EQUAL = 1;
|
||||
const ERR_DIFF_EQUAL = 2;
|
||||
|
||||
// The assert module provides functions that throw
|
||||
// AssertionError's when particular conditions are not met. The
|
||||
// assert module must conform to the following interface.
|
||||
@ -283,7 +287,8 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
|
||||
expected,
|
||||
message,
|
||||
operator: 'deepStrictEqual',
|
||||
stackStartFn: deepStrictEqual
|
||||
stackStartFn: deepStrictEqual,
|
||||
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -296,7 +301,8 @@ function notDeepStrictEqual(actual, expected, message) {
|
||||
expected,
|
||||
message,
|
||||
operator: 'notDeepStrictEqual',
|
||||
stackStartFn: notDeepStrictEqual
|
||||
stackStartFn: notDeepStrictEqual,
|
||||
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -308,7 +314,8 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
|
||||
expected,
|
||||
message,
|
||||
operator: 'strictEqual',
|
||||
stackStartFn: strictEqual
|
||||
stackStartFn: strictEqual,
|
||||
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -320,7 +327,8 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
|
||||
expected,
|
||||
message,
|
||||
operator: 'notStrictEqual',
|
||||
stackStartFn: notStrictEqual
|
||||
stackStartFn: notStrictEqual,
|
||||
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -132,22 +132,164 @@ class SystemError extends makeNodeError(Error) {
|
||||
}
|
||||
}
|
||||
|
||||
function createErrDiff(actual, expected, operator) {
|
||||
var other = '';
|
||||
var res = '';
|
||||
var lastPos = 0;
|
||||
var end = '';
|
||||
var skipped = false;
|
||||
const actualLines = util
|
||||
.inspect(actual, { compact: false }).split('\n');
|
||||
const expectedLines = util
|
||||
.inspect(expected, { compact: false }).split('\n');
|
||||
const msg = `Input A expected to ${operator} input B:\n` +
|
||||
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
|
||||
const skippedMsg = ' ... Lines skipped';
|
||||
|
||||
// Remove all ending lines that match (this optimizes the output for
|
||||
// readability by reducing the number of total changed lines).
|
||||
var a = actualLines[actualLines.length - 1];
|
||||
var b = expectedLines[expectedLines.length - 1];
|
||||
var i = 0;
|
||||
while (a === b) {
|
||||
if (i++ < 2) {
|
||||
end = `\n ${a}${end}`;
|
||||
} else {
|
||||
other = a;
|
||||
}
|
||||
actualLines.pop();
|
||||
expectedLines.pop();
|
||||
a = actualLines[actualLines.length - 1];
|
||||
b = expectedLines[expectedLines.length - 1];
|
||||
}
|
||||
if (i > 3) {
|
||||
end = `\n...${end}`;
|
||||
skipped = true;
|
||||
}
|
||||
if (other !== '') {
|
||||
end = `\n ${other}${end}`;
|
||||
other = '';
|
||||
}
|
||||
|
||||
const maxLines = Math.max(actualLines.length, expectedLines.length);
|
||||
var printedLines = 0;
|
||||
for (i = 0; i < maxLines; i++) {
|
||||
// Only extra expected lines exist
|
||||
const cur = i - lastPos;
|
||||
if (actualLines.length < i + 1) {
|
||||
if (cur > 1 && i > 2) {
|
||||
if (cur > 4) {
|
||||
res += '\n...';
|
||||
skipped = true;
|
||||
} else if (cur > 3) {
|
||||
res += `\n ${expectedLines[i - 2]}`;
|
||||
printedLines++;
|
||||
}
|
||||
res += `\n ${expectedLines[i - 1]}`;
|
||||
printedLines++;
|
||||
}
|
||||
lastPos = i;
|
||||
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
|
||||
printedLines++;
|
||||
// Only extra actual lines exist
|
||||
} else if (expectedLines.length < i + 1) {
|
||||
if (cur > 1 && i > 2) {
|
||||
if (cur > 4) {
|
||||
res += '\n...';
|
||||
skipped = true;
|
||||
} else if (cur > 3) {
|
||||
res += `\n ${actualLines[i - 2]}`;
|
||||
printedLines++;
|
||||
}
|
||||
res += `\n ${actualLines[i - 1]}`;
|
||||
printedLines++;
|
||||
}
|
||||
lastPos = i;
|
||||
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
|
||||
printedLines++;
|
||||
// Lines diverge
|
||||
} else if (actualLines[i] !== expectedLines[i]) {
|
||||
if (cur > 1 && i > 2) {
|
||||
if (cur > 4) {
|
||||
res += '\n...';
|
||||
skipped = true;
|
||||
} else if (cur > 3) {
|
||||
res += `\n ${actualLines[i - 2]}`;
|
||||
printedLines++;
|
||||
}
|
||||
res += `\n ${actualLines[i - 1]}`;
|
||||
printedLines++;
|
||||
}
|
||||
lastPos = i;
|
||||
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
|
||||
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
|
||||
printedLines += 2;
|
||||
// Lines are identical
|
||||
} else {
|
||||
res += other;
|
||||
other = '';
|
||||
if (cur === 1 || i === 0) {
|
||||
res += `\n ${actualLines[i]}`;
|
||||
printedLines++;
|
||||
}
|
||||
}
|
||||
// Inspected object to big (Show ~20 rows max)
|
||||
if (printedLines > 20 && i < maxLines - 2) {
|
||||
return `${msg}${skippedMsg}\n${res}\n...${other}\n...`;
|
||||
}
|
||||
}
|
||||
return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}`;
|
||||
}
|
||||
|
||||
class AssertionError extends Error {
|
||||
constructor(options) {
|
||||
if (typeof options !== 'object' || options === null) {
|
||||
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
|
||||
}
|
||||
var { actual, expected, message, operator, stackStartFn } = options;
|
||||
var {
|
||||
actual,
|
||||
expected,
|
||||
message,
|
||||
operator,
|
||||
stackStartFn,
|
||||
errorDiff = 0
|
||||
} = options;
|
||||
|
||||
if (message != null) {
|
||||
super(message);
|
||||
} else {
|
||||
if (util === null) util = require('util');
|
||||
|
||||
if (actual && actual.stack && actual instanceof Error)
|
||||
actual = `${actual.name}: ${actual.message}`;
|
||||
if (expected && expected.stack && expected instanceof Error)
|
||||
expected = `${expected.name}: ${expected.message}`;
|
||||
if (util === null) util = require('util');
|
||||
super(`${util.inspect(actual).slice(0, 128)} ` +
|
||||
`${operator} ${util.inspect(expected).slice(0, 128)}`);
|
||||
|
||||
if (errorDiff === 0) {
|
||||
let res = util.inspect(actual);
|
||||
let other = util.inspect(expected);
|
||||
if (res.length > 128)
|
||||
res = `${res.slice(0, 125)}...`;
|
||||
if (other.length > 128)
|
||||
other = `${other.slice(0, 125)}...`;
|
||||
super(`${res} ${operator} ${other}`);
|
||||
} else if (errorDiff === 1) {
|
||||
// In case the objects are equal but the operator requires unequal, show
|
||||
// the first object and say A equals B
|
||||
const res = util
|
||||
.inspect(actual, { compact: false }).split('\n');
|
||||
|
||||
if (res.length > 20) {
|
||||
res[19] = '...';
|
||||
while (res.length > 20) {
|
||||
res.pop();
|
||||
}
|
||||
}
|
||||
// Only print a single object.
|
||||
super(`Identical input passed to ${operator}:\n${res.join('\n')}`);
|
||||
} else {
|
||||
super(createErrDiff(actual, expected, operator));
|
||||
}
|
||||
}
|
||||
|
||||
this.generatedMessage = !message;
|
||||
|
@ -711,7 +711,8 @@ assert.throws(() => {
|
||||
assert.strictEqual('A'.repeat(1000), '');
|
||||
}, common.expectsError({
|
||||
code: 'ERR_ASSERTION',
|
||||
message: new RegExp(`^'${'A'.repeat(127)} strictEqual ''$`) }));
|
||||
message: /^'A{124}\.\.\. strictEqual ''$/
|
||||
}));
|
||||
|
||||
{
|
||||
// bad args to AssertionError constructor should throw TypeError
|
||||
@ -752,7 +753,6 @@ common.expectsError(
|
||||
assert.equal(assert.notEqual, assert.notStrictEqual);
|
||||
assert.equal(assert.notDeepEqual, assert.notDeepStrictEqual);
|
||||
assert.equal(Object.keys(assert).length, Object.keys(a).length);
|
||||
/* eslint-enable no-restricted-properties */
|
||||
assert(7);
|
||||
common.expectsError(
|
||||
() => assert(),
|
||||
@ -786,6 +786,145 @@ common.expectsError(
|
||||
}
|
||||
);
|
||||
Error.stackTraceLimit = tmpLimit;
|
||||
|
||||
// Test error diffs
|
||||
const start = 'Input A expected to deepStrictEqual input B:';
|
||||
const actExp = '\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
|
||||
const plus = '\u001b[32m+\u001b[39m';
|
||||
const minus = '\u001b[31m-\u001b[39m';
|
||||
let message = [
|
||||
start,
|
||||
`${actExp} ... Lines skipped`,
|
||||
'',
|
||||
' [',
|
||||
' [',
|
||||
'...',
|
||||
' 2,',
|
||||
`${minus} 3`,
|
||||
`${plus} '3'`,
|
||||
' ]',
|
||||
'...',
|
||||
' 5',
|
||||
' ]'].join('\n');
|
||||
assert.throws(
|
||||
() => assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]),
|
||||
{ message });
|
||||
|
||||
message = [
|
||||
start,
|
||||
`${actExp} ... Lines skipped`,
|
||||
'',
|
||||
' [',
|
||||
' 1,',
|
||||
'...',
|
||||
' 0,',
|
||||
`${plus} 1,`,
|
||||
' 1,',
|
||||
'...',
|
||||
' 1',
|
||||
' ]'
|
||||
].join('\n');
|
||||
assert.throws(
|
||||
() => assert.deepEqual(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1]),
|
||||
{ message });
|
||||
|
||||
message = [
|
||||
start,
|
||||
`${actExp} ... Lines skipped`,
|
||||
'',
|
||||
' [',
|
||||
' 1,',
|
||||
'...',
|
||||
' 0,',
|
||||
`${minus} 1,`,
|
||||
' 1,',
|
||||
'...',
|
||||
' 1',
|
||||
' ]'
|
||||
].join('\n');
|
||||
assert.throws(
|
||||
() => assert.deepEqual(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1]),
|
||||
{ message });
|
||||
|
||||
message = [
|
||||
start,
|
||||
actExp,
|
||||
'',
|
||||
' [',
|
||||
' 1,',
|
||||
`${minus} 2,`,
|
||||
`${plus} 1,`,
|
||||
' 1,',
|
||||
' 1,',
|
||||
' 0,',
|
||||
`${minus} 1,`,
|
||||
' 1',
|
||||
' ]'
|
||||
].join('\n');
|
||||
assert.throws(
|
||||
() => assert.deepEqual(
|
||||
[1, 2, 1, 1, 0, 1, 1],
|
||||
[1, 1, 1, 1, 0, 1]),
|
||||
{ message });
|
||||
|
||||
message = [
|
||||
start,
|
||||
actExp,
|
||||
'',
|
||||
`${minus} [`,
|
||||
`${minus} 1,`,
|
||||
`${minus} 2,`,
|
||||
`${minus} 1`,
|
||||
`${minus} ]`,
|
||||
`${plus} undefined`,
|
||||
].join('\n');
|
||||
assert.throws(
|
||||
() => assert.deepEqual([1, 2, 1]),
|
||||
{ message });
|
||||
|
||||
message = [
|
||||
start,
|
||||
actExp,
|
||||
'',
|
||||
' [',
|
||||
`${minus} 1,`,
|
||||
' 2,',
|
||||
' 1',
|
||||
' ]'
|
||||
].join('\n');
|
||||
assert.throws(
|
||||
() => assert.deepEqual([1, 2, 1], [2, 1]),
|
||||
{ message });
|
||||
|
||||
message = `${start}\n` +
|
||||
`${actExp} ... Lines skipped\n` +
|
||||
'\n' +
|
||||
' [\n' +
|
||||
`${minus} 1,\n`.repeat(10) +
|
||||
'...\n' +
|
||||
`${plus} 2,\n`.repeat(10) +
|
||||
'...';
|
||||
assert.throws(
|
||||
() => assert.deepEqual(Array(12).fill(1), Array(12).fill(2)),
|
||||
{ message });
|
||||
|
||||
// notDeepEqual tests
|
||||
message = 'Identical input passed to notDeepStrictEqual:\n[\n 1\n]';
|
||||
assert.throws(
|
||||
() => assert.notDeepEqual([1], [1]),
|
||||
{ message });
|
||||
|
||||
message = 'Identical input passed to notDeepStrictEqual:' +
|
||||
`\n[${'\n 1,'.repeat(18)}\n...`;
|
||||
const data = Array(21).fill(1);
|
||||
assert.throws(
|
||||
() => assert.notDeepEqual(data, data),
|
||||
{ message });
|
||||
/* eslint-enable no-restricted-properties */
|
||||
}
|
||||
|
||||
common.expectsError(
|
||||
|
Loading…
x
Reference in New Issue
Block a user