assert: improve loose assertion message

So far the error message from a loose assertion is not always very
informative and could be misleading. This is fixed by:

* showing more from the actual error message
* having a better error description
* not using custom inspection
* inspecting a higher depth
* inspecting the full array instead of up to 100 entries

PR-URL: https://github.com/nodejs/node/pull/22155
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Ruben Bridgewater 2018-08-06 18:20:44 +02:00
parent 566d11a1ca
commit 1d859ef532
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
2 changed files with 88 additions and 110 deletions

View File

@ -12,9 +12,13 @@ let white = '';
const kReadableOperator = { const kReadableOperator = {
deepStrictEqual: 'Expected inputs to be strictly deep-equal:', deepStrictEqual: 'Expected inputs to be strictly deep-equal:',
notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
strictEqual: 'Expected inputs to be strictly equal:', strictEqual: 'Expected inputs to be strictly equal:',
deepEqual: 'Expected inputs to be loosely deep-equal:',
equal: 'Expected inputs to be loosely equal:',
notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
notStrictEqual: 'Expected "actual" to be strictly unequal to:', notStrictEqual: 'Expected "actual" to be strictly unequal to:',
notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
notEqual: 'Expected "actual" to be loosely unequal to:',
notIdentical: 'Inputs identical but not reference equal:', notIdentical: 'Inputs identical but not reference equal:',
}; };
@ -50,7 +54,7 @@ function inspectValue(val) {
// Assert does not detect proxies currently. // Assert does not detect proxies currently.
showProxy: false showProxy: false
} }
).split('\n'); );
} }
function createErrDiff(actual, expected, operator) { function createErrDiff(actual, expected, operator) {
@ -59,8 +63,9 @@ function createErrDiff(actual, expected, operator) {
let lastPos = 0; let lastPos = 0;
let end = ''; let end = '';
let skipped = false; let skipped = false;
const actualLines = inspectValue(actual); const actualInspected = inspectValue(actual);
const expectedLines = inspectValue(expected); const actualLines = actualInspected.split('\n');
const expectedLines = inspectValue(expected).split('\n');
const msg = kReadableOperator[operator] + const msg = kReadableOperator[operator] +
`\n${green}+ actual${white} ${red}- expected${white}`; `\n${green}+ actual${white} ${red}- expected${white}`;
const skippedMsg = ` ${blue}...${white} Lines skipped`; const skippedMsg = ` ${blue}...${white} Lines skipped`;
@ -126,7 +131,7 @@ function createErrDiff(actual, expected, operator) {
// E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() }) // E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() })
if (maxLines === 0) { if (maxLines === 0) {
// We have to get the result again. The lines were all removed before. // We have to get the result again. The lines were all removed before.
const actualLines = inspectValue(actual); const actualLines = actualInspected.split('\n');
// Only remove lines in case it makes sense to collapse those. // Only remove lines in case it makes sense to collapse those.
// TODO: Accept env to always show the full error. // TODO: Accept env to always show the full error.
@ -268,7 +273,7 @@ class AssertionError extends Error {
operator === 'notStrictEqual') { operator === 'notStrictEqual') {
// In case the objects are equal but the operator requires unequal, show // In case the objects are equal but the operator requires unequal, show
// the first object and say A equals B // the first object and say A equals B
const res = inspectValue(actual); const res = inspectValue(actual).split('\n');
const base = kReadableOperator[operator]; const base = kReadableOperator[operator];
// Only remove lines in case it makes sense to collapse those. // Only remove lines in case it makes sense to collapse those.
@ -287,13 +292,29 @@ class AssertionError extends Error {
super(`${base}\n\n${res.join('\n')}\n`); super(`${base}\n\n${res.join('\n')}\n`);
} }
} else { } else {
let res = inspect(actual); let res = inspectValue(actual);
let other = inspect(expected); let other = '';
if (res.length > 128) const knownOperators = kReadableOperator[operator];
res = `${res.slice(0, 125)}...`; if (operator === 'notDeepEqual' || operator === 'notEqual') {
if (other.length > 128) res = `${kReadableOperator[operator]}\n\n${res}`;
other = `${other.slice(0, 125)}...`; if (res.length > 1024) {
super(`${res} ${operator} ${other}`); res = `${res.slice(0, 1021)}...`;
}
} else {
other = `${inspectValue(expected)}`;
if (res.length > 512) {
res = `${res.slice(0, 509)}...`;
}
if (other.length > 512) {
other = `${other.slice(0, 509)}...`;
}
if (operator === 'deepEqual' || operator === 'equal') {
res = `${knownOperators}\n\n${res}\n\nshould equal\n\n`;
} else {
other = ` ${operator} ${other}`;
}
}
super(`${res}${other}`);
} }
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const common = require('../common'); require('../common');
const assert = require('assert'); const assert = require('assert');
const util = require('util'); const util = require('util');
const { AssertionError } = assert; const { AssertionError } = assert;
@ -16,18 +16,23 @@ if (process.stdout.isTTY)
// Template tag function turning an error message into a RegExp // Template tag function turning an error message into a RegExp
// for assert.throws() // for assert.throws()
function re(literals, ...values) { function re(literals, ...values) {
let result = literals[0]; let result = 'Expected inputs to be loosely deep-equal:\n\n';
const escapeRE = /[\\^$.*+?()[\]{}|=!<>:-]/g;
for (const [i, value] of values.entries()) { for (const [i, value] of values.entries()) {
const str = util.inspect(value); const str = util.inspect(value, {
compact: false,
depth: 1000,
customInspect: false,
maxArrayLength: Infinity,
breakLength: Infinity
});
// Need to escape special characters. // Need to escape special characters.
result += str.replace(escapeRE, '\\$&'); result += str;
result += literals[i + 1]; result += literals[i + 1];
} }
return common.expectsError({ return {
code: 'ERR_ASSERTION', code: 'ERR_ASSERTION',
message: new RegExp(`^${result}$`) message: result
}); };
} }
// The following deepEqual tests might seem very weird. // The following deepEqual tests might seem very weird.
@ -173,13 +178,6 @@ assert.throws(
} }
} }
common.expectsError(() => {
assert.deepEqual(new Set([{ a: 0 }]), new Set([{ a: 1 }]));
}, {
code: 'ERR_ASSERTION',
message: /^Set { { a: 0 } } deepEqual Set { { a: 1 } }$/
});
function assertDeepAndStrictEqual(a, b) { function assertDeepAndStrictEqual(a, b) {
assert.deepEqual(a, b); assert.deepEqual(a, b);
assert.deepStrictEqual(a, b); assert.deepStrictEqual(a, b);
@ -189,13 +187,19 @@ function assertDeepAndStrictEqual(a, b) {
} }
function assertNotDeepOrStrict(a, b, err) { function assertNotDeepOrStrict(a, b, err) {
assert.throws(() => assert.deepEqual(a, b), err || re`${a} deepEqual ${b}`); assert.throws(
() => assert.deepEqual(a, b),
err || re`${a}\n\nshould equal\n\n${b}`
);
assert.throws( assert.throws(
() => assert.deepStrictEqual(a, b), () => assert.deepStrictEqual(a, b),
err || { code: 'ERR_ASSERTION' } err || { code: 'ERR_ASSERTION' }
); );
assert.throws(() => assert.deepEqual(b, a), err || re`${b} deepEqual ${a}`); assert.throws(
() => assert.deepEqual(b, a),
err || re`${b}\n\nshould equal\n\n${a}`
);
assert.throws( assert.throws(
() => assert.deepStrictEqual(b, a), () => assert.deepStrictEqual(b, a),
err || { code: 'ERR_ASSERTION' } err || { code: 'ERR_ASSERTION' }
@ -225,6 +229,7 @@ assertNotDeepOrStrict(new Set([1, 2, 3]), new Set([1, 2, 3, 4]));
assertNotDeepOrStrict(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); assertNotDeepOrStrict(new Set([1, 2, 3, 4]), new Set([1, 2, 3]));
assertDeepAndStrictEqual(new Set(['1', '2', '3']), new Set(['1', '2', '3'])); assertDeepAndStrictEqual(new Set(['1', '2', '3']), new Set(['1', '2', '3']));
assertDeepAndStrictEqual(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]])); assertDeepAndStrictEqual(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]]));
assertNotDeepOrStrict(new Set([{ a: 0 }]), new Set([{ a: 1 }]));
{ {
const a = [ 1, 2 ]; const a = [ 1, 2 ];
@ -626,41 +631,16 @@ assert.throws(
assert.notDeepEqual(new Date(), new Date(2000, 3, 14)); assert.notDeepEqual(new Date(), new Date(2000, 3, 14));
assert.deepEqual(/a/, /a/); assertDeepAndStrictEqual(/a/, /a/);
assert.deepEqual(/a/g, /a/g); assertDeepAndStrictEqual(/a/g, /a/g);
assert.deepEqual(/a/i, /a/i); assertDeepAndStrictEqual(/a/i, /a/i);
assert.deepEqual(/a/m, /a/m); assertDeepAndStrictEqual(/a/m, /a/m);
assert.deepEqual(/a/igm, /a/igm); assertDeepAndStrictEqual(/a/igm, /a/igm);
assert.throws(() => assert.deepEqual(/ab/, /a/), assertNotDeepOrStrict(/ab/, /a/);
{ assertNotDeepOrStrict(/a/g, /a/);
code: 'ERR_ASSERTION', assertNotDeepOrStrict(/a/i, /a/);
name: 'AssertionError [ERR_ASSERTION]', assertNotDeepOrStrict(/a/m, /a/);
message: '/ab/ deepEqual /a/' assertNotDeepOrStrict(/a/igm, /a/im);
});
assert.throws(() => assert.deepEqual(/a/g, /a/),
{
code: 'ERR_ASSERTION',
name: 'AssertionError [ERR_ASSERTION]',
message: '/a/g deepEqual /a/'
});
assert.throws(() => assert.deepEqual(/a/i, /a/),
{
code: 'ERR_ASSERTION',
name: 'AssertionError [ERR_ASSERTION]',
message: '/a/i deepEqual /a/'
});
assert.throws(() => assert.deepEqual(/a/m, /a/),
{
code: 'ERR_ASSERTION',
name: 'AssertionError [ERR_ASSERTION]',
message: '/a/m deepEqual /a/'
});
assert.throws(() => assert.deepEqual(/a/igm, /a/im),
{
code: 'ERR_ASSERTION',
name: 'AssertionError [ERR_ASSERTION]',
message: '/a/gim deepEqual /a/im'
});
{ {
const re1 = /a/g; const re1 = /a/g;
@ -720,23 +700,32 @@ nameBuilder2.prototype = Object;
nb2 = new nameBuilder2('Ryan', 'Dahl'); nb2 = new nameBuilder2('Ryan', 'Dahl');
assert.deepEqual(nb1, nb2); assert.deepEqual(nb1, nb2);
// Primitives and object. // Primitives
assert.throws(() => assert.deepEqual(null, {}), AssertionError); assertNotDeepOrStrict(null, {});
assert.throws(() => assert.deepEqual(undefined, {}), AssertionError); assertNotDeepOrStrict(undefined, {});
assert.throws(() => assert.deepEqual('a', ['a']), AssertionError); assertNotDeepOrStrict('a', ['a']);
assert.throws(() => assert.deepEqual('a', { 0: 'a' }), AssertionError); assertNotDeepOrStrict('a', { 0: 'a' });
assert.throws(() => assert.deepEqual(1, {}), AssertionError); assertNotDeepOrStrict(1, {});
assert.throws(() => assert.deepEqual(true, {}), AssertionError); assertNotDeepOrStrict(true, {});
assert.throws(() => assert.deepEqual(Symbol(), {}), AssertionError); assertNotDeepOrStrict(Symbol(), {});
assertNotDeepOrStrict(Symbol(), Symbol());
assertOnlyDeepEqual(4, '4');
assertOnlyDeepEqual(true, 1);
{
const s = Symbol();
assertDeepAndStrictEqual(s, s);
}
// Primitive wrappers and object. // Primitive wrappers and object.
assert.deepEqual(new String('a'), ['a']); assertOnlyDeepEqual(new String('a'), ['a']);
assert.deepEqual(new String('a'), { 0: 'a' }); assertOnlyDeepEqual(new String('a'), { 0: 'a' });
assert.deepEqual(new Number(1), {}); assertOnlyDeepEqual(new Number(1), {});
assert.deepEqual(new Boolean(true), {}); assertOnlyDeepEqual(new Boolean(true), {});
// Same number of keys but different key names. // Same number of keys but different key names.
assert.throws(() => assert.deepEqual({ a: 1 }, { b: 1 }), AssertionError); assertNotDeepOrStrict({ a: 1 }, { b: 1 });
assert.deepStrictEqual(new Date(2000, 3, 14), new Date(2000, 3, 14)); assert.deepStrictEqual(new Date(2000, 3, 14), new Date(2000, 3, 14));
@ -757,11 +746,6 @@ assert.throws(
assert.notDeepStrictEqual(new Date(), new Date(2000, 3, 14)); assert.notDeepStrictEqual(new Date(), new Date(2000, 3, 14));
assert.deepStrictEqual(/a/, /a/);
assert.deepStrictEqual(/a/g, /a/g);
assert.deepStrictEqual(/a/i, /a/i);
assert.deepStrictEqual(/a/m, /a/m);
assert.deepStrictEqual(/a/igm, /a/igm);
assert.throws( assert.throws(
() => assert.deepStrictEqual(/ab/, /a/), () => assert.deepStrictEqual(/ab/, /a/),
{ {
@ -871,33 +855,6 @@ obj2 = new Constructor2('Ryan', 'Dahl');
assert.deepStrictEqual(obj1, obj2); assert.deepStrictEqual(obj1, obj2);
// primitives
assert.throws(() => assert.deepStrictEqual(4, '4'), AssertionError);
assert.throws(() => assert.deepStrictEqual(true, 1), AssertionError);
assert.throws(() => assert.deepStrictEqual(Symbol(), Symbol()),
AssertionError);
const s = Symbol();
assert.deepStrictEqual(s, s);
// Primitives and object.
assert.throws(() => assert.deepStrictEqual(null, {}), AssertionError);
assert.throws(() => assert.deepStrictEqual(undefined, {}), AssertionError);
assert.throws(() => assert.deepStrictEqual('a', ['a']), AssertionError);
assert.throws(() => assert.deepStrictEqual('a', { 0: 'a' }), AssertionError);
assert.throws(() => assert.deepStrictEqual(1, {}), AssertionError);
assert.throws(() => assert.deepStrictEqual(true, {}), AssertionError);
assert.throws(() => assert.deepStrictEqual(Symbol(), {}), AssertionError);
// Primitive wrappers and object.
assert.throws(() => assert.deepStrictEqual(new String('a'), ['a']),
AssertionError);
assert.throws(() => assert.deepStrictEqual(new String('a'), { 0: 'a' }),
AssertionError);
assert.throws(() => assert.deepStrictEqual(new Number(1), {}), AssertionError);
assert.throws(() => assert.deepStrictEqual(new Boolean(true), {}),
AssertionError);
// Check extra properties on errors. // Check extra properties on errors.
{ {
const a = new TypeError('foo'); const a = new TypeError('foo');