Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

assert: fix _deepEqual and improve assert.md #11128

Closed
wants to merge 2 commits into from
Closed
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: 57 additions & 16 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
@@ -43,13 +43,17 @@ changes:
-->

Tests for deep equality between the `actual` and `expected` parameters.
Primitive values are compared with the equal comparison operator ( `==` ).

Only enumerable "own" properties are considered. The `deepEqual()`
implementation does not test object prototypes, attached symbols, or
non-enumerable properties. This can lead to some potentially surprising
results. For example, the following example does not throw an `AssertionError`
because the properties on the [`Error`][] object are non-enumerable:
Primitive values are compared with the [Abstract Equality Comparison][]
( `==` ).

Only [enumerable "own" properties][] are considered. The
[`assert.deepEqual()`][] implementation does not test the
[`[[Prototype]]`][prototype-spec] of objects, attached symbols, or
non-enumerable properties — for such checks, consider using
[assert.deepStrictEqual()][] instead. This can lead to some
potentially surprising results. For example, the following example does not
throw an `AssertionError` because the properties on the [`Error`][] object are
not enumerable:

```js
// WARNING: This does not throw an AssertionError!
@@ -113,17 +117,20 @@ changes:
description: Handle non-`Uint8Array` typed arrays correctly.
-->

Generally identical to `assert.deepEqual()` with two exceptions. First,
primitive values are compared using the strict equality operator ( `===` ).
Second, object comparisons include a strict equality check of their prototypes.
Generally identical to `assert.deepEqual()` with two exceptions:

1. Primitive values are compared using the [Strict Equality Comparison][]
( `===` ).
2. [`[[Prototype]]`][prototype-spec] of objects are compared using
the [Strict Equality Comparison][] too.

```js
const assert = require('assert');

assert.deepEqual({a:1}, {a:'1'});
assert.deepEqual({a: 1}, {a: '1'});
// OK, because 1 == '1'

assert.deepStrictEqual({a:1}, {a:'1'});
assert.deepStrictEqual({a: 1}, {a: '1'});
// AssertionError: { a: 1 } deepStrictEqual { a: '1' }
// because 1 !== '1' using strict equality
```
@@ -200,7 +207,7 @@ added: v0.1.21
-->

Tests shallow, coercive equality between the `actual` and `expected` parameters
using the equal comparison operator ( `==` ).
using the [Abstract Equality Comparison][] ( `==` ).

```js
const assert = require('assert');
@@ -330,7 +337,7 @@ the `message` parameter is undefined, a default error message is assigned.
added: v0.1.21
-->

Tests shallow, coercive inequality with the not equal comparison operator
Tests shallow, coercive inequality with the [Abstract Equality Comparison][]
( `!=` ).

```js
@@ -355,7 +362,7 @@ parameter is undefined, a default error message is assigned.
added: v0.1.21
-->

Tests strict inequality as determined by the strict not equal operator
Tests strict inequality as determined by the [Strict Equality Comparison][]
( `!==` ).

```js
@@ -407,7 +414,8 @@ assert.ok(false, 'it\'s false');
added: v0.1.21
-->

Tests strict equality as determined by the strict equality operator ( `===` ).
Tests strict equality as determined by the [Strict Equality Comparison][]
( `===` ).

```js
const assert = require('assert');
@@ -493,10 +501,43 @@ assert.throws(myFunction, 'missing foo', 'did not throw with expected message');
assert.throws(myFunction, /missing foo/, 'did not throw with expected message');
```

## Caveats

For the following cases, consider using ES2015 [`Object.is()`][],
which uses the [SameValueZero][] comparison.

```js
const a = 0;
const b = -a;
assert.notStrictEqual(a, b);
// AssertionError: 0 !== -0
// Strict Equality Comparison doesn't distinguish between -0 and +0...
assert(!Object.is(a, b));
// but Object.is() does!

const str1 = "foo";
const str2 = "foo";
assert.strictEqual(str1 / 1, str2 / 1);
// AssertionError: NaN === NaN
// Strict Equality Comparison can't be used to check NaN...
assert(Object.is(str1 / 1, str2 / 1));
// but Object.is() can!
```

