Skip to content

Commit b4db0b2

Browse files
committed
assert: allow printf-style messages as assertion error
Also add functions as allowed message input. This allows to have leavy message computation to become cheaper.
1 parent f1b56d6 commit b4db0b2

File tree

3 files changed

+228
-27
lines changed

3 files changed

+228
-27
lines changed

doc/api/assert.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1807,14 +1807,24 @@ argument gets considered.
18071807
<!-- YAML
18081808
added: v0.1.21
18091809
changes:
1810+
- version: REPLACEME
1811+
pr-url: https://github.com/nodejs/node/pull/XXXXX
1812+
description: Message may now be a `printf`-like format string or function.
18101813
- version: v10.0.0
18111814
pr-url: https://github.com/nodejs/node/pull/17003
18121815
description: Used comparison changed from Strict Equality to `Object.is()`.
18131816
-->
18141817

18151818
* `actual` {any}
18161819
* `expected` {any}
1817-
* `message` {string|Error}
1820+
* `message` {string|Error|Function} Postfix `printf`-like arguments in case
1821+
it's used as format string.
1822+
If message is a function, it is called in case of a comparison failure. The
1823+
function receives the `actual` and `expected` arguments and has to return a
1824+
string that is going to be used as error message.
1825+
`printf`-like format strings and functions are beneficial for performance
1826+
reasons in case arguments are passed through. In addition, it allows nice
1827+
formatting with ease.
18181828

18191829
Tests strict equality between the `actual` and `expected` parameters as
18201830
determined by [`Object.is()`][].
@@ -1843,8 +1853,17 @@ const oranges = 2;
18431853
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
18441854
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
18451855

1856+
assert.strictEqual(apples, oranges, 'apples %s !== oranges %s', apples, oranges);
1857+
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
1858+
18461859
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
18471860
// TypeError: Inputs are not identical
1861+
1862+
assert.strictEqual(apples, oranges, () => {
1863+
// Do 'heavy' computations
1864+
return 'My error string';
1865+
});
1866+
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
18481867
```
18491868

18501869
```cjs

