diff --git a/lib/assert.js b/lib/assert.js index 09c32693805b9e..e63175ad77800c 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -77,301 +77,10 @@ let warned = false; // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The // assert module must conform to the following interface. - const assert = module.exports = ok; const NO_EXCEPTION_SENTINEL = {}; -// All of the following functions must throw an AssertionError -// when a corresponding condition is not met, with a message that -// may be undefined if not provided. All assertion methods provide -// both the actual and expected values to the assertion error for -// display purposes. - -function innerFail(obj) { - if (obj.message instanceof Error) throw obj.message; - - throw new AssertionError(obj); -} - -/** - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @param {string} [operator] - * @param {Function} [stackStartFn] - */ -function fail(actual, expected, message, operator, stackStartFn) { - const argsLen = arguments.length; - - let internalMessage = false; - if (actual == null && argsLen <= 1) { - internalMessage = true; - message = 'Failed'; - } else if (argsLen === 1) { - message = actual; - actual = undefined; - } else { - if (warned === false) { - warned = true; - process.emitWarning( - 'assert.fail() with more than one argument is deprecated. ' + - 'Please use assert.strictEqual() instead or only pass a message.', - 'DeprecationWarning', - 'DEP0094', - ); - } - if (argsLen === 2) - operator = '!='; - } - - if (message instanceof Error) throw message; - - const errArgs = { - actual, - expected, - operator: operator === undefined ? 'fail' : operator, - stackStartFn: stackStartFn || fail, - message, - }; - const err = new AssertionError(errArgs); - if (internalMessage) { - err.generatedMessage = true; - } - throw err; -} - -assert.fail = fail; - -// The AssertionError is defined in internal/error. -assert.AssertionError = AssertionError; - -/** - * Pure assertion tests whether a value is truthy, as determined - * by !!value. - * @param {...any} args - * @returns {void} - */ -function ok(...args) { - innerOk(ok, args.length, ...args); -} -assert.ok = ok; - -/** - * The equality assertion tests shallow, coercive equality with ==. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -/* eslint-disable no-restricted-properties */ -assert.equal = function equal(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - // eslint-disable-next-line eqeqeq - if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { - innerFail({ - actual, - expected, - message, - operator: '==', - stackStartFn: equal, - }); - } -}; - -/** - * The non-equality assertion tests for whether two objects are not - * equal with !=. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notEqual = function notEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - // eslint-disable-next-line eqeqeq - if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { - innerFail({ - actual, - expected, - message, - operator: '!=', - stackStartFn: notEqual, - }); - } -}; - -/** - * The deep equivalence assertion tests a deep equality relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.deepEqual = function deepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isDeepEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'deepEqual', - stackStartFn: deepEqual, - }); - } -}; - -/** - * The deep non-equivalence assertion tests for any deep inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notDeepEqual = function notDeepEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (isDeepEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'notDeepEqual', - stackStartFn: notDeepEqual, - }); - } -}; -/* eslint-enable */ - -/** - * The deep strict equivalence assertion tests a deep strict equality - * relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isDeepStrictEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'deepStrictEqual', - stackStartFn: deepStrictEqual, - }); - } -}; - -/** - * The deep strict non-equivalence assertion tests for any deep strict - * inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notDeepStrictEqual = notDeepStrictEqual; -function notDeepStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (isDeepStrictEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'notDeepStrictEqual', - stackStartFn: notDeepStrictEqual, - }); - } -} - -/** - * The strict equivalence assertion tests a strict equality relation. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.strictEqual = function strictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (!ObjectIs(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'strictEqual', - stackStartFn: strictEqual, - }); - } -}; - -/** - * The strict non-equivalence assertion tests for any strict inequality. - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.notStrictEqual = function notStrictEqual(actual, expected, message) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (ObjectIs(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'notStrictEqual', - stackStartFn: notStrictEqual, - }); - } -}; - -/** - * The strict equivalence assertion test between two objects - * @param {any} actual - * @param {any} expected - * @param {string | Error} [message] - * @returns {void} - */ -assert.partialDeepStrictEqual = function partialDeepStrictEqual( - actual, - expected, - message, -) { - if (arguments.length < 2) { - throw new ERR_MISSING_ARGS('actual', 'expected'); - } - if (isDeepEqual === undefined) lazyLoadComparison(); - if (!isPartialStrictEqual(actual, expected)) { - innerFail({ - actual, - expected, - message, - operator: 'partialDeepStrictEqual', - stackStartFn: partialDeepStrictEqual, - }); - } -}; - class Comparison { constructor(obj, keys, actual) { for (const key of keys) { @@ -389,438 +98,746 @@ class Comparison { } } -function compareExceptionKey(actual, expected, key, message, keys, fn) { - if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { - if (!message) { - // Create placeholder objects to create a nice output. - const a = new Comparison(actual, keys); - const b = new Comparison(expected, keys, actual); +class Assert { + constructor(options = {}) { + this.options = options; + this.AssertionError = AssertionError; + } + + // All of the following functions must throw an AssertionError + // when a corresponding condition is not met, with a message that + // may be undefined if not provided. All assertion methods provide + // both the actual and expected values to the assertion error for + // display purposes. + + #innerFail(obj) { + if (obj.message instanceof Error) throw obj.message; + + throw new AssertionError(obj); + } + + #internalMatch(string, regexp, message, fn) { + if (!isRegExp(regexp)) { + throw new ERR_INVALID_ARG_TYPE( + 'regexp', 'RegExp', regexp, + ); + } + const match = fn === Assert.prototype.match; + if (typeof string !== 'string' || + RegExpPrototypeExec(regexp, string) !== null !== match) { + if (message instanceof Error) { + throw message; + } + + const generatedMessage = !message; + // 'The input was expected to not match the regular expression ' + + message ||= (typeof string !== 'string' ? + 'The "string" argument must be of type string. Received type ' + + `${typeof string} (${inspect(string)})` : + (match ? + 'The input did not match the regular expression ' : + 'The input was expected to not match the regular expression ') + + `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); const err = new AssertionError({ - actual: a, - expected: b, - operator: 'deepStrictEqual', + actual: string, + expected: regexp, + message, + operator: fn.name, stackStartFn: fn, }); - err.actual = actual; - err.expected = expected; - err.operator = fn.name; + err.generatedMessage = generatedMessage; throw err; } - innerFail({ - actual, - expected, - message, - operator: fn.name, - stackStartFn: fn, - }); } -} -function expectedException(actual, expected, message, fn) { - let generatedMessage = false; - let throwError = false; + #compareExceptionKey(actual, expected, key, message, keys, fn) { + if (!(key in actual) || !isDeepStrictEqual(actual[key], expected[key])) { + if (!message) { + // Create placeholder objects to create a nice output. + const a = new Comparison(actual, keys); + const b = new Comparison(expected, keys, actual); + + const err = new AssertionError({ + actual: a, + expected: b, + operator: 'deepStrictEqual', + stackStartFn: fn, + }); + err.actual = actual; + err.expected = expected; + err.operator = fn.name; + throw err; + } + this.#innerFail({ + actual, + expected, + message, + operator: fn.name, + stackStartFn: fn, + }); + } + } + + #expectedException(actual, expected, message, fn) { + let generatedMessage = false; + let throwError = false; - if (typeof expected !== 'function') { - // Handle regular expressions. - if (isRegExp(expected)) { - const str = String(actual); - if (RegExpPrototypeExec(expected, str) !== null) - return; + if (typeof expected !== 'function') { + // Handle regular expressions. + if (isRegExp(expected)) { + const str = String(actual); + if (RegExpPrototypeExec(expected, str) !== null) + return; + if (!message) { + generatedMessage = true; + message = 'The input did not match the regular expression ' + + `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; + } + throwError = true; + // Handle primitives properly. + } else if (typeof actual !== 'object' || actual === null) { + const err = new AssertionError({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: fn, + }); + err.operator = fn.name; + throw err; + } else { + // Handle validation objects. + const keys = ObjectKeys(expected); + // Special handle errors to make sure the name and the message are + // compared as well. + if (expected instanceof Error) { + ArrayPrototypePush(keys, 'name', 'message'); + } else if (keys.length === 0) { + throw new ERR_INVALID_ARG_VALUE('error', + expected, 'may not be an empty object'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + for (const key of keys) { + if (typeof actual[key] === 'string' && + isRegExp(expected[key]) && + RegExpPrototypeExec(expected[key], actual[key]) !== null) { + continue; + } + this.#compareExceptionKey(actual, expected, key, message, keys, fn); + } + return; + } + // Guard instanceof against arrow functions as they don't have a prototype. + // Check for matching Error classes. + } else if (expected.prototype !== undefined && actual instanceof expected) { + return; + } else if (ObjectPrototypeIsPrototypeOf(Error, expected)) { if (!message) { generatedMessage = true; - message = 'The input did not match the regular expression ' + - `${inspect(expected)}. Input:\n\n${inspect(str)}\n`; + message = 'The error is expected to be an instance of ' + + `"${expected.name}". Received `; + if (isError(actual)) { + const name = (actual.constructor?.name) || + actual.name; + if (expected.name === name) { + message += 'an error with identical name but a different prototype.'; + } else { + message += `"${name}"`; + } + if (actual.message) { + message += `\n\nError message:\n\n${actual.message}`; + } + } else { + message += `"${inspect(actual, { depth: -1 })}"`; + } } throwError = true; - // Handle primitives properly. - } else if (typeof actual !== 'object' || actual === null) { + } else { + // Check validation functions return value. + const res = ReflectApply(expected, {}, [actual]); + if (res !== true) { + if (!message) { + generatedMessage = true; + const name = expected.name ? `"${expected.name}" ` : ''; + message = `The ${name}validation function is expected to return` + + ` "true". Received ${inspect(res)}`; + + if (isError(actual)) { + message += `\n\nCaught error:\n\n${actual}`; + } + } + throwError = true; + } + } + + if (throwError) { const err = new AssertionError({ actual, expected, message, - operator: 'deepStrictEqual', + operator: fn.name, stackStartFn: fn, }); - err.operator = fn.name; + err.generatedMessage = generatedMessage; throw err; - } else { - // Handle validation objects. - const keys = ObjectKeys(expected); - // Special handle errors to make sure the name and the message are - // compared as well. - if (expected instanceof Error) { - ArrayPrototypePush(keys, 'name', 'message'); - } else if (keys.length === 0) { - throw new ERR_INVALID_ARG_VALUE('error', - expected, 'may not be an empty object'); + } + } + + #expectsError(stackStartFn, actual, error, message) { + if (typeof error === 'string') { + if (arguments.length === 4) { + throw new ERR_INVALID_ARG_TYPE('error', + ['Object', 'Error', 'Function', 'RegExp'], + error); } - if (isDeepEqual === undefined) lazyLoadComparison(); - for (const key of keys) { - if (typeof actual[key] === 'string' && - isRegExp(expected[key]) && - RegExpPrototypeExec(expected[key], actual[key]) !== null) { - continue; + if (typeof actual === 'object' && actual !== null) { + if (actual.message === error) { + throw new ERR_AMBIGUOUS_ARGUMENT( + 'error/message', + `The error message "${actual.message}" is identical to the message.`, + ); } - compareExceptionKey(actual, expected, key, message, keys, fn); + } else if (actual === error) { + throw new ERR_AMBIGUOUS_ARGUMENT( + 'error/message', + `The error "${actual}" is identical to the message.`, + ); } - return; + message = error; + error = undefined; + } else if (error != null && + typeof error !== 'object' && + typeof error !== 'function') { + throw new ERR_INVALID_ARG_TYPE('error', + ['Object', 'Error', 'Function', 'RegExp'], + error); } - // Guard instanceof against arrow functions as they don't have a prototype. - // Check for matching Error classes. - } else if (expected.prototype !== undefined && actual instanceof expected) { - return; - } else if (ObjectPrototypeIsPrototypeOf(Error, expected)) { - if (!message) { - generatedMessage = true; - message = 'The error is expected to be an instance of ' + - `"${expected.name}". Received `; - if (isError(actual)) { - const name = (actual.constructor?.name) || - actual.name; - if (expected.name === name) { - message += 'an error with identical name but a different prototype.'; - } else { - message += `"${name}"`; - } - if (actual.message) { - message += `\n\nError message:\n\n${actual.message}`; - } - } else { - message += `"${inspect(actual, { depth: -1 })}"`; + + if (actual === NO_EXCEPTION_SENTINEL) { + let details = ''; + if (error?.name) { + details += ` (${error.name})`; } + details += message ? `: ${message}` : '.'; + const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception'; + this.#innerFail({ + actual: undefined, + expected: error, + operator: stackStartFn.name, + message: `Missing expected ${fnType}${details}`, + stackStartFn, + }); } - throwError = true; - } else { - // Check validation functions return value. - const res = ReflectApply(expected, {}, [actual]); - if (res !== true) { - if (!message) { - generatedMessage = true; - const name = expected.name ? `"${expected.name}" ` : ''; - message = `The ${name}validation function is expected to return` + - ` "true". Received ${inspect(res)}`; - if (isError(actual)) { - message += `\n\nCaught error:\n\n${actual}`; - } + if (!error) + return; + + this.#expectedException(actual, error, message, stackStartFn); + } + + #expectsNoError(stackStartFn, actual, error, message) { + if (actual === NO_EXCEPTION_SENTINEL) + return; + + if (typeof error === 'string') { + message = error; + error = undefined; + } + + if (!error || this.#hasMatchingError(actual, error)) { + const details = message ? `: ${message}` : '.'; + const fnType = stackStartFn === Assert.prototype.doesNotReject ? + 'rejection' : 'exception'; + this.#innerFail({ + actual, + expected: error, + operator: stackStartFn.name, + message: `Got unwanted ${fnType}${details}\n` + + `Actual message: "${actual?.message}"`, + stackStartFn, + }); + } + throw actual; + } + + #hasMatchingError(actual, expected) { + if (typeof expected !== 'function') { + if (isRegExp(expected)) { + const str = String(actual); + return RegExpPrototypeExec(expected, str) !== null; } - throwError = true; + throw new ERR_INVALID_ARG_TYPE( + 'expected', ['Function', 'RegExp'], expected, + ); + } + // Guard instanceof against arrow functions as they don't have a prototype. + if (expected.prototype !== undefined && actual instanceof expected) { + return true; } + if (ObjectPrototypeIsPrototypeOf(Error, expected)) { + return false; + } + return ReflectApply(expected, {}, [actual]) === true; + } + + async #waitForActual(promiseFn) { + let resultPromise; + if (typeof promiseFn === 'function') { + // Return a rejected promise if `promiseFn` throws synchronously. + resultPromise = promiseFn(); + // Fail in case no promise is returned. + if (!this.#checkIsPromise(resultPromise)) { + throw new ERR_INVALID_RETURN_VALUE('instance of Promise', + 'promiseFn', resultPromise); + } + } else if (this.#checkIsPromise(promiseFn)) { + resultPromise = promiseFn; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'promiseFn', ['Function', 'Promise'], promiseFn); + } + + try { + await resultPromise; + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; } - if (throwError) { - const err = new AssertionError({ + #getActual(fn) { + validateFunction(fn, 'fn'); + try { + fn(); + } catch (e) { + return e; + } + return NO_EXCEPTION_SENTINEL; + } + + #checkIsPromise(obj) { + // Accept native ES6 promises and promises that are implemented in a similar + // way. Do not accept thenables that use a function as `obj` and that have no + // `catch` handler. + return isPromise(obj) || + (obj !== null && typeof obj === 'object' && + typeof obj.then === 'function' && + typeof obj.catch === 'function'); + } + + /** + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args + * @returns {void} + */ + ok(...args) { + innerOk(this.ok, args.length, ...args); + } + + /** + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @param {string} [operator] + * @param {Function} [stackStartFn] + */ + fail(actual, expected, message, operator, stackStartFn) { + const argsLen = arguments.length; + + let internalMessage = false; + if (actual == null && argsLen <= 1) { + internalMessage = true; + message = 'Failed'; + } else if (argsLen === 1) { + message = actual; + actual = undefined; + } else { + if (warned === false) { + warned = true; + process.emitWarning( + 'assert.fail() with more than one argument is deprecated. ' + + 'Please use assert.strictEqual() instead or only pass a message.', + 'DeprecationWarning', + 'DEP0094', + ); + } + if (argsLen === 2) + operator = '!='; + } + + if (message instanceof Error) throw message; + + const errArgs = { actual, expected, + operator: operator === undefined ? 'fail' : operator, + stackStartFn: stackStartFn || this.fail, message, - operator: fn.name, - stackStartFn: fn, - }); - err.generatedMessage = generatedMessage; + }; + const err = new AssertionError(errArgs); + if (internalMessage) { + err.generatedMessage = true; + } throw err; } -} -function getActual(fn) { - validateFunction(fn, 'fn'); - try { - fn(); - } catch (e) { - return e; - } - return NO_EXCEPTION_SENTINEL; -} + /** + * The equality assertion tests shallow, coercive equality with ==. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + equal(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + // eslint-disable-next-line eqeqeq + if (actual != expected && (!NumberIsNaN(actual) || !NumberIsNaN(expected))) { + this.#innerFail({ + actual, + expected, + message, + operator: '==', + stackStartFn: this.equal, + }); + } + }; -function checkIsPromise(obj) { - // Accept native ES6 promises and promises that are implemented in a similar - // way. Do not accept thenables that use a function as `obj` and that have no - // `catch` handler. - return isPromise(obj) || - (obj !== null && typeof obj === 'object' && - typeof obj.then === 'function' && - typeof obj.catch === 'function'); -} + /** + * The non-equality assertion tests for whether two objects are not + * equal with !=. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + // eslint-disable-next-line eqeqeq + if (actual == expected || (NumberIsNaN(actual) && NumberIsNaN(expected))) { + this.#innerFail({ + actual, + expected, + message, + operator: '!=', + stackStartFn: this.notEqual, + }); + } + }; -async function waitForActual(promiseFn) { - let resultPromise; - if (typeof promiseFn === 'function') { - // Return a rejected promise if `promiseFn` throws synchronously. - resultPromise = promiseFn(); - // Fail in case no promise is returned. - if (!checkIsPromise(resultPromise)) { - throw new ERR_INVALID_RETURN_VALUE('instance of Promise', - 'promiseFn', resultPromise); - } - } else if (checkIsPromise(promiseFn)) { - resultPromise = promiseFn; - } else { - throw new ERR_INVALID_ARG_TYPE( - 'promiseFn', ['Function', 'Promise'], promiseFn); - } + /** + * The deep equivalence assertion tests a deep equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + deepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepEqual(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'deepEqual', + stackStartFn: this.deepEqual, + }); + } + }; - try { - await resultPromise; - } catch (e) { - return e; - } - return NO_EXCEPTION_SENTINEL; -} + /** + * The deep non-equivalence assertion tests for any deep inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notDeepEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (isDeepEqual(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'notDeepEqual', + stackStartFn: this.notDeepEqual, + }); + } + }; -function expectsError(stackStartFn, actual, error, message) { - if (typeof error === 'string') { - if (arguments.length === 4) { - throw new ERR_INVALID_ARG_TYPE('error', - ['Object', 'Error', 'Function', 'RegExp'], - error); + /** + * The deep strict equivalence assertion tests a deep strict equality + * relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + deepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); } - if (typeof actual === 'object' && actual !== null) { - if (actual.message === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error message "${actual.message}" is identical to the message.`, - ); - } - } else if (actual === error) { - throw new ERR_AMBIGUOUS_ARGUMENT( - 'error/message', - `The error "${actual}" is identical to the message.`, - ); + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isDeepStrictEqual(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: this.deepStrictEqual, + }); } - message = error; - error = undefined; - } else if (error != null && - typeof error !== 'object' && - typeof error !== 'function') { - throw new ERR_INVALID_ARG_TYPE('error', - ['Object', 'Error', 'Function', 'RegExp'], - error); - } + }; - if (actual === NO_EXCEPTION_SENTINEL) { - let details = ''; - if (error?.name) { - details += ` (${error.name})`; - } - details += message ? `: ${message}` : '.'; - const fnType = stackStartFn === assert.rejects ? 'rejection' : 'exception'; - innerFail({ - actual: undefined, - expected: error, - operator: stackStartFn.name, - message: `Missing expected ${fnType}${details}`, - stackStartFn, - }); + /** + * The deep strict non-equivalence assertion tests for any deep strict + * inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notDeepStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (isDeepStrictEqual(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'notDeepStrictEqual', + stackStartFn: this.notDeepStrictEqual, + }); + } } - if (!error) - return; - - expectedException(actual, error, message, stackStartFn); -} + /** + * The strict equivalence assertion tests a strict equality relation. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + strictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (!ObjectIs(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'strictEqual', + stackStartFn: this.strictEqual, + }); + } + }; -function hasMatchingError(actual, expected) { - if (typeof expected !== 'function') { - if (isRegExp(expected)) { - const str = String(actual); - return RegExpPrototypeExec(expected, str) !== null; + /** + * The strict non-equivalence assertion tests for any strict inequality. + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + notStrictEqual(actual, expected, message) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); } - throw new ERR_INVALID_ARG_TYPE( - 'expected', ['Function', 'RegExp'], expected, - ); - } - // Guard instanceof against arrow functions as they don't have a prototype. - if (expected.prototype !== undefined && actual instanceof expected) { - return true; - } - if (ObjectPrototypeIsPrototypeOf(Error, expected)) { - return false; - } - return ReflectApply(expected, {}, [actual]) === true; -} + if (ObjectIs(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'notStrictEqual', + stackStartFn: this.notStrictEqual, + }); + } + }; -function expectsNoError(stackStartFn, actual, error, message) { - if (actual === NO_EXCEPTION_SENTINEL) - return; + /** + * The strict equivalence assertion test between two objects + * @param {any} actual + * @param {any} expected + * @param {string | Error} [message] + * @returns {void} + */ + partialDeepStrictEqual( + actual, + expected, + message, + ) { + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('actual', 'expected'); + } + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isPartialStrictEqual(actual, expected)) { + this.#innerFail({ + actual, + expected, + message, + operator: 'partialDeepStrictEqual', + stackStartFn: this.partialDeepStrictEqual, + }); + } + }; - if (typeof error === 'string') { - message = error; - error = undefined; - } + /** + * Expects the `string` input to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ + match(string, regexp, message) { + this.#internalMatch(string, regexp, message, Assert.prototype.match); + }; - if (!error || hasMatchingError(actual, error)) { - const details = message ? `: ${message}` : '.'; - const fnType = stackStartFn === assert.doesNotReject ? - 'rejection' : 'exception'; - innerFail({ - actual, - expected: error, - operator: stackStartFn.name, - message: `Got unwanted ${fnType}${details}\n` + - `Actual message: "${actual?.message}"`, - stackStartFn, - }); - } - throw actual; -} + /** + * Expects the `string` input not to match the regular expression. + * @param {string} string + * @param {RegExp} regexp + * @param {string | Error} [message] + * @returns {void} + */ + doesNotMatch(string, regexp, message) { + this.#internalMatch(string, regexp, message, Assert.prototype.doesNotMatch); + }; -/** - * Expects the function `promiseFn` to throw an error. - * @param {() => any} promiseFn - * @param {...any} [args] - * @returns {void} - */ -assert.throws = function throws(promiseFn, ...args) { - expectsError(throws, getActual(promiseFn), ...args); -}; + /** + * Expects the function `promiseFn` to throw an error. + * @param {() => any} promiseFn + * @param {...any} [args] + * @returns {void} + */ + throws(promiseFn, ...args) { + this.#expectsError(Assert.prototype.throws, this.#getActual(promiseFn), ...args); + }; -/** - * Expects `promiseFn` function or its value to reject. - * @param {() => Promise} promiseFn - * @param {...any} [args] - * @returns {Promise} - */ -assert.rejects = async function rejects(promiseFn, ...args) { - expectsError(rejects, await waitForActual(promiseFn), ...args); -}; + /** + * Expects `promiseFn` function or its value to reject. + * @param {() => Promise} promiseFn + * @param {...any} [args] + * @returns {Promise} + */ + async rejects(promiseFn, ...args) { + this.#expectsError(Assert.prototype.rejects, await this.#waitForActual(promiseFn), ...args); + }; -/** - * Asserts that the function `fn` does not throw an error. - * @param {() => any} fn - * @param {...any} [args] - * @returns {void} - */ -assert.doesNotThrow = function doesNotThrow(fn, ...args) { - expectsNoError(doesNotThrow, getActual(fn), ...args); -}; + /** + * Asserts that the function `fn` does not throw an error. + * @param {() => any} fn + * @param {...any} [args] + * @returns {void} + */ + doesNotThrow(fn, ...args) { + this.#expectsNoError(Assert.prototype.doesNotThrow, this.#getActual(fn), ...args); + }; -/** - * Expects `fn` or its value to not reject. - * @param {() => Promise} fn - * @param {...any} [args] - * @returns {Promise} - */ -assert.doesNotReject = async function doesNotReject(fn, ...args) { - expectsNoError(doesNotReject, await waitForActual(fn), ...args); -}; + /** + * Expects `fn` or its value to not reject. + * @param {() => Promise} fn + * @param {...any} [args] + * @returns {Promise} + */ + async doesNotReject(fn, ...args) { + this.#expectsNoError(Assert.prototype.doesNotReject, await this.#waitForActual(fn), ...args); + }; -/** - * Throws `value` if the value is not `null` or `undefined`. - * @param {any} err - * @returns {void} - */ -assert.ifError = function ifError(err) { - if (err !== null && err !== undefined) { - let message = 'ifError got unwanted exception: '; - if (typeof err === 'object' && typeof err.message === 'string') { - if (err.message.length === 0 && err.constructor) { - message += err.constructor.name; + /** + * Throws `value` if the value is not `null` or `undefined`. + * @param {any} err + * @returns {void} + */ + ifError(err) { + if (err !== null && err !== undefined) { + let message = 'ifError got unwanted exception: '; + if (typeof err === 'object' && typeof err.message === 'string') { + if (err.message.length === 0 && err.constructor) { + message += err.constructor.name; + } else { + message += err.message; + } } else { - message += err.message; + message += inspect(err); } - } else { - message += inspect(err); - } - const newErr = new AssertionError({ - actual: err, - expected: null, - operator: 'ifError', - message, - stackStartFn: ifError, - }); - - // Make sure we actually have a stack trace! - const origStack = err.stack; - - if (typeof origStack === 'string') { - // This will remove any duplicated frames from the error frames taken - // from within `ifError` and add the original error frames to the newly - // created ones. - const origStackStart = StringPrototypeIndexOf(origStack, '\n at'); - if (origStackStart !== -1) { - const originalFrames = StringPrototypeSplit( - StringPrototypeSlice(origStack, origStackStart + 1), - '\n', - ); - // Filter all frames existing in err.stack. - let newFrames = StringPrototypeSplit(newErr.stack, '\n'); - for (const errFrame of originalFrames) { - // Find the first occurrence of the frame. - const pos = ArrayPrototypeIndexOf(newFrames, errFrame); - if (pos !== -1) { - // Only keep new frames. - newFrames = ArrayPrototypeSlice(newFrames, 0, pos); - break; + const newErr = new AssertionError({ + actual: err, + expected: null, + operator: 'ifError', + message, + stackStartFn: this.ifError, + }); + + // Make sure we actually have a stack trace! + const origStack = err.stack; + + if (typeof origStack === 'string') { + // This will remove any duplicated frames from the error frames taken + // from within `ifError` and add the original error frames to the newly + // created ones. + const origStackStart = StringPrototypeIndexOf(origStack, '\n at'); + if (origStackStart !== -1) { + const originalFrames = StringPrototypeSplit( + StringPrototypeSlice(origStack, origStackStart + 1), + '\n', + ); + // Filter all frames existing in err.stack. + let newFrames = StringPrototypeSplit(newErr.stack, '\n'); + for (const errFrame of originalFrames) { + // Find the first occurrence of the frame. + const pos = ArrayPrototypeIndexOf(newFrames, errFrame); + if (pos !== -1) { + // Only keep new frames. + newFrames = ArrayPrototypeSlice(newFrames, 0, pos); + break; + } } + const stackStart = ArrayPrototypeJoin(newFrames, '\n'); + const stackEnd = ArrayPrototypeJoin(originalFrames, '\n'); + newErr.stack = `${stackStart}\n${stackEnd}`; } - const stackStart = ArrayPrototypeJoin(newFrames, '\n'); - const stackEnd = ArrayPrototypeJoin(originalFrames, '\n'); - newErr.stack = `${stackStart}\n${stackEnd}`; } - } - throw newErr; - } -}; - -function internalMatch(string, regexp, message, fn) { - if (!isRegExp(regexp)) { - throw new ERR_INVALID_ARG_TYPE( - 'regexp', 'RegExp', regexp, - ); - } - const match = fn === assert.match; - if (typeof string !== 'string' || - RegExpPrototypeExec(regexp, string) !== null !== match) { - if (message instanceof Error) { - throw message; - } - - const generatedMessage = !message; - - // 'The input was expected to not match the regular expression ' + - message ||= (typeof string !== 'string' ? - 'The "string" argument must be of type string. Received type ' + - `${typeof string} (${inspect(string)})` : - (match ? - 'The input did not match the regular expression ' : - 'The input was expected to not match the regular expression ') + - `${inspect(regexp)}. Input:\n\n${inspect(string)}\n`); - const err = new AssertionError({ - actual: string, - expected: regexp, - message, - operator: fn.name, - stackStartFn: fn, - }); - err.generatedMessage = generatedMessage; - throw err; - } + throw newErr; + } + }; } -/** - * Expects the `string` input to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] - * @returns {void} - */ -assert.match = function match(string, regexp, message) { - internalMatch(string, regexp, message, match); -}; +const assertInstance = new Assert(); +['ok', 'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', + 'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual', + 'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch', + 'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError'].forEach((name) => { + assertInstance[name] = assertInstance[name].bind(assertInstance); +}); /** - * Expects the `string` input not to match the regular expression. - * @param {string} string - * @param {RegExp} regexp - * @param {string | Error} [message] + * Pure assertion tests whether a value is truthy, as determined + * by !!value. + * @param {...any} args * @returns {void} */ -assert.doesNotMatch = function doesNotMatch(string, regexp, message) { - internalMatch(string, regexp, message, doesNotMatch); -}; +function ok(...args) { + innerOk(ok, args.length, ...args); +} +ObjectAssign(assert, assertInstance); +assert.ok = ok; /** * Expose a strict only variant of assert. @@ -831,6 +848,8 @@ function strict(...args) { innerOk(strict, args.length, ...args); } +assert.AssertionError = AssertionError; + assert.strict = ObjectAssign(strict, assert, { equal: assert.strictEqual, deepEqual: assert.deepStrictEqual, @@ -838,4 +857,7 @@ assert.strict = ObjectAssign(strict, assert, { notDeepEqual: assert.notDeepStrictEqual, }); +assert.strict.Assert = Assert; assert.strict.strict = assert.strict; + +module.exports.Assert = Assert; diff --git a/test/fixtures/errors/error_exit.snapshot b/test/fixtures/errors/error_exit.snapshot index 778165dc25c4fc..de4ba32c4560a1 100644 --- a/test/fixtures/errors/error_exit.snapshot +++ b/test/fixtures/errors/error_exit.snapshot @@ -1,13 +1,13 @@ Exiting with code=1 node:assert:* - throw new AssertionError(obj); - ^ + throw new AssertionError(obj); + ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 1 !== 2 - at Object. (*error_exit.js:*:*) { + at new AssertionError (node:internal*assert*assertion_error:*:*) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 1, diff --git a/test/fixtures/errors/if-error-has-good-stack.snapshot b/test/fixtures/errors/if-error-has-good-stack.snapshot index 9296b25f10b7c6..30477c483de7f8 100644 --- a/test/fixtures/errors/if-error-has-good-stack.snapshot +++ b/test/fixtures/errors/if-error-has-good-stack.snapshot @@ -1,12 +1,12 @@ node:assert:* - throw newErr; - ^ + throw newErr; + ^ AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error + at new AssertionError (node:internal*assert*assertion_error:*:*) + at Assert.ifError (node:assert:*:*) at z (*if-error-has-good-stack.js:*:*) at y (*if-error-has-good-stack.js:*:*) - at x (*if-error-has-good-stack.js:*:*) - at Object. (*if-error-has-good-stack.js:*:*) at c (*if-error-has-good-stack.js:*:*) at b (*if-error-has-good-stack.js:*:*) at a (*if-error-has-good-stack.js:*:*) diff --git a/test/fixtures/test-runner/output/describe_it.snapshot b/test/fixtures/test-runner/output/describe_it.snapshot index 67d4af7f1b9f45..2cda9f1a6c7ae6 100644 --- a/test/fixtures/test-runner/output/describe_it.snapshot +++ b/test/fixtures/test-runner/output/describe_it.snapshot @@ -154,6 +154,9 @@ not ok 14 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 15 - resolve pass diff --git a/test/fixtures/test-runner/output/dot_reporter.snapshot b/test/fixtures/test-runner/output/dot_reporter.snapshot index 5abbb979667cfd..1e06b8eea8b607 100644 --- a/test/fixtures/test-runner/output/dot_reporter.snapshot +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -61,6 +61,9 @@ Failed tests: * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index 3b1d15022af704..b0fb1af4dc2224 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -119,6 +119,9 @@ true !== false * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/output.snapshot b/test/fixtures/test-runner/output/output.snapshot index ffbe91759bb859..9dc6a2c0d5755f 100644 --- a/test/fixtures/test-runner/output/output.snapshot +++ b/test/fixtures/test-runner/output/output.snapshot @@ -157,6 +157,9 @@ not ok 13 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 14 - resolve pass diff --git a/test/fixtures/test-runner/output/output_cli.snapshot b/test/fixtures/test-runner/output/output_cli.snapshot index 7f989f14c619cf..8fc99aa548c5ce 100644 --- a/test/fixtures/test-runner/output/output_cli.snapshot +++ b/test/fixtures/test-runner/output/output_cli.snapshot @@ -157,6 +157,9 @@ not ok 13 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 14 - resolve pass diff --git a/test/fixtures/test-runner/output/source_mapped_locations.snapshot b/test/fixtures/test-runner/output/source_mapped_locations.snapshot index 8cf210da817aae..6fc9d3c455b379 100644 --- a/test/fixtures/test-runner/output/source_mapped_locations.snapshot +++ b/test/fixtures/test-runner/output/source_mapped_locations.snapshot @@ -22,6 +22,9 @@ not ok 1 - fails * * * + * + * + * ... 1..1 # tests 1 diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot index 6c11b9ba6d4a39..35530531cf8cdf 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -166,6 +166,9 @@ * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot index a428b1140ac812..8f57e8714d4f6f 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -169,6 +169,9 @@ * * * + * + * + * * { generatedMessage: true, code: 'ERR_ASSERTION', diff --git a/test/parallel/test-assert-class.js b/test/parallel/test-assert-class.js new file mode 100644 index 00000000000000..8a3550aa4e37ff --- /dev/null +++ b/test/parallel/test-assert-class.js @@ -0,0 +1,106 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +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'; +} + +test('Assert class basic instance', () => { + const assertInstance = new Assert(); + + assertInstance.ok(assert.AssertionError.prototype instanceof Error, + 'assert.AssertionError instanceof Error'); + assertInstance.ok(true); + assertInstance.throws( + () => { assertInstance.fail(); }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'Failed', + operator: 'fail', + actual: undefined, + expected: undefined, + generatedMessage: true, + stack: /Failed/ + } + ); + assertInstance.equal(undefined, undefined); + assertInstance.notEqual(true, false); + assertInstance.throws( + () => assertInstance.deepEqual(/a/), + { code: 'ERR_MISSING_ARGS' } + ); + assertInstance.throws( + () => assertInstance.notDeepEqual('test'), + { code: 'ERR_MISSING_ARGS' } + ); + assertInstance.notStrictEqual(2, '2'); + assertInstance.throws(() => assertInstance.strictEqual(2, '2'), + assertInstance.AssertionError, 'strictEqual(2, \'2\')'); + assertInstance.throws( + () => { + assertInstance.partialDeepStrictEqual({ a: true }, { a: false }, 'custom message'); + }, + { + code: 'ERR_ASSERTION', + name: 'AssertionError', + message: 'custom message\n+ actual - expected\n\n {\n+ a: true\n- a: false\n }\n' + } + ); + assertInstance.throws( + () => assertInstance.match(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be an instance of RegExp. ' + + "Received type string ('string')" + } + ); + assertInstance.throws( + () => assertInstance.doesNotMatch(/abc/, 'string'), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "regexp" argument must be an instance of RegExp. ' + + "Received type string ('string')" + } + ); + + /* eslint-disable no-restricted-syntax */ + { + function thrower(errorConstructor) { + throw new errorConstructor({}); + } + + let threw = false; + try { + assertInstance.doesNotThrow(() => thrower(TypeError), assertInstance.AssertionError); + } catch (e) { + threw = true; + assertInstance.ok(e instanceof TypeError); + } + assertInstance.ok(threw, 'assertInstance.doesNotThrow with an explicit error is eating extra errors'); + } + { + let threw = false; + const rangeError = new RangeError('my range'); + + try { + assertInstance.doesNotThrow(() => { + throw new TypeError('wrong type'); + }, TypeError, rangeError); + } catch (e) { + threw = true; + assertInstance.ok(e.message.includes(rangeError.message)); + assertInstance.ok(e instanceof assertInstance.AssertionError); + assertInstance.ok(!e.stack.includes('doesNotThrow'), e); + } + assertInstance.ok(threw); + } + /* eslint-enable no-restricted-syntax */ +}); diff --git a/test/parallel/test-fs-promises.js b/test/parallel/test-fs-promises.js index 796ad3224c4dba..d2f523742798e4 100644 --- a/test/parallel/test-fs-promises.js +++ b/test/parallel/test-fs-promises.js @@ -58,7 +58,7 @@ assert.strictEqual( code: 'ENOENT', name: 'Error', message: /^ENOENT: no such file or directory, access/, - stack: /at async ok\.rejects/ + stack: /at async Assert\.rejects/ } ).then(common.mustCall()); diff --git a/test/parallel/test-runner-assert.js b/test/parallel/test-runner-assert.js index 2c495baca0afd2..236f1851d6d262 100644 --- a/test/parallel/test-runner-assert.js +++ b/test/parallel/test-runner-assert.js @@ -7,6 +7,8 @@ test('expected methods are on t.assert', (t) => { const uncopiedKeys = [ 'AssertionError', 'strict', + 'Assert', + 'options', ]; const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key)); const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();