test_runner: support test plans · nodejs/node@2f59529 · GitHub
Skip to content

Commit 2f59529

Browse files
cjihrigmarco-ippolito
authored andcommitted
test_runner: support test plans
Co-Authored-By: Marco Ippolito <marcoippolito54@gmail.com> PR-URL: #52860 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent f74beb5 commit 2f59529

6 files changed

Lines changed: 321 additions & 4 deletions

File tree

doc/api/test.md

Lines changed: 57 additions & 1 deletion

lib/internal/test_runner/runner.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ function run(options = kEmptyObject) {
462462
watch,
463463
setup,
464464
only,
465+
plan,
465466
} = options;
466467

467468
if (files != null) {
@@ -534,7 +535,7 @@ function run(options = kEmptyObject) {
534535
});
535536
}
536537

537-
const root = createTestTree({ __proto__: null, concurrency, timeout, signal });
538+
const root = createTestTree({ __proto__: null, concurrency, timeout, signal, plan });
538539
root.harness.shouldColorizeTestFiles ||= shouldColorizeTestFiles(root);
539540

540541
if (process.env.NODE_TEST_CONTEXT !== undefined) {

lib/internal/test_runner/test.js

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
MathMax,
1313
Number,
1414
ObjectDefineProperty,
15+
ObjectEntries,
1516
ObjectSeal,
1617
PromisePrototypeThen,
1718
PromiseResolve,
@@ -88,6 +89,7 @@ const {
8889
testOnlyFlag,
8990
} = parseCommandLine();
9091
let kResistStopPropagation;
92+
let assertObj;
9193
let findSourceMap;
9294
let noopTestStream;
9395

@@ -101,6 +103,19 @@ function lazyFindSourceMap(file) {
101103
return findSourceMap(file);
102104
}
103105

106+
function lazyAssertObject() {
107+
if (assertObj === undefined) {
108+
assertObj = new SafeMap();
109+
const assert = require('assert');
110+
for (const { 0: key, 1: value } of ObjectEntries(assert)) {
111+
if (typeof value === 'function') {
112+
assertObj.set(value, key);
113+
}
114+
}
115+
}
116+
return assertObj;
117+
}
118+
104119
function stopTest(timeout, signal) {
105120
const deferred = createDeferredPromise();
106121
const abortListener = addAbortListener(signal, deferred.resolve);
@@ -153,7 +168,25 @@ function testMatchesPattern(test, patterns) {
153168
);
154169
}
155170

171+
class TestPlan {
172+
constructor(count) {
173+
validateUint32(count, 'count', 0);
174+
this.expected = count;
175+
this.actual = 0;
176+
}
177+
178+
check() {
179+
if (this.actual !== this.expected) {
180+
throw new ERR_TEST_FAILURE(
181+
`plan expected ${this.expected} assertions but received ${this.actual}`,
182+
kTestCodeFailure,
183+
);
184+
}
185+
}
186+
}
187+
156188
class TestContext {
189+
#assert;
157190
#test;
158191

159192
constructor(test) {
@@ -180,6 +213,36 @@ class TestContext {
180213
this.#test.diagnostic(message);
181214
}
182215

216+
plan(count) {
217+
if (this.#test.plan !== null) {
218+
throw new ERR_TEST_FAILURE(
219+
'cannot set plan more than once',
220+
kTestCodeFailure,
221+
);
222+
}
223+
224+
this.#test.plan = new TestPlan(count);
225+
}
226+
227+
get assert() {
228+
if (this.#assert === undefined) {
229+
const { plan } = this.#test;
230+
const assertions = lazyAssertObject();
231+
const assert = { __proto__: null };
232+
233+
this.#assert = assert;
234+
for (const { 0: method, 1: name } of assertions.entries()) {
235+
assert[name] = (...args) => {
236+
if (plan !== null) {
237+
plan.actual++;
238+
}
239+
return ReflectApply(method, assert, args);
240+
};
241+
}
242+
}
243+
return this.#assert;
244+
}
245+
183246
get mock() {
184247
this.#test.mock ??= new MockTracker();
185248
return this.#test.mock;
@@ -203,6 +266,11 @@ class TestContext {
203266
loc: getCallerLocation(),
204267
};
205268

269+
const { plan } = this.#test;
270+
if (plan !== null) {
271+
plan.actual++;
272+
}
273+
206274
const subtest = this.#test.createSubtest(
207275
// eslint-disable-next-line no-use-before-define
208276
Test, name, options, fn, overrides,
@@ -257,7 +325,7 @@ class Test extends AsyncResource {
257325
super('Test');
258326

259327
let { fn, name, parent } = options;
260-
const { concurrency, loc, only, timeout, todo, skip, signal } = options;
328+
const { concurrency, loc, only, timeout, todo, skip, signal, plan } = options;
261329

262330
if (typeof fn !== 'function') {
263331
fn = noop;
@@ -373,6 +441,8 @@ class Test extends AsyncResource {
373441
this.fn = fn;
374442
this.harness = null; // Configured on the root test by the test harness.
375443
this.mock = null;
444+
this.plan = null;
445+
this.expectedAssertions = plan;
376446
this.cancelled = false;
377447
this.skipped = skip !== undefined && skip !== false;
378448
this.isTodo = todo !== undefined && todo !== false;
@@ -703,6 +773,11 @@ class Test extends AsyncResource {
703773

704774
const hookArgs = this.getRunArgs();
705775
const { args, ctx } = hookArgs;
776+
777+
if (this.plan === null && this.expectedAssertions) {
778+
ctx.plan(this.expectedAssertions);
779+
}
780+
706781
const after = async () => {
707782
if (this.hooks.after.length > 0) {
708783
await this.runHook('after', hookArgs);
@@ -754,7 +829,7 @@ class Test extends AsyncResource {
754829
this.postRun();
755830
return;
756831
}
757-
832+
this.plan?.check();
758833
this.pass();
759834
await afterEach();
760835
await after();
Lines changed: 79 additions & 0 deletions

0 commit comments

Comments
 (0)