assert: move CallTracker to EOL

The `assert.CallTracker` API has been deprecateed since v20. The
`node:test` `mock.fn` API provides a better alternative for the
functionality.

PR-URL: https://github.com/nodejs/node/pull/58006
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
This commit is contained in:
James M Snell 2025-04-24 07:18:11 -07:00
parent 9148e965b4
commit b21574d63b
10 changed files with 22 additions and 784 deletions

View File

@ -215,331 +215,6 @@ try {
}
```
## Class: `assert.CallTracker`
<!-- YAML
added:
- v14.2.0
- v12.19.0
changes:
- version: v20.1.0
pr-url: https://github.com/nodejs/node/pull/47740
description: the `assert.CallTracker` class has been deprecated and will be
removed in a future version.
-->
> Stability: 0 - Deprecated
This feature is deprecated and will be removed in a future version.
Please consider using alternatives such as the
[`mock`][] helper function.
### `new assert.CallTracker()`
<!-- YAML
added:
- v14.2.0
- v12.19.0
-->
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.
```mjs
import assert from 'node:assert';
import process from 'node:process';
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();
});
```
```cjs
const assert = require('node:assert');
const process = require('node:process');
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:
- v14.2.0
- v12.19.0
-->
* `fn` {Function} **Default:** A no-op function.
* `exact` {number} **Default:** `1`.
* Returns: {Function} A 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.
```mjs
import assert from 'node: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);
```
```cjs
const assert = require('node: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.getCalls(fn)`
<!-- YAML
added:
- v18.8.0
- v16.18.0
-->
* `fn` {Function}
* Returns: {Array} An array with all the calls to a tracked function.
* Object {Object}
* `thisArg` {Object}
* `arguments` {Array} the arguments passed to the tracked function
```mjs
import assert from 'node:assert';
const tracker = new assert.CallTracker();
function func() {}
const callsfunc = tracker.calls(func);
callsfunc(1, 2, 3);
assert.deepStrictEqual(tracker.getCalls(callsfunc),
[{ thisArg: undefined, arguments: [1, 2, 3] }]);
```
```cjs
const assert = require('node:assert');
// Creates call tracker.
const tracker = new assert.CallTracker();
function func() {}
const callsfunc = tracker.calls(func);
callsfunc(1, 2, 3);
assert.deepStrictEqual(tracker.getCalls(callsfunc),
[{ thisArg: undefined, arguments: [1, 2, 3] }]);
```
### `tracker.report()`
<!-- YAML
added:
- v14.2.0
- v12.19.0
-->
* Returns: {Array} An 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.
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.
```mjs
import assert from 'node: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);
// Returns an array containing information on callsfunc()
console.log(tracker.report());
// [
// {
// message: 'Expected the func function to be executed 2 time(s) but was
// executed 0 time(s).',
// actual: 0,
// expected: 2,
// operator: 'func',
// stack: stack trace
// }
// ]
```
```cjs
const assert = require('node: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);
// Returns an array containing information on callsfunc()
console.log(tracker.report());
// [
// {
// message: 'Expected the func function to be executed 2 time(s) but was
// executed 0 time(s).',
// actual: 0,
// expected: 2,
// operator: 'func',
// stack: stack trace
// }
// ]
```
### `tracker.reset([fn])`
<!-- YAML
added:
- v18.8.0
- v16.18.0
-->
* `fn` {Function} a tracked function to reset.
Reset calls of the call tracker.
If a tracked function is passed as an argument, the calls will be reset for it.
If no arguments are passed, all tracked functions will be reset.
```mjs
import assert from 'node:assert';
const tracker = new assert.CallTracker();
function func() {}
const callsfunc = tracker.calls(func);
callsfunc();
// Tracker was called once
assert.strictEqual(tracker.getCalls(callsfunc).length, 1);
tracker.reset(callsfunc);
assert.strictEqual(tracker.getCalls(callsfunc).length, 0);
```
```cjs
const assert = require('node:assert');
const tracker = new assert.CallTracker();
function func() {}
const callsfunc = tracker.calls(func);
callsfunc();
// Tracker was called once
assert.strictEqual(tracker.getCalls(callsfunc).length, 1);
tracker.reset(callsfunc);
assert.strictEqual(tracker.getCalls(callsfunc).length, 0);
```
### `tracker.verify()`
<!-- YAML
added:
- v14.2.0
- v12.19.0
-->
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.
```mjs
import assert from 'node: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();
```
```cjs
const assert = require('node: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
@ -2760,7 +2435,6 @@ assert.partialDeepStrictEqual(
[`===` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality
[`==` operator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality
[`AssertionError`]: #class-assertassertionerror
[`CallTracker`]: #class-assertcalltracker
[`Class`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
[`ERR_INVALID_RETURN_VALUE`]: errors.md#err_invalid_return_value
[`Error.captureStackTrace`]: errors.md#errorcapturestacktracetargetobject-constructoropt
@ -2777,9 +2451,5 @@ assert.partialDeepStrictEqual(
[`assert.strictEqual()`]: #assertstrictequalactual-expected-message
[`assert.throws()`]: #assertthrowsfn-error-message
[`getColorDepth()`]: tty.md#writestreamgetcolordepthenv
[`mock`]: test.md#mocking
[`process.on('exit')`]: process.md#event-exit
[`tracker.calls()`]: #trackercallsfn-exact
[`tracker.verify()`]: #trackerverify
[enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties
[prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots

View File

@ -3570,16 +3570,17 @@ be added when a function is bound to an `AsyncResource`.
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/00000
description: End-of-Life.
- version: v20.1.0
pr-url: https://github.com/nodejs/node/pull/47740
description: Runtime deprecation.
-->
Type: Runtime
Type: End-of-Life
In a future version of Node.js, [`assert.CallTracker`][],
will be removed.
Consider using alternatives such as the [`mock`][] helper function.
The `assert.CallTracker` API has been removed.
### DEP0174: calling `promisify` on a function that returns a `Promise`
@ -3926,7 +3927,6 @@ upon `require('node:module').builtinModules`.
[`Server.listen({fd: <number>})`]: net.md#serverlistenhandle-backlog-callback
[`String.prototype.toWellFormed`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toWellFormed
[`WriteStream.open()`]: fs.md#class-fswritestream
[`assert.CallTracker`]: assert.md#class-assertcalltracker
[`assert`]: assert.md
[`asyncResource.runInAsyncScope()`]: async_context.md#asyncresourceruninasyncscopefn-thisarg-args
[`buffer.subarray`]: buffer.md#bufsubarraystart-end
@ -3988,7 +3988,6 @@ upon `require('node:module').builtinModules`.
[`message.socket`]: http.md#messagesocket
[`message.trailersDistinct`]: http.md#messagetrailersdistinct
[`message.trailers`]: http.md#messagetrailers
[`mock`]: test.md#mocking
[`module.createRequire()`]: module.md#modulecreaterequirefilename
[`os.networkInterfaces()`]: os.md#osnetworkinterfaces
[`os.tmpdir()`]: os.md#ostmpdir

View File

@ -54,10 +54,9 @@ const {
isPromise,
isRegExp,
} = require('internal/util/types');
const { isError, deprecate } = require('internal/util');
const { isError } = require('internal/util');
const { innerOk } = require('internal/assert/utils');
const CallTracker = require('internal/assert/calltracker');
const {
validateFunction,
} = require('internal/validators');
@ -823,8 +822,6 @@ assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
internalMatch(string, regexp, message, doesNotMatch);
};
assert.CallTracker = deprecate(CallTracker, 'assert.CallTracker is deprecated.', 'DEP0173');
/**
* Expose a strict only variant of assert.
* @param {...any} args

View File

@ -1,153 +0,0 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeSlice,
Error,
FunctionPrototype,
ObjectFreeze,
Proxy,
ReflectApply,
SafeSet,
SafeWeakMap,
} = primordials;
const {
codes: {
ERR_INVALID_ARG_VALUE,
ERR_UNAVAILABLE_DURING_EXIT,
},
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
const {
validateUint32,
} = require('internal/validators');
const noop = FunctionPrototype;
class CallTrackerContext {
#expected;
#calls;
#name;
#stackTrace;
constructor({ expected, stackTrace, name }) {
this.#calls = [];
this.#expected = expected;
this.#stackTrace = stackTrace;
this.#name = name;
}
track(thisArg, args) {
const argsClone = ObjectFreeze(ArrayPrototypeSlice(args));
ArrayPrototypePush(this.#calls, ObjectFreeze({ thisArg, arguments: argsClone }));
}
get delta() {
return this.#calls.length - this.#expected;
}
reset() {
this.#calls = [];
}
getCalls() {
return ObjectFreeze(ArrayPrototypeSlice(this.#calls));
}
report() {
if (this.delta !== 0) {
const message = `Expected the ${this.#name} function to be ` +
`executed ${this.#expected} time(s) but was ` +
`executed ${this.#calls.length} time(s).`;
return {
message,
actual: this.#calls.length,
expected: this.#expected,
operator: this.#name,
stack: this.#stackTrace,
};
}
}
}
class CallTracker {
#callChecks = new SafeSet();
#trackedFunctions = new SafeWeakMap();
#getTrackedFunction(tracked) {
if (!this.#trackedFunctions.has(tracked)) {
throw new ERR_INVALID_ARG_VALUE('tracked', tracked, 'is not a tracked function');
}
return this.#trackedFunctions.get(tracked);
}
reset(tracked) {
if (tracked === undefined) {
this.#callChecks.forEach((check) => check.reset());
return;
}
this.#getTrackedFunction(tracked).reset();
}
getCalls(tracked) {
return this.#getTrackedFunction(tracked).getCalls();
}
calls(fn, expected = 1) {
if (process._exiting)
throw new ERR_UNAVAILABLE_DURING_EXIT();
if (typeof fn === 'number') {
expected = fn;
fn = noop;
} else if (fn === undefined) {
fn = noop;
}
validateUint32(expected, 'expected', true);
const context = new CallTrackerContext({
expected,
// eslint-disable-next-line no-restricted-syntax
stackTrace: new Error(),
name: fn.name || 'calls',
});
const tracked = new Proxy(fn, {
__proto__: null,
apply(fn, thisArg, argList) {
context.track(thisArg, argList);
return ReflectApply(fn, thisArg, argList);
},
});
this.#callChecks.add(context);
this.#trackedFunctions.set(tracked, context);
return tracked;
}
report() {
const errors = [];
for (const context of this.#callChecks) {
const message = context.report();
if (message !== undefined) {
ArrayPrototypePush(errors, message);
}
}
return errors;
}
verify() {
const errors = this.report();
if (errors.length === 0) {
return;
}
const message = errors.length === 1 ?
errors[0].message :
'Functions were not called the expected number of times';
throw new AssertionError({
message,
details: errors,
});
}
}
module.exports = CallTracker;

