Skip to content

Commit 83a7ffe

Browse files
MoLowdanielleadams
authored andcommitted
assert: add getCalls and reset to callTracker
PR-URL: #44191 Reviewed-By: Erick Wendel <erick.workspace@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent c9602fa commit 83a7ffe

File tree

3 files changed

+239
-34
lines changed

3 files changed

+239
-34
lines changed

doc/api/assert.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,47 @@ function func() {}
322322
const callsfunc = tracker.calls(func);
323323
```
324324

325+
### `tracker.getCalls(fn)`
326+
327+
<!-- YAML
328+
added: REPLACEME
329+
-->
330+
331+
* `fn` {Function}.
332+
333+
* Returns: {Array} with all the calls to a tracked function.
334+
335+
* Object {Object}
336+
* `thisArg` {Object}
337+
* `arguments` {Array} the arguments passed to the tracked function
338+
339+
```mjs
340+
import assert from 'node:assert';
341+
342+
const tracker = new assert.CallTracker();
343+
344+
function func() {}
345+
const callsfunc = tracker.calls(func);
346+
callsfunc(1, 2, 3);
347+
348+
assert.deepStrictEqual(tracker.getCalls(callsfunc),
349+
[{ thisArg: this, arguments: [1, 2, 3 ] }]);
350+
```
351+
352+
```cjs
353+
const assert = require('node:assert');
354+
355+
// Creates call tracker.
356+
const tracker = new assert.CallTracker();
357+
358+
function func() {}
359+
const callsfunc = tracker.calls(func);
360+
callsfunc(1, 2, 3);
361+
362+
assert.deepStrictEqual(tracker.getCalls(callsfunc),
363+
[{ thisArg: this, arguments: [1, 2, 3 ] }]);
364+
```
365+
325366
### `tracker.report()`
326367

327368
<!-- YAML
@@ -395,6 +436,48 @@ tracker.report();
395436
// ]
396437
```
397438

439+
### `tracker.reset([fn])`
440+
441+
<!-- YAML
442+
added: REPLACEME
443+
-->
444+
445+
* `fn` {Function} a tracked function to reset.
446+
447+
reset calls of the call tracker.
448+
if a tracked function is passed as an argument, the calls will be reset for it.
449+
if no arguments are passed, all tracked functions will be reset
450+
451+
```mjs
452+
import assert from 'node:assert';
453+
454+
const tracker = new assert.CallTracker();
455+
456+
function func() {}
457+
const callsfunc = tracker.calls(func);
458+
459+
callsfunc();
460+
// Tracker was called once
461+
tracker.getCalls(callsfunc).length === 1;
462+
463+
tracker.reset(callsfunc);
464+
tracker.getCalls(callsfunc).length === 0;
465+
```
466+
467+
```cjs
468+
const assert = require('node:assert');
469+
470+
function func() {}
471+
const callsfunc = tracker.calls(func);
472+
473+
callsfunc();
474+
// Tracker was called once
475+
tracker.getCalls(callsfunc).length === 1;
476+
477+
tracker.reset(callsfunc);
478+
tracker.getCalls(callsfunc).length === 0;
479+
```
480+
398481
### `tracker.verify()`
399482

400483
<!-- YAML

lib/internal/assert/calltracker.js

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
const {
44
ArrayPrototypePush,
5+
ArrayPrototypeSlice,
56
Error,
67
FunctionPrototype,
8+
ObjectFreeze,
79
Proxy,
810
ReflectApply,
911
SafeSet,
12+
SafeWeakMap,
1013
} = primordials;
1114