lib/assert.js

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
ArrayPrototypePush,
2828
ArrayPrototypeSlice,
2929
Error,
30+
ErrorPrototypeToString,
3031
NumberIsNaN,
3132
ObjectAssign,
3233
ObjectDefineProperty,
@@ -53,7 +54,7 @@ const {
5354
},
5455
} = require('internal/errors');
5556
const AssertionError = require('internal/assert/assertion_error');
56-
const { inspect } = require('internal/util/inspect');
57+
const { inspect, format } = require('internal/util/inspect');
5758
const {
5859
isPromise,
5960
isRegExp,
@@ -142,9 +143,51 @@ function Assert(options) {
142143
// instance's custom options.
143144

144145
function innerFail(obj) {
145-
if (obj.message instanceof Error) throw obj.message;
146+
if (obj.message.length === 0) {
147+
obj.message = undefined;
148+
} else if (typeof obj.message[0] === 'string') {
149+
if (obj.message.length > 1) {
150+
obj.message = format(...obj.message);
151+
} else {
152+
obj.message = obj.message[0];
153+
}
154+
} else if (isError(obj.message[0])) {
155+
if (obj.message.length > 1) {
156+
throw new ERR_AMBIGUOUS_ARGUMENT(
157+
'message',
158+
`The error message was passed as error object "${ErrorPrototypeToString(obj.message[0])}" has trailing arguments that would be ignored.`,
159+
);
160+
}
161+
throw obj.message[0];
162+
} else if (typeof obj.message[0] === 'function') {
163+
if (obj.message.length > 1) {
164+
throw new ERR_AMBIGUOUS_ARGUMENT(
165+
'message',
166+
`The error message with function "${obj.message[0].name || 'anonymous'}" has trailing arguments that would be ignored.`,
167+
);
168+
}
169+
try {
170+
obj.message = obj.message[0](obj.actual, obj.expected);
171+
if (typeof obj.message !== 'string') {
172+
obj.message = undefined;
173+
}
174+
} catch {
175+
// Ignore and use default message instead
176+
obj.message = undefined;
177+
}
178+
} else {
179+
throw new ERR_INVALID_ARG_TYPE(
180+
'message',
181+
['string', 'function'],
182+
obj.message[0],
183+
);
184+
}
146185

147-
throw new AssertionError(obj);
186+
const error = new AssertionError(obj);
187+
if (obj.generatedMessage !== undefined) {
188+
error.generatedMessage = obj.generatedMessage;
189+
}
190+
throw error;
148191
}
149192

150193
/**
@@ -208,7 +251,7 @@ Assert.prototype.ok = function ok(...args) {
208251
* @param {string | Error} [message]
209252
* @returns {void}
210253
*/
211-
Assert.prototype.equal = function equal(actual, expected, message) {
254+
Assert.prototype.equal = function equal(actual, expected, ...message) {
212255
if (arguments.length < 2) {
213256
throw new ERR_MISSING_ARGS('actual', 'expected');
214257
}
@@ -233,7 +276,7 @@ Assert.prototype.equal = function equal(actual, expected, message) {
233276
* @param {string | Error} [message]
234277
* @returns {void}
235278
*/
236-
Assert.prototype.notEqual = function notEqual(actual, expected, message) {
279+
Assert.prototype.notEqual = function notEqual(actual, expected, ...message) {
237280
if (arguments.length < 2) {
238281
throw new ERR_MISSING_ARGS('actual', 'expected');
239282
}
@@ -257,7 +300,7 @@ Assert.prototype.notEqual = function notEqual(actual, expected, message) {
257300
* @param {string | Error} [message]
258301
* @returns {void}
259302
*/
260-
Assert.prototype.deepEqual = function deepEqual(actual, expected, message) {
303+
Assert.prototype.deepEqual = function deepEqual(actual, expected, ...message) {
261304
if (arguments.length < 2) {
262305
throw new ERR_MISSING_ARGS('actual', 'expected');
263306
}
@@ -281,7 +324,7 @@ Assert.prototype.deepEqual = function deepEqual(actual, expected, message) {
281324
* @param {string | Error} [message]
282325
* @returns {void}
283326
*/
284-
Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, message) {
327+
Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, ...message) {
285328
if (arguments.length < 2) {
286329
throw new ERR_MISSING_ARGS('actual', 'expected');
287330
}
@@ -306,7 +349,7 @@ Assert.prototype.notDeepEqual = function notDeepEqual(actual, expected, message)
306349
* @param {string | Error} [message]
307350
* @returns {void}
308351
*/
309-
Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
352+
Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, ...message) {
310353
if (arguments.length < 2) {
311354
throw new ERR_MISSING_ARGS('actual', 'expected');
312355
}
@@ -332,7 +375,7 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me
332375
* @returns {void}
333376
*/
334377
Assert.prototype.notDeepStrictEqual = notDeepStrictEqual;
335-
function notDeepStrictEqual(actual, expected, message) {
378+
function notDeepStrictEqual(actual, expected, ...message) {
336379
if (arguments.length < 2) {
337380
throw new ERR_MISSING_ARGS('actual', 'expected');
338381
}
@@ -356,7 +399,7 @@ function notDeepStrictEqual(actual, expected, message) {
356399
* @param {string | Error} [message]
357400
* @returns {void}
358401
*/
359-
Assert.prototype.strictEqual = function strictEqual(actual, expected, message) {
402+
Assert.prototype.strictEqual = function strictEqual(actual, expected, ...message) {
360403
if (arguments.length < 2) {
361404
throw new ERR_MISSING_ARGS('actual', 'expected');
362405
}
@@ -379,7 +422,7 @@ Assert.prototype.strictEqual = function strictEqual(actual, expected, message) {
379422
* @param {string | Error} [message]
380423
* @returns {void}
381424
*/
382-
Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, message) {
425+
Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, ...message) {
383426
if (arguments.length < 2) {
384427
throw new ERR_MISSING_ARGS('actual', 'expected');
385428
}
@@ -405,7 +448,7 @@ Assert.prototype.notStrictEqual = function notStrictEqual(actual, expected, mess
405448
Assert.prototype.partialDeepStrictEqual = function partialDeepStrictEqual(
406449
actual,
407450
expected,
408-
message,
451+
...message
409452
) {
410453
if (arguments.length < 2) {
411454
throw new ERR_MISSING_ARGS('actual', 'expected');
@@ -462,7 +505,7 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
462505
innerFail({
463506
actual,
464507
expected,
465-
message,
508+
message: [message],
466509
operator: fn.name,
467510
stackStartFn: fn,
468511
diff: this?.[kOptions]?.diff,
@@ -664,7 +707,7 @@ function expectsError(stackStartFn, actual, error, message) {
664707
actual: undefined,
665708
expected: error,
666709
operator: stackStartFn.name,
667-
message: `Missing expected ${fnType}${details}`,
710+
message: [`Missing expected ${fnType}${details}`],
668711
stackStartFn,
669712
diff: this?.[kOptions]?.diff,
670713
});
@@ -713,8 +756,8 @@ function expectsNoError(stackStartFn, actual, error, message) {
713756
actual,
714757
expected: error,
715758
operator: stackStartFn.name,
716-
message: `Got unwanted ${fnType}${details}\n` +
717-
`Actual message: "${actual?.message}"`,
759+
message: [`Got unwanted ${fnType}${details}\n` +
760+
`Actual message: "${actual?.message}"`],
718761
stackStartFn,
719762
diff: this?.[kOptions]?.diff,
720763
});
@@ -832,30 +875,26 @@ function internalMatch(string, regexp, message, fn) {
832875
const match = fn === Assert.prototype.match;
833876
if (typeof string !== 'string' ||
834877
RegExpPrototypeExec(regexp, string) !== null !== match) {
835-
if (message instanceof Error) {
836-
throw message;
837-
}
838878

839-
const generatedMessage = !message;
879+
const generatedMessage = message.length === 0;
840880

841881
// 'The input was expected to not match the regular expression ' +
842-
message ||= (typeof string !== 'string' ?
882+
message[0] ||= (typeof string !== 'string' ?
843883
'The "string" argument must be of type string. Received type ' +
844884
`${typeof string} (${inspect(string)})` :
845885
(match ?
846886
'The input did not match the regular expression ' :
847887
'The input was expected to not match the regular expression ') +
848888
`${inspect(regexp)}. Input:\n\n${inspect(string)}\n`);
849-
const err = new AssertionError({
889+
innerFail({
850890
actual: string,
851891
expected: regexp,
852892
message,
853893
operator: fn.name,
854894
stackStartFn: fn,
855895
diff: this?.[kOptions]?.diff,
896+
generatedMessage: generatedMessage,
856897
});
857-
err.generatedMessage = generatedMessage;
858-
throw err;
859898
}
860899
}
861900

@@ -866,7 +905,7 @@ function internalMatch(string, regexp, message, fn) {
866905
* @param {string | Error} [message]
867906
* @returns {void}
868907
*/
869-
Assert.prototype.match = function match(string, regexp, message) {
908+
Assert.prototype.match = function match(string, regexp, ...message) {
870909
internalMatch(string, regexp, message, match);
871910
};
872911

@@ -877,7 +916,7 @@ Assert.prototype.match = function match(string, regexp, message) {
877916
* @param {string | Error} [message]
878917
* @returns {void}
879918
*/
880-
Assert.prototype.doesNotMatch = function doesNotMatch(string, regexp, message) {
919+
Assert.prototype.doesNotMatch = function doesNotMatch(string, regexp, ...message) {
881920
internalMatch(string, regexp, message, doesNotMatch);
882921
};
883922

0 commit comments

Comments
 (0)