View File

@ -1,128 +0,0 @@
'use strict';
const common = require('../common');
const assert = require('assert');
// This test ensures that assert.CallTracker.calls() works as intended.
const tracker = new assert.CallTracker();
function bar() {}
const err = {
code: 'ERR_INVALID_ARG_TYPE',
};
// Ensures calls() throws on invalid input types.
assert.throws(() => {
const callsbar = tracker.calls(bar, '1');
callsbar();
}, err
);
assert.throws(() => {
const callsbar = tracker.calls(bar, 0.1);
callsbar();
}, { code: 'ERR_OUT_OF_RANGE' }
);
assert.throws(() => {
const callsbar = tracker.calls(bar, true);
callsbar();
}, err
);
assert.throws(() => {
const callsbar = tracker.calls(bar, () => {});
callsbar();
}, err
);
assert.throws(() => {
const callsbar = tracker.calls(bar, null);
callsbar();
}, err
);
// Expects an error as tracker.calls() cannot be called within a process exit
// handler.
process.on('exit', () => {
assert.throws(() => tracker.calls(bar, 1), {
code: 'ERR_UNAVAILABLE_DURING_EXIT',
});
});
const msg = 'Expected to throw';
function func() {
throw new Error(msg);
}
const callsfunc = tracker.calls(func, 1);
// Expects callsfunc() to call func() which throws an error.
assert.throws(
() => callsfunc(),
{ message: msg }
);
{
const tracker = new assert.CallTracker();
const callsNoop = tracker.calls(1);
callsNoop();
tracker.verify();
}
{
const tracker = new assert.CallTracker();
const callsNoop = tracker.calls(undefined, 1);
callsNoop();
tracker.verify();
}
{
function func() {}
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(callsfunc.length, 0);
}
{
function func(a, b, c = 2) {}
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(callsfunc.length, 2);
}
{
function func(a, b, c = 2) {}
delete func.length;
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(Object.hasOwn(callsfunc, 'length'), false);
}
{
const ArrayIteratorPrototype = Reflect.getPrototypeOf(
Array.prototype.values()
);
const { next } = ArrayIteratorPrototype;
ArrayIteratorPrototype.next = common.mustNotCall(
'%ArrayIteratorPrototype%.next'
);
Object.prototype.get = common.mustNotCall('%Object.prototype%.get');
const customPropertyValue = Symbol();
function func(a, b, c = 2) {
return a + b + c;
}
func.customProperty = customPropertyValue;
Object.defineProperty(func, 'length', { get: common.mustNotCall() });
const tracker = new assert.CallTracker();
const callsfunc = tracker.calls(func);
assert.strictEqual(Object.hasOwn(callsfunc, 'length'), true);
assert.strictEqual(callsfunc.customProperty, customPropertyValue);
assert.strictEqual(callsfunc(1, 2, 3), 6);
ArrayIteratorPrototype.next = next;
delete Object.prototype.get;
}

