mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
lib: restructure assert to become a class
PR-URL: https://github.com/nodejs/node/pull/58253 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
3090def635
commit
4f5d11e6fb
15 changed files with 823 additions and 49 deletions
153
lib/assert.js
153
lib/assert.js
|
@ -21,6 +21,7 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypeForEach,
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypePush,
|
||||
|
@ -28,6 +29,7 @@ const {
|
|||
Error,
|
||||
NumberIsNaN,
|
||||
ObjectAssign,
|
||||
ObjectDefineProperty,
|
||||
ObjectIs,
|
||||
ObjectKeys,
|
||||
ObjectPrototypeIsPrototypeOf,
|
||||
|
@ -37,11 +39,13 @@ const {
|
|||
StringPrototypeIndexOf,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeSplit,
|
||||
Symbol,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
codes: {
|
||||
ERR_AMBIGUOUS_ARGUMENT,
|
||||
ERR_CONSTRUCT_CALL_REQUIRED,
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
ERR_INVALID_RETURN_VALUE,
|
||||
|
@ -54,13 +58,16 @@ const {
|
|||
isPromise,
|
||||
isRegExp,
|
||||
} = require('internal/util/types');
|
||||
const { isError } = require('internal/util');
|
||||
const { isError, setOwnProperty } = require('internal/util');
|
||||
const { innerOk } = require('internal/assert/utils');
|
||||
|
||||
const {
|
||||
validateFunction,
|
||||
validateOneOf,
|
||||
} = require('internal/validators');
|
||||
|
||||
const kOptions = Symbol('options');
|
||||
|
||||
let isDeepEqual;
|
||||
let isDeepStrictEqual;
|
||||
let isPartialStrictEqual;
|
||||
|
@ -80,12 +87,60 @@ const assert = module.exports = ok;
|
|||
|
||||
const NO_EXCEPTION_SENTINEL = {};
|
||||
|
||||
/**
|
||||
* Assert options.
|
||||
* @typedef {object} AssertOptions
|
||||
* @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors.
|
||||
* @property {boolean} [strict=true] - If set to true, non-strict methods behave like their corresponding
|
||||
* strict methods.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @class Assert
|
||||
* @param {AssertOptions} [options] - Optional configuration for assertions.
|
||||
* @throws {ERR_CONSTRUCT_CALL_REQUIRED} If not called with `new`.
|
||||
*/
|
||||
function Assert(options) {
|
||||
if (!new.target) {
|
||||
throw new ERR_CONSTRUCT_CALL_REQUIRED('Assert');
|
||||
}
|
||||
|
||||
options = ObjectAssign({ __proto__: null, strict: true }, options);
|
||||
|
||||
const allowedDiffs = ['simple', 'full'];
|
||||
if (options.diff !== undefined) {
|
||||
validateOneOf(options.diff, 'options.diff', allowedDiffs);
|
||||
}
|
||||
|
||||
this.AssertionError = AssertionError;
|
||||
ObjectDefineProperty(this, kOptions, {
|
||||
__proto__: null,
|
||||
value: options,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
if (options.strict) {
|
||||
this.equal = this.strictEqual;
|
||||
this.deepEqual = this.deepStrictEqual;
|
||||
this.notEqual = this.notStrictEqual;
|
||||
this.notDeepEqual = this.notDeepStrictEqual;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
// DESTRUCTURING WARNING: All Assert.prototype methods use optional chaining
|
||||
// (this?.[kOptions]) to safely access instance configuration. When methods are
|
||||
// destructured from an Assert instance (e.g., const {strictEqual} = myAssert),
|
||||
// they lose their `this` context and will use default behavior instead of the
|
||||
// instance's custom options.
|
||||
|
||||
function innerFail(obj) {
|
||||
if (obj.message instanceof Error) throw obj.message;
|
||||
|
||||
|
@ -96,7 +151,7 @@ function innerFail(obj) {
|
|||
* Throws an AssertionError with the given message.
|
||||
* @param {any | Error} [message]
|
||||
*/
|
||||
function fail(message) {
|
||||
Assert.prototype.fail = function fail(message) {
|
||||
if (isError(message)) throw message;
|
||||
|
||||
let internalMessage = false;
|
||||
|
@ -105,19 +160,22 @@ function fail(message) {
|
|||
internalMessage = true;
|
||||
}
|
||||
|
||||
// IMPORTANT: When adding new references to `this`, ensure they use optional chaining
|
||||
// (this?.[kOptions]?.diff) to handle cases where the method is destructured from an
|
||||
// Assert instance and loses its context. Destructured methods will fall back
|
||||
// to default behavior when `this` is undefined.
|
||||
const errArgs = {
|
||||
operator: 'fail',
|
||||
stackStartFn: fail,
|
||||
message,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
};
|
||||
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;
|
||||
|
@ -131,7 +189,17 @@ assert.AssertionError = AssertionError;
|
|||
function ok(...args) {
|
||||
innerOk(ok, args.length, ...args);
|
||||
}
|
||||
assert.ok = ok;
|
||||
|
||||
/**
|
||||
* Pure assertion tests whether a value is truthy, as determined
|
||||
* by !!value.
|
||||
* Duplicated as the other `ok` function is supercharged and exposed as default export.
|
||||
* @param {...any} args
|
||||
* @returns {void}
|
||||
*/
|
||||
Assert.prototype.ok = function ok(...args) {
|
||||
innerOk(ok, args.length, ...args);
|
||||
};
|
||||
|
||||
/**
|
||||
* The equality assertion tests shallow, coercive equality with ==.
|
||||
|
@ -140,8 +208,7 @@ assert.ok = ok;
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
/* eslint-disable no-restricted-properties */
|
||||
assert.equal = function equal(actual, expected, message) {
|
||||
Assert.prototype.equal = function equal(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -153,6 +220,7 @@ assert.equal = function equal(actual, expected, message) {
|
|||
message,
|
||||
operator: '==',
|
||||
stackStartFn: equal,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -165,7 +233,7 @@ assert.equal = function equal(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.notEqual = function notEqual(actual, expected, message) {
|
||||
Assert.prototype.notEqual = function notEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -177,6 +245,7 @@ assert.notEqual = function notEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: '!=',
|
||||
stackStartFn: notEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -188,7 +257,7 @@ assert.notEqual = function notEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.deepEqual = function deepEqual(actual, expected, message) {
|
||||
Assert.prototype.deepEqual = function deepEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -200,6 +269,7 @@ assert.deepEqual = function deepEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: 'deepEqual',
|
||||
stackStartFn: deepEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -211,7 +281,7 @@ assert.deepEqual = function deepEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
|
||||
Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -223,10 +293,10 @@ assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: 'notDeepEqual',
|
||||
stackStartFn: notDeepEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
/**
|
||||
* The deep strict equivalence assertion tests a deep strict equality
|
||||
|
@ -236,7 +306,7 @@ assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
|
||||
Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -248,6 +318,7 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: 'deepStrictEqual',
|
||||
stackStartFn: deepStrictEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -260,7 +331,7 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.notDeepStrictEqual = notDeepStrictEqual;
|
||||
Assert.prototype.notDeepStrictEqual = notDeepStrictEqual;
|
||||
function notDeepStrictEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
|
@ -273,6 +344,7 @@ function notDeepStrictEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: 'notDeepStrictEqual',
|
||||
stackStartFn: notDeepStrictEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -284,7 +356,7 @@ function notDeepStrictEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.strictEqual = function strictEqual(actual, expected, message) {
|
||||
Assert.prototype.strictEqual = function strictEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -295,6 +367,7 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: 'strictEqual',
|
||||
stackStartFn: strictEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -306,7 +379,7 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
|
||||
Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, message) {
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
@ -317,6 +390,7 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
|
|||
message,
|
||||
operator: 'notStrictEqual',
|
||||
stackStartFn: notStrictEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -328,7 +402,7 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.partialDeepStrictEqual = function partialDeepStrictEqual(
|
||||
Assert.prototype.partialDeepStrictEqual = function partialDeepStrictEqual(
|
||||
actual,
|
||||
expected,
|
||||
message,
|
||||
|
@ -344,6 +418,7 @@ assert.partialDeepStrictEqual = function partialDeepStrictEqual(
|
|||
message,
|
||||
operator: 'partialDeepStrictEqual',
|
||||
stackStartFn: partialDeepStrictEqual,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -377,6 +452,7 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
|
|||
expected: b,
|
||||
operator: 'deepStrictEqual',
|
||||
stackStartFn: fn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
err.actual = actual;
|
||||
err.expected = expected;
|
||||
|
@ -389,6 +465,7 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
|
|||
message,
|
||||
operator: fn.name,
|
||||
stackStartFn: fn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -418,6 +495,7 @@ function expectedException(actual, expected, message, fn) {
|
|||
message,
|
||||
operator: 'deepStrictEqual',
|
||||
stackStartFn: fn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
err.operator = fn.name;
|
||||
throw err;
|
||||
|
@ -493,6 +571,7 @@ function expectedException(actual, expected, message, fn) {
|
|||
message,
|
||||
operator: fn.name,
|
||||
stackStartFn: fn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
err.generatedMessage = generatedMessage;
|
||||
throw err;
|
||||
|
@ -580,20 +659,21 @@ function expectsError(stackStartFn, actual, error, message) {
|
|||
details += ` (${error.name})`;
|
||||
}
|
||||
details += message ? `: ${message}` : '.';
|
||||
const fnType = stackStartFn === assert.rejects ? 'rejection' : 'exception';
|
||||
const fnType = stackStartFn === Assert.prototype.rejects ? 'rejection' : 'exception';
|
||||
innerFail({
|
||||
actual: undefined,
|
||||
expected: error,
|
||||
operator: stackStartFn.name,
|
||||
message: `Missing expected ${fnType}${details}`,
|
||||
stackStartFn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
|
||||
if (!error)
|
||||
return;
|
||||
|
||||
expectedException(actual, error, message, stackStartFn);
|
||||
expectedException.call(this, actual, error, message, stackStartFn);
|
||||
}
|
||||
|
||||
function hasMatchingError(actual, expected) {
|
||||
|
@ -627,7 +707,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
|
|||
|
||||
if (!error || hasMatchingError(actual, error)) {
|
||||
const details = message ? `: ${message}` : '.';
|
||||
const fnType = stackStartFn === assert.doesNotReject ?
|
||||
const fnType = stackStartFn === Assert.prototype.doesNotReject ?
|
||||
'rejection' : 'exception';
|
||||
innerFail({
|
||||
actual,
|
||||
|
@ -636,6 +716,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
|
|||
message: `Got unwanted ${fnType}${details}\n` +
|
||||
`Actual message: "${actual?.message}"`,
|
||||
stackStartFn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
}
|
||||
throw actual;
|
||||
|
@ -647,7 +728,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
|
|||
* @param {...any} [args]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.throws = function throws(promiseFn, ...args) {
|
||||
Assert.prototype.throws = function throws(promiseFn, ...args) {
|
||||
expectsError(throws, getActual(promiseFn), ...args);
|
||||
};
|
||||
|
||||
|
@ -657,7 +738,7 @@ assert.throws = function throws(promiseFn, ...args) {
|
|||
* @param {...any} [args]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
assert.rejects = async function rejects(promiseFn, ...args) {
|
||||
Assert.prototype.rejects = async function rejects(promiseFn, ...args) {
|
||||
expectsError(rejects, await waitForActual(promiseFn), ...args);
|
||||
};
|
||||
|
||||
|
@ -667,7 +748,7 @@ assert.rejects = async function rejects(promiseFn, ...args) {
|
|||
* @param {...any} [args]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.doesNotThrow = function doesNotThrow(fn, ...args) {
|
||||
Assert.prototype.doesNotThrow = function doesNotThrow(fn, ...args) {
|
||||
expectsNoError(doesNotThrow, getActual(fn), ...args);
|
||||
};
|
||||
|
||||
|
@ -677,7 +758,7 @@ assert.doesNotThrow = function doesNotThrow(fn, ...args) {
|
|||
* @param {...any} [args]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
assert.doesNotReject = async function doesNotReject(fn, ...args) {
|
||||
Assert.prototype.doesNotReject = async function doesNotReject(fn, ...args) {
|
||||
expectsNoError(doesNotReject, await waitForActual(fn), ...args);
|
||||
};
|
||||
|
||||
|
@ -686,7 +767,7 @@ assert.doesNotReject = async function doesNotReject(fn, ...args) {
|
|||
* @param {any} err
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.ifError = function ifError(err) {
|
||||
Assert.prototype.ifError = function ifError(err) {
|
||||
if (err !== null && err !== undefined) {
|
||||
let message = 'ifError got unwanted exception: ';
|
||||
if (typeof err === 'object' && typeof err.message === 'string') {
|
||||
|
@ -705,6 +786,7 @@ assert.ifError = function ifError(err) {
|
|||
operator: 'ifError',
|
||||
message,
|
||||
stackStartFn: ifError,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
|
||||
// Make sure we actually have a stack trace!
|
||||
|
@ -747,7 +829,7 @@ function internalMatch(string, regexp, message, fn) {
|
|||
'regexp', 'RegExp', regexp,
|
||||
);
|
||||
}
|
||||
const match = fn === assert.match;
|
||||
const match = fn === Assert.prototype.match;
|
||||
if (typeof string !== 'string' ||
|
||||
RegExpPrototypeExec(regexp, string) !== null !== match) {
|
||||
if (message instanceof Error) {
|
||||
|
@ -770,6 +852,7 @@ function internalMatch(string, regexp, message, fn) {
|
|||
message,
|
||||
operator: fn.name,
|
||||
stackStartFn: fn,
|
||||
diff: this?.[kOptions]?.diff,
|
||||
});
|
||||
err.generatedMessage = generatedMessage;
|
||||
throw err;
|
||||
|
@ -783,7 +866,7 @@ function internalMatch(string, regexp, message, fn) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.match = function match(string, regexp, message) {
|
||||
Assert.prototype.match = function match(string, regexp, message) {
|
||||
internalMatch(string, regexp, message, match);
|
||||
};
|
||||
|
||||
|
@ -794,7 +877,7 @@ assert.match = function match(string, regexp, message) {
|
|||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
|
||||
Assert.prototype.doesNotMatch = function doesNotMatch(string, regexp, message) {
|
||||
internalMatch(string, regexp, message, doesNotMatch);
|
||||
};
|
||||
|
||||
|
@ -807,6 +890,17 @@ function strict(...args) {
|
|||
innerOk(strict, args.length, ...args);
|
||||
}
|
||||
|
||||
// TODO(aduh95): take `ok` from `Assert.prototype` instead of a self-ref in a next major.
|
||||
assert.ok = assert;
|
||||
ArrayPrototypeForEach([
|
||||
'fail', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual',
|
||||
'deepStrictEqual', 'notDeepStrictEqual', 'strictEqual',
|
||||
'notStrictEqual', 'partialDeepStrictEqual', 'match', 'doesNotMatch',
|
||||
'throws', 'rejects', 'doesNotThrow', 'doesNotReject', 'ifError',
|
||||
], (name) => {
|
||||
setOwnProperty(assert, name, Assert.prototype[name]);
|
||||
});
|
||||
|
||||
assert.strict = ObjectAssign(strict, assert, {
|
||||
equal: assert.strictEqual,
|
||||
deepEqual: assert.deepStrictEqual,
|
||||
|
@ -814,4 +908,7 @@ assert.strict = ObjectAssign(strict, assert, {
|
|||
notDeepEqual: assert.notDeepStrictEqual,
|
||||
});
|
||||
|
||||
assert.strict.Assert = Assert;
|
||||
assert.strict.strict = assert.strict;
|
||||
|
||||
assert.Assert = Assert;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue