Skip to content

Commit c87ed21

Browse files
DavenportEmmatargos
authored andcommitted
assert: port common.mustCall() to assert
Fixes: #31392 PR-URL: #31982 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Zeyu Yang <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Denys Otrishko <[email protected]>
1 parent a87dfd2 commit c87ed21

10 files changed

+388
-3
lines changed

doc/api/assert.md

+135
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,137 @@ try {
149149
}
150150
```
151151

152+
## Class: `assert.CallTracker`
153+
154+
### `new assert.CallTracker()`
155+
<!-- YAML
156+
added: REPLACEME
157+
-->
158+
159+
Creates a new [`CallTracker`][] object which can be used to track if functions
160+
were called a specific number of times. The `tracker.verify()` must be called
161+
for the verification to take place. The usual pattern would be to call it in a
162+
[`process.on('exit')`][] handler.
163+
164+
```js
165+
const assert = require('assert');
166+
167+
const tracker = new assert.CallTracker();
168+
169+
function func() {}
170+
171+
// callsfunc() must be called exactly 1 time before tracker.verify().
172+
const callsfunc = tracker.calls(func, 1);
173+
174+
callsfunc();
175+
176+
// Calls tracker.verify() and verifies if all tracker.calls() functions have
177+
// been called exact times.
178+
process.on('exit', () => {
179+
tracker.verify();
180+
});
181+
```
182+
183+
### `tracker.calls([fn][, exact])`
184+
<!-- YAML
185+
added: REPLACEME
186+
-->
187+
188+
* `fn` {Function} **Default** A no-op function.
189+
* `exact` {number} **Default** `1`.
190+
* Returns: {Function} that wraps `fn`.
191+
192+
The wrapper function is expected to be called exactly `exact` times. If the
193+
function has not been called exactly `exact` times when
194+
[`tracker.verify()`][] is called, then [`tracker.verify()`][] will throw an
195+
error.
196+
197+
```js
198+
const assert = require('assert');
199+
200+
// Creates call tracker.
201+
const tracker = new assert.CallTracker();
202+
203+
function func() {}
204+
205+
// Returns a function that wraps func() that must be called exact times
206+
// before tracker.verify().
207+
const callsfunc = tracker.calls(func);
208+
```
209+
210+
### `tracker.report()`
211+
<!-- YAML
212+
added: REPLACEME
213+
-->
214+
215+
* Returns: {Array} of objects containing information about the wrapper functions
216+
returned by [`tracker.calls()`][].
217+
* Object {Object}
218+
* `message` {string}
219+
* `actual` {number} The actual number of times the function was called.
220+
* `expected` {number} The number of times the function was expected to be
221+
called.
222+
* `operator` {string} The name of the function that is wrapped.
223+
* `stack` {Object} A stack trace of the function.
224+
225+
The arrays contains information about the expected and actual number of calls of
226+
the functions that have not been called the expected number of times.
227+
228+
```js
229+
const assert = require('assert');
230+
231+
// Creates call tracker.
232+
const tracker = new assert.CallTracker();
233+
234+
function func() {}
235+
236+
function foo() {}
237+
238+
// Returns a function that wraps func() that must be called exact times
239+
// before tracker.verify().
240+
const callsfunc = tracker.calls(func, 2);
241+
242+
// Returns an array containing information on callsfunc()
243+
tracker.report();
244+
// [
245+
// {
246+
// message: 'Expected the func function to be executed 2 time(s) but was
247+
// executed 0 time(s).',
248+
// actual: 0,
249+
// expected: 2,
250+
// operator: 'func',
251+
// stack: stack trace
252+
// }
253+
// ]
254+
```
255+
256+
### `tracker.verify()`
257+
<!-- YAML
258+
added: REPLACEME
259+
-->
260+
261+
Iterates through the list of functions passed to
262+
[`tracker.calls()`][] and will throw an error for functions that
263+
have not been called the expected number of times.
264+
265+
```js
266+
const assert = require('assert');
267+
268+
// Creates call tracker.
269+
const tracker = new assert.CallTracker();
270+
271+
function func() {}
272+
273+
// Returns a function that wraps func() that must be called exact times
274+
// before tracker.verify().
275+
const callsfunc = tracker.calls(func, 2);
276+
277+
callsfunc();
278+
279+
// Will throw an error since callsfunc() was only called once.
280+
tracker.verify();
281+
```
282+
152283
## `assert(value[, message])`
153284
<!-- YAML
154285
added: v0.5.9
@@ -1429,6 +1560,7 @@ argument.
14291560
[`TypeError`]: errors.html#errors_class_typeerror
14301561
[`WeakMap`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
14311562
[`WeakSet`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet
1563+
[`CallTracker`]: #assert_class_assert_calltracker
14321564
[`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message
14331565
[`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message
14341566
[`assert.doesNotThrow()`]: #assert_assert_doesnotthrow_fn_error_message
@@ -1440,6 +1572,9 @@ argument.
14401572
[`assert.ok()`]: #assert_assert_ok_value_message
14411573
[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message
14421574
[`assert.throws()`]: #assert_assert_throws_fn_error_message
1575+
[`process.on('exit')`]: process.html#process_event_exit
1576+
[`tracker.calls()`]: #assert_class_assert_CallTracker#tracker_calls
1577+
[`tracker.verify()`]: #assert_class_assert_CallTracker#tracker_verify
14431578
[strict assertion mode]: #assert_strict_assertion_mode
14441579
[Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison
14451580
[Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript

doc/api/errors.md

+7
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,12 @@ A `Transform` stream finished with data still in the write buffer.
19671967

19681968
The initialization of a TTY failed due to a system error.
19691969

1970+
<a id="ERR_UNAVAILABLE_DURING_EXIT"></a>
1971+
### `ERR_UNAVAILABLE_DURING_EXIT`
1972+
1973+
Function was called within a [`process.on('exit')`][] handler that shouldn't be
1974+
called within [`process.on('exit')`][] handler.
1975+
19701976
<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
19711977
### `ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET`
19721978

@@ -2545,6 +2551,7 @@ such as `process.stdout.on('data')`.
25452551
[`net`]: net.html
25462552
[`new URL(input)`]: url.html#url_constructor_new_url_input_base
25472553
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
2554+
[`process.on('exit')`]: process.html#Event:-`'exit'`
25482555
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
25492556
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
25502557
[`readable._read()`]: stream.html#stream_readable_read_size_1

lib/assert.js

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const { NativeModule } = require('internal/bootstrap/loaders');
5151
const { isError } = require('internal/util');
5252

5353
const errorCache = new Map();
54+
const CallTracker = require('internal/assert/calltracker');
5455

5556
let isDeepEqual;
5657
let isDeepStrictEqual;
@@ -928,6 +929,8 @@ assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
928929
internalMatch(string, regexp, message, doesNotMatch);
929930
};
930931

932+
assert.CallTracker = CallTracker;
933+
931934
// Expose a strict only variant of assert
932935
function strict(...args) {
933936
innerOk(strict, args.length, ...args);

lib/internal/assert/assertion_error.js

+17-3
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ class AssertionError extends Error {
312312
message,
313313
operator,
314314
stackStartFn,
315+
details,
315316
// Compatibility with older versions.
316317
stackStartFunction
317318
} = options;
@@ -426,9 +427,22 @@ class AssertionError extends Error {
426427
configurable: true
427428
});
428429
this.code = 'ERR_ASSERTION';
429-
this.actual = actual;
430-
this.expected = expected;
431-
this.operator = operator;
430+
if (details) {
431+
this.actual = undefined;
432+
this.expected = undefined;
433+
this.operator = undefined;
434+
for (let i = 0; i < details.length; i++) {
435+
this['message ' + i] = details[i].message;
436+
this['actual ' + i] = details[i].actual;
437+
this['expected ' + i] = details[i].expected;
438+
this['operator ' + i] = details[i].operator;
439+
this['stack trace ' + i] = details[i].stack;
440+
}
441+
} else {
442+
this.actual = actual;
443+
this.expected = expected;
444+
this.operator = operator;
445+
}
432446
// eslint-disable-next-line no-restricted-syntax
433447
Error.captureStackTrace(this, stackStartFn || stackStartFunction);
434448
// Create error message including the error code in the name.

lib/internal/assert/calltracker.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use strict';
2+
3+
const {
4+
Error,
5+
SafeSet,
6+
} = primordials;
7+
8+
const {
9+
codes: {
10+
ERR_UNAVAILABLE_DURING_EXIT,
11+
},
12+
} = require('internal/errors');
13+
const AssertionError = require('internal/assert/assertion_error');
14+
const {
15+
validateUint32,
16+
} = require('internal/validators');
17+
18+
const noop = () => {};
19+
20+
class CallTracker {
21+
22+
#callChecks = new SafeSet()
23+
24+
calls(fn, exact = 1) {
25+
if (process._exiting)
26+
throw new ERR_UNAVAILABLE_DURING_EXIT();
27+
if (typeof fn === 'number') {
28+
exact = fn;
29+
fn = noop;
30+
} else if (fn === undefined) {
31+
fn = noop;
32+
}
33+
34+
validateUint32(exact, 'exact', true);
35+
36+
const context = {
37+
exact,
38+
actual: 0,
39+
// eslint-disable-next-line no-restricted-syntax
40+
stackTrace: new Error(),
41+
name: fn.name || 'calls'
42+
};
43+
const callChecks = this.#callChecks;
44+
callChecks.add(context);
45+
46+
return function() {
47+
context.actual++;
48+
if (context.actual === context.exact) {
49+
// Once function has reached its call count remove it from
50+
// callChecks set to prevent memory leaks.
51+
callChecks.delete(context);
52+
}
53+
// If function has been called more than expected times, add back into
54+
// callchecks.
55+
if (context.actual === context.exact + 1) {
56+
callChecks.add(context);
57+
}
58+
return fn.apply(this, arguments);
59+
};
60+
}
61+
62+
report() {
63+
const errors = [];
64+
for (const context of this.#callChecks) {
65+
// If functions have not been called exact times
66+
if (context.actual !== context.exact) {
67+
const message = `Expected the ${context.name} function to be ` +
68+
`executed ${context.exact} time(s) but was ` +
69+
`executed ${context.actual} time(s).`;
70+
errors.push({
71+
message,
72+
actual: context.actual,
73+
expected: context.exact,
74+
operator: context.name,
75+
stack: context.stackTrace
76+
});
77+
}
78+
}
79+
return errors;
80+
}
81+
82+
verify() {
83+
const errors = this.report();
84+
if (errors.length > 0) {
85+
throw new AssertionError({
86+
message: 'Function(s) were not called the expected number of times',
87+
details: errors,
88+
});
89+
}
90+
}
91+
}
92+
93+
module.exports = CallTracker;

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,8 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
13801380
E('ERR_TRANSFORM_WITH_LENGTH_0',
13811381
'Calling transform done when writableState.length != 0', Error);
13821382
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
1383+
E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' +
1384+
'handler', Error);
13831385
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
13841386
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
13851387
'callback was already active',

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
'lib/zlib.js',
9797
'lib/internal/assert.js',
9898
'lib/internal/assert/assertion_error.js',
99+
'lib/internal/assert/calltracker.js',
99100
'lib/internal/async_hooks.js',
100101
'lib/internal/buffer.js',
101102
'lib/internal/cli_table.js',

0 commit comments

Comments
 (0)