View File

@ -1,73 +0,0 @@
'use strict';
require('../common');
const assert = require('assert');
const { describe, it } = require('node:test');
describe('assert.CallTracker.getCalls()', { concurrency: !process.env.TEST_PARALLEL }, () => {
const tracker = new assert.CallTracker();
it('should return empty list when no calls', () => {
const fn = tracker.calls();
assert.deepStrictEqual(tracker.getCalls(fn), []);
});
it('should return calls', () => {
const fn = tracker.calls(() => {});
const arg1 = {};
const arg2 = {};
fn(arg1, arg2);
fn.call(arg2, arg2);
assert.deepStrictEqual(tracker.getCalls(fn), [
{ arguments: [arg1, arg2], thisArg: undefined },
{ arguments: [arg2], thisArg: arg2 }]);
});
it('should throw when getting calls of a non-tracked function', () => {
[() => {}, 1, true, null, undefined, {}, []].forEach((fn) => {
assert.throws(() => tracker.getCalls(fn), { code: 'ERR_INVALID_ARG_VALUE' });
});
});
it('should return a frozen object', () => {
const fn = tracker.calls();
fn();
const calls = tracker.getCalls(fn);
assert.throws(() => calls.push(1), /object is not extensible/);
assert.throws(() => Object.assign(calls[0], { foo: 'bar' }), /object is not extensible/);
assert.throws(() => calls[0].arguments.push(1), /object is not extensible/);
});
});
describe('assert.CallTracker.reset()', () => {
const tracker = new assert.CallTracker();
it('should reset calls', () => {
const fn = tracker.calls();
fn();
fn();
fn();
assert.strictEqual(tracker.getCalls(fn).length, 3);
tracker.reset(fn);
assert.deepStrictEqual(tracker.getCalls(fn), []);
});
it('should reset all calls', () => {
const fn1 = tracker.calls();
const fn2 = tracker.calls();
fn1();
fn2();
assert.strictEqual(tracker.getCalls(fn1).length, 1);
assert.strictEqual(tracker.getCalls(fn2).length, 1);
tracker.reset();
assert.deepStrictEqual(tracker.getCalls(fn1), []);
assert.deepStrictEqual(tracker.getCalls(fn2), []);
});
it('should throw when resetting a non-tracked function', () => {
[() => {}, 1, true, null, {}, []].forEach((fn) => {
assert.throws(() => tracker.reset(fn), { code: 'ERR_INVALID_ARG_VALUE' });
});
});
});

