• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypePush,
5  ArrayPrototypeSlice,
6  Error,
7  FunctionPrototype,
8  ObjectFreeze,
9  Proxy,
10  ReflectApply,
11  SafeSet,
12  SafeWeakMap,
13} = primordials;
14
15const {
16  codes: {
17    ERR_UNAVAILABLE_DURING_EXIT,
18    ERR_INVALID_ARG_VALUE,
19  },
20} = require('internal/errors');
21const AssertionError = require('internal/assert/assertion_error');
22const {
23  validateUint32,
24} = require('internal/validators');
25
26const noop = FunctionPrototype;
27
28class CallTrackerContext {
29  #expected;
30  #calls;
31  #name;
32  #stackTrace;
33  constructor({ expected, stackTrace, name }) {
34    this.#calls = [];
35    this.#expected = expected;
36    this.#stackTrace = stackTrace;
37    this.#name = name;
38  }
39
40  track(thisArg, args) {
41    const argsClone = ObjectFreeze(ArrayPrototypeSlice(args));
42    ArrayPrototypePush(this.#calls, ObjectFreeze({ thisArg, arguments: argsClone }));
43  }
44
45  get delta() {
46    return this.#calls.length - this.#expected;
47  }
48
49  reset() {
50    this.#calls = [];
51  }
52  getCalls() {
53    return ObjectFreeze(ArrayPrototypeSlice(this.#calls));
54  }
55
56  report() {
57    if (this.delta !== 0) {
58      const message = `Expected the ${this.#name} function to be ` +
59                      `executed ${this.#expected} time(s) but was ` +
60                      `executed ${this.#calls.length} time(s).`;
61      return {
62        message,
63        actual: this.#calls.length,
64        expected: this.#expected,
65        operator: this.#name,
66        stack: this.#stackTrace,
67      };
68    }
69  }
70}
71
72class CallTracker {
73
74  #callChecks = new SafeSet();
75  #trackedFunctions = new SafeWeakMap();
76
77  #getTrackedFunction(tracked) {
78    if (!this.#trackedFunctions.has(tracked)) {
79      throw new ERR_INVALID_ARG_VALUE('tracked', tracked, 'is not a tracked function');
80    }
81    return this.#trackedFunctions.get(tracked);
82  }
83
84  reset(tracked) {
85    if (tracked === undefined) {
86      this.#callChecks.forEach((check) => check.reset());
87      return;
88    }
89
90    this.#getTrackedFunction(tracked).reset();
91  }
92
93  getCalls(tracked) {
94    return this.#getTrackedFunction(tracked).getCalls();
95  }
96
97  calls(fn, expected = 1) {
98    if (process._exiting)
99      throw new ERR_UNAVAILABLE_DURING_EXIT();
100    if (typeof fn === 'number') {
101      expected = fn;
102      fn = noop;
103    } else if (fn === undefined) {
104      fn = noop;
105    }
106
107    validateUint32(expected, 'expected', true);
108
109    const context = new CallTrackerContext({
110      expected,
111      // eslint-disable-next-line no-restricted-syntax
112      stackTrace: new Error(),
113      name: fn.name || 'calls',
114    });
115    const tracked = new Proxy(fn, {
116      __proto__: null,
117      apply(fn, thisArg, argList) {
118        context.track(thisArg, argList);
119        return ReflectApply(fn, thisArg, argList);
120      },
121    });
122    this.#callChecks.add(context);
123    this.#trackedFunctions.set(tracked, context);
124    return tracked;
125  }
126
127  report() {
128    const errors = [];
129    for (const context of this.#callChecks) {
130      const message = context.report();
131      if (message !== undefined) {
132        ArrayPrototypePush(errors, message);
133      }
134    }
135    return errors;
136  }
137
138  verify() {
139    const errors = this.report();
140    if (errors.length === 0) {
141      return;
142    }
143    const message = errors.length === 1 ?
144      errors[0].message :
145      'Functions were not called the expected number of times';
146    throw new AssertionError({
147      message,
148      details: errors,
149    });
150  }
151}
152
153module.exports = CallTracker;
154