Skip to content

Commit d162975

Browse files
pmarchiniRafaelGSS
authored andcommitted
test_runner: refactor testPlan counter increse
PR-URL: #56765 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Chemi Atlow <[email protected]>
1 parent f706061 commit d162975

File tree

7 files changed

+382
-31
lines changed

7 files changed

+382
-31
lines changed

doc/api/test.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3367,18 +3367,36 @@ added:
33673367

33683368
The name of the test.
33693369

3370-
### `context.plan(count)`
3370+
### `context.plan(count[,options])`
33713371

33723372
<!-- YAML
33733373
added:
33743374
- v22.2.0
3375+
- v20.15.0
33753376
changes:
3376-
- version: v22.13.0
3377+
- version:
3378+
- REPLACEME
3379+
- v23.9.0
3380+
pr-url: https://github.com/nodejs/node/pull/56765
3381+
description: Add the `options` parameter.
3382+
- version:
3383+
- v23.4.0
3384+
- v22.13.0
33773385
pr-url: https://github.com/nodejs/node/pull/55895
33783386
description: This function is no longer experimental.
33793387
-->
33803388

33813389
* `count` {number} The number of assertions and subtests that are expected to run.
3390+
* `options` {Object} Additional options for the plan.
3391+
* `wait` {boolean|number} The wait time for the plan:
3392+
* If `true`, the plan waits indefinitely for all assertions and subtests to run.
3393+
* If `false`, the plan performs an immediate check after the test function completes,
3394+
without waiting for any pending assertions or subtests.
3395+
Any assertions or subtests that complete after this check will not be counted towards the plan.
3396+
* If a number, it specifies the maximum wait time in milliseconds
3397+
before timing out while waiting for expected assertions and subtests to be matched.
3398+
If the timeout is reached, the test will fail.
3399+
**Default:** `false`.
33823400

