Skip to content

Commit

Permalink
assert: add getCalls and reset to callTracker
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jul 5, 2022
1 parent b993789 commit ecd3ff7
Showing 1 changed file with 84 additions and 27 deletions.
111 changes: 84 additions & 27 deletions lib/internal/assert/calltracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ const {
Proxy,
ReflectApply,
SafeSet,
SafeWeakMap,
} = primordials;

const {
codes: {
ERR_UNAVAILABLE_DURING_EXIT,
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
Expand All @@ -21,66 +23,121 @@ const {

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) {
ArrayPrototypePush(this.#calls, { thisArg, arguments: args });
}

get delta() {
return this.#calls.length - this.#expected;
}

reset() {
this.#calls = [];
}
getCalls() {
return 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) {
ArrayPrototypeForEach(this.#callChecks, check => check.reset());
return;
}

calls(fn, exact = 1) {
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') {
exact = fn;
expected = fn;
fn = noop;
} else if (fn === undefined) {
fn = noop;
}

validateUint32(exact, 'exact', true);
validateUint32(expected, 'expected', true);

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

return new Proxy(fn, {
const tracked = new Proxy(fn, {
__proto__: null,
apply(fn, thisArg, argList) {
context.actual++;
if (context.actual === context.exact) {
context.track(thisArg, argList);
if (context.delta === 0) {
// 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) {
if (context.delta === 1) {
// If function has been called more than expected times, add back into
// callchecks.
callChecks.add(context);
}
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) {
// 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).`;
ArrayPrototypePush(errors, {
message,
actual: context.actual,
expected: context.exact,
operator: context.name,
stack: context.stackTrace
});
const message = context.report();
if (message !== undefined) {
ArrayPrototypePush(errors, message);
}
}
return errors;
Expand Down

0 comments on commit ecd3ff7

Please sign in to comment.