1215
const {
1316
codes: {
1417
ERR_UNAVAILABLE_DURING_EXIT,
18+
ERR_INVALID_ARG_VALUE,
1519
},
1620
} = require('internal/errors');
1721
const AssertionError = require('internal/assert/assertion_error');
@@ -21,66 +25,111 @@ const {
2125

2226
const noop = FunctionPrototype;
2327

28+
class 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+
2472
class CallTracker {
2573

2674
#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+
}
2789

28-
calls(fn, exact = 1) {
90+
this.#getTrackedFunction(tracked).reset();
91+
}
92+
93+
getCalls(tracked) {
94+
return this.#getTrackedFunction(tracked).getCalls();
95+
}
96+
97+
calls(fn, expected = 1) {
2998
if (process._exiting)
3099
throw new ERR_UNAVAILABLE_DURING_EXIT();
31100
if (typeof fn === 'number') {
32-
exact = fn;
101+
expected = fn;
33102
fn = noop;
34103
} else if (fn === undefined) {
35104
fn = noop;
36105
}
37106

38-
validateUint32(exact, 'exact', true);
107+
validateUint32(expected, 'expected', true);
39108

40-
const context = {
41-
exact,
42-
actual: 0,
109+
const context = new CallTrackerContext({
110+
expected,
43111
// eslint-disable-next-line no-restricted-syntax
44112
stackTrace: new Error(),
45113
name: fn.name || 'calls'
46-
};
47-
const callChecks = this.#callChecks;
48-
callChecks.add(context);
49-
50-
return new Proxy(fn, {
114+
});
115+
const tracked = new Proxy(fn, {
51116
__proto__: null,
52117
apply(fn, thisArg, argList) {
53-
context.actual++;
54-
if (context.actual === context.exact) {
55-
// Once function has reached its call count remove it from
56-
// callChecks set to prevent memory leaks.
57-
callChecks.delete(context);
58-
}
59-
// If function has been called more than expected times, add back into
60-
// callchecks.
61-
if (context.actual === context.exact + 1) {
62-
callChecks.add(context);
63-
}
118+
context.track(thisArg, argList);
64119
return ReflectApply(fn, thisArg, argList);
65120
},
66121
});
122+
this.#callChecks.add(context);
123+
this.#trackedFunctions.set(tracked, context);
124+
return tracked;
67125
}
68126

69127
report() {
70128
const errors = [];
71129
for (const context of this.#callChecks) {
72-
// If functions have not been called exact times
73-
if (context.actual !== context.exact) {
74-
const message = `Expected the ${context.name} function to be ` +
75-
`executed ${context.exact} time(s) but was ` +
76-
`executed ${context.actual} time(s).`;
77-
ArrayPrototypePush(errors, {
78-
message,
79-
actual: context.actual,
80-
expected: context.exact,
81-
operator: context.name,
82-
stack: context.stackTrace
83-
});
130+
const message = context.report();
131+
if (message !== undefined) {
132+
ArrayPrototypePush(errors, message);
84133
}
85134
}
86135
return errors;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert');
4+
const { describe, it } = require('node:test');
5+
6+
7+
describe('assert.CallTracker.getCalls()', { concurrency: true }, () => {
8+
const tracker = new assert.CallTracker();
9+
10+
it('should return empty list when no calls', () => {
11+
const fn = tracker.calls();
12+
assert.deepStrictEqual(tracker.getCalls(fn), []);
13+
});
14+
15+
it('should return calls', () => {
16+
const fn = tracker.calls(() => {});
17+
const arg1 = {};
18+
const arg2 = {};
19+
fn(arg1, arg2);
20+
fn.call(arg2, arg2);
21+
assert.deepStrictEqual(tracker.getCalls(fn), [
22+
{ arguments: [arg1, arg2], thisArg: undefined },
23+
{ arguments: [arg2], thisArg: arg2 }]);
24+
});
25+
26+
it('should throw when getting calls of a non-tracked function', () => {
27+
[() => {}, 1, true, null, undefined, {}, []].forEach((fn) => {
28+
assert.throws(() => tracker.getCalls(fn), { code: 'ERR_INVALID_ARG_VALUE' });
29+
});
30+
});
31+
32+
it('should return a frozen object', () => {
33+
const fn = tracker.calls();
34+
fn();
35+
const calls = tracker.getCalls(fn);
36+
assert.throws(() => calls.push(1), /object is not extensible/);
37+
assert.throws(() => Object.assign(calls[0], { foo: 'bar' }), /object is not extensible/);
38+
assert.throws(() => calls[0].arguments.push(1), /object is not extensible/);
39+
});
40+
});
41+
42+
describe('assert.CallTracker.reset()', () => {
43+
const tracker = new assert.CallTracker();
44+
45+
it('should reset calls', () => {
46+
const fn = tracker.calls();
47+
fn();
48+
fn();
49+
fn();
50+
assert.strictEqual(tracker.getCalls(fn).length, 3);
51+
tracker.reset(fn);
52+
assert.deepStrictEqual(tracker.getCalls(fn), []);
53+
});
54+
55+
it('should reset all calls', () => {
56+
const fn1 = tracker.calls();
57+
const fn2 = tracker.calls();
58+
fn1();
59+
fn2();
60+
assert.strictEqual(tracker.getCalls(fn1).length, 1);
61+
assert.strictEqual(tracker.getCalls(fn2).length, 1);
62+
tracker.reset();
63+
assert.deepStrictEqual(tracker.getCalls(fn1), []);
64+
assert.deepStrictEqual(tracker.getCalls(fn2), []);
65+
});
66+
67+
68+
it('should throw when resetting a non-tracked function', () => {
69+
[() => {}, 1, true, null, {}, []].forEach((fn) => {
70+
assert.throws(() => tracker.reset(fn), { code: 'ERR_INVALID_ARG_VALUE' });
71+
});
72+
});
73+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy