Skip to content

Commit 8c2df73

Browse files
authored
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 ee8939c commit 8c2df73

File tree

7 files changed

+376
-31
lines changed

7 files changed

+376
-31
lines changed

doc/api/test.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3381,13 +3381,17 @@ added:
33813381

33823382
The name of the test.
33833383

3384-
### `context.plan(count)`
3384+
### `context.plan(count[,options])`
33853385

33863386
<!-- YAML
33873387
added:
33883388
- v22.2.0
33893389
- v20.15.0
33903390
changes:
3391+
- version:
3392+
- REPLACEME
3393+
pr-url: https://github.com/nodejs/node/pull/56765
3394+
description: Add the `options` parameter.
33913395
- version:
33923396
- v23.4.0
33933397
- v22.13.0
@@ -3396,6 +3400,16 @@ changes:
33963400
-->
33973401

33983402
* `count` {number} The number of assertions and subtests that are expected to run.
3403+
* `options` {Object} Additional options for the plan.
3404+
* `wait` {boolean|number} The wait time for the plan:
3405+
* If `true`, the plan waits indefinitely for all assertions and subtests to run.
3406+
* If `false`, the plan performs an immediate check after the test function completes,
3407+
without waiting for any pending assertions or subtests.
3408+
Any assertions or subtests that complete after this check will not be counted towards the plan.
3409+
* If a number, it specifies the maximum wait time in milliseconds
3410+
before timing out while waiting for expected assertions and subtests to be matched.
3411+
If the timeout is reached, the test will fail.
3412+
**Default:** `false`.
33993413

34003414
This function is used to set the number of assertions and subtests that are expected to run
34013415
within the test. If the number of assertions and subtests that run does not match the
@@ -3434,6 +3448,26 @@ test('planning with streams', (t, done) => {
34343448
});
34353449
```
34363450

3451+
When using the `wait` option, you can control how long the test will wait for the expected assertions.
3452+
For example, setting a maximum wait time ensures that the test will wait for asynchronous assertions
3453+
to complete within the specified timeframe:
3454+
3455+
```js
3456+
test('plan with wait: 2000 waits for async assertions', (t) => {
3457+
t.plan(1, { wait: 2000 }); // Waits for up to 2 seconds for the assertion to complete.
3458+
3459+
const asyncActivity = () => {
3460+
setTimeout(() => {
3461+
t.assert.ok(true, 'Async assertion completed within the wait time');
3462+
}, 1000); // Completes after 1 second, within the 2-second wait time.
3463+
};
3464+
3465+
asyncActivity(); // The test will pass because the assertion is completed in time.
3466+
});
3467+
```
3468+
3469+
Note: If a `wait` timeout is specified, it begins counting down only after the test function finishes executing.
3470+
34373471
### `context.runOnly(shouldRunOnlyTests)`
34383472

34393473
<!-- YAML

lib/internal/test_runner/test.js

Lines changed: 93 additions & 13 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(
@@ -968,35 +1034,49 @@ class Test extends AsyncResource {
9681034
const runArgs = ArrayPrototypeSlice(args);
9691035
ArrayPrototypeUnshift(runArgs, this.fn, ctx);
9701036

1037+
const promises = [];
9711038
if (this.fn.length === runArgs.length - 1) {
972-
// This test is using legacy Node.js error first callbacks.
1039+
// This test is using legacy Node.js error-first callbacks.
9731040
const { promise, cb } = createDeferredCallback();
974-
9751041
ArrayPrototypePush(runArgs, cb);
1042+
9761043
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
9771044

9781045
if (isPromise(ret)) {
9791046
this.fail(new ERR_TEST_FAILURE(
9801047
'passed a callback but also returned a Promise',
9811048
kCallbackAndPromisePresent,
9821049
));
983-
await SafePromiseRace([ret, stopPromise]);
1050+
ArrayPrototypePush(promises, ret);
9841051
} else {
985-
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
1052+
ArrayPrototypePush(promises, PromiseResolve(promise));
9861053
}
9871054
} else {
9881055
// This test is synchronous or using Promises.
9891056
const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
990-
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
1057+
ArrayPrototypePush(promises, PromiseResolve(promise));
9911058
}
9921059

1060+
ArrayPrototypePush(promises, stopPromise);
1061+
1062+
// Wait for the race to finish
1063+
await SafePromiseRace(promises);
1064+
9931065
this[kShouldAbort]();
9941066

9951067
if (this.subtestsPromise !== null) {
9961068
await SafePromiseRace([this.subtestsPromise.promise, stopPromise]);
9971069
}
9981070

999-
this.plan?.check();
1071+
if (this.plan !== null) {
1072+
const planPromise = this.plan?.check();
1073+
// If the plan returns a promise, it means that it is waiting for more assertions to be made before
1074+
// continuing.
1075+
if (planPromise) {
1076+
await SafePromiseRace([planPromise, stopPromise]);
1077+
}
1078+
}
1079+
10001080
this.pass();
10011081
await afterEach();
10021082
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)