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
73 changes: 66 additions & 7 deletions lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,41 @@ function isSimpleDiff(actual, inspectedActual, expected, inspectedExpected) {
return typeof actual !== 'object' || actual === null || typeof expected !== 'object' || expected === null;
}

// Produce a short, human-readable label for the prototype of `val` to be used
// when surfacing prototype-mismatch information in the assertion diff. The
// returned label is intentionally compact so that it can be appended to the
// existing diff without overwhelming it.
function describePrototype(val) {
const proto = ObjectGetPrototypeOf(val);
if (proto === null) {
return '[Object: null prototype]';
}
const ctor = proto.constructor;
if (typeof ctor === 'function' && typeof ctor.name === 'string' && ctor.name !== '') {
return ctor.name;
}
return '<anonymous>';
}

// Detect the case where two values inspect identically but are not
// deep-strict-equal because their prototypes differ. In that situation the
// usual diff is unhelpful (it shows the same text on both sides), so we want
// to make the prototype mismatch explicit. Only top-level objects/functions
// are considered; primitives reach this code path only when they trivially
// fail simpler comparisons.
function hasTopLevelPrototypeMismatch(actual, expected) {
if (actual === null || expected === null) {
return false;
}
const typeofActual = typeof actual;
const typeofExpected = typeof expected;
if ((typeofActual !== 'object' && typeofActual !== 'function') ||
(typeofExpected !== 'object' && typeofExpected !== 'function')) {
return false;
}
return ObjectGetPrototypeOf(actual) !== ObjectGetPrototypeOf(expected);
}

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

Expand All @@ -204,15 +239,39 @@ function createErrDiff(actual, expected, operator, customMessage, diffType = 'si
skipped = true;
}
} else if (inspectedActual === inspectedExpected) {
// Handles the case where the objects are structurally the same but different references
operator = 'notIdentical';
if (inspectedSplitActual.length > 50 && diffType !== 'full') {
message = `${ArrayPrototypeJoin(ArrayPrototypeSlice(inspectedSplitActual, 0, 50), '\n')}\n...}`;
skipped = true;
// The two values inspect identically. For deep-equality operators this
// typically means the prototypes differ in a way `inspect` cannot show
// (e.g. anonymous classes, or two distinct prototype objects with the
// same shape). Surface the prototype mismatch explicitly when present;
// otherwise fall back to the existing "not reference-equal" message,
// which covers e.g. `strictEqual([], [])`.
const prototypeMismatch = (operator === 'deepStrictEqual' ||
operator === 'partialDeepStrictEqual') &&
hasTopLevelPrototypeMismatch(actual, expected);
if (prototypeMismatch) {
const actualProto = describePrototype(actual);
const expectedProto = describePrototype(expected);
const protoLine = `Object prototypes differ: ${actualProto} !== ${expectedProto}`;
let body;
if (inspectedSplitActual.length > 50 && diffType !== 'full') {
body = `${ArrayPrototypeJoin(ArrayPrototypeSlice(inspectedSplitActual, 0, 50), '\n')}\n...}`;
skipped = true;
} else {
body = ArrayPrototypeJoin(inspectedSplitActual, '\n');
}
message = `${protoLine}\n\n${body}`;
header = '';
} else {
message = ArrayPrototypeJoin(inspectedSplitActual, '\n');
// Handles the case where the objects are structurally the same but different references
operator = 'notIdentical';
if (inspectedSplitActual.length > 50 && diffType !== 'full') {
message = `${ArrayPrototypeJoin(ArrayPrototypeSlice(inspectedSplitActual, 0, 50), '\n')}\n...}`;
skipped = true;
} else {
message = ArrayPrototypeJoin(inspectedSplitActual, '\n');
}
header = '';
}
header = '';
} else {
const checkCommaDisparity = actual != null && typeof actual === 'object';
const diff = myersDiff(inspectedSplitActual, inspectedSplitExpected, checkCommaDisparity);
Expand Down
115 changes: 115 additions & 0 deletions test/parallel/test-assert-deep-prototype-diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use strict';
require('../common');
const assert = require('assert');
const { test } = require('node:test');

// Disable colored output to prevent color codes from breaking assertion
// message comparisons. This should only be an issue when process.stdout
// is a TTY.
if (process.stdout.isTTY) {
process.env.NODE_DISABLE_COLORS = '1';
}

// Regression tests for https://github.com/nodejs/node/issues/50397.
//
// When `assert.deepStrictEqual` fails solely because the two values have
// different prototypes, the diff used to render as `{} !== {}` (or similar),
// giving the user no clue about why the assertion failed. The assertion
// error formatter now surfaces the prototype mismatch explicitly.

test('deepStrictEqual surfaces anonymous-class prototype mismatch', () => {
const A = (() => class {})();
const B = (() => class {})();
assert.throws(
() => assert.deepStrictEqual(new A(), new B()),
(err) => {
assert.strictEqual(err.code, 'ERR_ASSERTION');
assert.match(err.message, /Object prototypes differ:/);
// The previous "Values have same structure but are not reference-equal"
// message must no longer be used for prototype-only mismatches because
// it is misleading: the values do not even have the same prototype.
assert.doesNotMatch(err.message, /same structure but are not reference-equal/);
return true;
}
);
});

test('deepStrictEqual surfaces named-class prototype mismatch', () => {
class Foo {}
class Bar {}
assert.throws(
() => assert.deepStrictEqual(new Foo(), new Bar()),
(err) => {
assert.strictEqual(err.code, 'ERR_ASSERTION');
// Both class names should appear somewhere in the rendered message:
// either in the existing inspect-based diff (`+ Foo {}` / `- Bar {}`)
// or in the new "Object prototypes differ:" line. The important
// guarantee is that the user can identify both prototypes from the
// error message alone.
assert.match(err.message, /Foo/);
assert.match(err.message, /Bar/);
return true;
}
);
});

test('deepStrictEqual surfaces null-prototype mismatch', () => {
const a = { __proto__: null };
const b = {};
assert.throws(
() => assert.deepStrictEqual(a, b),
(err) => {
assert.strictEqual(err.code, 'ERR_ASSERTION');
assert.match(err.message, /null prototype/);
return true;
}
);
});

test('deepStrictEqual prototype-mismatch message is helpful for empty objects', () => {
// This is the most pathological case: both sides inspect identically as
// `{}`, so without the prototype-mismatch information the diff body alone
// is useless. The fix must produce an explanatory line.
const A = (() => class {})();
const B = (() => class {})();
let captured;
try {
assert.deepStrictEqual(new A(), new B());
} catch (err) {
captured = err;
}
assert.ok(captured, 'deepStrictEqual should have thrown');
assert.match(captured.message, /Object prototypes differ:/);
});

test('strictEqual on structurally-equal arrays still uses notIdentical message', () => {
// Sanity check that the new code path does not regress the existing
// behavior of `strictEqual([], [])`. That comparison continues to use
// the "Values have same structure but are not reference-equal" message
// because the prototypes do match (both are Array.prototype).
assert.throws(
() => assert.strictEqual([], []),
{
code: 'ERR_ASSERTION',
message: 'Values have same structure but are not reference-equal:\n\n[]\n',
}
);
});

test('deepStrictEqual on structurally-equal values with same prototype still fails clearly', () => {
// When the prototypes match but the values are not reference-equal, the
// existing notIdentical fallback should still apply (deepStrictEqual on
// two equal-shape objects with the same prototype should normally pass;
// here we use objects whose enumerable properties differ to exercise the
// ordinary diff path and confirm it is unaffected).
assert.throws(
() => assert.deepStrictEqual({ a: 1 }, { a: 2 }),
(err) => {
assert.strictEqual(err.code, 'ERR_ASSERTION');
// The ordinary diff path must NOT mention prototype differences
// because the prototypes are identical.
assert.doesNotMatch(err.message, /Object prototypes differ:/);
return true;
}
);
});
Loading