Skip to content

Commit 8186a56

Browse files
DavenportEmmaBridgeAR
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 99f4af4 commit 8186a56

10 files changed

+388
-3
lines changed

doc/api/assert.md

+135
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,137 @@ try {
147147
}
148148
```
149149

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

19731973
The initialization of a TTY failed due to a system error.
19741974

1975+
<a id="ERR_UNAVAILABLE_DURING_EXIT"></a>
1976+
### `ERR_UNAVAILABLE_DURING_EXIT`
1977+
1978+
Function was called within a [`process.on('exit')`][] handler that shouldn't be
1979+
called within [`process.on('exit')`][] handler.
1980+
19751981
<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
19761982
### `ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET`
19771983

@@ -2533,6 +2539,7 @@ such as `process.stdout.on('data')`.
25332539
[`net`]: net.html
25342540
[`new URL(input)`]: url.html#url_constructor_new_url_input_base
25352541
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
2542+
[`process.on('exit')`]: process.html#Event:-`'exit'`
25362543
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
25372544
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
25382545
[`readable._read()`]: stream.html#stream_readable_read_size_1

lib/assert.js

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

5252
const errorCache = new Map();
53+
const CallTracker = require('internal/assert/calltracker');
5354

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

931+
assert.CallTracker = CallTracker;
932+
930933
// Expose a strict only variant of assert
931934
function strict(...args) {
932935
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
@@ -1378,6 +1378,8 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
13781378
E('ERR_TRANSFORM_WITH_LENGTH_0',
13791379
'Calling transform done when writableState.length != 0', Error);
13801380
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
1381+
E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' +
1382+
'handler', Error);
13811383
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
13821384
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
13831385
'callback was already active',

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
'lib/zlib.js',
9696
'lib/internal/assert.js',
9797
'lib/internal/assert/assertion_error.js',
98+
'lib/internal/assert/calltracker.js',
9899
'lib/internal/async_hooks.js',
99100
'lib/internal/buffer.js',
100101
'lib/internal/cli_table.js',

0 commit comments

Comments
 (0)