diff --git a/benchmark/assert/deepequal-buffer.js b/benchmark/assert/deepequal-buffer.js index 2a7d9e3bed7c38..9e86aa231e69d2 100644 --- a/benchmark/assert/deepequal-buffer.js +++ b/benchmark/assert/deepequal-buffer.js @@ -1,10 +1,16 @@ 'use strict'; const common = require('../common.js'); const assert = require('assert'); + const bench = common.createBenchmark(main, { - n: [1e3], - len: [1e2], - method: ['strict', 'nonstrict'] + n: [1e5], + len: [1e2, 1e4], + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ] }); function main(conf) { @@ -12,14 +18,16 @@ function main(conf) { const len = +conf.len; var i; - const data = Buffer.allocUnsafe(len); + const data = Buffer.allocUnsafe(len + 1); const actual = Buffer.alloc(len); const expected = Buffer.alloc(len); + const expectedWrong = Buffer.alloc(len + 1); data.copy(actual); data.copy(expected); + data.copy(expectedWrong); switch (conf.method) { - case 'strict': + case 'deepEqual': bench.start(); for (i = 0; i < n; ++i) { // eslint-disable-next-line no-restricted-properties @@ -27,13 +35,28 @@ function main(conf) { } bench.end(n); break; - case 'nonstrict': + case 'deepStrictEqual': bench.start(); for (i = 0; i < n; ++i) { assert.deepStrictEqual(actual, expected); } bench.end(n); break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; default: throw new Error('Unsupported method'); } diff --git a/benchmark/assert/deepequal-object.js b/benchmark/assert/deepequal-object.js new file mode 100644 index 00000000000000..d1a6cb69944055 --- /dev/null +++ b/benchmark/assert/deepequal-object.js @@ -0,0 +1,73 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e6], + size: [1e2, 1e3, 1e4], + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ] +}); + +function createObj(source, add = '') { + return source.map((n) => ({ + foo: 'yarp', + nope: { + bar: `123${add}`, + a: [1, 2, 3], + baz: n + } + })); +} + +function main(conf) { + const size = +conf.size; + // TODO: Fix this "hack" + const n = (+conf.n) / size; + var i; + + const source = Array.apply(null, Array(size)); + const actual = createObj(source); + const expected = createObj(source); + const expectedWrong = createObj(source, '4'); + + switch (conf.method) { + case 'deepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(actual, expected); + } + bench.end(n); + break; + case 'deepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.deepStrictEqual(actual, expected); + } + bench.end(n); + break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; + default: + throw new Error('Unsupported method'); + } +} diff --git a/benchmark/assert/deepequal-prims-and-objs-big-array-set.js b/benchmark/assert/deepequal-prims-and-objs-big-array-set.js new file mode 100644 index 00000000000000..1e356cea51efd6 --- /dev/null +++ b/benchmark/assert/deepequal-prims-and-objs-big-array-set.js @@ -0,0 +1,119 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const primValues = { + 'null': null, + 'undefined': undefined, + 'string': 'a', + 'number': 1, + 'boolean': true, + 'object': { 0: 'a' }, + 'array': [1, 2, 3], + 'new-array': new Array([1, 2, 3]) +}; + +const bench = common.createBenchmark(main, { + prim: Object.keys(primValues), + n: [25], + len: [1e5], + method: [ + 'deepEqual_Array', + 'deepStrictEqual_Array', + 'notDeepEqual_Array', + 'notDeepStrictEqual_Array', + 'deepEqual_Set', + 'deepStrictEqual_Set', + 'notDeepEqual_Set', + 'notDeepStrictEqual_Set' + ] +}); + +function main(conf) { + const prim = primValues[conf.prim]; + const n = +conf.n; + const len = +conf.len; + const actual = []; + const expected = []; + const expectedWrong = []; + var i; + + for (var x = 0; x < len; x++) { + actual.push(prim); + expected.push(prim); + expectedWrong.push(prim); + } + expectedWrong.pop(); + expectedWrong.push('b'); + + // Note: primitives are only added once to a set + const actualSet = new Set(actual); + const expectedSet = new Set(expected); + const expectedWrongSet = new Set(expectedWrong); + + switch (conf.method) { + case 'deepEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(actual, expected); + } + bench.end(n); + break; + case 'deepStrictEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + assert.deepStrictEqual(actual, expected); + } + bench.end(n); + break; + case 'notDeepEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'deepEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(actualSet, expectedSet); + } + bench.end(n); + break; + case 'deepStrictEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + assert.deepStrictEqual(actualSet, expectedSet); + } + bench.end(n); + break; + case 'notDeepEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actualSet, expectedWrongSet); + } + bench.end(n); + break; + case 'notDeepStrictEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actualSet, expectedWrongSet); + } + bench.end(n); + break; + default: + throw new Error('Unsupported method'); + } +} diff --git a/benchmark/assert/deepequal-prims-and-objs-big-array.js b/benchmark/assert/deepequal-prims-and-objs-big-array.js deleted file mode 100644 index 69eda8af087dfa..00000000000000 --- a/benchmark/assert/deepequal-prims-and-objs-big-array.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; -const common = require('../common.js'); -const assert = require('assert'); - -const primValues = { - 'null': null, - 'undefined': undefined, - 'string': 'a', - 'number': 1, - 'boolean': true, - 'object': { 0: 'a' }, - 'array': [1, 2, 3], - 'new-array': new Array([1, 2, 3]) -}; - -const bench = common.createBenchmark(main, { - prim: Object.keys(primValues), - n: [25], - len: [1e5], - method: ['strict', 'nonstrict'] -}); - -function main(conf) { - const prim = primValues[conf.prim]; - const n = +conf.n; - const len = +conf.len; - const actual = []; - const expected = []; - var i; - - for (var x = 0; x < len; x++) { - actual.push(prim); - expected.push(prim); - } - - switch (conf.method) { - case 'strict': - bench.start(); - for (i = 0; i < n; ++i) { - // eslint-disable-next-line no-restricted-properties - assert.deepEqual(actual, expected); - } - bench.end(n); - break; - case 'nonstrict': - bench.start(); - for (i = 0; i < n; ++i) { - assert.deepStrictEqual(actual, expected); - } - bench.end(n); - break; - default: - throw new Error('Unsupported method'); - } -} diff --git a/benchmark/assert/deepequal-prims-and-objs-big-loop.js b/benchmark/assert/deepequal-prims-and-objs-big-loop.js index 781c5ad754e723..2800b51c491bcf 100644 --- a/benchmark/assert/deepequal-prims-and-objs-big-loop.js +++ b/benchmark/assert/deepequal-prims-and-objs-big-loop.js @@ -16,7 +16,12 @@ const primValues = { const bench = common.createBenchmark(main, { prim: Object.keys(primValues), n: [1e6], - method: ['strict', 'nonstrict'] + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ] }); function main(conf) { @@ -24,11 +29,12 @@ function main(conf) { const n = +conf.n; const actual = prim; const expected = prim; + const expectedWrong = 'b'; var i; // Creates new array to avoid loop invariant code motion switch (conf.method) { - case 'strict': + case 'deepEqual': bench.start(); for (i = 0; i < n; ++i) { // eslint-disable-next-line no-restricted-properties @@ -36,13 +42,28 @@ function main(conf) { } bench.end(n); break; - case 'nonstrict': + case 'deepStrictEqual': bench.start(); for (i = 0; i < n; ++i) { assert.deepStrictEqual([actual], [expected]); } bench.end(n); break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual([actual], [expectedWrong]); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual([actual], [expectedWrong]); + } + bench.end(n); + break; default: throw new Error('Unsupported method'); } diff --git a/benchmark/assert/deepequal-typedarrays.js b/benchmark/assert/deepequal-typedarrays.js index 00c6ca5adf2835..5d51d53d13e70f 100644 --- a/benchmark/assert/deepequal-typedarrays.js +++ b/benchmark/assert/deepequal-typedarrays.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../common.js'); const assert = require('assert'); + const bench = common.createBenchmark(main, { type: [ 'Int8Array', @@ -14,7 +15,12 @@ const bench = common.createBenchmark(main, { 'Uint8ClampedArray', ], n: [1], - method: ['strict', 'nonstrict'], + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ], len: [1e6] }); @@ -26,10 +32,12 @@ function main(conf) { const actual = new clazz(len); const expected = new clazz(len); + const expectedWrong = Buffer.alloc(len); + expectedWrong[100] = 123; var i; switch (conf.method) { - case 'strict': + case 'deepEqual': bench.start(); for (i = 0; i < n; ++i) { // eslint-disable-next-line no-restricted-properties @@ -37,13 +45,28 @@ function main(conf) { } bench.end(n); break; - case 'nonstrict': + case 'deepStrictEqual': bench.start(); for (i = 0; i < n; ++i) { assert.deepStrictEqual(actual, expected); } bench.end(n); break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; default: throw new Error('Unsupported method'); } diff --git a/benchmark/assert/throws.js b/benchmark/assert/throws.js new file mode 100644 index 00000000000000..fa3105f2dc69a7 --- /dev/null +++ b/benchmark/assert/throws.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e6], + method: [ + 'doesNotThrow', + 'throws', + 'throws_TypeError', + 'throws_RegExp' + ] +}); + +function main(conf) { + const n = +conf.n; + const throws = () => { throw new TypeError('foobar'); }; + const doesNotThrow = () => { return 'foobar'; }; + const regExp = /foobar/; + const message = 'failure'; + var i; + + switch (conf.method) { + case 'doesNotThrow': + bench.start(); + for (i = 0; i < n; ++i) { + assert.doesNotThrow(doesNotThrow); + } + bench.end(n); + break; + case 'throws': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line assert-throws-arguments + assert.throws(throws); + } + bench.end(n); + break; + case 'throws_TypeError': + bench.start(); + for (i = 0; i < n; ++i) { + assert.throws(throws, TypeError, message); + } + bench.end(n); + break; + case 'throws_RegExp': + bench.start(); + for (i = 0; i < n; ++i) { + assert.throws(throws, regExp, message); + } + bench.end(n); + break; + default: + throw new Error(`Unsupported method ${conf.method}`); + } +} diff --git a/lib/assert.js b/lib/assert.js index 902bb82ce2b113..b66061b29b0614 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -25,13 +25,7 @@ const util = require('util'); const { isSet, isMap } = process.binding('util'); const { objectToString } = require('internal/util'); const { Buffer } = require('buffer'); - -var errors; -function lazyErrors() { - if (!errors) - errors = require('internal/errors'); - return errors; -} +const errors = require('internal/errors'); // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The @@ -51,7 +45,6 @@ const assert = module.exports = ok; // display purposes. function innerFail(actual, expected, message, operator, stackStartFunction) { - const errors = lazyErrors(); throw new errors.AssertionError({ message, actual, @@ -82,7 +75,7 @@ assert.fail = fail; // new assert.AssertionError({ message: message, // actual: actual, // expected: expected }); -assert.AssertionError = lazyErrors().AssertionError; +assert.AssertionError = errors.AssertionError; // Pure assertion tests whether a value is truthy, as determined @@ -122,23 +115,35 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { } }; +// Check if they have the same source and flags function areSimilarRegExps(a, b) { return a.source === b.source && a.flags === b.flags; } +// For small buffers it's faster to compare the buffer in a loop. +// The c++ barrier takes the advantage of the faster compare otherwise. +// 300 was the number after which compare became faster. function areSimilarTypedArrays(a, b) { + const len = a.byteLength; + if (len !== b.byteLength) { + return false; + } + if (len < 300) { + for (var offset = 0; offset < len; offset++) { + if (a[offset] !== b[offset]) { + return false; + } + } + return true; + } return compare(Buffer.from(a.buffer, a.byteOffset, - a.byteLength), + len), 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]'; } @@ -147,109 +152,125 @@ function isArguments(tag) { return tag === '[object Arguments]'; } -function innerDeepEqual(actual, expected, strict, memos) { - // All identical values are equivalent, as determined by ===. - if (actual === expected) { - return true; - } - - // For primitives / functions - // (determined by typeof value !== 'object'), - // or null, equivalence is determined by === or ==. - if (isNullOrNonObj(actual) && isNullOrNonObj(expected)) { - // eslint-disable-next-line eqeqeq - return strict ? actual === expected : actual == expected; - } +function isObjectOrArrayTag(tag) { + return tag === '[object Array]' || tag === '[object Object]'; +} - // 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) +// 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 +// +// Typed arrays and buffers are checked 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. +// +// For strict comparison, objects should have +// a) The same built-in type tags +// b) The same prototypes. +function strictDeepEqual(actual, expected) { + if (actual === null || expected === null || + typeof actual !== 'object' || typeof expected !== 'object') { return false; - - // 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)) { + if (actualTag !== expectedTag) { + return false; + } + if (Object.getPrototypeOf(actual) !== Object.getPrototypeOf(expected)) { + return false; + } + if (isObjectOrArrayTag(actualTag)) { + // Skip testing the part below and continue in the callee function. + return; + } + if (util.isDate(actual)) { + if (actual.getTime() !== expected.getTime()) { return false; } - - if (actualTag !== expectedTag) { + } else if (util.isRegExp(actual)) { + if (!areSimilarRegExps(actual, expected)) { + return false; + } + } else if (!isFloatTypedArrayTag(actualTag) && ArrayBuffer.isView(actual)) { + if (!areSimilarTypedArrays(actual, expected)) { return false; } - } - // 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. + // 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; + } + } +} - // If both values are Date objects, - // check if the time underneath are equal first. +function looseDeepEqual(actual, expected) { + if (actual === null || typeof actual !== 'object') { + if (expected === null || typeof expected !== 'object') { + // eslint-disable-next-line eqeqeq + return actual == expected; + } + return false; + } + if (expected === null || typeof expected !== 'object') { + return false; + } 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. } + return true; } - - // 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. } + return true; } - + const actualTag = objectToString(actual); + const expectedTag = objectToString(expected); + if (actualTag === expectedTag) { + if (!isFloatTypedArrayTag(actualTag) && !isObjectOrArrayTag(actualTag) && + ArrayBuffer.isView(actual)) { + return areSimilarTypedArrays(actual, expected); + } // Ensure reflexivity of deepEqual with `arguments` objects. // See https://github.com/nodejs/node-v0.x-archive/pull/7178 - if (isArguments(actualTag) !== isArguments(expectedTag)) { + } else 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. - } +function innerDeepEqual(actual, expected, strict, memos) { + // All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + } - // 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; - } + // 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 other Object pairs, including Array objects and Maps, + // For all remaining Object pairs, including Array, objects and Maps, // 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) @@ -258,24 +279,42 @@ function innerDeepEqual(actual, expected, strict, memos) { // Note: this accounts for both named and indexed properties on Arrays. // Use memos to handle cycles. - if (!memos) { + if (memos === undefined) { memos = { actual: new Map(), expected: new Map(), position: 0 }; } else { + if (memos.actual.has(actual)) { + return memos.actual.get(actual) === memos.expected.get(expected); + } memos.position++; } - if (memos.actual.has(actual)) { - return memos.actual.get(actual) === memos.expected.get(expected); + 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.expected.set(expected, memos.position); - const areEq = objEquiv(actual, expected, strict, memos); + const areEq = objEquiv(actual, expected, strict, aKeys, memos); memos.actual.delete(actual); memos.expected.delete(expected); @@ -285,24 +324,20 @@ function innerDeepEqual(actual, expected, strict, memos) { function setHasSimilarElement(set, val1, usedEntries, strict, memo) { if (set.has(val1)) { - if (usedEntries) + if (usedEntries !== null) usedEntries.add(val1); return true; } // In strict mode the only things which can match a primitive or a function // will already be detected by set.has(val1). - if (strict && (util.isPrimitive(val1) || util.isFunction(val1))) + if (strict && (typeof val1 !== 'object' || val1 === null)) return false; // Otherwise go looking. for (const val2 of set) { - if (usedEntries && usedEntries.has(val2)) - continue; - - if (innerDeepEqual(val1, val2, strict, memo)) { - if (usedEntries) - usedEntries.add(val2); + if (!usedEntries.has(val2) && innerDeepEqual(val1, val2, strict, memo)) { + usedEntries.add(val2); return true; } } @@ -329,10 +364,10 @@ function setEquiv(a, b, strict, memo) { // that case this initialization is done lazily to avoid the allocation & // bookkeeping cost. Unfortunately, we can't get away with that in non-strict // mode. - let usedEntries = null; + let usedEntries = strict === true ? null : new Set(); for (const val1 of a) { - if (usedEntries == null && (!strict || typeof val1 === 'object')) + if (usedEntries === null && typeof val1 === 'object') usedEntries = new Set(); // If the value doesn't exist in the second set by reference, and its an @@ -358,26 +393,22 @@ function mapHasSimilarEntry(map, key1, item1, usedEntries, strict, memo) { // doing it here improves performance of the common case when reference-equal // keys exist (which includes all primitive-valued keys). if (map.has(key1) && innerDeepEqual(item1, map.get(key1), strict, memo)) { - if (usedEntries) + if (usedEntries !== null) usedEntries.add(key1); return true; } - if (strict && (util.isPrimitive(key1) || util.isFunction(key1))) + if (strict && (typeof key1 !== 'object' || key1 === null)) return false; for (const [key2, item2] of map) { - // This case is checked above. - if (key2 === key1) - continue; - - if (usedEntries && usedEntries.has(key2)) + // The first part is checked above. + if (key2 === key1 || usedEntries.has(key2)) continue; if (innerDeepEqual(key1, key2, strict, memo) && innerDeepEqual(item1, item2, strict, memo)) { - if (usedEntries) - usedEntries.add(key2); + usedEntries.add(key2); return true; } } @@ -393,56 +424,31 @@ function mapEquiv(a, b, strict, memo) { if (a.size !== b.size) return false; - let usedEntries = null; + let usedEntries = strict === true ? null : new Set(); - for (const [key1, item1] of a) { - if (usedEntries == null && (!strict || typeof key1 === 'object')) + for (const [key, item] of a) { + if (usedEntries === null && typeof key === 'object') usedEntries = new Set(); // Just like setEquiv above, this hunt makes this function O(n^2) when // using objects and lists as keys - if (!mapHasSimilarEntry(b, key1, item1, usedEntries, strict, memo)) + if (!mapHasSimilarEntry(b, key, item, usedEntries, strict, memo)) return false; } return true; } -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; - - 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 (aKeys.length !== bKeys.length) - return false; - - // The pair must have the same set of keys (although not - // necessarily in the same order). - aKeys.sort(); - bKeys.sort(); - // Cheap key test: - for (i = aKeys.length - 1; i >= 0; i--) { - if (aKeys[i] !== bKeys[i]) - return false; - } - +function objEquiv(a, b, strict, keys, memos) { // Sets and maps don't have their entries accessible via normal object // properties. if (isSet(a)) { - if (!isSet(b) || !setEquiv(a, b, strict, actualVisitedObjects)) + if (!isSet(b) || !setEquiv(a, b, strict, memos)) return false; } else if (isSet(b)) { return false; - } - - if (isMap(a)) { - if (!isMap(b) || !mapEquiv(a, b, strict, actualVisitedObjects)) + } else if (isMap(a)) { + if (!isMap(b) || !mapEquiv(a, b, strict, memos)) return false; } else if (isMap(b)) { return false; @@ -450,9 +456,9 @@ function objEquiv(a, b, strict, actualVisitedObjects) { // The pair must have equivalent values for every corresponding key. // Possibly expensive deep test: - for (i = aKeys.length - 1; i >= 0; i--) { - key = aKeys[i]; - if (!innerDeepEqual(a[key], b[key], strict, actualVisitedObjects)) + for (var i = 0; i < keys.length; i++) { + const key = keys[i]; + if (!innerDeepEqual(a[key], b[key], strict, memos)) return false; } return true; @@ -489,45 +495,32 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) { }; function expectedException(actual, expected) { - // actual is guaranteed to be an Error object, but we need to check expected. - if (!expected) { - return false; - } - - if (objectToString(expected) === '[object RegExp]') { + if (typeof expected !== 'function') { + // Should be a RegExp, if not fail hard return expected.test(actual); } - - try { - if (actual instanceof expected) { - return true; - } - } catch (e) { - // Ignore. The instanceof check doesn't work for arrow functions. + // Guard instanceof against arrow functions as they don't have a prototype. + if (expected.prototype !== undefined && actual instanceof expected) { + return true; } - if (Error.isPrototypeOf(expected)) { return false; } - return expected.call({}, actual) === true; } function tryBlock(block) { - var error; try { block(); } catch (e) { - error = e; + return e; } - return error; } function innerThrows(shouldThrow, block, expected, message) { - var actual; + var details = ''; if (typeof block !== 'function') { - const errors = lazyErrors(); throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'block', 'function', block); } @@ -537,28 +530,24 @@ function innerThrows(shouldThrow, block, expected, message) { expected = null; } - actual = tryBlock(block); - - message = (expected && expected.name ? ' (' + expected.name + ')' : '') + - (message ? ': ' + message : '.'); - - if (shouldThrow && !actual) { - innerFail(actual, expected, 'Missing expected exception' + message, fail); - } - - const userProvidedMessage = typeof message === 'string'; - const isUnwantedException = !shouldThrow && util.isError(actual); - const isUnexpectedException = !shouldThrow && actual && !expected; - - if ((isUnwantedException && - userProvidedMessage && - expectedException(actual, expected)) || - isUnexpectedException) { - innerFail(actual, expected, 'Got unwanted exception' + message, fail); - } + const actual = tryBlock(block); - if ((shouldThrow && actual && expected && - !expectedException(actual, expected)) || (!shouldThrow && actual)) { + if (shouldThrow === true) { + if (actual === undefined) { + if (expected && expected.name) { + details += ` (${expected.name})`; + } + details += message ? `: ${message}` : '.'; + fail(actual, expected, `Missing expected exception${details}`, fail); + } + if (expected && expectedException(actual, expected) === false) { + throw actual; + } + } else if (actual !== undefined) { + if (!expected || expectedException(actual, expected)) { + details = message ? `: ${message}` : '.'; + fail(actual, expected, `Got unwanted exception${details}`, fail); + } throw actual; } } diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 4ad46e54f786cc..47871cfe463360 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -46,7 +46,6 @@ class AssertionError extends Error { throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); } const util = lazyUtil(); - const assert = lazyAssert(); const message = options.message || `${util.inspect(options.actual).slice(0, 128)} ` + `${options.operator} ` + @@ -59,8 +58,7 @@ class AssertionError extends Error { this.actual = options.actual; this.expected = options.expected; this.operator = options.operator; - const stackStartFunction = options.stackStartFunction || assert.fail; - Error.captureStackTrace(this, stackStartFunction); + Error.captureStackTrace(this, options.stackStartFunction); } } diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 382123ef870256..d35a6d27355df8 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -36,6 +36,16 @@ assert.ok(a.AssertionError.prototype instanceof Error, assert.throws(makeBlock(a, false), a.AssertionError, 'ok(false)'); +// Using a object as second arg results in a failure +assert.throws( + () => { assert.throws(() => { throw new Error(); }, { foo: 'bar' }); }, + common.expectsError({ + type: TypeError, + message: 'expected.test is not a function' + }) +); + + assert.doesNotThrow(makeBlock(a, true), a.AssertionError, 'ok(true)'); assert.doesNotThrow(makeBlock(a, 'test', 'ok(\'test\')')); @@ -423,8 +433,7 @@ assert.throws(makeBlock(thrower, TypeError)); assert.ok(e instanceof TypeError, 'type'); } assert.strictEqual(true, threw, - 'a.throws with an explicit error is eating extra errors', - a.AssertionError); + 'a.throws with an explicit error is eating extra errors'); } // doesNotThrow should pass through all errors