From 12537abba881443690011f4b8f0e823fbdf17160 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 14 Jun 2022 10:54:50 +0300 Subject: [PATCH 01/24] test_runner: expose `describe` and `it` --- lib/internal/test_runner/harness.js | 41 ++++++++++---- lib/internal/test_runner/test.js | 85 +++++++++++++++++++---------- lib/test.js | 4 +- 3 files changed, 89 insertions(+), 41 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 66544d91522495..93e711d2825dd6 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -4,6 +4,7 @@ const { createHook, executionAsyncId, } = require('async_hooks'); +const console = require('console'); const { codes: { ERR_TEST_FAILURE, @@ -11,7 +12,10 @@ const { } = require('internal/errors'); const { Test } = require('internal/test_runner/test'); -function createProcessEventHandler(eventName, rootTest, testResources) { + +const testResources = new SafeMap(); + +function createProcessEventHandler(eventName, rootTest) { return (err) => { // Check if this error is coming from a test. If it is, fail the test. const test = testResources.get(executionAsyncId()); @@ -31,12 +35,13 @@ function createProcessEventHandler(eventName, rootTest, testResources) { test.fail(new ERR_TEST_FAILURE(err, eventName)); test.postRun(); + } else { + console.error(err); } }; } function setup(root) { - const testResources = new SafeMap(); const hook = createHook({ init(asyncId, type, triggerAsyncId, resource) { if (resource instanceof Test) { @@ -58,9 +63,9 @@ function setup(root) { hook.enable(); const exceptionHandler = - createProcessEventHandler('uncaughtException', root, testResources); + createProcessEventHandler('uncaughtException', root); const rejectionHandler = - createProcessEventHandler('unhandledRejection', root, testResources); + createProcessEventHandler('unhandledRejection', root); process.on('uncaughtException', exceptionHandler); process.on('unhandledRejection', rejectionHandler); @@ -113,19 +118,31 @@ function setup(root) { root.reporter.pipe(process.stdout); root.reporter.version(); + return root; } -function test(name, options, fn) { - // If this is the first test encountered, bootstrap the test harness. - if (this.subtests.length === 0) { - setup(this); - } +const root = setup(new Test({ name: '' })); +function test(name, options, fn) { const subtest = this.createSubtest(name, options, fn); - return subtest.start(); } -const root = new Test({ name: '' }); +function describe(name, options, fn) { + const parent = testResources.get(executionAsyncId()) || root; + const suite = parent.createSubSuite(name, options, fn); + if (parent === root) { + suite.run(); + } +} + +function it(name, options, fn) { + const parent = testResources.get(executionAsyncId()) || root; + parent.createSubtest(name, options, fn); +} -module.exports = FunctionPrototypeBind(test, root); +module.exports = { + test: FunctionPrototypeBind(test, root), + describe, + it, +}; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 2876b3d66640cd..42bc58f40e3d5b 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -36,6 +36,32 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only'); // TODO(cjihrig): Use uv_available_parallelism() once it lands. const rootConcurrency = isTestRunner ? cpus().length : 1; + +function parseTestProps(parent, name, options, fn, overrides) { + if (typeof name === 'function') { + fn = name; + } else if (name !== null && typeof name === 'object') { + fn = options; + options = name; + } else if (typeof options === 'function') { + fn = options; + } + + if (options === null || typeof options !== 'object') { + options = kEmptyObject; + } + + // If this test has already ended, attach this test to the root test so + // that the error can be properly reported. + if (parent.finished) { + while (parent.parent !== null) { + parent = parent.parent; + } + } + + return { fn, name, parent, ...options, ...overrides }; +} + class TestContext { #test; @@ -193,34 +219,11 @@ class Test extends AsyncResource { } } - createSubtest(name, options, fn) { - if (typeof name === 'function') { - fn = name; - } else if (name !== null && typeof name === 'object') { - fn = options; - options = name; - } else if (typeof options === 'function') { - fn = options; - } + createSubtest(...props) { + const test = new Test(parseTestProps(this, ...props)); - if (options === null || typeof options !== 'object') { - options = kEmptyObject; - } - - let parent = this; - - // If this test has already ended, attach this test to the root test so - // that the error can be properly reported. - if (this.finished) { - while (parent.parent !== null) { - parent = parent.parent; - } - } - - const test = new Test({ fn, name, parent, ...options }); - - if (parent.waitingOn === 0) { - parent.waitingOn = test.testNumber; + if (test.parent.waitingOn === 0) { + test.parent.waitingOn = test.testNumber; } if (this.finished) { @@ -232,10 +235,22 @@ class Test extends AsyncResource { ); } - ArrayPrototypePush(parent.subtests, test); + ArrayPrototypePush(test.parent.subtests, test); return test; } + createSubSuite(...props) { + // eslint-disable-next-line no-use-before-define + const suite = new Suite(parseTestProps(this, ...props)); + + if (suite.parent.waitingOn === 0) { + suite.parent.waitingOn = suite.testNumber; + } + + ArrayPrototypePush(suite.parent.subtests, suite); + return suite; + } + cancel() { if (this.endTime !== null) { return; @@ -454,5 +469,19 @@ class Test extends AsyncResource { } } } +class Suite extends Test { + constructor(options) { + super(options); + + this.runInAsyncScope(this.fn); + this.fn = () => {}; + } + async run() { + for (const subtest of this.subtests) { + await subtest.run(); + } + await super.run(); + } +} module.exports = { kDefaultIndent, kSubtestsFailed, kTestCodeFailure, Test }; diff --git a/lib/test.js b/lib/test.js index fa319fa17b37bd..7ebc852092b93b 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,8 +1,10 @@ 'use strict'; -const test = require('internal/test_runner/harness'); +const { test, describe, it } = require('internal/test_runner/harness'); const { emitExperimentalWarning } = require('internal/util'); emitExperimentalWarning('The test runner'); module.exports = test; module.exports.test = test; +module.exports.describe = describe; +module.exports.it = it; From 591cba34a8c466e736fadef0bb0ac76911b6f32f Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 14 Jun 2022 13:28:49 +0300 Subject: [PATCH 02/24] fix --- lib/internal/test_runner/test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 42bc58f40e3d5b..f090622df9362c 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -477,10 +477,13 @@ class Suite extends Test { this.fn = () => {}; } async run() { + this.parent.activeSubtests++; + this.startTime = hrtime(); for (const subtest of this.subtests) { await subtest.run(); } - await super.run(); + this.pass(); + this.postRun(); } } From f9ac5d15b86981afcf1558ceefc97729ad8c75bd Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 14 Jun 2022 14:02:00 +0300 Subject: [PATCH 03/24] fixes --- lib/internal/main/test_runner.js | 2 +- lib/internal/test_runner/harness.js | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index 71bf21782f39f3..0a47535fccfd51 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -20,7 +20,7 @@ const { ERR_TEST_FAILURE, }, } = require('internal/errors'); -const test = require('internal/test_runner/harness'); +const { test } = require('internal/test_runner/harness'); const { kSubtestsFailed } = require('internal/test_runner/test'); const { isSupportedFileType, diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 93e711d2825dd6..d034f91b4a712e 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -14,6 +14,8 @@ const { Test } = require('internal/test_runner/test'); const testResources = new SafeMap(); +const root = new Test({ name: '' }); +let wasRootSetup = false; function createProcessEventHandler(eventName, rootTest) { return (err) => { @@ -42,6 +44,9 @@ function createProcessEventHandler(eventName, rootTest) { } function setup(root) { + if (wasRootSetup) { + return root; + } const hook = createHook({ init(asyncId, type, triggerAsyncId, resource) { if (resource instanceof Test) { @@ -118,18 +123,18 @@ function setup(root) { root.reporter.pipe(process.stdout); root.reporter.version(); + + wasRootSetup = true; return root; } -const root = setup(new Test({ name: '' })); - function test(name, options, fn) { - const subtest = this.createSubtest(name, options, fn); + const subtest = setup(root).createSubtest(name, options, fn); return subtest.start(); } function describe(name, options, fn) { - const parent = testResources.get(executionAsyncId()) || root; + const parent = testResources.get(executionAsyncId()) || setup(root); const suite = parent.createSubSuite(name, options, fn); if (parent === root) { suite.run(); @@ -137,7 +142,7 @@ function describe(name, options, fn) { } function it(name, options, fn) { - const parent = testResources.get(executionAsyncId()) || root; + const parent = testResources.get(executionAsyncId()) || setup(root); parent.createSubtest(name, options, fn); } From 54d46fd336a78fb1914eeeff49bce3584b5d384d Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 14 Jun 2022 22:24:05 +0300 Subject: [PATCH 04/24] no console --- lib/internal/test_runner/harness.js | 35 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index d034f91b4a712e..10dac48192e43f 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -4,7 +4,6 @@ const { createHook, executionAsyncId, } = require('async_hooks'); -const console = require('console'); const { codes: { ERR_TEST_FAILURE, @@ -22,24 +21,24 @@ function createProcessEventHandler(eventName, rootTest) { // Check if this error is coming from a test. If it is, fail the test. const test = testResources.get(executionAsyncId()); - if (test !== undefined) { - if (test.finished) { - // If the test is already finished, report this as a top level - // diagnostic since this is a malformed test. - const msg = `Warning: Test "${test.name}" generated asynchronous ` + - 'activity after the test ended. This activity created the error ' + - `"${err}" and would have caused the test to fail, but instead ` + - `triggered an ${eventName} event.`; - - rootTest.diagnostic(msg); - return; - } - - test.fail(new ERR_TEST_FAILURE(err, eventName)); - test.postRun(); - } else { - console.error(err); + if (test === undefined) { + throw err; + } + + if (test.finished) { + // If the test is already finished, report this as a top level + // diagnostic since this is a malformed test. + const msg = `Warning: Test "${test.name}" generated asynchronous ` + + 'activity after the test ended. This activity created the error ' + + `"${err}" and would have caused the test to fail, but instead ` + + `triggered an ${eventName} event.`; + + rootTest.diagnostic(msg); + return; } + + test.fail(new ERR_TEST_FAILURE(err, eventName)); + test.postRun(); }; } From 7c4de4045f6293c8e057a290aef7af5b9e2d849e Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 14 Jun 2022 22:30:49 +0300 Subject: [PATCH 05/24] lint --- lib/internal/test_runner/harness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 10dac48192e43f..f3c4a84ca6e4ec 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -24,7 +24,7 @@ function createProcessEventHandler(eventName, rootTest) { if (test === undefined) { throw err; } - + if (test.finished) { // If the test is already finished, report this as a top level // diagnostic since this is a malformed test. From 531419c3be2f3b674045ceb544c8ec70a2543504 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Wed, 15 Jun 2022 16:20:34 +0300 Subject: [PATCH 06/24] skipn only --- lib/internal/test_runner/harness.js | 30 +++++++++++++++++++++-------- lib/internal/test_runner/test.js | 3 ++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index f3c4a84ca6e4ec..3b0eb4fe5a04fd 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -132,21 +132,35 @@ function test(name, options, fn) { return subtest.start(); } -function describe(name, options, fn) { - const parent = testResources.get(executionAsyncId()) || setup(root); - const suite = parent.createSubSuite(name, options, fn); +function getParent() { + return testResources.get(executionAsyncId()) || setup(root); +} + +function describe(name, options, fn, overrides) { + const parent = getParent(); + const suite = parent.createSubSuite(name, options, fn, overrides); if (parent === root) { suite.run(); } + return suite; +} + +function it(name, options, fn, overrides) { + return getParent().createSubtest(name, options, fn, overrides); } -function it(name, options, fn) { - const parent = testResources.get(executionAsyncId()) || setup(root); - parent.createSubtest(name, options, fn); +function enrichMethod(method) { + ['skip', 'only'].forEach((keyword) => { + method[keyword] = function(name, options, fn) { + return method(name, options, fn, { [keyword]: true }); + } + }); + + return method; } module.exports = { test: FunctionPrototypeBind(test, root), - describe, - it, + describe: enrichMethod(describe), + it: enrichMethod(it), }; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index f090622df9362c..ec9f9492eb33a5 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -479,7 +479,8 @@ class Suite extends Test { async run() { this.parent.activeSubtests++; this.startTime = hrtime(); - for (const subtest of this.subtests) { + const subtests = this.skipped ? [] : this.subtests; + for (const subtest of subtests) { await subtest.run(); } this.pass(); From 86a07cba9707859c344af2dfa1431e4f06716b12 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Wed, 15 Jun 2022 20:57:53 +0300 Subject: [PATCH 07/24] lint --- lib/internal/test_runner/harness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 3b0eb4fe5a04fd..6425fa142f5d7c 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -153,7 +153,7 @@ function enrichMethod(method) { ['skip', 'only'].forEach((keyword) => { method[keyword] = function(name, options, fn) { return method(name, options, fn, { [keyword]: true }); - } + }; }); return method; From aab75a496fed0c48bf4bc6b7bbf33290d8487069 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Wed, 15 Jun 2022 21:04:15 +0300 Subject: [PATCH 08/24] fix --- lib/internal/test_runner/test.js | 57 ++++++++++++++++---------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index ec9f9492eb33a5..e5f65f928608a8 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -36,32 +36,6 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only'); // TODO(cjihrig): Use uv_available_parallelism() once it lands. const rootConcurrency = isTestRunner ? cpus().length : 1; - -function parseTestProps(parent, name, options, fn, overrides) { - if (typeof name === 'function') { - fn = name; - } else if (name !== null && typeof name === 'object') { - fn = options; - options = name; - } else if (typeof options === 'function') { - fn = options; - } - - if (options === null || typeof options !== 'object') { - options = kEmptyObject; - } - - // If this test has already ended, attach this test to the root test so - // that the error can be properly reported. - if (parent.finished) { - while (parent.parent !== null) { - parent = parent.parent; - } - } - - return { fn, name, parent, ...options, ...overrides }; -} - class TestContext { #test; @@ -219,8 +193,35 @@ class Test extends AsyncResource { } } + #parseProps(name, options, fn, overrides) { + if (typeof name === 'function') { + fn = name; + } else if (name !== null && typeof name === 'object') { + fn = options; + options = name; + } else if (typeof options === 'function') { + fn = options; + } + + if (options === null || typeof options !== 'object') { + options = kEmptyObject; + } + + let parent = this; + + // If this test has already ended, attach this test to the root test so + // that the error can be properly reported. + if (this.finished) { + while (parent.parent !== null) { + parent = parent.parent; + } + } + + return { fn, name, parent, ...options, ...overrides }; + } + createSubtest(...props) { - const test = new Test(parseTestProps(this, ...props)); + const test = new Test(this.#parseProps(...props)); if (test.parent.waitingOn === 0) { test.parent.waitingOn = test.testNumber; @@ -241,7 +242,7 @@ class Test extends AsyncResource { createSubSuite(...props) { // eslint-disable-next-line no-use-before-define - const suite = new Suite(parseTestProps(this, ...props)); + const suite = new Suite(this.#parseProps(this, ...props)); if (suite.parent.waitingOn === 0) { suite.parent.waitingOn = suite.testNumber; From b3c6d9f862ba0fc629ce5120021962d3faabf3a6 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Thu, 16 Jun 2022 12:17:48 +0300 Subject: [PATCH 09/24] CR --- lib/internal/test_runner/harness.js | 39 +++++++++++------------------ lib/internal/test_runner/test.js | 12 ++++++--- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 6425fa142f5d7c..35b15d5eeaff9b 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -1,5 +1,5 @@ 'use strict'; -const { FunctionPrototypeBind, SafeMap } = primordials; +const { FunctionPrototypeBind, ArrayPrototypeForEach, ReflectApply, SafeMap } = primordials; const { createHook, executionAsyncId, @@ -132,35 +132,26 @@ function test(name, options, fn) { return subtest.start(); } -function getParent() { - return testResources.get(executionAsyncId()) || setup(root); -} - -function describe(name, options, fn, overrides) { - const parent = getParent(); - const suite = parent.createSubSuite(name, options, fn, overrides); - if (parent === root) { - suite.run(); +function runInParentContext(factory) { + function cb(name, options, fn, overrides) { + const parent = testResources.get(executionAsyncId()) || setup(root); + const subtest = ReflectApply(parent[factory], parent, [name, options, fn, overrides]); + if (parent === root) { + subtest.run(); + } + return subtest; } - return suite; -} -function it(name, options, fn, overrides) { - return getParent().createSubtest(name, options, fn, overrides); -} - -function enrichMethod(method) { - ['skip', 'only'].forEach((keyword) => { - method[keyword] = function(name, options, fn) { - return method(name, options, fn, { [keyword]: true }); + ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => { + cb[keyword] = (name, options, fn) => { + return cb(name, options, fn, { [keyword]: true }); }; }); - - return method; + return (name, options, fn) => cb(name, options, fn); } module.exports = { test: FunctionPrototypeBind(test, root), - describe: enrichMethod(describe), - it: enrichMethod(it), + describe: runInParentContext('createSubSuite'), + it: runInParentContext('createSubtest'), }; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index e5f65f928608a8..954f9998a70794 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -2,9 +2,12 @@ const { ArrayPrototypePush, ArrayPrototypeShift, + ArrayPrototypeReduce, FunctionPrototype, Number, SafeMap, + PromiseResolve, + ReflectApply, } = primordials; const { AsyncResource } = require('async_hooks'); const { @@ -221,7 +224,7 @@ class Test extends AsyncResource { } createSubtest(...props) { - const test = new Test(this.#parseProps(...props)); + const test = new Test(ReflectApply(this.#parseProps, this, props)); if (test.parent.waitingOn === 0) { test.parent.waitingOn = test.testNumber; @@ -242,7 +245,7 @@ class Test extends AsyncResource { createSubSuite(...props) { // eslint-disable-next-line no-use-before-define - const suite = new Suite(this.#parseProps(this, ...props)); + const suite = new Suite(ReflectApply(this.#parseProps, this, props)); if (suite.parent.waitingOn === 0) { suite.parent.waitingOn = suite.testNumber; @@ -481,9 +484,10 @@ class Suite extends Test { this.parent.activeSubtests++; this.startTime = hrtime(); const subtests = this.skipped ? [] : this.subtests; - for (const subtest of subtests) { + await ArrayPrototypeReduce(subtests, async (prev, subtest) => { + await prev; await subtest.run(); - } + }, PromiseResolve()); this.pass(); this.postRun(); } From 48ec31a49b04244ff9a5209d853edee73cf0e987 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Thu, 16 Jun 2022 12:22:52 +0300 Subject: [PATCH 10/24] fix --- lib/internal/test_runner/harness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 35b15d5eeaff9b..6e25cc5c749745 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -147,7 +147,7 @@ function runInParentContext(factory) { return cb(name, options, fn, { [keyword]: true }); }; }); - return (name, options, fn) => cb(name, options, fn); + return cb; } module.exports = { From 32a7259a252ce78e7a928926dded3f93aa814fd8 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Fri, 17 Jun 2022 10:10:00 +0300 Subject: [PATCH 11/24] refactor --- lib/internal/test_runner/harness.js | 26 +- lib/internal/test_runner/test.js | 78 ++-- lib/internal/test_runner/utils.js | 39 +- test/message/test_runner_desctibe_it.js | 303 +++++++++++++++ test/message/test_runner_desctibe_it.out | 450 +++++++++++++++++++++++ 5 files changed, 835 insertions(+), 61 deletions(-) create mode 100644 test/message/test_runner_desctibe_it.js create mode 100644 test/message/test_runner_desctibe_it.out diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 6e25cc5c749745..c1dd6e481ecc2b 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -1,5 +1,5 @@ 'use strict'; -const { FunctionPrototypeBind, ArrayPrototypeForEach, ReflectApply, SafeMap } = primordials; +const { FunctionPrototypeBind, ArrayPrototypeForEach, SafeMap } = primordials; const { createHook, executionAsyncId, @@ -9,7 +9,7 @@ const { ERR_TEST_FAILURE, }, } = require('internal/errors'); -const { Test } = require('internal/test_runner/test'); +const { Test, ItTest, Suite } = require('internal/test_runner/test'); const testResources = new SafeMap(); @@ -128,23 +128,27 @@ function setup(root) { } function test(name, options, fn) { - const subtest = setup(root).createSubtest(name, options, fn); + const subtest = setup(root).createSubtest(Test, name, options, fn); return subtest.start(); } -function runInParentContext(factory) { - function cb(name, options, fn, overrides) { +function runInParentContext(Factory) { + function run(name, options, fn, overrides) { const parent = testResources.get(executionAsyncId()) || setup(root); - const subtest = ReflectApply(parent[factory], parent, [name, options, fn, overrides]); + const subtest = parent.createSubtest(Factory, name, options, fn, overrides); if (parent === root) { - subtest.run(); + subtest.start(); } return subtest; } - ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => { + const cb = (name, options, fn) => { + return run(name, options, fn); + }; + + ArrayPrototypeForEach(['skip', 'todo'], (keyword) => { cb[keyword] = (name, options, fn) => { - return cb(name, options, fn, { [keyword]: true }); + return run(name, options, fn, { [keyword]: true }); }; }); return cb; @@ -152,6 +156,6 @@ function runInParentContext(factory) { module.exports = { test: FunctionPrototypeBind(test, root), - describe: runInParentContext('createSubSuite'), - it: runInParentContext('createSubtest'), + describe: runInParentContext(Suite), + it: runInParentContext(ItTest), }; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 954f9998a70794..36ba14e7014096 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -7,7 +7,6 @@ const { Number, SafeMap, PromiseResolve, - ReflectApply, } = primordials; const { AsyncResource } = require('async_hooks'); const { @@ -18,6 +17,7 @@ const { } = require('internal/errors'); const { getOptionValue } = require('internal/options'); const { TapStream } = require('internal/test_runner/tap_stream'); +const { createDeferredCallback } = require('internal/test_runner/utils'); const { createDeferredPromise, kEmptyObject, @@ -28,7 +28,6 @@ const { cpus } = require('os'); const { bigint: hrtime } = process.hrtime; const kCallbackAndPromisePresent = 'callbackAndPromisePresent'; const kCancelledByParent = 'cancelledByParent'; -const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; const kParentAlreadyFinished = 'parentAlreadyFinished'; const kSubtestsFailed = 'subtestsFailed'; const kTestCodeFailure = 'testCodeFailure'; @@ -63,7 +62,8 @@ class TestContext { } test(name, options, fn) { - const subtest = this.#test.createSubtest(name, options, fn); + // eslint-disable-next-line no-use-before-define + const subtest = this.#test.createSubtest(Test, name, options, fn); return subtest.start(); } @@ -196,7 +196,7 @@ class Test extends AsyncResource { } } - #parseProps(name, options, fn, overrides) { + createSubtest(Factory, name, options, fn, overrides) { if (typeof name === 'function') { fn = name; } else if (name !== null && typeof name === 'object') { @@ -220,17 +220,14 @@ class Test extends AsyncResource { } } - return { fn, name, parent, ...options, ...overrides }; - } - - createSubtest(...props) { - const test = new Test(ReflectApply(this.#parseProps, this, props)); + const test = new Factory({ fn, name, parent, ...options, ...overrides }); if (test.parent.waitingOn === 0) { test.parent.waitingOn = test.testNumber; } if (this.finished) { + test.startTime = test.startTime || hrtime(); test.fail( new ERR_TEST_FAILURE( 'test could not be started because its parent finished', @@ -243,18 +240,6 @@ class Test extends AsyncResource { return test; } - createSubSuite(...props) { - // eslint-disable-next-line no-use-before-define - const suite = new Suite(ReflectApply(this.#parseProps, this, props)); - - if (suite.parent.waitingOn === 0) { - suite.parent.waitingOn = suite.testNumber; - } - - ArrayPrototypePush(suite.parent.subtests, suite); - return suite; - } - cancel() { if (this.endTime !== null) { return; @@ -317,39 +302,22 @@ class Test extends AsyncResource { return this.run(); } + getRunArgs() { + const ctx = new TestContext(this); + return { ctx, args: [ctx] }; + } + async run() { this.parent.activeSubtests++; this.startTime = hrtime(); try { - const ctx = new TestContext(this); + const { args, ctx } = this.getRunArgs(); - if (this.fn.length === 2) { + if (this.fn.length === args.length + 1) { // This test is using legacy Node.js error first callbacks. - const { promise, resolve, reject } = createDeferredPromise(); - let calledCount = 0; - const ret = this.runInAsyncScope(this.fn, ctx, ctx, (err) => { - calledCount++; - - // If the callback is called a second time, let the user know, but - // don't let them know more than once. - if (calledCount > 1) { - if (calledCount === 2) { - throw new ERR_TEST_FAILURE( - 'callback invoked multiple times', - kMultipleCallbackInvocations - ); - } - - return; - } - - if (err) { - return reject(err); - } - - resolve(); - }); + const { promise, cb } = createDeferredCallback(); + const ret = this.runInAsyncScope(this.fn, ctx, ...args, cb); if (isPromise(ret)) { this.fail(new ERR_TEST_FAILURE( @@ -362,7 +330,7 @@ class Test extends AsyncResource { } } else { // This test is synchronous or using Promises. - await this.runInAsyncScope(this.fn, ctx, ctx); + await this.runInAsyncScope(this.fn, ctx, ...args); } this.pass(); @@ -473,13 +441,25 @@ class Test extends AsyncResource { } } } + +class ItTest extends Test { + getRunArgs() { + return { ctx: {}, args: [] }; + } +} class Suite extends Test { constructor(options) { super(options); this.runInAsyncScope(this.fn); this.fn = () => {}; + this.finished = true; // Forbid adding subtests to this suite } + + start() { + return this.run(); + } + async run() { this.parent.activeSubtests++; this.startTime = hrtime(); @@ -493,4 +473,4 @@ class Suite extends Test { } } -module.exports = { kDefaultIndent, kSubtestsFailed, kTestCodeFailure, Test }; +module.exports = { kDefaultIndent, kSubtestsFailed, kTestCodeFailure, Test, Suite, ItTest }; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 09803d33aeb508..be39fa0add1fc7 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -1,6 +1,14 @@ 'use strict'; const { RegExpPrototypeExec } = primordials; const { basename } = require('path'); +const { createDeferredPromise } = require('internal/util'); +const { + codes: { + ERR_TEST_FAILURE, + }, +} = require('internal/errors'); + +const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; const kSupportedFileExtensions = /\.[cm]?js$/; const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/; @@ -12,4 +20,33 @@ function isSupportedFileType(p) { return RegExpPrototypeExec(kSupportedFileExtensions, p) !== null; } -module.exports = { isSupportedFileType, doesPathMatchFilter }; +function createDeferredCallback() { + let calledCount = 0; + const { promise, resolve, reject } = createDeferredPromise(); + const cb = (err) => { + calledCount++; + + // If the callback is called a second time, let the user know, but + // don't let them know more than once. + if (calledCount > 1) { + if (calledCount === 2) { + throw new ERR_TEST_FAILURE( + 'callback invoked multiple times', + kMultipleCallbackInvocations + ); + } + + return; + } + + if (err) { + return reject(err); + } + + resolve(); + }; + + return { promise, cb }; +} + +module.exports = { isSupportedFileType, doesPathMatchFilter, createDeferredCallback }; diff --git a/test/message/test_runner_desctibe_it.js b/test/message/test_runner_desctibe_it.js new file mode 100644 index 00000000000000..3f82762d91b4b6 --- /dev/null +++ b/test/message/test_runner_desctibe_it.js @@ -0,0 +1,303 @@ +// Flags: --no-warnings +'use strict'; +require('../common'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const util = require('util'); + + +it.todo('sync pass todo', () => { + +}); + +it('sync pass todo with message', { todo: 'this is a passing todo' }, () => { +}); + +it.todo('sync fail todo', () => { + throw new Error('thrown from sync fail todo'); +}); + +it('sync fail todo with message', { todo: 'this is a failing todo' }, () => { + throw new Error('thrown from sync fail todo with message'); +}); + +it.skip('sync skip pass', () => { +}); + +it('sync skip pass with message', { skip: 'this is skipped' }, () => { +}); + +it('sync pass', () => { +}); + +it('sync throw fail', () => { + throw new Error('thrown from sync throw fail'); +}); + +it.skip('async skip pass', async () => { +}); + +it('async pass', async () => { + +}); + +it('async throw fail', async () => { + throw new Error('thrown from async throw fail'); +}); + +it('async assertion fail', async () => { + // Make sure the assert module is handled. + assert.strictEqual(true, false); +}); + +it('resolve pass', () => { + return Promise.resolve(); +}); + +it('reject fail', () => { + return Promise.reject(new Error('rejected from reject fail')); +}); + +it('unhandled rejection - passes but warns', () => { + Promise.reject(new Error('rejected from unhandled rejection fail')); +}); + +it('async unhandled rejection - passes but warns', async () => { + Promise.reject(new Error('rejected from async unhandled rejection fail')); +}); + +it('immediate throw - passes but warns', () => { + setImmediate(() => { + throw new Error('thrown from immediate throw fail'); + }); +}); + +it('immediate reject - passes but warns', () => { + setImmediate(() => { + Promise.reject(new Error('rejected from immediate reject fail')); + }); +}); + +it('immediate resolve pass', () => { + return new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); +}); + +describe('subtest sync throw fail', () => { + it('+sync throw fail', () => { + throw new Error('thrown from subtest sync throw fail'); + }); +}); + +it('sync throw non-error fail', async () => { + throw Symbol('thrown symbol from sync throw non-error fail'); +}); + +describe('level 0a', { concurrency: 4 }, () => { + it('level 1a', async () => { + const p1a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1000); + }); + + return p1a; + }); + + it('level 1b', async () => { + const p1b = new Promise((resolve) => { + resolve(); + }); + + return p1b; + }); + + it('level 1c', async () => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 2000); + }); + + return p1c; + }); + + it('level 1d', async () => { + const p1c = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1500); + }); + + return p1c; + }); + + const p0a = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 3000); + }); + + return p0a; +}); + +describe('top level', { concurrency: 2 }, () => { + it('+long running', async () => { + return new Promise((resolve, reject) => { + setTimeout(resolve, 3000).unref(); + }); + }); + + describe('+short running', async () => { + it('++short running', async () => {}); + }); +}); + +describe('invalid subtest - pass but subtest fails', () => { + setImmediate(() => { + it('invalid subtest fail', () => { + throw new Error('this should not be thrown'); + }); + }); +}); + +it.skip('sync skip option', () => { + throw new Error('this should not be executed'); +}); + +it('sync skip option with message', { skip: 'this is skipped' }, () => { + throw new Error('this should not be executed'); +}); + +it('sync skip option is false fail', { skip: false }, () => { + throw new Error('this should be executed'); +}); + +// A test with no arguments provided. +it(); + +// A test with only a named function provided. +it(function functionOnly() {}); + +// A test with only an anonymous function provided. +it(() => {}); + +// A test with only a name provided. +it('test with only a name provided'); + +// A test with an empty string name. +it(''); + +// A test with only options provided. +it({ skip: true }); + +// A test with only a name and options provided. +it('test with a name and options provided', { skip: true }); + +// A test with only options and a function provided. +it({ skip: true }, function functionAndOptions() {}); + +// A test whose description needs to be escaped. +it('escaped description \\ # \\#\\'); + +// A test whose skip message needs to be escaped. +it('escaped skip message', { skip: '#skip' }); + +// A test whose todo message needs to be escaped. +it('escaped todo message', { todo: '#todo' }); + +it('callback pass', (done) => { + setImmediate(done); +}); + +it('callback fail', (done) => { + setImmediate(() => { + done(new Error('callback failure')); + }); +}); + +it('sync t is this in test', function() { + assert.deepStrictEqual(this, {}); +}); + +it('async t is this in test', async function() { + assert.deepStrictEqual(this, {}); +}); + +it('callback t is this in test', function(done) { + assert.deepStrictEqual(this, {}); + done(); +}); + +it('callback also returns a Promise', async (done) => { + throw new Error('thrown from callback also returns a Promise'); +}); + +it('callback throw', (done) => { + throw new Error('thrown from callback throw'); +}); + +it('callback called twice', (done) => { + done(); + done(); +}); + +it('callback called twice in different ticks', (done) => { + setImmediate(done); + done(); +}); + +it('callback called twice in future tick', (done) => { + setImmediate(() => { + done(); + done(); + }); +}); + +it('callback async throw', (done) => { + setImmediate(() => { + throw new Error('thrown from callback async throw'); + }); +}); + +it('callback async throw after done', (done) => { + setImmediate(() => { + throw new Error('thrown from callback async throw after done'); + }); + + done(); +}); + +it('custom inspect symbol fail', () => { + const obj = { + [util.inspect.custom]() { + return 'customized'; + }, + foo: 1 + }; + + throw obj; +}); + +it('custom inspect symbol that throws fail', () => { + const obj = { + [util.inspect.custom]() { + throw new Error('bad-inspect'); + }, + foo: 1 + }; + + throw obj; +}); + +describe('subtest sync throw fails', () => { + it('sync throw fails at first', () => { + throw new Error('thrown from subtest sync throw fails at first'); + }); + it('sync throw fails at second', () => { + throw new Error('thrown from subtest sync throw fails at second'); + }); +}); diff --git a/test/message/test_runner_desctibe_it.out b/test/message/test_runner_desctibe_it.out new file mode 100644 index 00000000000000..c3d2a30c247e73 --- /dev/null +++ b/test/message/test_runner_desctibe_it.out @@ -0,0 +1,450 @@ +TAP version 13 +ok 1 - sync pass todo # TODO + --- + duration_ms: * + ... +ok 2 - sync pass todo with message # TODO this is a passing todo + --- + duration_ms: * + ... +not ok 3 - sync fail todo # TODO + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from sync fail todo' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +not ok 4 - sync fail todo with message # TODO this is a failing todo + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from sync fail todo with message' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + * + * + * + ... +ok 5 - sync skip pass # SKIP + --- + duration_ms: * + ... +ok 6 - sync skip pass with message # SKIP this is skipped + --- + duration_ms: * + ... +ok 7 - sync pass + --- + duration_ms: * + ... +not ok 8 - sync throw fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from sync throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +ok 9 - async skip pass # SKIP + --- + duration_ms: * + ... +ok 10 - async pass + --- + duration_ms: * + ... +not ok 11 - async throw fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from async throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +not ok 12 - async assertion fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: |- + Expected values to be strictly equal: + + true !== false + + code: 'ERR_ASSERTION' + stack: |- + * + * + * + * + * + * + * + ... +ok 13 - resolve pass + --- + duration_ms: * + ... +not ok 14 - reject fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'rejected from reject fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +ok 15 - unhandled rejection - passes but warns + --- + duration_ms: * + ... +ok 16 - async unhandled rejection - passes but warns + --- + duration_ms: * + ... +ok 17 - immediate throw - passes but warns + --- + duration_ms: * + ... +ok 18 - immediate reject - passes but warns + --- + duration_ms: * + ... +ok 19 - immediate resolve pass + --- + duration_ms: * + ... + not ok 1 - +sync throw fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from subtest sync throw fail' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..1 +not ok 20 - subtest sync throw fail + --- + duration_ms: * + failureType: 'subtestsFailed' + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + ... +not ok 21 - sync throw non-error fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'Symbol(thrown symbol from sync throw non-error fail)' + code: 'ERR_TEST_FAILURE' + ... + ok 1 - level 1a + --- + duration_ms: * + ... + ok 2 - level 1b + --- + duration_ms: * + ... + ok 3 - level 1c + --- + duration_ms: * + ... + ok 4 - level 1d + --- + duration_ms: * + ... + 1..4 +ok 22 - level 0a + --- + duration_ms: * + ... + ok 1 - +long running + --- + duration_ms: * + ... + ok 1 - ++short running + --- + duration_ms: * + ... + 1..1 + ok 2 - +short running + --- + duration_ms: * + ... + 1..2 +ok 23 - top level + --- + duration_ms: * + ... +ok 24 - invalid subtest - pass but subtest fails + --- + duration_ms: * + ... +ok 25 - sync skip option # SKIP + --- + duration_ms: * + ... +ok 26 - sync skip option with message # SKIP this is skipped + --- + duration_ms: * + ... +not ok 27 - sync skip option is false fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'this should be executed' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +ok 28 - + --- + duration_ms: * + ... +ok 29 - functionOnly + --- + duration_ms: * + ... +ok 30 - + --- + duration_ms: * + ... +ok 31 - test with only a name provided + --- + duration_ms: * + ... +ok 32 - + --- + duration_ms: * + ... +ok 33 - # SKIP + --- + duration_ms: * + ... +ok 34 - test with a name and options provided # SKIP + --- + duration_ms: * + ... +ok 35 - functionAndOptions # SKIP + --- + duration_ms: * + ... +ok 36 - escaped description \\ \# \\\#\\ + --- + duration_ms: * + ... +ok 37 - escaped skip message # SKIP \#skip + --- + duration_ms: * + ... +ok 38 - escaped todo message # TODO \#todo + --- + duration_ms: * + ... +ok 39 - callback pass + --- + duration_ms: * + ... +not ok 40 - callback fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'callback failure' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +ok 41 - sync t is this in test + --- + duration_ms: * + ... +ok 42 - async t is this in test + --- + duration_ms: * + ... +ok 43 - callback t is this in test + --- + duration_ms: * + ... +not ok 44 - callback also returns a Promise + --- + duration_ms: * + failureType: 'callbackAndPromisePresent' + error: 'passed a callback but also returned a Promise' + code: 'ERR_TEST_FAILURE' + ... +not ok 45 - callback throw + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from callback throw' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... +not ok 46 - callback called twice + --- + duration_ms: * + failureType: 'multipleCallbackInvocations' + error: 'callback invoked multiple times' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + ... +ok 47 - callback called twice in different ticks + --- + duration_ms: * + ... +not ok 48 - callback called twice in future tick + --- + duration_ms: * + failureType: 'uncaughtException' + error: 'callback invoked multiple times' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... +not ok 49 - callback async throw + --- + duration_ms: * + failureType: 'uncaughtException' + error: 'thrown from callback async throw' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... +ok 50 - callback async throw after done + --- + duration_ms: * + ... +not ok 51 - custom inspect symbol fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'customized' + code: 'ERR_TEST_FAILURE' + ... +not ok 52 - custom inspect symbol that throws fail + --- + duration_ms: * + failureType: 'testCodeFailure' + error: |- + { + foo: 1, + [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] + } + code: 'ERR_TEST_FAILURE' + ... + not ok 1 - sync throw fails at first + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from subtest sync throw fails at first' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + * + ... + not ok 2 - sync throw fails at second + --- + duration_ms: * + failureType: 'testCodeFailure' + error: 'thrown from subtest sync throw fails at second' + code: 'ERR_TEST_FAILURE' + stack: |- + * + * + * + * + * + * + ... + 1..2 +not ok 53 - subtest sync throw fails + --- + duration_ms: * + failureType: 'subtestsFailed' + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + ... +not ok 54 - invalid subtest fail + --- + duration_ms: * + failureType: 'parentAlreadyFinished' + error: 'test could not be started because its parent finished' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... +1..54 +# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. +# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. +# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. +# tests 54 +# pass 23 +# fail 17 +# skipped 9 +# todo 5 +# duration_ms * From 1ee83262b9ff40d5923f84e1d70285a4849a2994 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Fri, 17 Jun 2022 10:15:23 +0300 Subject: [PATCH 12/24] reduce diff --- lib/internal/test_runner/test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 36ba14e7014096..457d727e817be0 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -222,8 +222,8 @@ class Test extends AsyncResource { const test = new Factory({ fn, name, parent, ...options, ...overrides }); - if (test.parent.waitingOn === 0) { - test.parent.waitingOn = test.testNumber; + if (parent.waitingOn === 0) { + parent.waitingOn = test.testNumber; } if (this.finished) { @@ -236,7 +236,7 @@ class Test extends AsyncResource { ); } - ArrayPrototypePush(test.parent.subtests, test); + ArrayPrototypePush(parent.subtests, test); return test; } From be7adc6e09ee84273b858c51d3eca79a6ad6b9e1 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sun, 19 Jun 2022 11:08:56 +0300 Subject: [PATCH 13/24] post merge --- test/message/test_runner_desctibe_it.out | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/message/test_runner_desctibe_it.out b/test/message/test_runner_desctibe_it.out index c3d2a30c247e73..8479837d333e7b 100644 --- a/test/message/test_runner_desctibe_it.out +++ b/test/message/test_runner_desctibe_it.out @@ -1,12 +1,15 @@ TAP version 13 +# Subtest: sync pass todo ok 1 - sync pass todo # TODO --- duration_ms: * ... +# Subtest: sync pass todo with message ok 2 - sync pass todo with message # TODO this is a passing todo --- duration_ms: * ... +# Subtest: sync fail todo not ok 3 - sync fail todo # TODO --- duration_ms: * @@ -22,6 +25,7 @@ not ok 3 - sync fail todo # TODO * * ... +# Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo --- duration_ms: * @@ -40,18 +44,22 @@ not ok 4 - sync fail todo with message # TODO this is a failing todo * * ... +# Subtest: sync skip pass ok 5 - sync skip pass # SKIP --- duration_ms: * ... +# Subtest: sync skip pass with message ok 6 - sync skip pass with message # SKIP this is skipped --- duration_ms: * ... +# Subtest: sync pass ok 7 - sync pass --- duration_ms: * ... +# Subtest: sync throw fail not ok 8 - sync throw fail --- duration_ms: * @@ -67,14 +75,17 @@ not ok 8 - sync throw fail * * ... +# Subtest: async skip pass ok 9 - async skip pass # SKIP --- duration_ms: * ... +# Subtest: async pass ok 10 - async pass --- duration_ms: * ... +# Subtest: async throw fail not ok 11 - async throw fail --- duration_ms: * @@ -90,6 +101,7 @@ not ok 11 - async throw fail * * ... +# Subtest: async assertion fail not ok 12 - async assertion fail --- duration_ms: * @@ -109,10 +121,12 @@ not ok 12 - async assertion fail * * ... +# Subtest: resolve pass ok 13 - resolve pass --- duration_ms: * ... +# Subtest: reject fail not ok 14 - reject fail --- duration_ms: * @@ -128,26 +142,33 @@ not ok 14 - reject fail * * ... +# Subtest: unhandled rejection - passes but warns ok 15 - unhandled rejection - passes but warns --- duration_ms: * ... +# Subtest: async unhandled rejection - passes but warns ok 16 - async unhandled rejection - passes but warns --- duration_ms: * ... +# Subtest: immediate throw - passes but warns ok 17 - immediate throw - passes but warns --- duration_ms: * ... +# Subtest: immediate reject - passes but warns ok 18 - immediate reject - passes but warns --- duration_ms: * ... +# Subtest: immediate resolve pass ok 19 - immediate resolve pass --- duration_ms: * ... +# Subtest: subtest sync throw fail + # Subtest: +sync throw fail not ok 1 - +sync throw fail --- duration_ms: * @@ -170,6 +191,7 @@ not ok 20 - subtest sync throw fail error: '1 subtest failed' code: 'ERR_TEST_FAILURE' ... +# Subtest: sync throw non-error fail not ok 21 - sync throw non-error fail --- duration_ms: * @@ -177,18 +199,23 @@ not ok 21 - sync throw non-error fail error: 'Symbol(thrown symbol from sync throw non-error fail)' code: 'ERR_TEST_FAILURE' ... +# Subtest: level 0a + # Subtest: level 1a ok 1 - level 1a --- duration_ms: * ... + # Subtest: level 1b ok 2 - level 1b --- duration_ms: * ... + # Subtest: level 1c ok 3 - level 1c --- duration_ms: * ... + # Subtest: level 1d ok 4 - level 1d --- duration_ms: * @@ -198,10 +225,14 @@ ok 22 - level 0a --- duration_ms: * ... +# Subtest: top level + # Subtest: +long running ok 1 - +long running --- duration_ms: * ... + # Subtest: +short running + # Subtest: ++short running ok 1 - ++short running --- duration_ms: * @@ -216,18 +247,22 @@ ok 23 - top level --- duration_ms: * ... +# Subtest: invalid subtest - pass but subtest fails ok 24 - invalid subtest - pass but subtest fails --- duration_ms: * ... +# Subtest: sync skip option ok 25 - sync skip option # SKIP --- duration_ms: * ... +# Subtest: sync skip option with message ok 26 - sync skip option with message # SKIP this is skipped --- duration_ms: * ... +# Subtest: sync skip option is false fail not ok 27 - sync skip option is false fail --- duration_ms: * @@ -243,54 +278,67 @@ not ok 27 - sync skip option is false fail * * ... +# Subtest: ok 28 - --- duration_ms: * ... +# Subtest: functionOnly ok 29 - functionOnly --- duration_ms: * ... +# Subtest: ok 30 - --- duration_ms: * ... +# Subtest: test with only a name provided ok 31 - test with only a name provided --- duration_ms: * ... +# Subtest: ok 32 - --- duration_ms: * ... +# Subtest: ok 33 - # SKIP --- duration_ms: * ... +# Subtest: test with a name and options provided ok 34 - test with a name and options provided # SKIP --- duration_ms: * ... +# Subtest: functionAndOptions ok 35 - functionAndOptions # SKIP --- duration_ms: * ... +# Subtest: escaped description \\ \# \\\#\\ ok 36 - escaped description \\ \# \\\#\\ --- duration_ms: * ... +# Subtest: escaped skip message ok 37 - escaped skip message # SKIP \#skip --- duration_ms: * ... +# Subtest: escaped todo message ok 38 - escaped todo message # TODO \#todo --- duration_ms: * ... +# Subtest: callback pass ok 39 - callback pass --- duration_ms: * ... +# Subtest: callback fail not ok 40 - callback fail --- duration_ms: * @@ -301,18 +349,22 @@ not ok 40 - callback fail * * ... +# Subtest: sync t is this in test ok 41 - sync t is this in test --- duration_ms: * ... +# Subtest: async t is this in test ok 42 - async t is this in test --- duration_ms: * ... +# Subtest: callback t is this in test ok 43 - callback t is this in test --- duration_ms: * ... +# Subtest: callback also returns a Promise not ok 44 - callback also returns a Promise --- duration_ms: * @@ -320,6 +372,7 @@ not ok 44 - callback also returns a Promise error: 'passed a callback but also returned a Promise' code: 'ERR_TEST_FAILURE' ... +# Subtest: callback throw not ok 45 - callback throw --- duration_ms: * @@ -335,6 +388,7 @@ not ok 45 - callback throw * * ... +# Subtest: callback called twice not ok 46 - callback called twice --- duration_ms: * @@ -345,10 +399,12 @@ not ok 46 - callback called twice * * ... +# Subtest: callback called twice in different ticks ok 47 - callback called twice in different ticks --- duration_ms: * ... +# Subtest: callback called twice in future tick not ok 48 - callback called twice in future tick --- duration_ms: * @@ -358,6 +414,7 @@ not ok 48 - callback called twice in future tick stack: |- * ... +# Subtest: callback async throw not ok 49 - callback async throw --- duration_ms: * @@ -367,10 +424,12 @@ not ok 49 - callback async throw stack: |- * ... +# Subtest: callback async throw after done ok 50 - callback async throw after done --- duration_ms: * ... +# Subtest: custom inspect symbol fail not ok 51 - custom inspect symbol fail --- duration_ms: * @@ -378,6 +437,7 @@ not ok 51 - custom inspect symbol fail error: 'customized' code: 'ERR_TEST_FAILURE' ... +# Subtest: custom inspect symbol that throws fail not ok 52 - custom inspect symbol that throws fail --- duration_ms: * @@ -389,6 +449,8 @@ not ok 52 - custom inspect symbol that throws fail } code: 'ERR_TEST_FAILURE' ... +# Subtest: subtest sync throw fails + # Subtest: sync throw fails at first not ok 1 - sync throw fails at first --- duration_ms: * @@ -404,6 +466,7 @@ not ok 52 - custom inspect symbol that throws fail * * ... + # Subtest: sync throw fails at second not ok 2 - sync throw fails at second --- duration_ms: * @@ -426,6 +489,7 @@ not ok 53 - subtest sync throw fails error: '2 subtests failed' code: 'ERR_TEST_FAILURE' ... +# Subtest: invalid subtest fail not ok 54 - invalid subtest fail --- duration_ms: * From 646cb0d02c4cf1338305ac2c4589f15e946f5b93 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sun, 19 Jun 2022 11:33:16 +0300 Subject: [PATCH 14/24] CR --- lib/internal/test_runner/harness.js | 6 +++++- lib/internal/test_runner/test.js | 15 ++++++++++----- lib/internal/test_runner/utils.js | 6 +++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index c1dd6e481ecc2b..763516246b8410 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -1,5 +1,9 @@ 'use strict'; -const { FunctionPrototypeBind, ArrayPrototypeForEach, SafeMap } = primordials; +const { + ArrayPrototypeForEach, + FunctionPrototypeBind, + SafeMap, +} = primordials; const { createHook, executionAsyncId, diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 457d727e817be0..debe19341cea54 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,12 +1,14 @@ 'use strict'; const { ArrayPrototypePush, - ArrayPrototypeShift, ArrayPrototypeReduce, + ArrayPrototypeShift, + ArrayPrototypeUnshift, FunctionPrototype, Number, - SafeMap, PromiseResolve, + ReflectApply, + SafeMap, } = primordials; const { AsyncResource } = require('async_hooks'); const { @@ -313,11 +315,14 @@ class Test extends AsyncResource { try { const { args, ctx } = this.getRunArgs(); + ArrayPrototypeUnshift(args, this.fn, ctx); // Note that if it's not OK to mutate args, we need to first clone it. - if (this.fn.length === args.length + 1) { + if (this.fn.length === args.length - 1) { // This test is using legacy Node.js error first callbacks. const { promise, cb } = createDeferredCallback(); - const ret = this.runInAsyncScope(this.fn, ctx, ...args, cb); + + ArrayPrototypePush(args, cb); + const ret = ReflectApply(this.runInAsyncScope, this, args); if (isPromise(ret)) { this.fail(new ERR_TEST_FAILURE( @@ -330,7 +335,7 @@ class Test extends AsyncResource { } } else { // This test is synchronous or using Promises. - await this.runInAsyncScope(this.fn, ctx, ...args); + await ReflectApply(this.runInAsyncScope, this, args); } this.pass(); diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index be39fa0add1fc7..d98fa1e59b3610 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -49,4 +49,8 @@ function createDeferredCallback() { return { promise, cb }; } -module.exports = { isSupportedFileType, doesPathMatchFilter, createDeferredCallback }; +module.exports = { + createDeferredCallback, + doesPathMatchFilter, + isSupportedFileType, +}; From 40e1cd97f01d615588e45c4aa418e3e66edc1006 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sun, 19 Jun 2022 13:56:30 +0300 Subject: [PATCH 15/24] docs --- doc/api/test.md | 82 ++++++++++++++++++++++++++++- lib/internal/test_runner/harness.js | 7 ++- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 52620899191142..eca8b221c8671f 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -148,6 +148,33 @@ test('skip() method with message', (t) => { }); ``` +## `describe it` syntax + +Running tests can also be done using `describe` to declare a suite and `it` to declare a test. +A suite is used to organize and group related tests together. +`it` is an alias for `test`, except there is no test context passed, since nesting is done using suites, +as demonstrated in this example + +```js +import { describe, it } from 'node:test'; + +describe('A thing', () => { + it('should work', () => { + assert.strictEqual(1, 1); + }); + + is('should be ok', () => { + assert.strictEqual(2, 2); + }); + + describe('a nested thing', () => { + it('should work', () => { + assert.strictEqual(3, 3); + }); + }); +}); +``` + ### `only` tests If Node.js is started with the [`--test-only`][] command-line option, it is @@ -303,7 +330,7 @@ added: v18.0.0 * `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string is provided, that string is displayed in the test results as the reason why the test is `TODO`. **Default:** `false`. -* `fn` {Function|AsyncFunction} The function under test. This first argument +* `fn` {Function|AsyncFunction} The function under test. The first argument to this function is a [`TestContext`][] object. If the test uses callbacks, the callback function is passed as the second argument. **Default:** A no-op function. @@ -335,6 +362,57 @@ test('top level test', async (t) => { }); ``` +## `describe([name][, options][, fn])` + +* `name` {string} The name of the suite, which is displayed when reporting test + results. **Default:** The `name` property of `fn`, or `''` if `fn` + does not have a name. +* `options` {Object} Configuration options for the suite. supports the same options as `test([name][, options][, fn])` +* `fn` {Function} The function under suite. a synchronous function declaring all subtests and subsuites. **Default:** A no-op + function. +* Returns: `undefined`. + +The `describe()` function imported from the `test` module. Each +invocation of this function results in the creation of a Subtest and a test point in the TAP +output. +After invocation of top level `describe` functions, all top level tests and suites will execute + +## `describe.skip([name][, options][, fn])` + +shorthand for skipping a suite, +same as `describe([name], { skip: true }[, fn])` + +## `describe.todo([name][, options][, fn])` + +shorthand for marking a suite as `TODO`, +same as `describe([name], { skip: todo }[, fn])` +## `it([name][, options][, fn])` + +* `name` {string} The name of the test, which is displayed when reporting test + results. **Default:** The `name` property of `fn`, or `''` if `fn` + does not have a name. +* `options` {Object} Configuration options for the suite. supports the same options as `test([name][, options][, fn])`. +* `fn` {Function|AsyncFunction} The function under test. If the test uses callbacks, + the callback function is passed as an argument. **Default:** A no-op + function. +* Returns: `undefined`. + +The `it()` function is the value imported from the `test` module. +Each +invocation of this function results in the creation of a test point in the TAP +output. + + +## `it.skip([name][, options][, fn])` + +shorthand for skipping a test, +same as `it([name], { skip: true }[, fn])` + +## `it.todo([name][, options][, fn])` + +shorthand for marking a test as `TODO`, +same as `it([name], { skip: todo }[, fn])` + ## Class: `TestContext`