diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 77791b7231d593..fe5915dec13483 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -28,6 +28,8 @@ const { setupGlobalSetupTeardownFunctions, } = require('internal/test_runner/utils'); const { queueMicrotask } = require('internal/process/task_queues'); +const { TIMEOUT_MAX } = require('internal/timers'); +const { clearInterval, setInterval } = require('timers'); const { bigint: hrtime } = process.hrtime; const testResources = new SafeMap(); let globalRoot; @@ -228,11 +230,20 @@ function setupProcessState(root, globalOptions) { const rejectionHandler = createProcessEventHandler('unhandledRejection', root); const coverage = configureCoverage(root, globalOptions); - const exitHandler = async () => { + const exitHandler = async (kill) => { if (root.subtests.length === 0 && (root.hooks.before.length > 0 || root.hooks.after.length > 0)) { // Run global before/after hooks in case there are no tests await root.run(); } + + if (kill !== true && root.subtestsPromise !== null) { + // Wait for all subtests to finish, but keep the process alive in case + // there are no ref'ed handles left. + const keepAlive = setInterval(() => {}, TIMEOUT_MAX); + await root.subtestsPromise.promise; + clearInterval(keepAlive); + } + root.postRun(new ERR_TEST_FAILURE( 'Promise resolution is still pending but the event loop has already resolved', kCancelledByParent)); @@ -252,8 +263,8 @@ function setupProcessState(root, globalOptions) { } }; - const terminationHandler = () => { - exitHandler(); + const terminationHandler = async () => { + await exitHandler(true); process.exit(); }; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 6b3d087b67da66..14d9911ef4f62d 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -651,6 +651,8 @@ class Test extends AsyncResource { this.activeSubtests = 0; this.pendingSubtests = []; this.readySubtests = new SafeMap(); + this.unfinishedSubtests = new SafeSet(); + this.subtestsPromise = null; this.subtests = []; this.waitingOn = 0; this.finished = false; @@ -754,6 +756,11 @@ class Test extends AsyncResource { addReadySubtest(subtest) { this.readySubtests.set(subtest.childNumber, subtest); + + if (this.unfinishedSubtests.delete(subtest) && + this.unfinishedSubtests.size === 0) { + this.subtestsPromise.resolve(); + } } processReadySubtestRange(canSend) { @@ -815,6 +822,7 @@ class Test extends AsyncResource { if (parent.waitingOn === 0) { parent.waitingOn = test.childNumber; + parent.subtestsPromise = PromiseWithResolvers(); } if (preventAddingSubtests) { @@ -937,6 +945,7 @@ class Test extends AsyncResource { // If there is enough available concurrency to run the test now, then do // it. Otherwise, return a Promise to the caller and mark the test as // pending for later execution. + this.parent.unfinishedSubtests.add(this); this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType); if (this.root.harness.buildPromise || !this.parent.hasConcurrency()) { const deferred = PromiseWithResolvers(); @@ -1061,6 +1070,10 @@ class Test extends AsyncResource { this[kShouldAbort](); + if (this.subtestsPromise !== null) { + await SafePromiseRace([this.subtestsPromise.promise, stopPromise]); + } + if (this.plan !== null) { const planPromise = this.plan?.check(); // If the plan returns a promise, it means that it is waiting for more assertions to be made before diff --git a/test/fixtures/test-runner/output/default_output.snapshot b/test/fixtures/test-runner/output/default_output.snapshot index d0a83395733924..a58e14346ec727 100644 --- a/test/fixtures/test-runner/output/default_output.snapshot +++ b/test/fixtures/test-runner/output/default_output.snapshot @@ -3,13 +3,13 @@ [90m﹣ should skip [90m(*ms)[39m # SKIP[39m ▶ parent [31m✖ should fail [90m(*ms)[39m[39m - [31m✖ should pass but parent fail [90m(*ms)[39m[39m + [32m✔ should pass but parent fail [90m(*ms)[39m[39m [31m✖ parent [90m(*ms)[39m[39m [34mℹ tests 6[39m [34mℹ suites 0[39m -[34mℹ pass 1[39m +[34mℹ pass 2[39m [34mℹ fail 3[39m -[34mℹ cancelled 1[39m +[34mℹ cancelled 0[39m [34mℹ skipped 1[39m [34mℹ todo 0[39m [34mℹ duration_ms *[39m @@ -40,7 +40,3 @@ *[39m *[39m *[39m - -* -[31m✖ should pass but parent fail [90m(*ms)[39m[39m - [32m'test did not finish before its parent and was cancelled'[39m diff --git a/test/fixtures/test-runner/output/dot_reporter.snapshot b/test/fixtures/test-runner/output/dot_reporter.snapshot index fc2b58cfef8428..4ef804951dc99f 100644 --- a/test/fixtures/test-runner/output/dot_reporter.snapshot +++ b/test/fixtures/test-runner/output/dot_reporter.snapshot @@ -1,5 +1,5 @@ ..XX...X..XXX.X..... -XXX.....X..X...X.... +XXX............X.... .....X...XXX.XX..... XXXXXXX...XXXXX @@ -93,10 +93,6 @@ Failed tests: '1 subtest failed' ✖ sync throw non-error fail (*ms) Symbol(thrown symbol from sync throw non-error fail) -✖ +long running (*ms) - 'test did not finish before its parent and was cancelled' -✖ top level (*ms) - '1 subtest failed' ✖ sync skip option is false fail (*ms) Error: this should be executed * diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index aaa5dcd6ff9963..d1868bd4b6eaa9 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -186,12 +186,8 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail - - - -[Error [ERR_TEST_FAILURE]: test did not finish before its parent and was cancelled] { code: 'ERR_TEST_FAILURE', failureType: 'cancelledByParent', cause: 'test did not finish before its parent and was cancelled' } - - + + @@ -519,9 +515,9 @@ Error [ERR_TEST_FAILURE]: test could not be started because its parent finished - - - + + + diff --git a/test/fixtures/test-runner/output/no_refs.snapshot b/test/fixtures/test-runner/output/no_refs.snapshot index 310094947f9f96..8014b0343892f7 100644 --- a/test/fixtures/test-runner/output/no_refs.snapshot +++ b/test/fixtures/test-runner/output/no_refs.snapshot @@ -1,35 +1,23 @@ TAP version 13 # Subtest: does not keep event loop alive # Subtest: +does not keep event loop alive - not ok 1 - +does not keep event loop alive + ok 1 - +does not keep event loop alive --- duration_ms: * type: 'test' - location: '/test/fixtures/test-runner/output/no_refs.js:(LINE):11' - failureType: 'cancelledByParent' - error: 'Promise resolution is still pending but the event loop has already resolved' - code: 'ERR_TEST_FAILURE' - stack: |- - * ... 1..1 -not ok 1 - does not keep event loop alive +ok 1 - does not keep event loop alive --- duration_ms: * type: 'test' - location: '/test/fixtures/test-runner/output/no_refs.js:(LINE):1' - failureType: 'cancelledByParent' - error: 'Promise resolution is still pending but the event loop has already resolved' - code: 'ERR_TEST_FAILURE' - stack: |- - * ... 1..1 # tests 2 # suites 0 -# pass 0 +# pass 2 # fail 0 -# cancelled 2 +# cancelled 0 # skipped 0 # todo 0 # duration_ms * diff --git a/test/fixtures/test-runner/output/output.snapshot b/test/fixtures/test-runner/output/output.snapshot index 36d9c289fa1615..044ac4137fa78d 100644 --- a/test/fixtures/test-runner/output/output.snapshot +++ b/test/fixtures/test-runner/output/output.snapshot @@ -288,14 +288,10 @@ ok 23 - level 0a ... # Subtest: top level # Subtest: +long running - not ok 1 - +long running + ok 1 - +long running --- duration_ms: * type: 'test' - location: '/test/fixtures/test-runner/output/output.js:(LINE):5' - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' ... # Subtest: +short running # Subtest: ++short running @@ -311,14 +307,10 @@ ok 23 - level 0a type: 'test' ... 1..2 -not ok 24 - top level +ok 24 - top level --- duration_ms: * type: 'test' - location: '/test/fixtures/test-runner/output/output.js:(LINE):1' - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' ... # Subtest: invalid subtest - pass but subtest fails ok 25 - invalid subtest - pass but subtest fails @@ -787,9 +779,9 @@ not ok 62 - invalid subtest fail # Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:(LINE):1 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 75 # suites 0 -# pass 34 -# fail 25 -# cancelled 3 +# pass 36 +# fail 24 +# cancelled 2 # skipped 9 # todo 4 # duration_ms * diff --git a/test/fixtures/test-runner/output/output_cli.snapshot b/test/fixtures/test-runner/output/output_cli.snapshot index 4546269836e9ca..eaa085d97d06d1 100644 --- a/test/fixtures/test-runner/output/output_cli.snapshot +++ b/test/fixtures/test-runner/output/output_cli.snapshot @@ -288,14 +288,10 @@ ok 23 - level 0a ... # Subtest: top level # Subtest: +long running - not ok 1 - +long running + ok 1 - +long running --- duration_ms: * type: 'test' - location: '/test/fixtures/test-runner/output/output.js:(LINE):5' - failureType: 'cancelledByParent' - error: 'test did not finish before its parent and was cancelled' - code: 'ERR_TEST_FAILURE' ... # Subtest: +short running # Subtest: ++short running @@ -311,14 +307,10 @@ ok 23 - level 0a type: 'test' ... 1..2 -not ok 24 - top level +ok 24 - top level --- duration_ms: * type: 'test' - location: '/test/fixtures/test-runner/output/output.js:(LINE):1' - failureType: 'subtestsFailed' - error: '1 subtest failed' - code: 'ERR_TEST_FAILURE' ... # Subtest: invalid subtest - pass but subtest fails ok 25 - invalid subtest - pass but subtest fails @@ -801,9 +793,9 @@ ok 63 - last test 1..63 # tests 77 # suites 0 -# pass 36 -# fail 25 -# cancelled 3 +# pass 38 +# fail 24 +# cancelled 2 # skipped 9 # todo 4 # duration_ms * diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot index 1892069327f92d..17eb9d01451d32 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -90,9 +90,9 @@ Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:269:1 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 75 suites 0 - pass 34 - fail 25 - cancelled 3 + pass 36 + fail 24 + cancelled 2 skipped 9 todo 4 duration_ms * @@ -203,10 +203,6 @@ sync throw non-error fail (*ms) Symbol(thrown symbol from sync throw non-error fail) -* - +long running (*ms) - 'test did not finish before its parent and was cancelled' - * sync skip option is false fail (*ms) Error: this should be executed diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot index 52dc40bb366e2c..2dd9e92deb1b38 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -93,9 +93,9 @@ Error: Test "callback async throw after done" at test/fixtures/test-runner/output/output.js:269:1 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 76 suites 0 - pass 35 - fail 25 - cancelled 3 + pass 37 + fail 24 + cancelled 2 skipped 9 todo 4 duration_ms * @@ -206,10 +206,6 @@ sync throw non-error fail (*ms) Symbol(thrown symbol from sync throw non-error fail) -* - +long running (*ms) - 'test did not finish before its parent and was cancelled' - * sync skip option is false fail (*ms) Error: this should be executed diff --git a/test/parallel/test-runner-output.mjs b/test/parallel/test-runner-output.mjs index 7332f03b129c72..8adac4dfa72b94 100644 --- a/test/parallel/test-runner-output.mjs +++ b/test/parallel/test-runner-output.mjs @@ -215,10 +215,6 @@ const tests = [ name: 'test-runner/output/unfinished-suite-async-error.js', flags: ['--test-reporter=tap'], }, - { - name: 'test-runner/output/unresolved_promise.js', - flags: ['--test-reporter=tap'], - }, { name: 'test-runner/output/default_output.js', transform: specTransform, tty: true }, { name: 'test-runner/output/arbitrary-output.js',