Skip to content
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 doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ added: v0.1.21
frames before this function.
* `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`.
Accepted values: `'simple'`, `'full'`.
* `skipPrototype` {boolean} If set to `true`, the error message does not report a
mismatch of the top-level prototypes when the operator is `'deepStrictEqual'`.
Defaults to `false`.

A subclass of {Error} that indicates the failure of an assertion.

Expand Down
3 changes: 3 additions & 0 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, ..
operator: 'deepStrictEqual',
stackStartFn: deepStrictEqual,
diff: this?.[kOptions]?.diff,
// The message must not report a prototype mismatch as the cause of the
// failure when the comparison itself ignored the prototypes.
skipPrototype: this?.[kOptions]?.skipPrototype,
});
}
};
Expand Down
66 changes: 62 additions & 4 deletions lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayPrototype,
ArrayPrototypeJoin,
ArrayPrototypePop,
ArrayPrototypeSlice,
Expand All @@ -9,6 +10,7 @@ const {
ObjectAssign,
ObjectDefineProperty,
ObjectGetPrototypeOf,
ObjectPrototype,
ObjectPrototypeHasOwnProperty,
SafeSet,
String,
Expand Down Expand Up @@ -182,9 +184,64 @@ function isSimpleDiff(actual, inspectedActual, expected, inspectedExpected) {
return typeof actual !== 'object' || actual === null || typeof expected !== 'object' || expected === null;
}

function createErrDiff(actual, expected, operator, customMessage, diffType = 'simple') {
function isNonDefaultPrototype(proto) {
return proto !== null && proto !== ObjectPrototype && proto !== ArrayPrototype;
}

// Returns a short human-readable identifier of the prototype, based on its
// constructor, that is safe to derive on exotic objects.
function getPrototypeName(proto) {
if (proto === null) {
return '(null prototype)';
}
try {
const ctor = proto.constructor;
if (typeof ctor === 'function') {
// Read `name` only once and validate the value before using it: a
// stateful getter may return a string on one access and a different
// value (or throw) on the next one.
const name = ctor.name;
if (typeof name === 'string' && name !== '') {
return name;
}
}
} catch {
// Ignore exotic prototypes that throw on property access.
}
return '(anonymous)';
}

function createErrDiff(actual, expected, operator, customMessage, diffType = 'simple', skipPrototype = false) {
operator = checkOperator(actual, expected, operator);

// `deepStrictEqual` requires both values to share the same prototype, but
// the structural diff may not make that difference obvious (e.g. when both
// values are inspected identically). Surface a mismatch of the top-level
// values explicitly when at least one of the prototypes is not a default
// prototype. Prototype differences of nested objects are not reported.
// Refs: https://github.com/nodejs/node/issues/50397
// Deriving the hint may run arbitrary code (e.g. a `getPrototypeOf` trap of
// a Proxy or a throwing getter) that can throw. Such a failure must not
// replace the assertion error nor leak, so the hint is simply omitted.
let prototypeMismatchMessage = '';
if (!skipPrototype && operator === 'deepStrictEqual' &&
actual !== null && typeof actual === 'object' &&
expected !== null && typeof expected === 'object') {
try {
const actualProto = ObjectGetPrototypeOf(actual);
const expectedProto = ObjectGetPrototypeOf(expected);
if (actualProto !== expectedProto &&
(isNonDefaultPrototype(actualProto) ||
isNonDefaultPrototype(expectedProto))) {
prototypeMismatchMessage =
`\nObject prototypes differ: ${getPrototypeName(actualProto)}` +
` !== ${getPrototypeName(expectedProto)}`;
}
} catch {
// Omit the hint when it cannot be derived safely.
}
}

let skipped = false;
let message = '';
const inspectedActual = inspectValue(actual);
Expand Down Expand Up @@ -232,7 +289,7 @@ function createErrDiff(actual, expected, operator, customMessage, diffType = 'si
const headerMessage = `${getErrorMessage(operator, customMessage)}\n${header}`;
const skippedMessage = skipped ? '\n... Skipped lines' : '';

return `${headerMessage}${skippedMessage}\n${message}\n`;
return `${headerMessage}${skippedMessage}\n${message}${prototypeMismatchMessage}\n`;
}

function addEllipsis(string) {
Expand All @@ -257,6 +314,7 @@ class AssertionError extends Error {
// Compatibility with older versions.
stackStartFunction,
diff = 'simple',
skipPrototype = false,
} = options;
let {
actual,
Expand All @@ -268,7 +326,7 @@ class AssertionError extends Error {

if (message != null) {
if (kMethodsWithCustomMessageDiff.has(operator)) {
super(createErrDiff(actual, expected, operator, message, diff));
super(createErrDiff(actual, expected, operator, message, diff, skipPrototype));
} else {
super(String(message));
}
Expand All @@ -288,7 +346,7 @@ class AssertionError extends Error {
}

if (kMethodsWithCustomMessageDiff.has(operator)) {
super(createErrDiff(actual, expected, operator, message, diff));
super(createErrDiff(actual, expected, operator, message, diff, skipPrototype));
} else if (operator === 'notDeepStrictEqual' ||
operator === 'notStrictEqual') {
// In case the objects are equal but the operator requires unequal, show
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/assert/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)];
* @property {string} operator Operator
* @property {Function} stackStartFn Stack start function
* @property {'simple' | 'full'} [diff] Diff mode
* @property {boolean} [skipPrototype] Set to `true` when the comparison
* ignored the prototypes, so the message does not report a prototype
* mismatch as the cause of the failure
* @property {boolean} [generatedMessage] Generated message
*/

Expand Down
12 changes: 8 additions & 4 deletions test/parallel/test-assert-deep.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ test('deepEqual', () => {
' 121,\n' +
' 122,\n' +
' 10\n' +
' ]\n'
' ]\n' +
'Object prototypes differ: Uint8Array !== Buffer\n'
}
);
assert.deepEqual(arr, buf);
Expand Down Expand Up @@ -138,7 +139,8 @@ test('date', () => {
code: 'ERR_ASSERTION',
message: `${defaultMsgStartFull}\n\n` +
'+ 2016-01-01T00:00:00.000Z\n- MyDate 2016-01-01T00:00:00.000Z' +
" {\n- '0': '1'\n- }\n"
" {\n- '0': '1'\n- }\n" +
'Object prototypes differ: Date !== MyDate\n'
}
);
assert.throws(
Expand All @@ -147,7 +149,8 @@ test('date', () => {
code: 'ERR_ASSERTION',
message: `${defaultMsgStartFull}\n\n` +
'+ MyDate 2016-01-01T00:00:00.000Z {\n' +
"+ '0': '1'\n+ }\n- 2016-01-01T00:00:00.000Z\n"
"+ '0': '1'\n+ }\n- 2016-01-01T00:00:00.000Z\n" +
'Object prototypes differ: MyDate !== Date\n'
}
);
});
Expand All @@ -162,7 +165,8 @@ test('regexp', () => {
{
code: 'ERR_ASSERTION',
message: `${defaultMsgStartFull}\n\n` +
"+ /test/\n- MyRegExp /test/ {\n- '0': '1'\n- }\n"
"+ /test/\n- MyRegExp /test/ {\n- '0': '1'\n- }\n" +
'Object prototypes differ: RegExp !== MyRegExp\n'
}
);
});
Expand Down
Loading
Loading