Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: porting common.mustCall to assert #31982

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
135 changes: 135 additions & 0 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,137 @@ try {
}
```

## Class: `assert.CallTracker`

### `new assert.CallTracker()`
<!-- YAML
added: REPLACEME
-->

Creates a new [`CallTracker`][] object which can be used to track if functions
were called a specific number of times. The `tracker.verify()` must be called
for the verification to take place. The usual pattern would be to call it in a
[`process.on('exit')`][] handler.

```js
const assert = require('assert');

const tracker = new assert.CallTracker();

function func() {}

// callsfunc() must be called exactly 1 time before tracker.verify().
const callsfunc = tracker.calls(func, 1);

callsfunc();

// Calls tracker.verify() and verifies if all tracker.calls() functions have
// been called exact times.
process.on('exit', () => {
tracker.verify();
});
```

### `tracker.calls([fn][, exact])`
<!-- YAML
added: REPLACEME
-->

* `fn` {Function} **Default** A no-op function.
* `exact` {number} **Default** `1`.
* Returns: {Function} that wraps `fn`.

The wrapper function is expected to be called exactly `exact` times. If the
function has not been called exactly `exact` times when
[`tracker.verify()`][] is called, then [`tracker.verify()`][] will throw an
error.

```js
const assert = require('assert');

// Creates call tracker.
const tracker = new assert.CallTracker();

function func() {}

// Returns a function that wraps func() that must be called exact times
// before tracker.verify().
const callsfunc = tracker.calls(func);
```

### `tracker.report()`
<!-- YAML
added: REPLACEME
-->

* Returns: {Array} of objects containing information about the wrapper functions
returned by [`tracker.calls()`][].
* Object {Object}
* `message` {string}
* `actual` {number} The actual number of times the function was called.
* `expected` {number} The number of times the function was expected to be
called.
* `operator` {string} The name of the function that is wrapped.
* `stack` {Object} A stack trace of the function.

ConorDavenport marked this conversation as resolved.
Show resolved Hide resolved
The arrays contains information about the expected and actual number of calls of
the functions that have not been called the expected number of times.

```js
const assert = require('assert');

// Creates call tracker.
const tracker = new assert.CallTracker();

function func() {}

function foo() {}

// Returns a function that wraps func() that must be called exact times
// before tracker.verify().
const callsfunc = tracker.calls(func, 2);

// Returns an array containing information on callsfunc()
tracker.report();
// [
// {
// message: 'Expected the func function to be executed 2 time(s) but was
// executed 0 time(s).',
jasnell marked this conversation as resolved.
Show resolved Hide resolved
// actual: 0,
// expected: 2,
// operator: 'func',
// stack: stack trace
jasnell marked this conversation as resolved.
Show resolved Hide resolved
// }
// ]
```

### `tracker.verify()`
<!-- YAML
added: REPLACEME
-->

Iterates through the list of functions passed to
[`tracker.calls()`][] and will throw an error for functions that
have not been called the expected number of times.

```js
const assert = require('assert');

// Creates call tracker.
const tracker = new assert.CallTracker();

function func() {}

// Returns a function that wraps func() that must be called exact times
// before tracker.verify().
const callsfunc = tracker.calls(func, 2);

callsfunc();