33833401
This function is used to set the number of assertions and subtests that are expected to run
33843402
within the test. If the number of assertions and subtests that run does not match the
@@ -3417,6 +3435,26 @@ test('planning with streams', (t, done) => {
34173435
});
34183436
```
34193437

3438+
When using the `wait` option, you can control how long the test will wait for the expected assertions.
3439+
For example, setting a maximum wait time ensures that the test will wait for asynchronous assertions
3440+
to complete within the specified timeframe:
3441+
3442+
```js
3443+
test('plan with wait: 2000 waits for async assertions', (t) => {
3444+
t.plan(1, { wait: 2000 }); // Waits for up to 2 seconds for the assertion to complete.
3445+
3446+
const asyncActivity = () => {
3447+
setTimeout(() => {
3448+
t.assert.ok(true, 'Async assertion completed within the wait time');
3449+
}, 1000); // Completes after 1 second, within the 2-second wait time.
3450+
};
3451+
3452+
asyncActivity(); // The test will pass because the assertion is completed in time.
3453+
});
3454+
```
3455+
3456+
Note: If a `wait` timeout is specified, it begins counting down only after the test function finishes executing.
3457+
34203458
### `context.runOnly(shouldRunOnlyTests)`
34213459

34223460
<!-- YAML

lib/internal/test_runner/test.js

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,22 +176,88 @@ function testMatchesPattern(test, patterns) {
176176
}
177177

178178
class TestPlan {
179-
constructor(count) {
179+
#waitIndefinitely = false;
180+
#planPromise = null;
181+
#timeoutId = null;
182+
183+
constructor(count, options = kEmptyObject) {
180184
validateUint32(count, 'count');
185+
validateObject(options, 'options');
181186
this.expected = count;
182187
this.actual = 0;
188+
189+
const { wait } = options;
190+
if (typeof wait === 'boolean') {
191+
this.wait = wait;
192+
this.#waitIndefinitely = wait;
193+
} else if (typeof wait === 'number') {
194+
validateNumber(wait, 'options.wait', 0, TIMEOUT_MAX);
195+
this.wait = wait;
196+
} else if (wait !== undefined) {
197+
throw new ERR_INVALID_ARG_TYPE('options.wait', ['boolean', 'number'], wait);
198+
}
199+
}
200+
201+
#planMet() {
202+
return this.actual === this.expected;
203+
}
204+
205+
#createTimeout(reject) {
206+
return setTimeout(() => {
207+
const err = new ERR_TEST_FAILURE(
208+
`plan timed out after ${this.wait}ms with ${this.actual} assertions when expecting ${this.expected}`,
209+
kTestTimeoutFailure,
210+
);
211+
reject(err);
212+
}, this.wait);
183213
}
184214

185215
check() {
186-
if (this.actual !== this.expected) {
216+
if (this.#planMet()) {
217+
if (this.#timeoutId) {
218+
clearTimeout(this.#timeoutId);
219+
this.#timeoutId = null;
220+
}
221+
if (this.#planPromise) {
222+
const { resolve } = this.#planPromise;
223+
resolve();
224+
this.#planPromise = null;
225+
}
226+
return;
227+
}
228+
229+
if (!this.#shouldWait()) {
187230
throw new ERR_TEST_FAILURE(
188231
`plan expected ${this.expected} assertions but received ${this.actual}`,
189232
kTestCodeFailure,
190233
);
191234
}
235+
236+
if (!this.#planPromise) {
237+
const { promise, resolve, reject } = PromiseWithResolvers();
238+
this.#planPromise = { __proto__: null, promise, resolve, reject };
239+
240+
if (!this.#waitIndefinitely) {
241+
this.#timeoutId = this.#createTimeout(reject);
242+
}
243+
}
244+
245+
return this.#planPromise.promise;
246+
}
247+
248+
count() {
249+
this.actual++;
250+
if (this.#planPromise) {
251+
this.check();
252+
}
253+
}
254+
255+
#shouldWait() {
256+
return this.wait !== undefined && this.wait !== false;
192257
}
193258
}
194259

260+
195261
class TestContext {
196262
#assert;
197263
#test;
@@ -228,15 +294,15 @@ class TestContext {
228294
this.#test.diagnostic(message);
229295
}
230296

231-
plan(count) {
297+
plan(count, options = kEmptyObject) {
232298
if (this.#test.plan !== null) {
233299
throw new ERR_TEST_FAILURE(
234300
'cannot set plan more than once',
235301
kTestCodeFailure,
236302
);
237303
}
238304

239-
this.#test.plan = new TestPlan(count);
305+
this.#test.plan = new TestPlan(count, options);
240306
}
241307

242308
get assert() {
@@ -249,7 +315,7 @@ class TestContext {
249315
map.forEach((method, name) => {
250316
assert[name] = (...args) => {
251317
if (plan !== null) {
252-
plan.actual++;
318+
plan.count();
253319
}
254320
return ReflectApply(method, this, args);
255321
};
@@ -260,7 +326,7 @@ class TestContext {
260326
// stacktrace from the correct starting point.
261327
function ok(...args) {
262328
if (plan !== null) {
263-
plan.actual++;
329+
plan.count();
264330
}
265331
innerOk(ok, args.length, ...args);
266332
}
@@ -296,7 +362,7 @@ class TestContext {
296362

297363
const { plan } = this.#test;
298364
if (plan !== null) {
299-
plan.actual++;
365+
plan.count();
300366
}
301367

302368
const subtest = this.#test.createSubtest(
@@ -958,30 +1024,46 @@ class Test extends AsyncResource {
9581024
const runArgs = ArrayPrototypeSlice(args);
9591025
ArrayPrototypeUnshift(runArgs, this.fn, ctx);
9601026

1027+
const promises = [];
9611028
if (this.fn.length === runArgs.length - 1) {
962-
// This test is using legacy Node.js error first callbacks.
1029+
// This test is using legacy Node.js error-first callbacks.
9631030
const { promise, cb } = createDeferredCallback();
964-
9651031
ArrayPrototypePush(runArgs, cb);
1032+
9661033
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
9671034

9681035
if (isPromise(ret)) {
9691036
this.fail(new ERR_TEST_FAILURE(
9701037
'passed a callback but also returned a Promise',
9711038
kCallbackAndPromisePresent,
9721039
));
973-
await SafePromiseRace([ret, stopPromise]);
1040+
ArrayPrototypePush(promises, ret);
9741041
} else {
975-
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
1042+
ArrayPrototypePush(promises, PromiseResolve(promise));
9761043
}
9771044
} else {
9781045
// This test is synchronous or using Promises.
9791046
const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
980-
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
1047+
ArrayPrototypePush(promises, PromiseResolve(promise));
9811048
}
9821049

1050+
ArrayPrototypePush(promises, stopPromise);
1051+
1052+
// Wait for the race to finish
1053+
await SafePromiseRace(promises);
1054+
9831055
this[kShouldAbort]();
9841056
this.plan?.check();
1057+
1058+
if (this.plan !== null) {
1059+
const planPromise = this.plan?.check();
1060+
// If the plan returns a promise, it means that it is waiting for more assertions to be made before
1061+
// continuing.
1062+
if (planPromise) {
1063+
await SafePromiseRace([planPromise, stopPromise]);
1064+
}
1065+
}
1066+
9851067
this.pass();
9861068
await afterEach();
9871069
await after();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
const { describe, it } = require('node:test');
3+
4+
describe('planning with wait', () => {
5+
it('planning with wait and passing', async (t) => {
6+
t.plan(1, { wait: 5000 });
7+
8+
const asyncActivity = () => {
9+
setTimeout(() => {
10+
t.assert.ok(true);
11+
}, 250);
12+
};
13+
14+
asyncActivity();
15+
});
16+
17+
it('planning with wait and failing', async (t) => {
18+
t.plan(1, { wait: 5000 });
19+
20+
const asyncActivity = () => {
21+
setTimeout(() => {
22+
t.assert.ok(false);
23+
}, 250);
24+
};
25+
26+
asyncActivity();
27+
});
28+
29+
it('planning wait time expires before plan is met', async (t) => {
30+
t.plan(2, { wait: 500 });
31+
32+
const asyncActivity = () => {
33+
setTimeout(() => {
34+
t.assert.ok(true);
35+
}, 50_000_000);
36+
};
37+
38+
asyncActivity();
39+
});
40+
41+
it(`planning with wait "options.wait : true" and passing`, async (t) => {
42+
t.plan(1, { wait: true });
43+
44+
const asyncActivity = () => {
45+
setTimeout(() => {
46+
t.assert.ok(true);
47+
}, 250);
48+
};
49+
50+
asyncActivity();
51+
});
52+
53+
it(`planning with wait "options.wait : true" and failing`, async (t) => {
54+
t.plan(1, { wait: true });
55+
56+
const asyncActivity = () => {
57+
setTimeout(() => {
58+
t.assert.ok(false);
59+
}, 250);
60+
};
61+
62+
asyncActivity();
63+
});
64+
65+
it(`planning with wait "options.wait : false" should not wait`, async (t) => {
66+
t.plan(1, { wait: false });
67+
68+
const asyncActivity = () => {
69+
setTimeout(() => {
70+
t.assert.ok(true);
71+
}, 500_000);
72+
};
73+
74+
asyncActivity();
75+
})
76+
});

0 commit comments

Comments
 (0)