Skip to content

Commit e1b312b

Browse files
kraenhansenclaude
andcommitted
Revert mustCall to process-exit pattern matching Node.js common.mustCall
Use a regular function wrapper with fn.apply(this, args) to preserve receiver binding, and verify call counts on process exit instead of returning a [wrapper, called] Promise tuple. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 163e086 commit e1b312b

4 files changed

Lines changed: 53 additions & 60 deletions

File tree

implementors/node/must-call.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
1+
const pendingCalls = [];
2+
13
/**
2-
* Wraps a function and returns a [wrapper, called] tuple.
3-
* - `wrapper` — call this in place of the original function
4-
* - `called` — a Promise that resolves (with the return value of fn) once
5-
* wrapper has been invoked
6-
*
7-
* If `fn` is omitted, a no-op function is used.
4+
* Wraps a function and asserts it is called exactly `exact` times before the
5+
* process exits. If `fn` is omitted, a no-op function is used.
86
*
97
* Usage:
10-
* const [onResolve, resolved] = mustCall((result) => {
8+
* promise.then(mustCall((result) => {
119
* assert.strictEqual(result, 42);
12-
* });
13-
* promise.then(onResolve);
14-
* await resolved;
10+
* }));
1511
*/
16-
const mustCall = (fn) => {
17-
let resolve;
18-
const called = new Promise((r) => { resolve = r; });
19-
const wrapper = (...args) => {
20-
const result = fn ? fn(...args) : undefined;
21-
resolve(result);
22-
return result;
12+
const mustCall = (fn, exact = 1) => {
13+
const entry = {
14+
exact,
15+
actual: 0,
16+
name: fn?.name || "<anonymous>",
17+
error: new Error(), // capture call-site stack
18+
};
19+
pendingCalls.push(entry);
20+
return function(...args) {
21+
entry.actual++;
22+
if (fn) return fn.apply(this, args);
2323
};
24-
return [wrapper, called];
2524
};
2625

2726
/**
@@ -33,4 +32,15 @@ const mustNotCall = (msg) => {
3332
};
3433
};
3534

35+
process.on("exit", () => {
36+
for (const entry of pendingCalls) {
37+
if (entry.actual !== entry.exact) {
38+
entry.error.message =
39+
`mustCall "${entry.name}" expected ${entry.exact} call(s) ` +
40+
`but got ${entry.actual}`;
41+
throw entry.error;
42+
}
43+
}
44+
});
45+
3646
Object.assign(globalThis, { mustCall, mustNotCall });

tests/harness/must-call.js

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,27 @@ if (typeof mustCall !== 'function') {
33
throw new Error('Expected a global mustCall function');
44
}
55

6-
// mustCall returns a [wrapper, called] tuple
6+
// mustCall returns a wrapper function (not a tuple)
77
{
8-
const [wrapper, called] = mustCall();
8+
const wrapper = mustCall();
99
if (typeof wrapper !== 'function') {
10-
throw new Error('mustCall()[0] must be a function');
11-
}
12-
if (!(called instanceof Promise)) {
13-
throw new Error('mustCall()[1] must be a Promise');
10+
throw new Error('mustCall() must return a function');
1411
}
1512
wrapper();
16-
await called;
1713
}
1814

1915
// mustCall forwards arguments and return value
2016
{
21-
const [wrapper, called] = mustCall((a, b) => a + b);
17+
const wrapper = mustCall((a, b) => a + b);
2218
const result = wrapper(2, 3);
2319
assert.strictEqual(result, 5);
24-
const resolvedValue = await called;
25-
assert.strictEqual(resolvedValue, 5);
2620
}
2721

2822
// mustCall without fn argument works as a no-op wrapper
2923
{
30-
const [wrapper, called] = mustCall();
24+
const wrapper = mustCall();
3125
const result = wrapper('ignored');
3226
assert.strictEqual(result, undefined);
33-
await called;
3427
}
3528

3629
// mustNotCall is a function

tests/js-native-api/3_callbacks/test.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
'use strict';
22
const addon = loadAddon('3_callbacks');
33

4-
let called = false;
5-
addon.RunCallback((msg) => {
4+
addon.RunCallback(mustCall((msg) => {
65
assert.strictEqual(msg, 'hello world');
7-
called = true;
8-
});
9-
assert(called);
6+
}));
107

118
function testRecv(desiredRecv) {
12-
let recvCalled = false;
13-
addon.RunCallbackWithRecv(function() {
9+
addon.RunCallbackWithRecv(mustCall(function() {
1410
assert.strictEqual(this, desiredRecv);
15-
recvCalled = true;
16-
}, desiredRecv);
17-
assert(recvCalled);
11+
}), desiredRecv);
1812
}
1913

2014
testRecv(undefined);

tests/js-native-api/test_promise/test.js

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,35 @@ const test_promise = loadAddon('test_promise');
55
{
66
const expected_result = 42;
77
const promise = test_promise.createPromise();
8-
const [onResolve, resolved] = mustCall((result) => {
9-
assert.strictEqual(result, expected_result);
10-
});
11-
promise.then(onResolve);
8+
promise.then(
9+
mustCall((result) => {
10+
assert.strictEqual(result, expected_result);
11+
}));
1212
test_promise.concludeCurrentPromise(expected_result, true);
13-
await resolved;
1413
}
1514

1615
// A rejection
1716
{
1817
const expected_result = 'It\'s not you, it\'s me.';
1918
const promise = test_promise.createPromise();
20-
const [onReject, rejected] = mustCall((result) => {
21-
assert.strictEqual(result, expected_result);
22-
});
23-
const [onThen, thenCalled] = mustCall();
24-
promise.then(mustNotCall(), onReject).then(onThen);
19+
promise.then(
20+
mustNotCall(),
21+
mustCall(function(result) {
22+
assert.strictEqual(result, expected_result);
23+
}))
24+
.then(mustCall());
2525
test_promise.concludeCurrentPromise(expected_result, false);
26-
await thenCalled;
2726
}
2827

2928
// Chaining
3029
{
3130
const expected_result = 'chained answer';
3231
const promise = test_promise.createPromise();
33-
const [onResolve, resolved] = mustCall((result) => {
34-
assert.strictEqual(result, expected_result);
35-
});
36-
promise.then(onResolve);
32+
promise.then(
33+
mustCall((result) => {
34+
assert.strictEqual(result, expected_result);
35+
}));
3736
test_promise.concludeCurrentPromise(Promise.resolve('chained answer'), true);
38-
await resolved;
3937
}
4038

4139
const promiseTypeTestPromise = test_promise.createPromise();
@@ -45,11 +43,9 @@ test_promise.concludeCurrentPromise(undefined, true);
4543
const rejectPromise = Promise.reject(-1);
4644
const expected_reason = -1;
4745
assert.strictEqual(test_promise.isPromise(rejectPromise), true);
48-
const [onCatch, caught] = mustCall((reason) => {
46+
rejectPromise.catch(mustCall((reason) => {
4947
assert.strictEqual(reason, expected_reason);
50-
});
51-
rejectPromise.catch(onCatch);
52-
await caught;
48+
}));
5349

5450
assert.strictEqual(test_promise.isPromise(2.4), false);
5551
assert.strictEqual(test_promise.isPromise('I promise!'), false);

0 commit comments

Comments
 (0)