assert: handle enumerable symbol keys
PR-URL: https://github.com/nodejs/node/pull/15169 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
parent
a8c0a43a22
commit
db2e093e05
@ -46,12 +46,11 @@ Primitive values are compared with the [Abstract Equality Comparison][]
|
|||||||
|
|
||||||
Only [enumerable "own" properties][] are considered. The
|
Only [enumerable "own" properties][] are considered. The
|
||||||
[`assert.deepEqual()`][] implementation does not test the
|
[`assert.deepEqual()`][] implementation does not test the
|
||||||
[`[[Prototype]]`][prototype-spec] of objects, attached symbols, or
|
[`[[Prototype]]`][prototype-spec] of objects or enumerable own [`Symbol`][]
|
||||||
non-enumerable properties — for such checks, consider using
|
properties. For such checks, consider using [assert.deepStrictEqual()][]
|
||||||
[`assert.deepStrictEqual()`][] instead. This can lead to some
|
instead. [`assert.deepEqual()`][] can have potentially surprising results. The
|
||||||
potentially surprising results. For example, the following example does not
|
following example does not throw an `AssertionError` because the properties on
|
||||||
throw an `AssertionError` because the properties on the [`RegExp`][] object are
|
the [RegExp][] object are not enumerable:
|
||||||
not enumerable:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// WARNING: This does not throw an AssertionError!
|
// WARNING: This does not throw an AssertionError!
|
||||||
@ -109,6 +108,9 @@ parameter is an instance of an `Error` then it will be thrown instead of the
|
|||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v1.2.0
|
added: v1.2.0
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/15169
|
||||||
|
description: Enumerable symbol properties are now compared.
|
||||||
- version: REPLACEME
|
- version: REPLACEME
|
||||||
pr-url: https://github.com/nodejs/node/pull/15036
|
pr-url: https://github.com/nodejs/node/pull/15036
|
||||||
description: NaN is now compared using the [SameValueZero][] comparison.
|
description: NaN is now compared using the [SameValueZero][] comparison.
|
||||||
@ -132,7 +134,7 @@ changes:
|
|||||||
* `expected` {any}
|
* `expected` {any}
|
||||||
* `message` {any}
|
* `message` {any}
|
||||||
|
|
||||||
Generally identical to `assert.deepEqual()` with a few exceptions:
|
Similar to `assert.deepEqual()` with the following exceptions:
|
||||||
|
|
||||||
1. Primitive values besides `NaN` are compared using the [Strict Equality
|
1. Primitive values besides `NaN` are compared using the [Strict Equality
|
||||||
Comparison][] ( `===` ). Set and Map values, Map keys and `NaN` are compared
|
Comparison][] ( `===` ). Set and Map values, Map keys and `NaN` are compared
|
||||||
@ -143,6 +145,7 @@ Generally identical to `assert.deepEqual()` with a few exceptions:
|
|||||||
3. [Type tags][Object.prototype.toString()] of objects should be the same.
|
3. [Type tags][Object.prototype.toString()] of objects should be the same.
|
||||||
4. [Object wrappers][] are compared both as objects and unwrapped values.
|
4. [Object wrappers][] are compared both as objects and unwrapped values.
|
||||||
5. `0` and `-0` are not considered equal.
|
5. `0` and `-0` are not considered equal.
|
||||||
|
6. Enumerable own [`Symbol`][] properties are compared as well.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
@ -185,6 +188,13 @@ assert.deepStrictEqual(-0, -0);
|
|||||||
// OK
|
// OK
|
||||||
assert.deepStrictEqual(0, -0);
|
assert.deepStrictEqual(0, -0);
|
||||||
// AssertionError: 0 deepStrictEqual -0
|
// AssertionError: 0 deepStrictEqual -0
|
||||||
|
|
||||||
|
const symbol1 = Symbol();
|
||||||
|
const symbol2 = Symbol();
|
||||||
|
assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol1]: 1 });
|
||||||
|
// OK, because it is the same symbol on both objects.
|
||||||
|
assert.deepStrictEqual({ [symbol1]: 1 }, { [symbol2]: 1 });
|
||||||
|
// Fails because symbol1 !== symbol2!
|
||||||
```
|
```
|
||||||
|
|
||||||
If the values are not equal, an `AssertionError` is thrown with a `message`
|
If the values are not equal, an `AssertionError` is thrown with a `message`
|
||||||
@ -712,6 +722,7 @@ For more information, see
|
|||||||
[`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
|
[`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
|
||||||
[`RegExp`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
[`RegExp`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||||
[`Set`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set
|
[`Set`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set
|
||||||
|
[`Symbol`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol
|
||||||
[`TypeError`]: errors.html#errors_class_typeerror
|
[`TypeError`]: errors.html#errors_class_typeerror
|
||||||
[`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message
|
[`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message
|
||||||
[`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message
|
[`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message
|
||||||
|
125
lib/assert.js
125
lib/assert.js
@ -24,6 +24,7 @@ const { compare } = process.binding('buffer');
|
|||||||
const { isSet, isMap, isDate, isRegExp } = process.binding('util');
|
const { isSet, isMap, isDate, isRegExp } = process.binding('util');
|
||||||
const { objectToString } = require('internal/util');
|
const { objectToString } = require('internal/util');
|
||||||
const errors = require('internal/errors');
|
const errors = require('internal/errors');
|
||||||
|
const { propertyIsEnumerable } = Object.prototype;
|
||||||
|
|
||||||
// The assert module provides functions that throw
|
// The assert module provides functions that throw
|
||||||
// AssertionError's when particular conditions are not met. The
|
// AssertionError's when particular conditions are not met. The
|
||||||
@ -165,7 +166,7 @@ function isObjectOrArrayTag(tag) {
|
|||||||
// For strict comparison, objects should have
|
// For strict comparison, objects should have
|
||||||
// a) The same built-in type tags
|
// a) The same built-in type tags
|
||||||
// b) The same prototypes.
|
// b) The same prototypes.
|
||||||
function strictDeepEqual(actual, expected) {
|
function strictDeepEqual(actual, expected, memos) {
|
||||||
if (typeof actual !== 'object') {
|
if (typeof actual !== 'object') {
|
||||||
return typeof actual === 'number' && Number.isNaN(actual) &&
|
return typeof actual === 'number' && Number.isNaN(actual) &&
|
||||||
Number.isNaN(expected);
|
Number.isNaN(expected);
|
||||||
@ -186,12 +187,12 @@ function strictDeepEqual(actual, expected) {
|
|||||||
// Check for sparse arrays and general fast path
|
// Check for sparse arrays and general fast path
|
||||||
if (actual.length !== expected.length)
|
if (actual.length !== expected.length)
|
||||||
return false;
|
return false;
|
||||||
// Skip testing the part below and continue in the callee function.
|
// Skip testing the part below and continue with the keyCheck.
|
||||||
return;
|
return keyCheck(actual, expected, true, memos);
|
||||||
}
|
}
|
||||||
if (actualTag === '[object Object]') {
|
if (actualTag === '[object Object]') {
|
||||||
// Skip testing the part below and continue in the callee function.
|
// Skip testing the part below and continue with the keyCheck.
|
||||||
return;
|
return keyCheck(actual, expected, true, memos);
|
||||||
}
|
}
|
||||||
if (isDate(actual)) {
|
if (isDate(actual)) {
|
||||||
if (actual.getTime() !== expected.getTime()) {
|
if (actual.getTime() !== expected.getTime()) {
|
||||||
@ -215,10 +216,8 @@ function strictDeepEqual(actual, expected) {
|
|||||||
}
|
}
|
||||||
// Buffer.compare returns true, so actual.length === expected.length
|
// Buffer.compare returns true, so actual.length === expected.length
|
||||||
// if they both only contain numeric keys, we don't need to exam further
|
// if they both only contain numeric keys, we don't need to exam further
|
||||||
if (Object.keys(actual).length === actual.length &&
|
return keyCheck(actual, expected, true, memos, actual.length,
|
||||||
Object.keys(expected).length === expected.length) {
|
expected.length);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (typeof actual.valueOf === 'function') {
|
} else if (typeof actual.valueOf === 'function') {
|
||||||
const actualValue = actual.valueOf();
|
const actualValue = actual.valueOf();
|
||||||
// Note: Boxed string keys are going to be compared again by Object.keys
|
// Note: Boxed string keys are going to be compared again by Object.keys
|
||||||
@ -232,15 +231,14 @@ function strictDeepEqual(actual, expected) {
|
|||||||
lengthActual = actual.length;
|
lengthActual = actual.length;
|
||||||
lengthExpected = expected.length;
|
lengthExpected = expected.length;
|
||||||
}
|
}
|
||||||
if (Object.keys(actual).length === lengthActual &&
|
return keyCheck(actual, expected, true, memos, lengthActual,
|
||||||
Object.keys(expected).length === lengthExpected) {
|
lengthExpected);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return keyCheck(actual, expected, true, memos);
|
||||||
}
|
}
|
||||||
|
|
||||||
function looseDeepEqual(actual, expected) {
|
function looseDeepEqual(actual, expected, memos) {
|
||||||
if (actual === null || typeof actual !== 'object') {
|
if (actual === null || typeof actual !== 'object') {
|
||||||
if (expected === null || typeof expected !== 'object') {
|
if (expected === null || typeof expected !== 'object') {
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line eqeqeq
|
||||||
@ -274,26 +272,10 @@ function looseDeepEqual(actual, expected) {
|
|||||||
} else if (isArguments(actualTag) || isArguments(expectedTag)) {
|
} else if (isArguments(actualTag) || isArguments(expectedTag)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return keyCheck(actual, expected, false, memos);
|
||||||
}
|
}
|
||||||
|
|
||||||
function innerDeepEqual(actual, expected, strict, memos) {
|
function keyCheck(actual, expected, strict, memos, lengthA, lengthB) {
|
||||||
// All identical values are equivalent, as determined by ===.
|
|
||||||
if (actual === expected) {
|
|
||||||
if (actual !== 0)
|
|
||||||
return true;
|
|
||||||
return strict ? Object.is(actual, expected) : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a boolean if (not) equal and undefined in case we have to check
|
|
||||||
// further.
|
|
||||||
const partialCheck = strict ?
|
|
||||||
strictDeepEqual(actual, expected) :
|
|
||||||
looseDeepEqual(actual, expected);
|
|
||||||
|
|
||||||
if (partialCheck !== undefined) {
|
|
||||||
return partialCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all remaining Object pairs, including Array, objects and Maps,
|
// For all remaining Object pairs, including Array, objects and Maps,
|
||||||
// equivalence is determined by having:
|
// equivalence is determined by having:
|
||||||
// a) The same number of owned enumerable properties
|
// a) The same number of owned enumerable properties
|
||||||
@ -301,6 +283,51 @@ function innerDeepEqual(actual, expected, strict, memos) {
|
|||||||
// c) Equivalent values for every corresponding key/index
|
// c) Equivalent values for every corresponding key/index
|
||||||
// d) For Sets and Maps, equal contents
|
// d) For Sets and Maps, equal contents
|
||||||
// Note: this accounts for both named and indexed properties on Arrays.
|
// Note: this accounts for both named and indexed properties on Arrays.
|
||||||
|
var aKeys = Object.keys(actual);
|
||||||
|
var bKeys = Object.keys(expected);
|
||||||
|
var i;
|
||||||
|
|
||||||
|
// The pair must have the same number of owned properties.
|
||||||
|
if (aKeys.length !== bKeys.length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
var symbolKeysA = Object.getOwnPropertySymbols(actual);
|
||||||
|
var symbolKeysB = Object.getOwnPropertySymbols(expected);
|
||||||
|
if (symbolKeysA.length !== 0) {
|
||||||
|
symbolKeysA = symbolKeysA.filter((k) =>
|
||||||
|
propertyIsEnumerable.call(actual, k));
|
||||||
|
symbolKeysB = symbolKeysB.filter((k) =>
|
||||||
|
propertyIsEnumerable.call(expected, k));
|
||||||
|
if (symbolKeysA.length !== symbolKeysB.length)
|
||||||
|
return false;
|
||||||
|
} else if (symbolKeysB.length !== 0 && symbolKeysB.filter((k) =>
|
||||||
|
propertyIsEnumerable.call(expected, k)).length !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (lengthA !== undefined) {
|
||||||
|
if (aKeys.length !== lengthA || bKeys.length !== lengthB)
|
||||||
|
return false;
|
||||||
|
if (symbolKeysA.length === 0)
|
||||||
|
return true;
|
||||||
|
aKeys = [];
|
||||||
|
bKeys = [];
|
||||||
|
}
|
||||||
|
if (symbolKeysA.length !== 0) {
|
||||||
|
aKeys.push(...symbolKeysA);
|
||||||
|
bKeys.push(...symbolKeysB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cheap key test:
|
||||||
|
const keys = {};
|
||||||
|
for (i = 0; i < aKeys.length; i++) {
|
||||||
|
keys[aKeys[i]] = true;
|
||||||
|
}
|
||||||
|
for (i = 0; i < aKeys.length; i++) {
|
||||||
|
if (keys[bKeys[i]] === undefined)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Use memos to handle cycles.
|
// Use memos to handle cycles.
|
||||||
if (memos === undefined) {
|
if (memos === undefined) {
|
||||||
@ -323,25 +350,6 @@ function innerDeepEqual(actual, expected, strict, memos) {
|
|||||||
memos.position++;
|
memos.position++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const aKeys = Object.keys(actual);
|
|
||||||
const bKeys = Object.keys(expected);
|
|
||||||
var i;
|
|
||||||
|
|
||||||
// The pair must have the same number of owned properties
|
|
||||||
// (keys incorporates hasOwnProperty).
|
|
||||||
if (aKeys.length !== bKeys.length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Cheap key test:
|
|
||||||
const keys = {};
|
|
||||||
for (i = 0; i < aKeys.length; i++) {
|
|
||||||
keys[aKeys[i]] = true;
|
|
||||||
}
|
|
||||||
for (i = 0; i < aKeys.length; i++) {
|
|
||||||
if (keys[bKeys[i]] === undefined)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
memos.actual.set(actual, memos.position);
|
memos.actual.set(actual, memos.position);
|
||||||
memos.expected.set(expected, memos.position);
|
memos.expected.set(expected, memos.position);
|
||||||
|
|
||||||
@ -353,6 +361,21 @@ function innerDeepEqual(actual, expected, strict, memos) {
|
|||||||
return areEq;
|
return areEq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function innerDeepEqual(actual, expected, strict, memos) {
|
||||||
|
// All identical values are equivalent, as determined by ===.
|
||||||
|
if (actual === expected) {
|
||||||
|
if (actual !== 0)
|
||||||
|
return true;
|
||||||
|
return strict ? Object.is(actual, expected) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check more closely if actual and expected are equal.
|
||||||
|
if (strict === true)
|
||||||
|
return strictDeepEqual(actual, expected, memos);
|
||||||
|
|
||||||
|
return looseDeepEqual(actual, expected, memos);
|
||||||
|
}
|
||||||
|
|
||||||
function setHasEqualElement(set, val1, strict, memo) {
|
function setHasEqualElement(set, val1, strict, memo) {
|
||||||
// Go looking.
|
// Go looking.
|
||||||
for (const val2 of set) {
|
for (const val2 of set) {
|
||||||
|
@ -507,8 +507,36 @@ assert.doesNotThrow(
|
|||||||
boxedSymbol.slow = true;
|
boxedSymbol.slow = true;
|
||||||
assertNotDeepOrStrict(boxedSymbol, {});
|
assertNotDeepOrStrict(boxedSymbol, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minus zero
|
// Minus zero
|
||||||
assertOnlyDeepEqual(0, -0);
|
assertOnlyDeepEqual(0, -0);
|
||||||
assertDeepAndStrictEqual(-0, -0);
|
assertDeepAndStrictEqual(-0, -0);
|
||||||
|
|
||||||
|
// Handle symbols (enumerable only)
|
||||||
|
{
|
||||||
|
const symbol1 = Symbol();
|
||||||
|
const obj1 = { [symbol1]: 1 };
|
||||||
|
const obj2 = { [symbol1]: 1 };
|
||||||
|
const obj3 = { [Symbol()]: 1 };
|
||||||
|
// Add a non enumerable symbol as well. It is going to be ignored!
|
||||||
|
Object.defineProperty(obj2, Symbol(), { value: 1 });
|
||||||
|
assertOnlyDeepEqual(obj1, obj3);
|
||||||
|
assertDeepAndStrictEqual(obj1, obj2);
|
||||||
|
// TypedArrays have a fast path. Test for this as well.
|
||||||
|
const a = new Uint8Array(4);
|
||||||
|
const b = new Uint8Array(4);
|
||||||
|
a[symbol1] = true;
|
||||||
|
b[symbol1] = false;
|
||||||
|
assertOnlyDeepEqual(a, b);
|
||||||
|
b[symbol1] = true;
|
||||||
|
assertDeepAndStrictEqual(a, b);
|
||||||
|
// The same as TypedArrays is valid for boxed primitives
|
||||||
|
const boxedStringA = new String('test');
|
||||||
|
const boxedStringB = new String('test');
|
||||||
|
boxedStringA[symbol1] = true;
|
||||||
|
assertOnlyDeepEqual(boxedStringA, boxedStringB);
|
||||||
|
boxedStringA[symbol1] = true;
|
||||||
|
assertDeepAndStrictEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
@ -13,10 +13,13 @@ function validateNormalizedArgs(input, output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test creation of normalized arguments.
|
// Test creation of normalized arguments.
|
||||||
validateNormalizedArgs([], [{}, null]);
|
const res = [{}, null];
|
||||||
validateNormalizedArgs([{ port: 1234 }], [{ port: 1234 }, null]);
|
res[normalizedArgsSymbol] = true;
|
||||||
validateNormalizedArgs([{ port: 1234 }, assert.fail],
|
validateNormalizedArgs([], res);
|
||||||
[{ port: 1234 }, assert.fail]);
|
res[0].port = 1234;
|
||||||
|
validateNormalizedArgs([{ port: 1234 }], res);
|
||||||
|
res[1] = assert.fail;
|
||||||
|
validateNormalizedArgs([{ port: 1234 }, assert.fail], res);
|
||||||
|
|
||||||
// Connecting to the server should fail with a standard array.
|
// Connecting to the server should fail with a standard array.
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user