nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
144 lines (124 loc) • 3.46 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/assert/calltracker.js
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import * as AssertionError from "nstdlib/lib/internal/assert/assertion_error";
import { validateUint32 } from "nstdlib/lib/internal/validators";
const { ERR_INVALID_ARG_VALUE, ERR_UNAVAILABLE_DURING_EXIT } = __codes__;
const noop = Function.prototype;
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 = Object.freeze(Array.prototype.slice.call(args));
Array.prototype.push.call(
this.#calls,
Object.freeze({ thisArg, arguments: argsClone }),
);
}
get delta() {
return this.#calls.length - this.#expected;
}
reset() {
this.#calls = [];
}
getCalls() {
return Object.freeze(Array.prototype.slice.call(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 Set();
#trackedFunctions = new WeakMap();
#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) {
Array.prototype.push.call(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,
});
}
}
export default CallTracker;