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