View File

@ -1,26 +0,0 @@
'use strict';
require('../common');
const assert = require('assert');
// This test ensures that the assert.CallTracker.report() works as intended.
const tracker = new assert.CallTracker();
function foo() {}
const callsfoo = tracker.calls(foo, 1);
// Ensures that foo was added to the callChecks array.
assert.strictEqual(tracker.report()[0].operator, 'foo');
callsfoo();
// Ensures that foo was removed from the callChecks array after being called the
// expected number of times.
assert.strictEqual(typeof tracker.report()[0], 'undefined');
callsfoo();
// Ensures that foo was added back to the callChecks array after being called
// more than the expected number of times.
assert.strictEqual(tracker.report()[0].operator, 'foo');

View File

@ -1,53 +0,0 @@
'use strict';
require('../common');
const assert = require('assert');
// This test ensures that assert.CallTracker.verify() works as intended.
const tracker = new assert.CallTracker();
const generic_msg = 'Functions were not called the expected number of times';
function foo() {}
function bar() {}
const callsfoo = tracker.calls(foo, 1);
const callsbar = tracker.calls(bar, 1);
// Expects an error as callsfoo() and callsbar() were called less than one time.
assert.throws(
() => tracker.verify(),
{ message: generic_msg }
);
callsfoo();
// Expects an error as callsbar() was called less than one time.
assert.throws(
() => tracker.verify(),
{ message: 'Expected the bar function to be executed 1 time(s) but was executed 0 time(s).' }
);
callsbar();
// Will throw an error if callsfoo() and callsbar isn't called exactly once.
tracker.verify();
const callsfoobar = tracker.calls(foo, 1);
callsfoo();
// Expects an error as callsfoo() was called more than once and callsfoobar() was called less than one time.
assert.throws(
() => tracker.verify(),
{ message: generic_msg }
);
callsfoobar();
// Expects an error as callsfoo() was called more than once
assert.throws(
() => tracker.verify(),
{ message: 'Expected the foo function to be executed 1 time(s) but was executed 2 time(s).' }
);

