Skip to content

Commit 9feaf8a

Browse files
committedApr 10, 2020
lib/doc/test: port common.mustCall() to assert
Fixes: nodejs#31392
1 parent 468bfd3 commit 9feaf8a

10 files changed

+387
-3
lines changed
 

‎doc/api/assert.md

+134
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,136 @@ 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, an exception is thrown.
193+
194+
```js
195+
const assert = require('assert');
196+
197+
// Creates call tracker.
198+
const tracker = new assert.CallTracker();
199+
200+
function func() {}
201+
202+
// Returns a function that wraps func() that must be called exact times
203+
// before tracker.verify().
204+
const callsfunc = tracker.calls(func);
205+
```
206+
207+
### `tracker.report()`
208+
<!-- YAML
209+
added: REPLACEME
210+
-->
211+
212+
* Returns: {Array} of objects containing information about the wrapper functions
213+
returned by [`tracker.calls()`][].
214+
* Object {Object}
215+
* `message` {string}
216+
* `actual` {number} The actual number of times the function was called.
217+
* `expected` {number} The number of times the function was expected to be
218+
called.
219+
* `operator` {string} The name of the function that is wrapped.
220+
* `stack` {Object} A stack trace of the function.
221+
222+
The arrays contains information about the expected and actual number of calls of
223+
the functions that have not been called the expected number of times.
224+
225+
```js
226+
const assert = require('assert');
227+
228+
// Creates call tracker.
229+
const tracker = new assert.CallTracker();
230+
231+
function func() {}
232+
233+
function foo() {}
234+
235+
// Returns a function that wraps func() that must be called exact times
236+
// before tracker.verify().
237+
const callsfunc = tracker.calls(func, 2);
238+
239+
// Returns an array containing information on callsfunc()
240+
tracker.report();
241+
// [
242+
// {
243+
// message: 'Expected the func function to be executed 2 time(s) but was
244+
// executed 0 time(s).',
245+
// actual: 0,
246+
// expected: 2,
247+
// operator: 'func',
248+
// stack: stack trace
249+
// }
250+
// ]
251+
```
252+
253+
### `tracker.verify()`
254+
<!-- YAML
255+
added: REPLACEME
256+
-->
257+
258+
Iterates through the list of functions passed to
259+
[`tracker.calls()`][] and will throw an error for functions that
260+
have not been called the expected number of times.
261+
262+
```js
263+
const assert = require('assert');
264+
265+
// Creates call tracker.
266+
const tracker = new assert.CallTracker();
267+
268+
function func() {}
269+
270+
// Returns a function that wraps func() that must be called exact times
271+
// before tracker.verify().
272+
const callsfunc = tracker.calls(func, 2);
273+
274+
callsfunc();
275+
276+
// Will throw an error since callsfunc() was only called once.
277+
tracker.verify();
278+
```
279+
150280
## `assert(value[, message])`
151281
<!-- YAML
152282
added: v0.5.9
@@ -1423,6 +1553,7 @@ argument.
14231553
[`TypeError`]: errors.html#errors_class_typeerror
14241554
[`WeakMap`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
14251555
[`WeakSet`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet
1556+
[`CallTracker`]: #assert_class_assert_calltracker
14261557
[`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message
14271558
[`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message
14281559
[`assert.doesNotThrow()`]: #assert_assert_doesnotthrow_fn_error_message
@@ -1434,6 +1565,9 @@ argument.
14341565
[`assert.ok()`]: #assert_assert_ok_value_message
14351566
[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message
14361567
[`assert.throws()`]: #assert_assert_throws_fn_error_message
1568+
[`process.on('exit')`]: process.html#process_event_exit
1569+
[`tracker.calls()`]: #assert_class_assert_CallTracker#tracker_calls
1570+
[`tracker.verify()`]: #assert_class_assert_CallTracker#tracker_verify
14371571
[strict assertion mode]: #assert_strict_assertion_mode
14381572
[Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison
14391573
[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
@@ -1957,6 +1957,12 @@ A `Transform` stream finished with data still in the write buffer.
19571957

19581958
The initialization of a TTY failed due to a system error.
19591959

1960+
<a id="ERR_UNAVAILABLE_DURING_EXIT"></a>
1961+
### `ERR_UNAVAILABLE_DURING_EXIT`
1962+
1963+
Function was called within a [`process.on('exit')`][] handler that shouldn't be
1964+
called within [`process.on('exit')`][] handler.
1965+
19601966
<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
19611967
### `ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET`
19621968

@@ -2521,6 +2527,7 @@ such as `process.stdout.on('data')`.
25212527
[`net`]: net.html
25222528
[`new URL(input)`]: url.html#url_constructor_new_url_input_base
25232529
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
2530+
[`process.on('exit')`]: process.html#Event:-`'exit'`
25242531
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
25252532
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
25262533
[`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;
@@ -931,6 +932,8 @@ assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
931932
internalMatch(string, regexp, message, doesNotMatch);
932933
};
933934

935+
assert.CallTracker = CallTracker;
936+
934937
// Expose a strict only variant of assert
935938
function strict(...args) {
936939
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
@@ -1344,6 +1344,8 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
13441344
E('ERR_TRANSFORM_WITH_LENGTH_0',
13451345
'Calling transform done when writableState.length != 0', Error);
13461346
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
1347+
E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' +
1348+
'handler', Error);
13471349
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
13481350
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
13491351
'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)
Please sign in to comment.