From 339ef9caed373e3c3eee1965ad0c0a3f2bf53522 Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Sat, 14 Sep 2024 20:48:38 -0400 Subject: [PATCH] test: wait for stream finish when --test-force-exit --- lib/internal/test_runner/test.js | 10 ++- lib/internal/test_runner/tests_stream.js | 6 +- lib/internal/test_runner/utils.js | 5 +- test-runner-force-exit-dot.snapshot | 24 ++++++ test-runner-force-exit-junit.snapshot | 41 +++++++++ test-runner-force-exit-spec.snapshot | 52 ++++++++++++ test-runner-force-exit-tap.snapshot | 48 +++++++++++ test/common/assertSnapshot.js | 4 +- .../output/test-runner-force-exit.js | 12 +++ test/parallel/test-runner-force-exit.mjs | 84 +++++++++++++++++++ 10 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 test-runner-force-exit-dot.snapshot create mode 100644 test-runner-force-exit-junit.snapshot create mode 100644 test-runner-force-exit-spec.snapshot create mode 100644 test-runner-force-exit-tap.snapshot create mode 100644 test/fixtures/test-runner/output/test-runner-force-exit.js create mode 100644 test/parallel/test-runner-force-exit.mjs diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 41db230119aab0..80065aa5416101 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,5 +1,6 @@ 'use strict'; const { + ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeReduce, @@ -20,6 +21,7 @@ const { RegExpPrototypeExec, SafeMap, SafePromiseAll, + SafePromiseAllReturnVoid, SafePromisePrototypeFinally, SafePromiseRace, SafeSet, @@ -41,7 +43,7 @@ const { }, } = require('internal/errors'); const { MockTracker } = require('internal/test_runner/mock/mock'); -const { TestsStream } = require('internal/test_runner/tests_stream'); +const { TestsStream, kReporterStreams } = require('internal/test_runner/tests_stream'); const { createDeferredCallback, countCompletedTest, @@ -64,6 +66,7 @@ const { setTimeout } = require('timers'); const { TIMEOUT_MAX } = require('internal/timers'); const { fileURLToPath } = require('internal/url'); const { availableParallelism } = require('os'); +const { finished } = require('stream/promises'); const { bigint: hrtime } = process.hrtime; const kCallbackAndPromisePresent = 'callbackAndPromisePresent'; const kCancelledByParent = 'cancelledByParent'; @@ -963,7 +966,10 @@ class Test extends AsyncResource { // any remaining ref'ed handles, then do that now. It is theoretically // possible that a ref'ed handle could asynchronously create more tests, // but the user opted into this behavior. - this.reporter.once('close', () => { + this.reporter.once('close', async () => { + await SafePromiseAllReturnVoid( + ArrayPrototypeMap(this.reporter[kReporterStreams], (stream) => finished(stream)), + ); process.exit(); }); this.harness.teardown(); diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index 08d4397ae64a3c..63ca1800eaff8b 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -3,11 +3,14 @@ const { ArrayPrototypePush, ArrayPrototypeShift, NumberMAX_SAFE_INTEGER, + SafeWeakSet, Symbol, } = primordials; const Readable = require('internal/streams/readable'); const kEmitMessage = Symbol('kEmitMessage'); +const kReporterStreams = Symbol('kReporterStreams'); + class TestsStream extends Readable { #buffer; #canPush; @@ -18,6 +21,7 @@ class TestsStream extends Readable { objectMode: true, highWaterMark: NumberMAX_SAFE_INTEGER, }); + this[kReporterStreams] = new SafeWeakSet(); this.#buffer = []; this.#canPush = true; } @@ -154,4 +158,4 @@ class TestsStream extends Readable { } } -module.exports = { TestsStream, kEmitMessage }; +module.exports = { TestsStream, kEmitMessage, kReporterStreams }; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 2fc0907c388780..1d275402431827 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -40,6 +40,7 @@ const { } = require('internal/errors'); const { compose } = require('stream'); const { validateInteger } = require('internal/validators'); +const { kReporterStreams } = require('internal/test_runner/tests_stream'); const coverageColors = { __proto__: null, @@ -297,7 +298,9 @@ function parseCommandLine() { for (let i = 0; i < reportersMap.length; i++) { const { reporter, destination } = reportersMap[i]; - compose(rootReporter, reporter).pipe(destination); + const stream = compose(rootReporter, reporter); + stream.pipe(destination); + ArrayPrototypePush(rootReporter[kReporterStreams], stream); } }); diff --git a/test-runner-force-exit-dot.snapshot b/test-runner-force-exit-dot.snapshot new file mode 100644 index 00000000000000..fd65527c3b3193 --- /dev/null +++ b/test-runner-force-exit-dot.snapshot @@ -0,0 +1,24 @@ +X.X + +Failed tests: + +✖ Failing test (*ms) + AssertionError [ERR_ASSERTION]: Failed + * + * + * + * + * + * + at new Promise () + * + * + at Array.map () { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: undefined, + expected: undefined, + operator: 'fail' + } +✖ Suite (*ms) + '1 subtest failed' diff --git a/test-runner-force-exit-junit.snapshot b/test-runner-force-exit-junit.snapshot new file mode 100644 index 00000000000000..8c812a1138fe3a --- /dev/null +++ b/test-runner-force-exit-junit.snapshot @@ -0,0 +1,41 @@ + + + + + +Error [ERR_TEST_FAILURE]: Failed + at new Promise (<anonymous>) + at Array.map (<anonymous>) { + code: 'ERR_TEST_FAILURE', + failureType: 'testCodeFailure', + cause: AssertionError [ERR_ASSERTION]: Failed + * + * + * + * + * + * + at new Promise (<anonymous>) + * + * + at Array.map (<anonymous>) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: undefined, + expected: undefined, + operator: 'fail' + } +} + + + + + + + + + + + + + diff --git a/test-runner-force-exit-spec.snapshot b/test-runner-force-exit-spec.snapshot new file mode 100644 index 00000000000000..560cd6281a7afa --- /dev/null +++ b/test-runner-force-exit-spec.snapshot @@ -0,0 +1,52 @@ +▶ Suite + ✖ Failing test (*ms) + AssertionError [ERR_ASSERTION]: Failed + * + * + * + * + * + * + at new Promise () + * + * + at Array.map () { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: undefined, + expected: undefined, + operator: 'fail' + } + + ✔ Passing test (*ms) +▶ Suite (*ms) +ℹ tests 2 +ℹ suites 1 +ℹ pass 1 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms * + +✖ failing tests: + +* +✖ Failing test (*ms) + AssertionError [ERR_ASSERTION]: Failed + * + * + * + * + * + * + at new Promise () + * + * + at Array.map () { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: undefined, + expected: undefined, + operator: 'fail' + } diff --git a/test-runner-force-exit-tap.snapshot b/test-runner-force-exit-tap.snapshot new file mode 100644 index 00000000000000..5632fe184af345 --- /dev/null +++ b/test-runner-force-exit-tap.snapshot @@ -0,0 +1,48 @@ +TAP version 13 +# Subtest: Suite + # Subtest: Failing test + not ok 1 - Failing test + --- + duration_ms: * + location: '/test/fixtures/test-runner/output/test-runner-force-exit.js:(LINE):5' + failureType: 'testCodeFailure' + error: 'Failed' + code: 'ERR_ASSERTION' + name: 'AssertionError' + operator: 'fail' + stack: |- + * + * + * + * + * + * + new Promise () + * + * + Array.map () + ... + # Subtest: Passing test + ok 2 - Passing test + --- + duration_ms: * + ... + 1..2 +not ok 1 - Suite + --- + duration_ms: * + type: 'suite' + location: '/test/fixtures/test-runner/output/test-runner-force-exit.js:(LINE):1' + failureType: 'subtestsFailed' + error: '1 subtest failed' + code: 'ERR_TEST_FAILURE' + ... +1..1 +# tests 2 +# suites 1 +# pass 1 +# fail 1 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/common/assertSnapshot.js b/test/common/assertSnapshot.js index e0793ce5394cda..5638bcb59e8894 100644 --- a/test/common/assertSnapshot.js +++ b/test/common/assertSnapshot.js @@ -72,7 +72,7 @@ async function assertSnapshot(actual, filename = process.argv[1]) { * @param {boolean} [options.tty] - whether to spawn the process in a pseudo-tty * @returns {Promise} */ -async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}) { +async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}, snapshot = filename) { if (tty && common.isWindows) { test({ skip: 'Skipping pseudo-tty tests, as pseudo terminals are not available on Windows.' }); return; @@ -88,7 +88,7 @@ async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ... [path.join(__dirname, '../..', 'tools/pseudo-tty.py'), process.execPath, ...flags, filename] : [...flags, filename]; const { stdout, stderr } = await common.spawnPromisified(executable, args, options); - await assertSnapshot(transform(`${stdout}${stderr}`), filename); + await assertSnapshot(transform(`${stdout}${stderr}`), snapshot); } module.exports = { diff --git a/test/fixtures/test-runner/output/test-runner-force-exit.js b/test/fixtures/test-runner/output/test-runner-force-exit.js new file mode 100644 index 00000000000000..3e5d13f243f4b2 --- /dev/null +++ b/test/fixtures/test-runner/output/test-runner-force-exit.js @@ -0,0 +1,12 @@ +const { describe, test } = require('node:test'); +const assert = require('node:assert'); + +describe('Suite', () => { + test('Failing test', () => { + assert.fail() + }) + + test('Passing test', () => { + assert.ok(true) + }) +}); diff --git a/test/parallel/test-runner-force-exit.mjs b/test/parallel/test-runner-force-exit.mjs new file mode 100644 index 00000000000000..d5b079aa12ceb5 --- /dev/null +++ b/test/parallel/test-runner-force-exit.mjs @@ -0,0 +1,84 @@ +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import * as snapshot from '../common/assertSnapshot.js'; +import { describe, it } from 'node:test'; +import { hostname } from 'node:os'; +import { fileURLToPath } from 'node:url'; + +function replaceTestDuration(str) { + return str + .replaceAll(/duration_ms: [0-9.]+/g, 'duration_ms: *') + .replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *'); +} + +const root = fileURLToPath(new URL('../..', import.meta.url)).slice(0, -1); + +const color = '(\\[\\d+m)'; +const stackTraceBasePath = new RegExp(`${color}\\(${root.replaceAll(/[\\^$*+?.()|[\]{}]/g, '\\$&')}/?${color}(.*)${color}\\)`, 'g'); + +function replaceSpecDuration(str) { + return str + .replaceAll(/[0-9.]+ms/g, '*ms') + .replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *') + .replace(stackTraceBasePath, '$3'); +} + +function replaceJunitDuration(str) { + return str + .replaceAll(/time="[0-9.]+"/g, 'time="*"') + .replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *') + .replaceAll(hostname(), 'HOSTNAME') + .replace(stackTraceBasePath, '$3'); +} + +function removeWindowsPathEscaping(str) { + return common.isWindows ? str.replaceAll(/\\\\/g, '\\') : str; +} + +function replaceTestLocationLine(str) { + return str.replaceAll(/(js:)(\d+)(:\d+)/g, '$1(LINE)$3'); +} + +const defaultTransform = snapshot.transform( + snapshot.replaceWindowsLineEndings, + snapshot.replaceStackTrace, + removeWindowsPathEscaping, + snapshot.replaceFullPaths, + snapshot.replaceWindowsPaths, + replaceTestDuration, + replaceTestLocationLine, +); + +const transformers = { + junit: snapshot.transform( + replaceJunitDuration, + snapshot.replaceWindowsLineEndings, + snapshot.replaceStackTrace, + snapshot.replaceWindowsPaths, + ), + spec: snapshot.transform( + replaceSpecDuration, + snapshot.replaceWindowsLineEndings, + snapshot.replaceStackTrace, + snapshot.replaceWindowsPaths, + ), + dot: snapshot.transform( + replaceSpecDuration, + snapshot.replaceWindowsLineEndings, + snapshot.replaceStackTrace, + snapshot.replaceWindowsPaths, + ), +}; + +describe('test runner output', { concurrency: true }, async () => { + for (const reporter of ['dot', 'junit', 'spec', 'tap']) { + await it(reporter, async () => { + await snapshot.spawnAndAssert( + fixtures.path('test-runner/output/test-runner-force-exit.js'), + transformers[reporter] ?? defaultTransform, + { flags: ['--test-force-exit', `--test-reporter=${reporter}`] }, + `test-runner-force-exit-${reporter}` + ); + }); + } +});