View File

@ -6,7 +6,6 @@ const test = require('node:test');
test('expected methods are on t.assert', (t) => {
const uncopiedKeys = [
'AssertionError',
'CallTracker',
'strict',
];
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));

View File

@ -459,10 +459,7 @@ const theData = 'hello';
const assert = require('assert');
const tracker = new assert.CallTracker();
process.on('exit', () => {
tracker.verify();
});
const { mock } = require('node:test');
// We create an interval to keep the event loop alive while
// we wait for the stream read to complete. The reason this is needed is because there's
@ -474,16 +471,25 @@ const theData = 'hello';
// from terminating at all unless the stream was consumed/closed.
const i = setInterval(() => {}, 1000);
parentPort.onmessage = tracker.calls(({ data }) => {
const innercb = mock.fn((result) => {
assert(!result.done);
assert(result.value instanceof Uint8Array);
clearInterval(i);
});
const cb = mock.fn(({ data }) => {
assert(isReadableStream(data));
const reader = data.getReader();
reader.read().then(tracker.calls((result) => {
assert(!result.done);
assert(result.value instanceof Uint8Array);
clearInterval(i);
}));
reader.read().then(innercb);
parentPort.close();
});
process.on('exit', () => {
assert.strictEqual(innercb.mock.callCount(), 1);
assert.strictEqual(cb.mock.callCount(), 1);
});
parentPort.onmessage = cb;
parentPort.onmessageerror = () => assert.fail('should not be called');
`, { eval: true });