// Will throw an error since callsfunc() was only called once.
tracker.verify();
```

## `assert(value[, message])`
<!-- YAML
added: v0.5.9
Expand Down Expand Up @@ -1423,6 +1554,7 @@ argument.
[`TypeError`]: errors.html#errors_class_typeerror
[`WeakMap`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
[`WeakSet`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet
[`CallTracker`]: #assert_class_assert_calltracker
[`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message
[`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message
[`assert.doesNotThrow()`]: #assert_assert_doesnotthrow_fn_error_message
Expand All @@ -1434,6 +1566,9 @@ argument.
[`assert.ok()`]: #assert_assert_ok_value_message
[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message
[`assert.throws()`]: #assert_assert_throws_fn_error_message
[`process.on('exit')`]: process.html#process_event_exit
[`tracker.calls()`]: #assert_class_assert_CallTracker#tracker_calls
[`tracker.verify()`]: #assert_class_assert_CallTracker#tracker_verify
[strict assertion mode]: #assert_strict_assertion_mode
[Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison
[Object wrappers]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript
Expand Down
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,12 @@ A `Transform` stream finished with data still in the write buffer.

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

<a id="ERR_UNAVAILABLE_DURING_EXIT"></a>
### `ERR_UNAVAILABLE_DURING_EXIT`

Function was called within a [`process.on('exit')`][] handler that shouldn't be
called within [`process.on('exit')`][] handler.

<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
### `ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET`

Expand Down Expand Up @@ -2521,6 +2527,7 @@ such as `process.stdout.on('data')`.
[`net`]: net.html
[`new URL(input)`]: url.html#url_constructor_new_url_input_base
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
[`process.on('exit')`]: process.html#Event:-`'exit'`
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
[`readable._read()`]: stream.html#stream_readable_read_size_1
Expand Down
3 changes: 3 additions & 0 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const { NativeModule } = require('internal/bootstrap/loaders');
const { isError } = require('internal/util');

const errorCache = new Map();
const CallTracker = require('internal/assert/calltracker');

let isDeepEqual;
let isDeepStrictEqual;
Expand Down Expand Up @@ -931,6 +932,8 @@ assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
internalMatch(string, regexp, message, doesNotMatch);
};

assert.CallTracker = CallTracker;

ConorDavenport marked this conversation as resolved.
Show resolved Hide resolved
// Expose a strict only variant of assert
function strict(...args) {
innerOk(strict, args.length, ...args);
Expand Down
20 changes: 17 additions & 3 deletions lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ class AssertionError extends Error {
message,
operator,
stackStartFn,
details,
// Compatibility with older versions.
stackStartFunction
} = options;
Expand Down Expand Up @@ -426,9 +427,22 @@ class AssertionError extends Error {
configurable: true
});
this.code = 'ERR_ASSERTION';
this.actual = actual;
this.expected = expected;
this.operator = operator;
if (details) {
this.actual = undefined;
this.expected = undefined;
this.operator = undefined;
for (let i = 0; i < details.length; i++) {
this['message ' + i] = details[i].message;
this['actual ' + i] = details[i].actual;
this['expected ' + i] = details[i].expected;
this['operator ' + i] = details[i].operator;
this['stack trace ' + i] = details[i].stack;
}
} else {
this.actual = actual;
this.expected = expected;
this.operator = operator;
}
// eslint-disable-next-line no-restricted-syntax
Error.captureStackTrace(this, stackStartFn || stackStartFunction);
// Create error message including the error code in the name.
Expand Down
93 changes: 93 additions & 0 deletions lib/internal/assert/calltracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

const {
Error,
SafeSet,
} = primordials;

const {
codes: {
ERR_UNAVAILABLE_DURING_EXIT,
},
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
const {
validateUint32,
} = require('internal/validators');

const noop = () => {};

class CallTracker {

#callChecks = new SafeSet()

calls(fn, exact = 1) {
if (process._exiting)
throw new ERR_UNAVAILABLE_DURING_EXIT();
if (typeof fn === 'number') {
exact = fn;
fn = noop;
} else if (fn === undefined) {
fn = noop;
}

validateUint32(exact, 'exact', true);

const context = {
exact,
actual: 0,
// eslint-disable-next-line no-restricted-syntax
stackTrace: new Error(),
name: fn.name || 'calls'
};
const callChecks = this.#callChecks;
callChecks.add(context);

return function() {
ConorDavenport marked this conversation as resolved.
Show resolved Hide resolved
context.actual++;
if (context.actual === context.exact) {
// Once function has reached its call count remove it from
// callChecks set to prevent memory leaks.
callChecks.delete(context);
}
// If function has been called more than expected times, add back into
// callchecks.
if (context.actual === context.exact + 1) {
callChecks.add(context);
}
return fn.apply(this, arguments);
};
}

report() {
const errors = [];
for (const context of this.#callChecks) {
// If functions have not been called exact times
if (context.actual !== context.exact) {
const message = `Expected the ${context.name} function to be ` +
`executed ${context.exact} time(s) but was ` +
`executed ${context.actual} time(s).`;
errors.push({
message,
actual: context.actual,
expected: context.exact,
operator: context.name,
stack: context.stackTrace
});
}
}
return errors;
}

verify() {
const errors = this.report();
if (errors.length > 0) {
throw new AssertionError({
message: 'Function(s) were not called the expected number of times',
details: errors,
});
}
}
}

module.exports = CallTracker;
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,8 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
E('ERR_TRANSFORM_WITH_LENGTH_0',
'Calling transform done when writableState.length != 0', Error);
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' +
'handler', Error);
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
'callback was already active',
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
'lib/zlib.js',
'lib/internal/assert.js',
'lib/internal/assert/assertion_error.js',
'lib/internal/assert/calltracker.js',
'lib/internal/async_hooks.js',
'lib/internal/buffer.js',
'lib/internal/cli_table.js',
Expand Down