Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0e20072

Browse filesBrowse files
MoLowruyadorno
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 b1590bb commit 0e20072
Copy full SHA for 0e20072

File tree

Expand file treeCollapse file tree

3 files changed

+239
-34
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+239
-34
lines changed
Open diff view settings
Collapse file

‎doc/api/assert.md‎

Copy file name to clipboardExpand all lines: doc/api/assert.md
+83Lines changed: 83 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎lib/internal/assert/calltracker.js‎

Copy file name to clipboardExpand all lines: lib/internal/assert/calltracker.js
+83-34Lines 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;
Collapse file
+73Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.