For more information, see
[MDN's guide on equality comparisons and sameness][mdn-equality-guide].

[`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message
[`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message
[`assert.ok()`]: #assert_assert_ok_value_message
[`assert.throws()`]: #assert_assert_throws_block_error_message
[`Error`]: errors.html#errors_class_error
[`RegExp`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
[`TypeError`]: errors.html#errors_class_typeerror
[Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison
[Strict Equality Comparison]: https://tc39.github.io/ecma262/#sec-strict-equality-comparison
[`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
[SameValueZero]: https://tc39.github.io/ecma262/#sec-samevaluezero
[prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots
[mdn-equality-guide]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness
[enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties
229 changes: 144 additions & 85 deletions lib/assert.js
Original file line number Diff line number Diff line change
@@ -23,8 +23,8 @@
// UTILITY
const compare = process.binding('buffer').compare;
const util = require('util');
const objectToString = require('internal/util').objectToString;
const Buffer = require('buffer').Buffer;
const pToString = (obj) => Object.prototype.toString.call(obj);

// The assert module provides functions that throw
// AssertionError's when particular conditions are not met. The
@@ -136,117 +136,177 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
}
};

function areSimilarRegExps(a, b) {
return a.source === b.source && a.flags === b.flags;
}

function areSimilarTypedArrays(a, b) {
return compare(Buffer.from(a.buffer,
a.byteOffset,
a.byteLength),
Buffer.from(b.buffer,
b.byteOffset,
b.byteLength)) === 0;
}

function isNullOrNonObj(object) {
return object === null || typeof object !== 'object';
}

function isFloatTypedArrayTag(tag) {
return tag === '[object Float32Array]' || tag === '[object Float64Array]';
}

function isArguments(tag) {
return tag === '[object Arguments]';
}

function _deepEqual(actual, expected, strict, memos) {
// All identical values are equivalent, as determined by ===.
if (actual === expected) {
return true;
}

// If both values are instances of buffers, equivalence is
// determined by comparing the values and ensuring the result
// === 0.
} else if (actual instanceof Buffer && expected instanceof Buffer) {
return compare(actual, expected) === 0;

// If the expected value is a Date object, the actual value is
// equivalent if it is also a Date object that refers to the same time.
} else if (util.isDate(actual) && util.isDate(expected)) {
return actual.getTime() === expected.getTime();

// If the expected value is a RegExp object, the actual value is
// equivalent if it is also a RegExp object with the same source and
// properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
} else if (util.isRegExp(actual) && util.isRegExp(expected)) {
return actual.source === expected.source &&
actual.global === expected.global &&
actual.multiline === expected.multiline &&
actual.lastIndex === expected.lastIndex &&
actual.ignoreCase === expected.ignoreCase;

// If both values are primitives, equivalence is determined by
// == or, if checking for strict equivalence, ===.
} else if ((actual === null || typeof actual !== 'object') &&
(expected === null || typeof expected !== 'object')) {
// For primitives / functions
// (determined by typeof value !== 'object'),
// or null, equivalence is determined by === or ==.
if (isNullOrNonObj(actual) && isNullOrNonObj(expected)) {
return strict ? actual === expected : actual == expected;
}

// If both values are instances of typed arrays, wrap their underlying
// ArrayBuffers in a Buffer to increase performance.
// This optimization requires the arrays to have the same type as checked by
// Object.prototype.toString (pToString). Never perform binary
// comparisons for Float*Arrays, though, since +0 === -0 is true despite the
// two values' bit patterns not being identical.
} else if (ArrayBuffer.isView(actual) && ArrayBuffer.isView(expected) &&
pToString(actual) === pToString(expected) &&
!(actual instanceof Float32Array ||
actual instanceof Float64Array)) {
return compare(Buffer.from(actual.buffer,
actual.byteOffset,
actual.byteLength),
Buffer.from(expected.buffer,
expected.byteOffset,
expected.byteLength)) === 0;

// For all other Object pairs, including Array objects, equivalence is
// determined by having the same number of owned properties (as verified
// with Object.prototype.hasOwnProperty.call), the same set of keys
// (although not necessarily the same order), equivalent values for every
// corresponding key, and an identical 'prototype' property. Note: this
// accounts for both named and indexed properties on Arrays.
} else {
memos = memos || {actual: [], expected: []};
// If they bypass the previous check, then at least
// one of them must be an non-null object.
// If the other one is null or undefined, they must not be equal.
if (actual === null || actual === undefined ||
expected === null || expected === undefined)
return false;

const actualIndex = memos.actual.indexOf(actual);
if (actualIndex !== -1) {
if (actualIndex === memos.expected.indexOf(expected)) {
return true;
}
// Notes: Type tags are historical [[Class]] properties that can be set by
// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS
// and retrieved using Object.prototype.toString.call(obj) in JS
// See https://tc39.github.io/ecma262/#sec-object.prototype.tostring
// for a list of tags pre-defined in the spec.
// There are some unspecified tags in the wild too (e.g. typed array tags).
// Since tags can be altered, they only serve fast failures
const actualTag = objectToString(actual);
const expectedTag = objectToString(expected);

// Passing null or undefined to Object.getPrototypeOf() will throw
// so this must done after previous checks.
// For strict comparison, objects should have
// a) The same prototypes.
// b) The same built-in type tags
if (strict) {
if (Object.getPrototypeOf(actual) !== Object.getPrototypeOf(expected)) {
return false;
}
}

memos.actual.push(actual);
memos.expected.push(expected);
// Do fast checks for builtin types.
// If they don't match, they must not be equal.
// If they match, return true for non-strict comparison.
// For strict comparison we need to exam further.

return objEquiv(actual, expected, strict, memos);
// If both values are Date objects,
// check if the time underneath are equal first.
if (util.isDate(actual) && util.isDate(expected)) {
if (actual.getTime() !== expected.getTime()) {
return false;
} else if (!strict) {
return true; // Skip further checks for non-strict comparison.
}
}
}

function isArguments(object) {
return Object.prototype.toString.call(object) === '[object Arguments]';
}
// If both values are RegExp, check if they have
// the same source and flags first
if (util.isRegExp(actual) && util.isRegExp(expected)) {
if (!areSimilarRegExps(actual, expected)) {
return false;
} else if (!strict) {
return true; // Skip further checks for non-strict comparison.
}
}

function objEquiv(a, b, strict, actualVisitedObjects) {
if (a === null || a === undefined || b === null || b === undefined)
// Ensure reflexivity of deepEqual with `arguments` objects.
// See https://github.com/nodejs/node-v0.x-archive/pull/7178
if (isArguments(actualTag) !== isArguments(expectedTag)) {
return false;
}

// Check typed arrays and buffers by comparing the content in their
// underlying ArrayBuffer. This optimization requires that it's
// reasonable to interpret their underlying memory in the same way,
// which is checked by comparing their type tags.
// (e.g. a Uint8Array and a Uint16Array with the same memory content
// could still be different because they will be interpreted differently)
// Never perform binary comparisons for Float*Arrays, though,
// since e.g. +0 === -0 is true despite the two values' bit patterns
// not being identical.
if (ArrayBuffer.isView(actual) && ArrayBuffer.isView(expected) &&
actualTag === expectedTag && !isFloatTypedArrayTag(actualTag)) {
if (!areSimilarTypedArrays(actual, expected)) {
return false;
} else if (!strict) {
return true; // Skip further checks for non-strict comparison.
}

// Buffer.compare returns true, so actual.length === expected.length
// if they both only contain numeric keys, we don't need to exam further
if (Object.keys(actual).length === actual.length &&
Object.keys(expected).length === expected.length) {
return true;
}
}

// For all other Object pairs, including Array objects,
// equivalence is determined by having:
// a) The same number of owned enumerable properties
// b) The same set of keys/indexes (although not necessarily the same order)
// c) Equivalent values for every corresponding key/index
// Note: this accounts for both named and indexed properties on Arrays.

// Use memos to handle cycles.
memos = memos || { actual: [], expected: [] };
const actualIndex = memos.actual.indexOf(actual);
if (actualIndex !== -1) {
if (actualIndex === memos.expected.indexOf(expected)) {
return true;
}
}
memos.actual.push(actual);
memos.expected.push(expected);

// If one is a primitive, the other must be the same.
return objEquiv(actual, expected, strict, memos);
}

function objEquiv(a, b, strict, actualVisitedObjects) {
// If one of them is a primitive, the other must be the same.
if (util.isPrimitive(a) || util.isPrimitive(b))
return a === b;
if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
return false;
const aIsArgs = isArguments(a);
const bIsArgs = isArguments(b);
if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs))
return false;
const ka = Object.keys(a);
const kb = Object.keys(b);

const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
var key, i;

// The pair must have the same number of owned properties (keys
// incorporates hasOwnProperty).
if (ka.length !== kb.length)
// The pair must have the same number of owned properties
// (keys incorporates hasOwnProperty).
if (aKeys.length !== bKeys.length)
return false;

// The pair must have the same set of keys (although not
// necessarily in the same order).
ka.sort();
kb.sort();
aKeys.sort();
bKeys.sort();
// Cheap key test:
for (i = ka.length - 1; i >= 0; i--) {
if (ka[i] !== kb[i])
for (i = aKeys.length - 1; i >= 0; i--) {
if (aKeys[i] !== bKeys[i])
return false;
}

// The pair must have equivalent values for every corresponding key.
// Possibly expensive deep test:
for (i = ka.length - 1; i >= 0; i--) {
key = ka[i];
for (i = aKeys.length - 1; i >= 0; i--) {
key = aKeys[i];
if (!_deepEqual(a[key], b[key], strict, actualVisitedObjects))
return false;
}
@@ -269,7 +329,6 @@ function notDeepStrictEqual(actual, expected, message) {
}
}


// The strict equality assertion tests strict equality, as determined by ===.
// assert.strictEqual(actual, expected, message_opt);

@@ -295,7 +354,7 @@ function expectedException(actual, expected) {
return false;
}

if (Object.prototype.toString.call(expected) === '[object RegExp]') {
if (objectToString(expected) === '[object RegExp]') {
return expected.test(actual);
}

110 changes: 110 additions & 0 deletions test/parallel/test-assert-deep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';
require('../common');
const assert = require('assert');
const util = require('util');

// Template tag function turning an error message into a RegExp
// for assert.throws()
function re(literals, ...values) {
let result = literals[0];
for (const [i, value] of values.entries()) {
const str = util.inspect(value);
// Need to escape special characters.
result += str.replace(/[\\^$.*+?()[\]{}|=!<>:-]/g, '\\$&');
result += literals[i + 1];
}
return new RegExp(`^AssertionError: ${result}$`);
}

// The following deepEqual tests might seem very weird.
// They just describe what it is now.
// That is why we discourage using deepEqual in our own tests.

// Turn off no-restricted-properties because we are testing deepEqual!
/* eslint-disable no-restricted-properties */

const arr = new Uint8Array([120, 121, 122, 10]);
const buf = Buffer.from(arr);
// They have different [[Prototype]]
assert.throws(() => assert.deepStrictEqual(arr, buf));
assert.doesNotThrow(() => assert.deepEqual(arr, buf));

const buf2 = Buffer.from(arr);
buf2.prop = 1;

assert.throws(() => assert.deepStrictEqual(buf2, buf));
assert.doesNotThrow(() => assert.deepEqual(buf2, buf));

const arr2 = new Uint8Array([120, 121, 122, 10]);
arr2.prop = 5;
assert.throws(() => assert.deepStrictEqual(arr, arr2));
assert.doesNotThrow(() => assert.deepEqual(arr, arr2));

const date = new Date('2016');

class MyDate extends Date {
constructor(...args) {
super(...args);
this[0] = '1';
}
}

const date2 = new MyDate('2016');

// deepEqual returns true as long as the time are the same,
// but deepStrictEqual checks own properties
assert.doesNotThrow(() => assert.deepEqual(date, date2));
assert.doesNotThrow(() => assert.deepEqual(date2, date));
assert.throws(() => assert.deepStrictEqual(date, date2),
re`${date} deepStrictEqual ${date2}`);
assert.throws(() => assert.deepStrictEqual(date2, date),
re`${date2} deepStrictEqual ${date}`);

class MyRegExp extends RegExp {
constructor(...args) {
super(...args);
this[0] = '1';
}
}

const re1 = new RegExp('test');
const re2 = new MyRegExp('test');

// deepEqual returns true as long as the regexp-specific properties
// are the same, but deepStrictEqual checks all properties
assert.doesNotThrow(() => assert.deepEqual(re1, re2));
assert.throws(() => assert.deepStrictEqual(re1, re2),
re`${re1} deepStrictEqual ${re2}`);

// For these weird cases, deepEqual should pass (at least for now),
// but deepStrictEqual should throw.
const similar = new Set([
{0: '1'}, // Object
{0: 1}, // Object
new String('1'), // Object
['1'], // Array
[1], // Array
date2, // Date with this[0] = '1'
re2, // RegExp with this[0] = '1'
new Int8Array([1]), // Int8Array
new Uint8Array([1]), // Uint8Array
new Int16Array([1]), // Int16Array
new Uint16Array([1]), // Uint16Array
new Int32Array([1]), // Int32Array
new Uint32Array([1]), // Uint32Array
Buffer.from([1]),
// Arguments {'0': '1'} is not here
// See https://github.com/nodejs/node-v0.x-archive/pull/7178
]);

for (const a of similar) {
for (const b of similar) {
if (a !== b) {
assert.doesNotThrow(() => assert.deepEqual(a, b));
assert.throws(() => assert.deepStrictEqual(a, b),
re`${a} deepStrictEqual ${b}`);
}
}
}

/* eslint-enable */
15 changes: 3 additions & 12 deletions test/parallel/test-assert.js
Original file line number Diff line number Diff line change
@@ -62,7 +62,6 @@ assert.doesNotThrow(makeBlock(a.notStrictEqual, 2, '2'),
'notStrictEqual(2, \'2\')');

// deepEqual joy!
// 7.2
assert.doesNotThrow(makeBlock(a.deepEqual, new Date(2000, 3, 14),
new Date(2000, 3, 14)),
'deepEqual(new Date(2000, 3, 14), new Date(2000, 3, 14))');
@@ -84,7 +83,6 @@ assert.doesNotThrow(makeBlock(
'notDeepEqual(new Date(), new Date(2000, 3, 14))'
);

// 7.3
assert.doesNotThrow(makeBlock(a.deepEqual, /a/, /a/));
assert.doesNotThrow(makeBlock(a.deepEqual, /a/g, /a/g));
assert.doesNotThrow(makeBlock(a.deepEqual, /a/i, /a/i));
@@ -104,20 +102,16 @@ assert.throws(makeBlock(a.deepEqual, /a/igm, /a/im),
{
const re1 = /a/g;
re1.lastIndex = 3;

assert.throws(makeBlock(a.deepEqual, re1, /a/g),
/^AssertionError: \/a\/g deepEqual \/a\/g$/);
assert.doesNotThrow(makeBlock(a.deepEqual, re1, /a/g),
/^AssertionError: \/a\/g deepEqual \/a\/g$/);
}


// 7.4
assert.doesNotThrow(makeBlock(a.deepEqual, 4, '4'), 'deepEqual(4, \'4\')');
assert.doesNotThrow(makeBlock(a.deepEqual, true, 1), 'deepEqual(true, 1)');
assert.throws(makeBlock(a.deepEqual, 4, '5'),
a.AssertionError,
'deepEqual( 4, \'5\')');

// 7.5
// having the same number of owned properties && the same set of keys
assert.doesNotThrow(makeBlock(a.deepEqual, {a: 4}, {a: 4}));
assert.doesNotThrow(makeBlock(a.deepEqual, {a: 4, b: '2'}, {a: 4, b: '2'}));
@@ -210,7 +204,6 @@ assert.doesNotThrow(
'notDeepStrictEqual(new Date(), new Date(2000, 3, 14))'
);

// 7.3 - strict
assert.doesNotThrow(makeBlock(a.deepStrictEqual, /a/, /a/));
assert.doesNotThrow(makeBlock(a.deepStrictEqual, /a/g, /a/g));
assert.doesNotThrow(makeBlock(a.deepStrictEqual, /a/i, /a/i));
@@ -240,10 +233,9 @@ assert.throws(
{
const re1 = /a/;
re1.lastIndex = 3;
assert.throws(makeBlock(a.deepStrictEqual, re1, /a/));
assert.doesNotThrow(makeBlock(a.deepStrictEqual, re1, /a/));
}

// 7.4 - strict
assert.throws(makeBlock(a.deepStrictEqual, 4, '4'),
a.AssertionError,
'deepStrictEqual(4, \'4\')');
@@ -256,7 +248,6 @@ assert.throws(makeBlock(a.deepStrictEqual, 4, '5'),
a.AssertionError,
'deepStrictEqual(4, \'5\')');

// 7.5 - strict
// having the same number of owned properties && the same set of keys
assert.doesNotThrow(makeBlock(a.deepStrictEqual, {a: 4}, {a: 4}));
assert.doesNotThrow(makeBlock(a.deepStrictEqual,
3 changes: 2 additions & 1 deletion test/parallel/test-child-process-spawnsync-input.js
Original file line number Diff line number Diff line change
@@ -87,7 +87,8 @@ options = {
ret = spawnSync('cat', [], options);

checkSpawnSyncRet(ret);
assert.deepStrictEqual(ret.stdout, options.input);
// Wrap options.input because Uint8Array and Buffer have different prototypes.
assert.deepStrictEqual(ret.stdout, Buffer.from(options.input));
assert.deepStrictEqual(ret.stderr, Buffer.from(''));

verifyBufOutput(spawnSync(process.execPath, args));
4 changes: 2 additions & 2 deletions test/parallel/test-fs-read.js
Original file line number Diff line number Diff line change
@@ -18,11 +18,11 @@ function test(bufferAsync, bufferSync, expected) {
common.mustCall((err, bytesRead) => {
assert.ifError(err);
assert.strictEqual(bytesRead, expected.length);
assert.deepStrictEqual(bufferAsync, Buffer.from(expected));
assert.deepStrictEqual(bufferAsync, expected);
}));

const r = fs.readSync(fd, bufferSync, 0, expected.length, 0);
assert.deepStrictEqual(bufferSync, Buffer.from(expected));
assert.deepStrictEqual(bufferSync, expected);
assert.strictEqual(r, expected.length);
}