2020-02-27 11:41:05 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const {
|
2020-11-06 14:20:06 +00:00
|
|
|
ArrayPrototypePush,
|
2022-08-11 19:27:34 +00:00
|
|
|
ArrayPrototypeSlice,
|
2020-02-27 11:41:05 +00:00
|
|
|
Error,
|
2020-11-06 14:20:06 +00:00
|
|
|
FunctionPrototype,
|
2022-08-11 19:27:34 +00:00
|
|
|
ObjectFreeze,
|
2022-05-06 22:00:53 +00:00
|
|
|
Proxy,
|
2020-11-06 14:20:06 +00:00
|
|
|
ReflectApply,
|
2020-02-27 11:41:05 +00:00
|
|
|
SafeSet,
|
2022-08-11 19:27:34 +00:00
|
|
|
SafeWeakMap,
|
2020-02-27 11:41:05 +00:00
|
|
|
} = primordials;
|
|
|
|
|
|
|
|
const {
|
|
|
|
codes: {
|
2022-08-11 19:27:34 +00:00
|
|
|
ERR_INVALID_ARG_VALUE,
|
2024-04-23 17:05:38 +00:00
|
|
|
ERR_UNAVAILABLE_DURING_EXIT,
|
2020-02-27 11:41:05 +00:00
|
|
|
},
|
|
|
|
} = require('internal/errors');
|
|
|
|
const AssertionError = require('internal/assert/assertion_error');
|
|
|
|
const {
|
|
|
|
validateUint32,
|
|
|
|
} = require('internal/validators');
|
|
|
|
|
2020-11-06 14:20:06 +00:00
|
|
|
const noop = FunctionPrototype;
|
2020-02-27 11:41:05 +00:00
|
|
|
|
2022-08-11 19:27:34 +00:00
|
|
|
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,
|
2023-02-28 11:14:11 +00:00
|
|
|
stack: this.#stackTrace,
|
2022-08-11 19:27:34 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-27 11:41:05 +00:00
|
|
|
class CallTracker {
|
|
|
|
|
2021-11-04 13:18:10 +00:00
|
|
|
#callChecks = new SafeSet();
|
2022-08-11 19:27:34 +00:00
|
|
|
#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;
|
|
|
|
}
|
2020-02-27 11:41:05 +00:00
|
|
|
|
2022-08-11 19:27:34 +00:00
|
|
|
this.#getTrackedFunction(tracked).reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
getCalls(tracked) {
|
|
|
|
return this.#getTrackedFunction(tracked).getCalls();
|
|
|
|
}
|
|
|
|
|
|
|
|
calls(fn, expected = 1) {
|
2020-02-27 11:41:05 +00:00
|
|
|
if (process._exiting)
|
|
|
|
throw new ERR_UNAVAILABLE_DURING_EXIT();
|
|
|
|
if (typeof fn === 'number') {
|
2022-08-11 19:27:34 +00:00
|
|
|
expected = fn;
|
2020-02-27 11:41:05 +00:00
|
|
|
fn = noop;
|
|
|
|
} else if (fn === undefined) {
|
|
|
|
fn = noop;
|
|
|
|
}
|
|
|
|
|
2022-08-11 19:27:34 +00:00
|
|
|
validateUint32(expected, 'expected', true);
|
2020-02-27 11:41:05 +00:00
|
|
|
|
2022-08-11 19:27:34 +00:00
|
|
|
const context = new CallTrackerContext({
|
|
|
|
expected,
|
2020-02-27 11:41:05 +00:00
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
|
|
stackTrace: new Error(),
|
2023-02-28 11:14:11 +00:00
|
|
|
name: fn.name || 'calls',
|
2022-08-11 19:27:34 +00:00
|
|
|
});
|
|
|
|
const tracked = new Proxy(fn, {
|
2022-05-06 22:00:53 +00:00
|
|
|
__proto__: null,
|
|
|
|
apply(fn, thisArg, argList) {
|
2022-08-11 19:27:34 +00:00
|
|
|
context.track(thisArg, argList);
|
2022-05-06 22:00:53 +00:00
|
|
|
return ReflectApply(fn, thisArg, argList);
|
|
|
|
},
|
|
|
|
});
|
2022-08-11 19:27:34 +00:00
|
|
|
this.#callChecks.add(context);
|
|
|
|
this.#trackedFunctions.set(tracked, context);
|
|
|
|
return tracked;
|
2020-02-27 11:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
report() {
|
|
|
|
const errors = [];
|
|
|
|
for (const context of this.#callChecks) {
|
2022-08-11 19:27:34 +00:00
|
|
|
const message = context.report();
|
|
|
|
if (message !== undefined) {
|
|
|
|
ArrayPrototypePush(errors, message);
|
2020-02-27 11:41:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return errors;
|
|
|
|
}
|
|
|
|
|
|
|
|
verify() {
|
|
|
|
const errors = this.report();
|
2022-07-08 10:17:35 +00:00
|
|
|
if (errors.length === 0) {
|
|
|
|
return;
|
2020-02-27 11:41:05 +00:00
|
|
|
}
|
2022-07-08 10:17:35 +00:00
|
|
|
const message = errors.length === 1 ?
|
|
|
|
errors[0].message :
|
|
|
|
'Functions were not called the expected number of times';
|
|
|
|
throw new AssertionError({
|
|
|
|
message,
|
|
|
|
details: errors,
|
|
|
|
});
|
2020-02-27 11:41:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = CallTracker;
|